--[[

    builtins.lua
    ------------
    Contains shell builtins, which are non-essential utility commands.

    This file expects to be called by shell.lua as a function, and the
      first argument of this function call must be the table in which
      the builtins are to be registered. (SHELL_BUILTINS)

    The return value of this file is the SHELL_SETTINGS_APPLY function,
      which applies shell settings to the currently active menu instance.

--]]

-- Retrieve the SHELL_BUILTINS table from the file arguments

local args = { ... }
local SHELL_BUILTINS = args[1]

--
--  Simple Builtins
--

-- `help` builtin

SHELL_BUILTINS.help = function ()
    print("Type in Lua code and press ENTER to execute.\n\n" ..
          "Additional commands:\n" ..
          "- tutorial : Brief tutorial\n" ..
          "- settings : Shell settings\n" ..
          "- clear    : Clear shell\n" ..
          "- help     : This message")
end

-- `clear` builtin

SHELL_BUILTINS.clear = function ()
    local shell = self.mints[self.selectedMint]
    shell.text = ""
end

--
--  Functionality for Entering/Exiting TUI Mode
--

-- Table to store pre-TUI state, uses menu instances as keys and tables
--   containing state info as values.
local PRE_TUI_STATE = {}

-- Function to use to overshadow default bindings in TUI mode
local NULL_FUNC = function () end

local function tui_disable ()
    -- Remove the TUI entry point
    self.tuiEntryPoint = nil

    -- Restore the pre-TUI state
    self.mints.shell.text = PRE_TUI_STATE[self].text
    self.mints.shell.scripts.press = PRE_TUI_STATE[self].pressScript
    self.scripts.active = PRE_TUI_STATE[self].activeScript
    self.keymap.UP = PRE_TUI_STATE[self].keyUpScript
    self.keymap.DOWN = PRE_TUI_STATE[self].keyDownScript
    self.keymap.ESCAPE = PRE_TUI_STATE[self].keyEscScript

    -- Remove overshadowing of default bindings
    self.keymap["SHIFT+UP"] = nil
    self.keymap["SHIFT+DOWN"] = nil
    self.keymap["SHIFT+LEFT"] = nil
    self.keymap["SHIFT+RIGHT"] = nil

    -- Remove read-only mode
    self.mints.shell.text.readOnly = false

    -- Remove the state from the table
    PRE_TUI_STATE[self] = nil

    -- Call the TUI quit callback if one exists
    if self.tuiQuitCallback then
	self.tuiQuitCallback()
        self.tuiQuitCallback = nil
    end
end

local function tui_enable (initText, pressScript, activeScript,
                           keyUpScript, keyDownScript,
                           initCallback, quitCallback)
    -- Create the TUI entry point
    self.tuiEntryPoint = function ()
        -- Capture the state
        PRE_TUI_STATE[self] = {}
        PRE_TUI_STATE[self].text = self.mints.shell.text.string
        PRE_TUI_STATE[self].pressScript = self.mints.shell.scripts.press
        PRE_TUI_STATE[self].activeScript = self.scripts.active
        PRE_TUI_STATE[self].keyUpScript = self.keymap.UP
        PRE_TUI_STATE[self].keyDownScript = self.keymap.DOWN
        PRE_TUI_STATE[self].keyEscScript = self.keymap.ESCAPE

        -- Overshadow the default bindings
        self.keymap["SHIFT+UP"] = NULL_FUNC
        self.keymap["SHIFT+DOWN"] = NULL_FUNC
        self.keymap["SHIFT+LEFT"] = NULL_FUNC
        self.keymap["SHIFT+RIGHT"] = NULL_FUNC

        -- Make read-only
        self.mints.shell.text.readOnly = true

        -- Alter the menu using the given arguments
        self.mints.shell.text = initText
        self.mints.shell.scripts.press = pressScript
        self.scripts.active = activeScript
	self.keymap.UP = keyUpScript
        self.keymap.DOWN = keyDownScript

        -- Bind ESC to TUI exit
        self.keymap.ESCAPE = tui_disable

        -- Execute init callback, save quit callback
        if initCallback then
            initCallback()
        end
        self.tuiQuitCallback = quitCallback
    end
end

--
--  `tutorial` builtin
--

