Console = {}

-- private variables
local SpeakTypesSettings = {
  say = { speakType = SpeakSay, color = '#FFFF00' },
  whisper = { speakType = SpeakWhisper, color = '#FFFF00' },
  yell = { speakType = SpeakYell, color = '#FFFF00' },
  broadcast = { speakType = SpeakBroadcast, color = '#F55E5E' },
  private = { speakType = SpeakPrivate, color = '#5FF7F7', private = true },
  privateRed = { speakType = SpeakPrivateRed, color = '#F55E5E', private = true },
  privatePlayerToPlayer = { speakType = SpeakPrivate, color = '#9F9DFD', private = true },
  privatePlayerToNpc = { speakType = SpeakPrivatePlayerToNpc, color = '#9F9DFD', private = true, npcChat = true },
  privateNpcToPlayer = { speakType = SpeakPrivateNpcToPlayer, color = '#5FF7F7', private = true, npcChat = true },
  channelYellow = { speakType = SpeakChannelYellow, color = '#FFFF00' },
  channelWhite = { speakType = SpeakChannelWhite, color = '#FFFFFF' },
  channelRed = { speakType = SpeakChannelRed, color = '#F55E5E' },
  channelOrange = { speakType = SpeakChannelOrange, color = '#FE6500' },
  monsterSay = { speakType = SpeakMonsterSay, color = '#FE6500', hideInConsole = true},
  monsterYell = { speakType = SpeakMonsterYell, color = '#FE6500', hideInConsole = true},
}

local SpeakTypes = {
  [SpeakSay] = SpeakTypesSettings.say,
  [SpeakWhisper] = SpeakTypesSettings.whisper,
  [SpeakYell] = SpeakTypesSettings.yell,
  [SpeakBroadcast] = SpeakTypesSettings.broadcast,
  [SpeakPrivate] = SpeakTypesSettings.private,
  [SpeakPrivateRed] = SpeakTypesSettings.privateRed,
  [SpeakPrivatePlayerToNpc] = SpeakTypesSettings.privatePlayerToNpc,
  [SpeakPrivateNpcToPlayer] = SpeakTypesSettings.privateNpcToPlayer,
  [SpeakChannelYellow] = SpeakTypesSettings.channelYellow,
  [SpeakChannelWhite] = SpeakTypesSettings.channelWhite,
  [SpeakChannelRed] = SpeakTypesSettings.channelRed,
  [SpeakChannelOrange] = SpeakTypesSettings.channelOrange,
  [SpeakMonsterSay] = SpeakTypesSettings.monsterSay,
  [SpeakMonsterYell] = SpeakTypesSettings.monsterYell,
}

local SayModes = {
  [1] = { speakTypeDesc = 'whisper', icon = 'icons/whisper.png' },
  [2] = { speakTypeDesc = 'say', icon = 'icons/say.png' },
  [3] = { speakTypeDesc = 'yell', icon = 'icons/yell.png' }
}

local MAX_HISTORY = 1000
local MAX_LINES = 100
local HELP_CHANNEL = 9

local consolePanel
local consoleContentPanel
local consoleTabBar
local consoleTextEdit
local channels
local channelsWindow
local ownPrivateName
local messageHistory = {}
local currentMessageIndex = 0
local ignoreNpcMessages = false

-- private functions
local function navigateMessageHistory(step)
  local numCommands = #messageHistory
  if numCommands > 0 then
    currentMessageIndex = math.min(math.max(currentMessageIndex + step, 0), numCommands)
    if currentMessageIndex > 0 then
      local command = messageHistory[numCommands - currentMessageIndex + 1]
      consoleTextEdit:setText(command)
    else
      consoleTextEdit:clearText()
    end
  end
end

function applyMessagePrefixies(name, level, message)
  if name then
    if Options.getOption('showLevelsInConsole') and level > 0 then
      message = name .. ' [' .. level .. ']: ' .. message
    else
      message = name .. ': ' .. message
    end
  end
  return message
end

