Terminal = { }

-- configs
local LogColors = { [LogInfo] = 'white',
                    [LogWarning] = 'yellow',
                    [LogError] = 'red' }
local MaxLogLines = 80
local LabelHeight = 16
local MaxHistory = 1000

-- private variables
local terminalWidget
local terminalButton
local logLocked = false
local commandEnv = newenv()
local commandLineEdit
local terminalBuffer
local commandHistory = { }
local currentHistoryIndex = 0

-- private functions
local function navigateCommand(step)
  local numCommands = #commandHistory
  if numCommands > 0 then
    currentHistoryIndex = math.min(math.max(currentHistoryIndex + step, 0), numCommands)
    if currentHistoryIndex > 0 then
      local command = commandHistory[numCommands - currentHistoryIndex + 1]
      commandLineEdit:setText(command)
    else
      commandLineEdit:clearText()
    end
  end
end

local function completeCommand()
  local cursorPos = commandLineEdit:getCursorPos()
  if cursorPos == 0 then return end

  local commandBegin = commandLineEdit:getText():sub(1, cursorPos)
  local possibleCommands = {}

  -- create a list containing all globals
  local allVars = table.copy(_G)
  table.merge(allVars, commandEnv)

  -- match commands
  for k,v in pairs(allVars) do
    if k:sub(1, cursorPos) == commandBegin then
      table.insert(possibleCommands, k)
    end
  end

  -- complete command with one match
  if #possibleCommands == 1 then
    commandLineEdit:setText(possibleCommands[1])
  -- show command matches
  elseif #possibleCommands > 0 then
    print('>> ' .. commandBegin)

    -- expand command
    local expandedComplete = commandBegin
    local done = false
    while not done do
      cursorPos = #commandBegin+1
      if #possibleCommands[1] < cursorPos then
        break
      end
      expandedComplete = commandBegin .. possibleCommands[1]:sub(cursorPos, cursorPos)
      for i,v in ipairs(possibleCommands) do
        if v:sub(1, #expandedComplete) ~= expandedComplete then
          done = true
        end
      end
      if not done then
        commandBegin = expandedComplete
      end
    end
    commandLineEdit:setText(commandBegin)

    for i,v in ipairs(possibleCommands) do
      print(v)
    end
  end
end

local function doCommand()
  local currentCommand = commandLineEdit:getText()
  Terminal.executeCommand(currentCommand)

  if commandLineEdit then
    commandLineEdit:clearText()
  end
  return true
end

local function onLog(level, message, time)
  -- debug messages are ignored
  if level == LogDebug then return end

  -- avoid logging while reporting logs (would cause a infinite loop)
  if logLocked then return end

  logLocked = true
  Terminal.addLine(message, LogColors[level])
  logLocked = false
end

-- public functions
function Terminal.init()
  terminalWidget = displayUI('terminal.otui')
  terminalWidget:setVisible(false)

  terminalButton = TopMenu.addButton('terminalButton', 'Terminal (Ctrl + T)', 'terminal.png', Terminal.toggle)
  Keyboard.bindKeyDown('Ctrl+T', Terminal.toggle)

  commandHistory = Settings.getList('terminal-history')

  commandLineEdit = terminalWidget:getChildById('commandLineEdit')
  Keyboard.bindKeyDown('Up', function() navigateCommand(1) end, commandLineEdit)
  Keyboard.bindKeyDown('Down', function() navigateCommand(-1) end, commandLineEdit)
  Keyboard.bindKeyDown('Tab', completeCommand, commandLineEdit)
  Keyboard.bindKeyDown('Enter', doCommand, commandLineEdit)

  terminalBuffer = terminalWidget:getChildById('terminalBuffer')
  Logger.setOnLog(onLog)
  Logger.fireOldMessages()
end

function Terminal.terminate()
  Settings.setList('terminal-history', commandHistory)
  Keyboard.unbindKeyDown('Ctrl+T')
  Logger.setOnLog(nil)
  terminalButton:destroy()
  terminalButton = nil
  commandLineEdit = nil
  terminalBuffer = nil
  terminalWidget:destroy()
  terminalWidget = nil
  commandEnv = nil
  Terminal = nil
end

function Terminal.toggle()
  if terminalWidget:isVisible() then
    Terminal.hide()
  else
    Terminal.show()
  end
end

function Terminal.show()
  terminalWidget:show()
  terminalWidget:lock()
end

function Terminal.hide()
  terminalWidget:unlock()
  terminalWidget:hide()
end

function Terminal.addLine(text, color)
  -- create new line label
  local numLines = terminalBuffer:getChildCount() + 1
  local label = createWidget('TerminalLabel', terminalBuffer)
  label:setId('terminalLabel' .. numLines)
  label:setText(text)
  label:setColor(color)

  -- delete old lines if needed
  if numLines > MaxLogLines then
    terminalBuffer:getChildByIndex(1):destroy()
  else
    terminalBuffer:setHeight(terminalBuffer:getHeight() + LabelHeight)
  end
end

function Terminal.executeCommand(command)
  if command == nil or #command == 0 then return end

  logLocked = true
  Logger.log(LogInfo, '>> ' .. command)
  logLocked = false

  -- detect and convert commands with simple syntax
  local realCommand
  if commandEnv[command] then
    if type(commandEnv[command]) == "function" then
      realCommand = command .. '()'
    else
      realCommand = 'print(' .. command .. ')'
    end
  else
    realCommand = command
  end

  -- reset current history index
  currentHistoryIndex = 0

  -- add new command to history
  table.insert(commandHistory, command)
  if #commandHistory > MaxHistory then
    table.remove(commandHistory, 1)
  end

  -- add command line
  Terminal.addLine(">> " .. command, "#ffffff")

  -- load command buffer
  local func, err = loadstring(realCommand, "@")

  -- check for syntax errors
  if not func then
    Logger.log(LogError, 'incorrect lua syntax: ' .. err:sub(5))
    return
  end

  -- setup func env to commandEnv
  setfenv(func, commandEnv)

  -- execute the command
  local ok, ret = pcall(func)
  if ok then
    -- if the command returned a value, print it
    if ret then print(ret) end
  else
    Logger.log(LogError, 'command failed: ' .. ret)
  end
end