-- configs
local LogColors = { [LogDebug] = 'pink',
                    [LogInfo] = 'white',
                    [LogWarning] = 'yellow',
                    [LogError] = 'red' }
local MaxLogLines = 128
local MaxHistory = 1000

local oldenv = getfenv(0)
setfenv(0, _G)
_G.commandEnv = runinsandbox('commands')
setfenv(0, oldenv)

-- private variables
local terminalWindow
local terminalButton
local logLocked = false
local commandTextEdit
local terminalBuffer
local commandHistory = { }
local currentHistoryIndex = 0
local poped = false
local oldPos
local oldSize
local firstShown = false
local flushEvent
local cachedLines = {}
local disabled = false
local allLines = {}

-- private functions
local function navigateCommand(step)
  if commandTextEdit:isMultiline() then
    return
  end

  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]
      commandTextEdit:setText(command)
      commandTextEdit:setCursorPos(-1)
    else
      commandTextEdit:clearText()
    end
  end
end

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

  local commandBegin = commandTextEdit: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
    commandTextEdit:setText(possibleCommands[1])
    commandTextEdit:setCursorPos(-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
    commandTextEdit:setText(commandBegin)
      commandTextEdit:setCursorPos(-1)

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

local function doCommand(textWidget)
  local currentCommand = textWidget:getText()
  executeCommand(currentCommand)
  textWidget:clearText()
  return true
end

local function addNewline(textWidget)
  if not textWidget:isOn() then
    textWidget:setOn(true)
  end
  textWidget:appendText('\n')
end

local function onCommandChange(textWidget, newText, oldText)
  local _, newLineCount = string.gsub(newText, '\n', '\n')
  textWidget:setHeight((newLineCount + 1) * textWidget.baseHeight)

  if newLineCount == 0 and textWidget:isOn() then
    textWidget:setOn(false)
  end
end

local function onLog(level, message, time)
  if disabled then return end
  -- avoid logging while reporting logs (would cause a infinite loop)
  if logLocked then return end

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

-- public functions
function init()
  terminalWindow = g_ui.displayUI('terminal')
  terminalWindow:setVisible(false)

  terminalWindow.onDoubleClick = popWindow

  terminalButton = modules.client_topmenu.addLeftButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle)
  terminalButton:setOn(false)
  g_keyboard.bindKeyDown('Ctrl+T', toggle)

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

  commandTextEdit = terminalWindow:getChildById('commandTextEdit')
  commandTextEdit:setHeight(commandTextEdit.baseHeight)
  connect(commandTextEdit, {onTextChange = onCommandChange})
  g_keyboard.bindKeyPress('Up', function() navigateCommand(1) end, commandTextEdit)
  g_keyboard.bindKeyPress('Down', function() navigateCommand(-1) end, commandTextEdit)
  g_keyboard.bindKeyPress('Ctrl+C',
    function()
      if commandTextEdit:hasSelection() or not terminalSelectText:hasSelection() then return false end
      g_window.setClipboardText(terminalSelectText:getSelection())
    return true
    end, commandTextEdit)
  g_keyboard.bindKeyDown('Tab', completeCommand, commandTextEdit)
  g_keyboard.bindKeyPress('Shift+Enter', addNewline, commandTextEdit)
  g_keyboard.bindKeyDown('Enter', doCommand, commandTextEdit)
  g_keyboard.bindKeyDown('Escape', hide, terminalWindow)

  terminalBuffer = terminalWindow:getChildById('terminalBuffer')
  terminalSelectText = terminalWindow:getChildById('terminalSelectText')
  terminalSelectText.onDoubleClick = popWindow
  terminalSelectText.onMouseWheel = function(a,b,c) terminalBuffer:onMouseWheel(b,c) end
  terminalBuffer.onScrollChange = function(self, value) terminalSelectText:setTextVirtualOffset(value) end

  g_logger.setOnLog(onLog)

  if not g_app.isRunning() then
    g_logger.fireOldMessages()
  elseif _G.terminalLines then
    for _,line in pairs(_G.terminalLines) do
      addLine(line.text, line.color)
    end
  end
end

function terminate()
  g_settings.setList('terminal-history', commandHistory)

  removeEvent(flushEvent)

  if poped then
    oldPos = terminalWindow:getPosition()
    oldSize = terminalWindow:getSize()
  end
  local settings = {
    size = oldSize,
    pos = oldPos,
    poped = poped
  }
  g_settings.setNode('terminal-window', settings)

  g_keyboard.unbindKeyDown('Ctrl+T')
  g_logger.setOnLog(nil)
  terminalWindow:destroy()
  terminalButton:destroy()
  commandEnv = nil
  _G.terminalLines = allLines
end

function hideButton()
  --terminalButton:hide()
end