-- hooked events
local function onCreatureSpeak(name, level, speaktype, message, channelId, creaturePos)
  if ignoreNpcMessages and speaktype == SpeakPrivateNpcToPlayer then return end
  local defaultMessage = speaktype < 3 and true or false
  speaktype = SpeakTypes[speaktype]
  if speaktype.hideInConsole then return end

  local composedMessage = applyMessagePrefixies(name, level, message)

  if speaktype.private then
    Console.addPrivateText(composedMessage, speaktype, name, false, name)
    if Options.getOption('showPrivateMessagesOnScreen') then
	  if(speaktype.speakType ~= SpeakPrivateNpcToPlayer) then
        TextMessage.displayPrivate(name .. ':\n' .. message)
	  end
    end
  else
    local channel = tr('Default')
    if not defaultMessage then
      channel = channels[channelId]
    end

    if channel then
      Console.addText(composedMessage, speaktype, channel, name)
    elseif channelId ~= 0 then
      -- server sent a message on a channel that is not open
      pwarning('message in channel id ' .. channelId .. ' which is unknown, this is a server bug, relogin if you want to see messages in this channel')
    end
  end
end

local function onOpenChannel(channelId, channelName)
  Console.addChannel(channelName, channelId)
end

local function onOpenPrivateChannel(receiver)
  Console.addPrivateChannel(receiver)
end

local function onOpenOwnPrivateChannel(channelId, channelName)
  local privateTab = Console.getTab(channelName)
  if privateTab == nil then
    Console.addChannel(channelName, channelId)
  end
  ownPrivateName = channelName
end

local function onCloseChannel(channelId)
  local channel = channels[channelId]
  if channel then
    local tab = Console.getTab(channel)
    if tab then
      consoleTabBar:removeTab(tab)
    end
    for k, v in pairs(channels) do
      if (k == tab.channelId) then channels[k] = nil end
    end
  end
end

local function doChannelListSubmit()
  local channelListPanel = channelsWindow:getChildById('channelList')
  local openPrivateChannelWith = channelsWindow:getChildById('openPrivateChannelWith'):getText()
  if openPrivateChannelWith ~= '' then
    g_game.openPrivateChannel(openPrivateChannelWith)
  else
    local selectedChannelLabel = channelListPanel:getFocusedChild()
    if not selectedChannelLabel then return end
    if selectedChannelLabel.channelId == 0xFFFF then
      g_game.openOwnChannel()
    else
      g_game.joinChannel(selectedChannelLabel.channelId)
    end
  end

  channelsWindow:destroy()
end

local function onChannelList(channelList)
  if channelsWindow then channelsWindow:destroy() end
  channelsWindow = g_ui.displayUI('channelswindow.otui')
  local channelListPanel = channelsWindow:getChildById('channelList')
  channelsWindow.onEnter = doChannelListSubmit
  channelsWindow.onDestroy = function() channelsWindow = nil end
  g_keyboard.bindKeyPress('Down', function() channelListPanel:focusNextChild(KeyboardFocusReason) end, channelsWindow)
  g_keyboard.bindKeyPress('Up', function() channelListPanel:focusPreviousChild(KeyboardFocusReason) end, channelsWindow)

  for k,v in pairs(channelList) do
    local channelId = v[1]
    local channelName = v[2]

    if #channelName > 0 then
      local label = g_ui.createWidget('ChannelListLabel', channelListPanel)
      label.channelId = channelId
      label:setText(channelName)

      label:setPhantom(false)
      label.onDoubleClick = doChannelListSubmit
    end
  end
end

local function onGameStart()
  -- open last channels
  local player = g_game.getLocalPlayer()
  if(player) then
    local char = player:getName()

    local lastChannelsOpen = g_settings.getNode('LastChannelsOpen')

    if(not table.empty(lastChannelsOpen) and lastChannelsOpen[char]) then
      for channelName, channelId in ipairs(lastChannelsOpen[char]) do
        channelId = tonumber(channelId)
        if channelId ~= 0 then
          if not table.find(channels, channelId) then
            g_game.joinChannel(channelId)
          end
        end
      end
    end
  end

  local tab = Console.getTab(tr('Default'))
  if tab then
    --[[
      Known Issue: The server is calling to open channels after
      onGameStart is executed causing it to focus the last tab opened.

      Fix: Don't save channels to the settings that are opened by the server.
    ]]
    addEvent(function() consoleTabBar:selectTab(tab) end, true)
  end
end

function Console.onTabChange(tabBar, tab)
  if tab:getText() == tr('Default') or tab:getText() == tr('Server Log') then
    consolePanel:getChildById('closeChannelButton'):disable()
  else
    consolePanel:getChildById('closeChannelButton'):enable()
  end