local TUTORIAL_SLIDES = {
    -- Slide 1
    [[Welcome to the shell tutorial.

In the shell, you can type in Lua code and press ENTER to execute it.

Some example Lua commands:

-- Print a string
$print("Hello, world!")

-- Change menu background
$self.appearance.backgroundColor = "WHITE"

Press ENTER to proceed]],
    -- Slide 2
    [[Within the shell, the following controls are available:

- Move cursor:
  SHIFT + ARROW KEYS

- Enter newline:
  SHIFT + ENTER

- Navigate command history:
  UP / DOWN

- Exit shell:
  ESC

Press ENTER to proceed]],
    -- Slide 3
    [[Using the shell, you can access all of Monospace Engine's scripting capabilities at runtime.

Everything you see in the engine, games, menus, even this tutorial, is built using these same features.

To learn more, check out the user manual shipped with the engine, or visit monospace.games/engine

== END OF TUTORIAL ==

Press ENTER to end tutorial]]
}

-- Table to store tutorial state, uses menu instances as keys and tables
--   containing tutorial state as values.
local TUTORIAL_STATE = {}

local function tutorial_press()
    local state = TUTORIAL_STATE[self]
    if state.textProgress < state.textLength then
        -- Pressed ENTER before reveal finished, reveal the whole text
        state.charFramesI = 0
        state.textProgress = state.textLength
        self.mints.shell.text = TUTORIAL_SLIDES[state.slide]
    else
        -- Pressed ENTER after reveal finished, go to next slide
        state.slide = state.slide + 1
        local newSlide = TUTORIAL_SLIDES[state.slide]
        if not newSlide then
            -- No next slide, quit tutorial
            tui_disable()
        else
            state.textProgress = 1
            state.textLength = string.len(newSlide)
        end
    end
end

local function tutorial_active ()
    -- Get the state
    local state = TUTORIAL_STATE[self]
    -- Create state if it does not already exist
    if not state then
        state = {}
        -- Current slide
        state.slide = 1
        -- Number of characters displayed in the current slide
        state.textProgress = 1
        -- Total number of characters in the current slide
        state.textLength = string.len(TUTORIAL_SLIDES[state.slide])
        -- Frame counters to limit the text reveal speed
        state.charFramesI = 0 -- Frames currently spent in latest char
        state.charFramesN = 2 -- Total frames to be spent between each two cars
        -- Save in the table
        TUTORIAL_STATE[self] = state
    end
    -- Text reveal
    if state.textProgress <= state.textLength then
        state.charFramesI = state.charFramesI + 1
        if state.charFramesI == state.charFramesN then
            -- Reveal the next character
            local currentSlide = TUTORIAL_SLIDES[state.slide]
            self.mints.shell.text = string.sub(currentSlide, 1,
                                               state.textProgress)
            state.textProgress = state.textProgress + 1
            -- Reset the frames spent in current character
            state.charFramesI = 0
            -- If we're at a paragraph ending, wait a bit longer
            if string.sub(currentSlide, state.textProgress,
                          state.textProgress + 1) == "\n\n" then
                state.charFramesN = 30
            else
                state.charFramesN = 2
            end
        end
    end
end

SHELL_BUILTINS.tutorial = function ()
    tui_enable("",
               -- Scripts
               tutorial_press, tutorial_active, nil, nil,
               -- Callbacks
               nil, function () TUTORIAL_STATE[self] = nil end)
end

--
--  `settings` builtin
--

--
--  Themes
--

-- Shell themes table
-- Alter this table to create/remove themes.
-- This table stores theme names inside tables instead of using them as keys
--   to maintain deterministic order.

local SHELL_THEMES = {
    {
        NAME = "C64",
        bgColor = "(134,122,222)",
        mintBgColor = "(72,58,170)",
        textColor = "(134,122,222)"
    },
    {
        NAME = "Summer",
        bgColor = "(0,158,218)",
        mintBgColor = "(240,240,240)",
        textColor = "(15,15,15)",
    },
    {
        NAME = "Peach",
        bgColor = "(255,203,165)",
        mintBgColor = "(0,0,0,200)",
        textColor = "(240,240,240)",
        borderWidth = "3 PIXELS",
        borderColor = "(255,213,42)",
        cursorColor = "(255,152,153)",
        bgPattern = "COMMON_UI_TILESET BG_PEACH"
    },
    {
        NAME = "Business",
        bgColor = "BLACK",
        mintBgColor = "BLACK",
        textColor = "(214,214,214)",
        borderWidth = "1 PIXEL",
        borderColor = "(214,214,214)",
        cursorColor = "(205,63,69)"
    }
}

