Console = { }

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

-- private variables
local consoleWidget
local logLocked = false
local commandEnv = createEnvironment()
local commandLineEdit
local consoleBuffer
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 = string.sub(commandLineEdit:getText(), 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 string.sub(k, 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 .. string.sub(possibleCommands[1], cursorPos, cursorPos)
      for i,v in ipairs(possibleCommands) do
        if string.sub(v, 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 onKeyPress(widget, keyCode, keyText, keyboardModifiers)
  if keyboardModifiers == KeyboardNoModifier then
    -- execute current command
    if keyCode == KeyReturn or keyCode == keyEnter then
      local currentCommand = commandLineEdit:getText()
      Console.executeCommand(currentCommand)
      commandLineEdit:clearText()
      return true
    -- navigate history up
    elseif keyCode == KeyUp then
      navigateCommand(1)
      return true
    -- navigate history down
    elseif keyCode == KeyDown then
      navigateCommand(-1)
      return true
    -- complete command
    elseif keyCode == KeyTab then
      completeCommand()
      return true
    end
  end
  return false
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
  Console.addLine(message, LogColors[level])
  logLocked = false
end

-- public functions
function Console.init()
  consoleWidget = UI.display('console.otui', { visible = false })
  connect(consoleWidget, { onKeyPress = onKeyPress })

  commandLineEdit = consoleWidget:getChildById('commandLineEdit')
  consoleBuffer = consoleWidget:getChildById('consoleBuffer')
  Logger.setOnLog(onLog)
  Logger.fireOldMessages()
end

function Console.terminate()
  Logger.setOnLog(nil)
  consoleWidget:destroy()
  commandLineEdit = nil
  consoleWidget = nil
end

function Console.addLine(text, color)
  -- create new line label
  local numLines = consoleBuffer:getChildCount() + 1
  local label = UILabel.create()
  consoleBuffer:addChild(label)
  label:setId('consoleLabel' .. numLines)
  label:setStyle('ConsoleLabel')
  label:setText(text)
  label:setForegroundColor(color)

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

function Console.executeCommand(command)
  -- 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)

  -- add command line
  Console.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