end

function Console.clear()
  local lastChannelsOpen = {}

  local player = g_game.getLocalPlayer()
  if(player) then
    local char = player:getName()
    lastChannelsOpen[char] = {}

    for channelId, channelName in pairs(channels) do
      table.insert(lastChannelsOpen[char], channelId)
    end
  end

  -- save last open channels
  g_settings.setNode('LastChannelsOpen', lastChannelsOpen)

  for _, channelName in pairs(channels) do
    local tab = consoleTabBar:getTab(channelName)
    consoleTabBar:removeTab(tab)
  end

  channels = {}

  consoleTabBar:getTab(tr('Default')).tabPanel:getChildById('consoleBuffer'):destroyChildren()
  consoleTabBar:getTab(tr('Server Log')).tabPanel:getChildById('consoleBuffer'):destroyChildren()

  local npcTab = consoleTabBar:getTab('NPCs')
  if npcTab then
    consoleTabBar:removeTab(npcTab)
  end

  consoleTextEdit:clearText()

  if channelsWindow then
    channelsWindow:destroy()
    channelsWindow = nil
  end
end

-- public functions
function Console.init()
  connect(g_game, { onCreatureSpeak = onCreatureSpeak,
                    onChannelList = onChannelList,
                    onOpenChannel = onOpenChannel,
                    onOpenPrivateChannel = onOpenPrivateChannel,
                    onOpenOwnPrivateChannel = onOpenOwnPrivateChannel,
                    onCloseChannel = onCloseChannel,
                    onGameStart = onGameStart,
                    onGameEnd = Console.clear })

  consolePanel = g_ui.loadUI('console.otui', GameInterface.getBottomPanel())
  consoleTextEdit = consolePanel:getChildById('consoleTextEdit')
  consoleContentPanel = consolePanel:getChildById('consoleContentPanel')
  consoleTabBar = consolePanel:getChildById('consoleTabBar')
  consoleTabBar:setContentWidget(consoleContentPanel)
  channels = {}

  Console.addTab(tr('Default'), true)
  Console.addTab(tr('Server Log'), false)

  g_keyboard.bindKeyPress('Shift+Up', function() navigateMessageHistory(1) end, consolePanel)
  g_keyboard.bindKeyPress('Shift+Down', function() navigateMessageHistory(-1) end, consolePanel)
  g_keyboard.bindKeyPress('Tab', function() consoleTabBar:selectNextTab() end, consolePanel)
  g_keyboard.bindKeyPress('Shift+Tab', function() consoleTabBar:selectPrevTab() end, consolePanel)
  g_keyboard.bindKeyDown('Enter', Console.sendCurrentMessage, consolePanel)
  g_keyboard.bindKeyPress('Ctrl+A', function() consoleTextEdit:clearText() end, consolePanel)

  -- apply buttom functions after loaded
  consolePanel:getChildById('nextChannelButton').onClick = function() consoleTabBar:selectNextTab() end
  consolePanel:getChildById('prevChannelButton').onClick = function() consoleTabBar:selectPrevTab() end
  consoleTabBar.onTabChange = Console.onTabChange

  -- tibia like hotkeys
  g_keyboard.bindKeyDown('Ctrl+O', g_game.requestChannels)
  g_keyboard.bindKeyDown('Ctrl+E', Console.removeCurrentTab)
  g_keyboard.bindKeyDown('Ctrl+H', Console.openHelp)
end

function Console.terminate()
  disconnect(g_game, { onCreatureSpeak = onCreatureSpeak,
                       onChannelList = onChannelList,
                       onOpenChannel = onOpenChannel,
                       onOpenPrivateChannel = onOpenPrivateChannel,
                       onOpenOwnPrivateChannel = onOpenPrivateChannel,
                       onCloseChannel = onCloseChannel,
                       onGameStart = onGameStart,
                       onGameEnd = Console.clear })

  for channelid, channelname in pairs(channels) do
    if tonumber(channelid) and tonumber(channelid) ~= 0 then
      g_game.leaveChannel(channelid)
    end
  end
  channels = {}

  g_keyboard.unbindKeyDown('Ctrl+O')
  g_keyboard.unbindKeyDown('Ctrl+E')
  g_keyboard.unbindKeyDown('Ctrl+H')

  if channelsWindow then
    channelsWindow:destroy()
    channelsWindow = nil
  end

  consolePanel:destroy()
  consolePanel = nil
  consoleTextEdit = nil
  consoleContentPanel = nil
  consoleTabBar = nil

  ownPrivateName = nil

  Console = nil