local function apply_shell_theme(themeName)
    local theme
    for _, v in ipairs(SHELL_THEMES) do
        if v.NAME == themeName then
            theme = v
            break
        end
    end
    if theme then
        self.appearance.backgroundColor = theme.bgColor
        self.appearance.mints.activeColor = theme.mintBgColor
        self.appearance.mints.text.activeColor = theme.textColor
        self.appearance.mints.border.width = theme.borderWidth
        self.appearance.mints.border.activeColor = theme.borderColor
        self.appearance.mints.text.cursor.color = theme.cursorColor
        if theme.bgPattern then
            -- Tile the background
            local widthInTiles = (self.dimensions.width.absoluteLength /
                                  (Window.scale * 16)) + 1
            local heightInTiles = (self.dimensions.height.absoluteLength /
                                   (Window.scale * 16)) + 1
            -- TODO: Assuming here that tiles used in background patterns
            -- are 4x4 in tile dimensions. Implement querying & fix.
            for i = 0, widthInTiles, 4 do
                for j = 0, heightInTiles, 4 do
                    self.canvas:Place(theme.bgPattern, i .. " TILES",
                                      j .. " TILES", 1)
                end
            end
        else
            -- Clear the canvas
            while (self.canvas.tiles[1]) do
                self.canvas.tiles[1]:Remove()
            end
        end
    end
end

--
--  Saving & Loading Shell Settings
--

-- Table containing active shell settings, initialized with the defaults
local SHELL_SETTINGS = {
    THEME = SHELL_THEMES[1].NAME,
    FONT = "C64",
    CURSOR = "Block"
}

local function save_shell_settings ()
    local settings = {"return {"}
    for i, v in pairs(SHELL_SETTINGS) do
        table.insert(settings, string.format("%s = \"%s\",", i, v))
    end
    table.insert(settings, "}")
    Storage["shell_settings.lua"] = table.concat(settings, "\n")
end

local function load_shell_settings ()
    if Storage["shell_settings.lua"] then
        SHELL_SETTINGS = loadstring(Storage["shell_settings.lua"])()
    end
end

local function apply_shell_settings ()
    -- Apply the theme setting
    apply_shell_theme(SHELL_SETTINGS.THEME)

    -- Apply the font setting
    self.appearance.mints.text.font = SHELL_SETTINGS.FONT

    -- Apply the cursor setting
    if SHELL_SETTINGS.CURSOR == "Block" then
        self.appearance.mints.text.cursor.type = "BLOCK"
    elseif SHELL_SETTINGS.CURSOR == "Bar" then
        self.appearance.mints.text.cursor.type = "CHARACTER"
    end
end

-- Load the existing settings
load_shell_settings()

--
--  Settings TUI
--

-- Table to store settings TUI state, uses menu instances as keys and tables
--   containing the state as values.
local SETTINGS_STATE = {}

-- Table containing presentation information for the settings
local SETTINGS_SUBMENUS = {
    {
        SUBMENU_NAME = "Change theme",
        SETTING_FIELD = "THEME",
        -- Options
        -- Theme names are added using the SHELL_THEMES table, see below
    },
    {
        SUBMENU_NAME = "Change font",
        SETTING_FIELD = "FONT",
        -- Options
        "C64",
        "ARCADE"
        -- Add font names here as necessary...
    },
    {
        SUBMENU_NAME = "Change cursor",
        SETTING_FIELD = "CURSOR",
        -- Options
        "Block",
        "Bar"
    },
}

-- Add existing themes to the theme submenu
for _, v in ipairs(SHELL_THEMES) do
    table.insert(SETTINGS_SUBMENUS[1], v.NAME)
end

local function settings_compose_text (menuIndex)
    -- Compose the text for the settings TUI according to the given menuIndex.
    -- menuIndex being nil means no submenu is active, being non-nil means
    --   the submenu indicated by SETTINGS_SUBMENUS[menuIndex] is active.

    local settingsText = {
        "Shell settings\n",
    }

    -- Insert the submenus into the text
    for _,v in ipairs(SETTINGS_SUBMENUS) do
        table.insert(settingsText, string.format("- [ ] %s", v.SUBMENU_NAME))
    end

    -- Insert the submenu options into the text
    if menuIndex then
        for i,v in ipairs(SETTINGS_SUBMENUS[menuIndex]) do
            table.insert(settingsText, 1 + menuIndex + i,
                         string.format("  - [ ] %s", v))
        end
    end

    -- Insert the controls section
    table.insert(settingsText, "\nControls:")
    if not menuIndex then
        table.insert(settingsText, "- ENTER: Enter submenu")
    else
        table.insert(settingsText, "- ENTER/ESC: Save setting")
    end
    table.insert(settingsText, "- UP/DOWN: Change selection")
    if not menuIndex then
        table.insert(settingsText, "- ESC: Return to shell")
    end

    return table.concat(settingsText, "\n")
end

