--[[

    shell.lua
    ---------
    Defines the Lua shell.

--]]

--
--  File-Level Variables
--

-- Prompt string
local SHELL_PROMPT = "$"
-- Output of the command currently being evaluated
local SHELL_OUTPUT_STR = ""
-- Message shown when the shell is launched
local SHELL_INIT_MSG =
    "Welcome to the Lua shell.\n" ..
    "Type `help` for info.\n\n" ..
    "READY.\n"
-- Shell builtins, to be loaded when necessary
local SHELL_BUILTINS = nil
-- Function to apply shell settings, loaded alongside builtins
local SHELL_SETTINGS_APPLY = nil
-- Empty function, used to disable default controls as necessary
local SHELL_NULL_FUNC = function () end

--
--  Essential Functions
--

local function shell_print(...)
    -- Replaces print during evaluation to redirect print calls to
    --   SHELL_OUTPUT_STR instead of stdout.
    local args = { ... }
    local n = select("#", ...)
    for i = 1, n do
        args[i] = tostring(args[i])
    end
    SHELL_OUTPUT_STR = SHELL_OUTPUT_STR .. table.concat(args, " ") .. "\n"
end

local function shell_eval()
    -- Main evaluation function

    -- Get the shell mint
    local shell = self.mints[self.selectedMint]

    -- If the cursor is outside of the editable range, re-focus the cursor on
    --   ENTER press instead of evaluating the latest command.
    if shell.text.cursorIndex < self.commandPosition then
	shell.text.cursorIndex = -1
        return
    end

    -- Replace print with shell_print
    local origPrint
    origPrint, _G["print"] = _G["print"], shell_print

    -- Get the last command
    local lastCommandStr = string.sub(shell.text.string, self.commandPosition)
    -- Store the last command in history
    self.commandHistory[self.commandHistoryLength] = lastCommandStr
    self.commandHistoryLength = self.commandHistoryLength + 1
    self.commandHistoryCurrent = self.commandHistoryLength

    -- If the last command corresponds to a builtin, execute it
    if SHELL_BUILTINS[lastCommandStr] then
	SHELL_BUILTINS[lastCommandStr]()
    else
        -- Not a builtin, load and execute as Lua code
        local func, errStr = loadstring(lastCommandStr)
        if func then
            local success, execErrStr = pcall(func)
            if not success then
                print(execErrStr)
            end
        else
            print(errStr)
        end
    end

    -- Place cursor to the end of the text
    shell.text.cursorIndex = -1

    -- Insert the output of the last command into the shell
    if shell.text.string ~= "" then
        shell.text:Insert("\n" .. SHELL_OUTPUT_STR .. SHELL_PROMPT)
    else
        shell.text:Insert(SHELL_OUTPUT_STR .. SHELL_PROMPT)
    end

    -- Update the command position
    self.commandPosition = shell.text.cursorIndex
    -- Restore the original print function
    _G["print"] = origPrint
    -- Flush the output string
    SHELL_OUTPUT_STR = ""

    -- If a TUI entry point function has been installed, call it
    -- This is used by the more complex builtins
    if self.tuiEntryPoint then
	self.tuiEntryPoint()
    end
end

--
--  History Navigation
--

local function history_navigate (direction)
    -- Direction -1 corresponds to past, +1 to future
    if self.commandHistoryLength == self.commandHistoryCurrent then
        -- Capture the current command in the history so that we can
        --   come back to it.
        local currentCommand = string.sub(self.mints.shell.text.string,
                                          self.commandPosition)
        self.commandHistory[self.commandHistoryLength] = currentCommand
    end
    local commandHistoryNext = self.commandHistoryCurrent + direction
    if self.commandHistory[commandHistoryNext] then
	-- A command exists in this direction, replace it with the
        --   current command.
        self.mints.shell.text.cursorIndex = self.commandPosition
        self.mints.shell.text:Delete(9999999) -- TODO: Replace
        self.mints.shell.text:Insert(self.commandHistory[commandHistoryNext])
        self.commandHistoryCurrent = commandHistoryNext
    end
end

--
--  Shell Menu Definition
--

CreateElement(
    "SHELL_MENU",
    "MENU OBJECT",
    {
        "IS_FULLSCREEN",
        APPEARANCE = {
            BACKGROUND_COLOR = "(134,122,222)",
            MINTS = {
                ACTIVE_COLOR = "(72,58,170)",
                TEXT = {
                    ALIGNMENT = "LEFT",
                    ACTIVE_COLOR = "(134,122,222)",
                    FONT = "C64",
                    CURSOR = {
                        TYPE = "BLOCK"
                    }
                }
            }
        },
        MINTS = {
            shell = {
                POSITION = "(1 TILE, 1 TILE)",
                DIMENSIONS = "(360 SLICES - 2 TILES x 360 SLICES - 2 TILES)",
                TEXT = SHELL_INIT_MSG .. SHELL_PROMPT,
                TYPE = "INPUT",
                PRESS_SCRIPT = shell_eval
            }
        },
        INIT_SCRIPT = function ()
            -- Define the commandPosition field - is an integer index
            --   referring to the beginning of the latest command.
            self.commandPosition = string.len(self.mints.shell.text.string) + 1
            -- Define the commandHistory field - is an array containing
            --   previous commands as strings.
            self.commandHistory = {}
            -- Helper variables for history functionality
            self.commandHistoryLength = 0
            self.commandHistoryCurrent = 0
            -- Load builtins if they have not been loaded
            if not SHELL_BUILTINS then
                SHELL_BUILTINS = {}
                local loadBuiltins, errMsg = loadfile(AbsPath("menus/creative/shell/builtins.lua"))
                if loadBuiltins then
                    SHELL_SETTINGS_APPLY = loadBuiltins(SHELL_BUILTINS)
                else
                    print("Failed to load builtins: %s", errMsg)
                end
            end
            -- Apply shell settings to the new shell
            if SHELL_SETTINGS_APPLY then
                SHELL_SETTINGS_APPLY()
            end
            -- Track shell dimensions so that we can re-apply settings on change
            -- Required for stuff like tiled backgrounds
            self.prevWidth = self.dimensions.width.absoluteLength
            self.prevHeight = self.dimensions.height.absoluteLength
        end,
        ACTIVE_SCRIPT = function ()
            local cursorPos = self.mints.shell.text.cursorIndex
            if cursorPos < self.commandPosition then
                self.mints.shell.text.readOnly = true
            elseif cursorPos == self.commandPosition then
                self.mints.shell.text.readOnly = false
                self.keymap["BACKSPACE"] = SHELL_NULL_FUNC
            else
                self.mints.shell.text.readOnly = false
                self.keymap["BACKSPACE"] = nil
            end
            -- If dimensions have changed re-apply the settings
            local currentWidth = self.dimensions.width.absoluteLength
            local currentHeight = self.dimensions.height.absoluteLength
            if currentWidth ~= self.prevWidth or
               currentHeight ~= self.prevHeight then
                SHELL_SETTINGS_APPLY()
                self.prevWidth, self.prevHeight = currentWidth, currentHeight
            end
        end,
        KEYMAP = {
            UP = function () history_navigate(-1) end,
            DOWN = function () history_navigate(1) end
        }
    }
)