end

function Console.setTextEditText(text)
  consoleTextEdit:setText(text)
end

function Console.openHelp()
  g_game.joinChannel(HELP_CHANNEL)
end

function Console.addTab(name, focus)
  local tab = Console.getTab(name)
  if(tab) then -- is channel already open
    if(not focus) then focus = true end
  else
    tab = consoleTabBar:addTab(name)
  end
  if focus then
    consoleTabBar:selectTab(tab)
  elseif name ~= tr('Server Log') then
    consoleTabBar:blinkTab(tab)
  end
  return tab
end

function Console.removeCurrentTab()
  local tab = consoleTabBar:getCurrentTab()
  if tab:getText() == tr('Default') or tab:getText() == tr('Server Log') then return end

  -- notificate the server that we are leaving the channel
  if tab.channelId then
    for k, v in pairs(channels) do
      if (k == tab.channelId) then channels[k] = nil end
    end
    g_game.leaveChannel(tab.channelId)
  elseif tab:getText() == "NPCs" then
    g_game.closeNpcChannel()
  end

  consoleTabBar:removeTab(tab)
end

function Console.getTab(name)
  return consoleTabBar:getTab(name)
end

function Console.getCurrentTab()
  return consoleTabBar:getCurrentTab()
end

function Console.addChannel(name, id)
  channels[id] = name
  local tab = Console.addTab(name, true)
  tab.channelId = id
  return tab
end

function Console.addPrivateChannel(receiver)
  channels[receiver] = receiver
  return Console.addTab(receiver, true)
end

function Console.addPrivateText(text, speaktype, name, isPrivateCommand, creatureName)
  local focus = false
  if speaktype.speakType == SpeakPrivateNpcToPlayer then
    name = 'NPCs'
    focus = true
  end

  local privateTab = Console.getTab(name)
  if privateTab == nil then
    if (Options.getOption('showPrivateMessagesInConsole') and not focus) or (isPrivateCommand and not privateTab) then
      privateTab = Console.getTab(tr('Default'))
    else
      privateTab = Console.addTab(name, focus)
      channels[name] = name
    end
    privateTab.npcChat = speaktype.npcChat
  elseif focus then
    consoleTabBar:selectTab(privateTab)
  end
  Console.addTabText(text, speaktype, privateTab, creatureName)
end

function Console.addText(text, speaktype, tabName, creatureName)
  local tab = Console.getTab(tabName)
  if tab ~= nil then
    Console.addTabText(text, speaktype, tab, creatureName)
  end
end

function Console.addTabText(text, speaktype, tab, creatureName)
  if Options.getOption('showTimestampsInConsole') then
    text = os.date('%H:%M') .. ' ' .. text
  end

  local panel = consoleTabBar:getTabPanel(tab)
  local consoleBuffer = panel:getChildById('consoleBuffer')
  local label = g_ui.createWidget('ConsoleLabel', consoleBuffer)
  label:setId('consoleLabel' .. panel:getChildCount())
  label:setText(text)
  label:setColor(speaktype.color)
  consoleTabBar:blinkTab(tab)

  label.onMouseRelease = function (self, mousePos, mouseButton) Console.popupMenu(mousePos, mouseButton, creatureName, text) end

  if consoleBuffer:getChildCount() > MAX_LINES then
    consoleBuffer:getFirstChild():destroy()
  end
end

function Console.popupMenu(mousePos, mouseButton, creatureName, text)
  if mouseButton == MouseRightButton then
    local menu = g_ui.createWidget('PopupMenu')
    if creatureName then
      if creatureName ~= g_game.getCharacterName() then
        menu:addOption(tr('Message to ' .. creatureName), function () g_game.openPrivateChannel(creatureName) end)
        if (not Player:hasVip(creatureName)) then
          menu:addOption(tr('Add to VIP list'), function () g_game.addVip(creatureName) end)
        end
        -- TODO ignore creatureName
        menu:addSeparator()
      end
      --TODO select all
      menu:addOption(tr('Copy message'), function () g_window.setClipboardText(text) end)

      if RuleViolation.hasWindowAccess() then
        menu:addSeparator()
        menu:addOption(tr('Rule Violation'), function() RuleViolation.show(creatureName, text:match('.+%:%s(.+)')) end)
      end

      menu:addSeparator()
      menu:addOption(tr('Copy name'), function () g_window.setClipboardText(creatureName) end)
    else
      --TODO select all
      menu:addOption(tr('Copy message'), function () g_window.setClipboardText(text) end)
    end
    menu:display(mousePos)
  end