local function settings_press ()
    local state = SETTINGS_STATE[self]
    if not state.menuIndex then
	state.menuIndex = state.selectionIndex
    end
end

local function settings_adjust_cursor (menuIndex, selectionIndex)
    local seekStr
    if not menuIndex then
        seekStr = "- [ ]"
    else
        seekStr = "  - [ ]"
    end
    local shellText = self.mints.shell.text.string
    -- Find the "selectionIndex"th occurrance of seekStr in shellText
    local _
    local seekEnd = 1
    for i = 1,selectionIndex do
        _, seekEnd = string.find(shellText, seekStr, seekEnd, true)
    end
    -- Adjust the cursor
    self.mints.shell.text.cursorIndex = seekEnd - 1
end

local function settings_submenu_find_active_setting (menuIndex)
    local subMenu = SETTINGS_SUBMENUS[menuIndex]
    for i, v in ipairs(subMenu) do
        if SHELL_SETTINGS[subMenu.SETTING_FIELD] == v then
            return i
        end
    end
    return 1
end

local function settings_leave_submenu ()
    -- Bound to the ESC key while a submenu is active
    local state = SETTINGS_STATE[self]
    state.menuIndex = nil
end

local function settings_active ()
    local state = SETTINGS_STATE[self]
    if not state then
        state = {}
        -- State variables:
        -- - state.menuIndex :: Currently selected submenu, used to index the
        --                      SETTINGS_SUBMENUS array.
        --                      nil indicates we're at the topmost menu.
        -- - state.prevMenuIndex :: Previous menuIndex, used to track changes
        -- - state.selectionIndex :: Current selection within the menu.
        --                           Within submenus changes to this apply
        --                           the new setting.
        -- - state.prevSelectionIndex :: Previous selectionIndex, used to
        --                               track changes.
        state.prevMenuIndex = 1 -- Initialize prev as non-nil to flush state
        SETTINGS_STATE[self] = state
    end
    local menuChanged = false
    if state.prevMenuIndex ~= state.menuIndex then
        -- The menu has changed, update the text
        self.mints.shell.text = settings_compose_text(state.menuIndex)
        menuChanged = true
        if state.menuIndex then
            -- Entered a submenu, find the appropriate selectionIndex
            state.selectionIndex = settings_submenu_find_active_setting(state.menuIndex)
            -- Bind ESC, ENTER to leave submenu
            self.keymap.ESCAPE = settings_leave_submenu
            self.mints.shell.scripts.press = settings_leave_submenu
        else
            -- Left a submenu
            if state.prevMenuIndex then
                state.selectionIndex = state.prevMenuIndex
            end
            -- Restore the ESC, ENTER keys
            self.keymap.ESCAPE = tui_disable
            self.mints.shell.scripts.press = settings_press
        end
        state.prevMenuIndex = state.menuIndex
    end
    if state.selectionIndex ~= state.prevSelectionIndex or menuChanged then
        -- Adjust cursor location
        settings_adjust_cursor(state.menuIndex, state.selectionIndex)
        -- If in a submenu, apply the change
        if state.menuIndex then
            local subMenu = SETTINGS_SUBMENUS[state.menuIndex]
            SHELL_SETTINGS[subMenu.SETTING_FIELD] = subMenu[state.selectionIndex]
            apply_shell_settings()
        end
        -- Update the selection index
        state.prevSelectionIndex = state.selectionIndex
    end
end

local function settings_select (direction)
    -- Handles up/down keypresses.
    -- Direction can be -1 and +1. -1 is up, +1 is down.
    local state = SETTINGS_STATE[self]
    -- Increment
    state.selectionIndex = state.selectionIndex + direction
    -- Bring back into range if it has gone out of it
    local maxValue
    if not state.menuIndex then
        maxValue = table.maxn(SETTINGS_SUBMENUS)
    else
        maxValue = table.maxn(SETTINGS_SUBMENUS[state.menuIndex])
    end
    if state.selectionIndex < 1 then
	state.selectionIndex = maxValue
    elseif state.selectionIndex > maxValue then
        state.selectionIndex = 1
    end
end

SHELL_BUILTINS.settings = function ()
    tui_enable("",
               -- Scripts
               settings_press, settings_active,
               function () settings_select(-1) end,
               function () settings_select(1) end,
               -- Callbacks
               nil,
               function ()
                   SETTINGS_STATE[self] = nil
                   self.mints.shell.text.cursorIndex = -1
                   save_shell_settings()
               end)
end

--
--  Return
--

-- Return the apply_shell_settings function as SHELL_SETTINGS_APPLY
return apply_shell_settings