function popWindow()
  if poped then
    oldPos = terminalWindow:getPosition()
    oldSize = terminalWindow:getSize()
    terminalWindow:fill('parent')
    terminalWindow:setOn(false)
    terminalWindow:getChildById('bottomResizeBorder'):disable()
    terminalWindow:getChildById('rightResizeBorder'):disable()
    terminalWindow:getChildById('titleBar'):hide()
    terminalWindow:getChildById('terminalScroll'):setMarginTop(0)
    terminalWindow:getChildById('terminalScroll'):setMarginBottom(0)
    terminalWindow:getChildById('terminalScroll'):setMarginRight(0)
    poped = false
  else
    terminalWindow:breakAnchors()
    terminalWindow:setOn(true)
    local size = oldSize or { width = g_window.getWidth()/2.5, height = g_window.getHeight()/4 }
    terminalWindow:setSize(size)
    local pos = oldPos or { x = 0, y = g_window.getHeight() }
    terminalWindow:setPosition(pos)
    terminalWindow:getChildById('bottomResizeBorder'):enable()
    terminalWindow:getChildById('rightResizeBorder'):enable()
    terminalWindow:getChildById('titleBar'):show()
    terminalWindow:getChildById('terminalScroll'):setMarginTop(18)
    terminalWindow:getChildById('terminalScroll'):setMarginBottom(1)
    terminalWindow:getChildById('terminalScroll'):setMarginRight(1)
    terminalWindow:bindRectToParent()
    poped = true
  end
end

function toggle()
  if terminalWindow:isVisible() then
    hide()
  else
    if not firstShown then
      local settings = g_settings.getNode('terminal-window')
      if settings then
        if settings.size then oldSize = settings.size end
        if settings.pos then oldPos = settings.pos end
        if settings.poped then popWindow() end
      end
      firstShown = true
    end
    show()
  end
end

function show()
  terminalWindow:show()
  terminalWindow:raise()
  terminalWindow:focus()
end

function hide()
  terminalWindow:hide()
end

function disable()
  --terminalButton:hide()
  g_keyboard.unbindKeyDown('Ctrl+T')
  disabled = true
end

function flushLines()
  local numLines = terminalBuffer:getChildCount() + #cachedLines
  local fulltext = terminalSelectText:getText()

  for _,line in pairs(cachedLines) do
    -- delete old lines if needed
    if numLines > MaxLogLines then
      local firstChild = terminalBuffer:getChildByIndex(1)
      if firstChild then
        local len = #firstChild:getText()
        firstChild:destroy()
        table.remove(allLines, 1)
        fulltext = string.sub(fulltext, len)
      end
    end

    local label = g_ui.createWidget('TerminalLabel', terminalBuffer)
    label:setId('terminalLabel' .. numLines)
    label:setText(line.text)
    label:setColor(line.color)

    table.insert(allLines, {text=line.text,color=line.color})

    fulltext = fulltext .. '\n' .. line.text
  end

  terminalSelectText:setText(fulltext)

  cachedLines = {}
  removeEvent(flushEvent)
  flushEvent = nil
end

function addLine(text, color)
  if not flushEvent then
    flushEvent = scheduleEvent(flushLines, 10)
  end

  text = string.gsub(text, '\t', '    ')
  table.insert(cachedLines, {text=text, color=color})
end

function terminalPrint(value)
  if type(value) == "table" then
    return print(json.encode(value, 2))
  end
  print(tostring(value))
end

function executeCommand(command)
  if command == nil or #string.gsub(command, '\n', '') == 0 then return end

  -- add command line
  addLine("> " .. command, "#ffffff")
  if g_game.getFeature(GameNoDebug) then
    addLine("Terminal is disabled on this server", "#ff8888")
    return    
  end

  -- reset current history index
  currentHistoryIndex = 0

  -- add new command to history
  if #commandHistory == 0 or commandHistory[#commandHistory] ~= command then
    table.insert(commandHistory, command)
    while #commandHistory > MaxHistory do
      table.remove(commandHistory, 1)
    end
  end

  -- detect and convert commands with simple syntax
  local realCommand
  if string.sub(command, 1, 1) == '=' then
    realCommand = 'modules.client_terminal.terminalPrint(' .. string.sub(command,2) .. ')'
  else
    realCommand = command
  end

  local func, err = loadstring(realCommand, "@")

  -- detect terminal commands
  if not func then
    local command_name = command:match('^([%w_]+)[%s]*.*')
    if command_name then
      local args = string.split(command:match('^[%w_]+[%s]*(.*)'), ' ')
      if commandEnv[command_name] and type(commandEnv[command_name]) == 'function' then
        func = function() modules.client_terminal.commandEnv[command_name](unpack(args)) end
      elseif command_name == command then
        addLine('ERROR: command not found', 'red')
        return
      end
    end
  end

  -- check for syntax errors
  if not func then
    addLine('ERROR: incorrect lua syntax: ' .. err:sub(5), 'red')
    return
  end
  
  commandEnv['player'] = g_game.getLocalPlayer()

  -- 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 addLine(ret, 'white') end
  else
    addLine('ERROR: command failed: ' .. ret, 'red')
  end
end

function clear()
  terminalBuffer:destroyChildren()
  terminalSelectText:setText('')
  cachedLines = {}
  allLines = {}
end