end

function Console.sendCurrentMessage()
  local message = consoleTextEdit:getText()
  if #message == 0 then return end
  consoleTextEdit:clearText()

  -- get current channel
  local tab = Console.getCurrentTab()

  -- handling chat commands
  local originalMessage = message
  local chatCommandSayMode
  local chatCommandPrivate
  local chatCommandPrivateReady

  local chatCommandMessage = message:match("^%#y (.*)")
  if chatCommandMessage ~= nil then chatCommandSayMode = 'yell' end -- player used yell command
  message = chatCommandMessage or message

  local chatCommandMessage = message:match("^%#w (.*)")
  if chatCommandMessage ~= nil then chatCommandSayMode = 'whisper' end -- player used whisper
  message = chatCommandMessage or message

  local findIni, findEnd, chatCommandInitial, chatCommandPrivate, chatCommandEnd, chatCommandMessage = message:find("([%*%@])(.+)([%*%@])(.*)")
  if findIni ~= nil and findIni == 1 then -- player used private chat command
    if chatCommandInitial == chatCommandEnd then
      chatCommandPrivateRepeat = false
      if chatCommandInitial == "*" then
        consoleTextEdit:setText('*'.. chatCommandPrivate .. '* ')
      end
      message = chatCommandMessage:trim()
      chatCommandPrivateReady = true
    end
  end

  message = message:gsub("^(%s*)(.*)","%2") -- remove space characters from message init
  if #message == 0 then return end

  -- add new command to history
  currentMessageIndex = 0
  if #messageHistory == 0 or messageHistory[#messageHistory] ~= originalMessage then
    table.insert(messageHistory, originalMessage)
    if #messageHistory > MAX_HISTORY then
      table.remove(messageHistory, 1)
    end
  end

  -- when talking on server log, the message goes to default channel
  local name = tab:getText()
  if name == tr('Server Log') then
    tab = Console.getTab(tr('Default'))
    name = tr('Default')
  end

  local speaktypedesc
  if (tab.channelId or name == tr('Default')) and not chatCommandPrivateReady then
    if name == tr('Default') then
      speaktypedesc = chatCommandSayMode or SayModes[consolePanel:getChildById('sayModeButton').sayMode].speakTypeDesc
      if speaktypedesc ~= 'say' then Console.sayModeChange(2) end -- head back to say mode
    else
      speaktypedesc = 'channelYellow'
    end

    g_game.talkChannel(SpeakTypesSettings[speaktypedesc].speakType, tab.channelId, message)
    return
  else
    local isPrivateCommand = false
    if chatCommandPrivateReady then
      speaktypedesc = 'privatePlayerToPlayer'
      name = chatCommandPrivate
      isPrivateCommand = true
    elseif tab.npcChat then
      speaktypedesc = 'privatePlayerToNpc'
    else
      speaktypedesc = 'privatePlayerToPlayer'
    end


    local speaktype = SpeakTypesSettings[speaktypedesc]
    local player = g_game.getLocalPlayer()
    g_game.talkPrivate(speaktype.speakType, name, message)

    message = applyMessagePrefixies(player:getName(), player:getLevel(), message)
    Console.addPrivateText(message, speaktype, name, isPrivateCommand, g_game.getCharacterName())
  end
end

function Console.sayModeChange(sayMode)
  local buttom = consolePanel:getChildById('sayModeButton')
  if sayMode == nil then
    sayMode = buttom.sayMode + 1
  end

  if sayMode > #SayModes then sayMode = 1 end

  buttom:setIcon(SayModes[sayMode].icon)
  buttom.sayMode = sayMode
end

function Console.getOwnPrivateTab()
  if not ownPrivateName then return end
  return Console.getTab(ownPrivateName)
end

function Console.ignoreNpcMessages(ignore)
  ignoreNpcMessages = ignore
end