local sandboxEnv = { }
for k,v in pairs(_ENV) do
  sandboxEnv[k] = v
end

_G.requireInjector()

local Config = require('config')
local Input  = require('input')
local Util   = require('util')

local colors     = _G.colors
local fs         = _G.fs
local keys       = _G.keys
local multishell = _ENV.multishell
local os         = _G.os
local printError = _G.printError
local shell      = _ENV.shell
local term       = _G.term
local window     = _G.window

local parentTerm = term.current()
local w,h = parentTerm.getSize()
local tabs = { }
local currentTab
local _tabId = 0
local overviewId
local runningTab
local tabsDirty = false
local closeInd = '*'
local hooks = { }
local hotkeys = { }
local downState = { }

multishell.term = term.current()

-- Default label
if not os.getComputerLabel() then
  local id = os.getComputerID()
  if _G.turtle then
    os.setComputerLabel('turtle_' .. id)
  elseif _G.pocket then
    os.setComputerLabel('pocket_' .. id)
  elseif _G.commands then
    os.setComputerLabel('command_' .. id)
  else
    os.setComputerLabel('computer_' .. id)
  end
end

if Util.getVersion() >= 1.76 then
  closeInd = '\215'
end

local config = {
  standard = {
    textColor  = colors.lightGray,
    tabBarTextColor = colors.lightGray,
    focusTextColor = colors.white,
    backgroundColor = colors.gray,
    tabBarBackgroundColor = colors.gray,
    focusBackgroundColor = colors.gray,
  },
  color = {
    textColor  = colors.lightGray,
    tabBarTextColor = colors.lightGray,
    focusTextColor = colors.white,
    backgroundColor = colors.gray,
    tabBarBackgroundColor = colors.gray,
    focusBackgroundColor = colors.gray,
  },
}
Config.load('multishell', config)

local _colors = config.standard
if parentTerm.isColor() then
  _colors = config.color
end

local function redrawMenu()
  if not tabsDirty then
    os.queueEvent('multishell_redraw')
    tabsDirty = true
  end
end

local function resumeTab(tab, event, eventData)
  if not tab or coroutine.status(tab.co) == 'dead' then
    return
  end

  if not tab.filter or tab.filter == event or event == "terminate" then
    eventData = eventData or { }
    term.redirect(tab.terminal)
    local previousTab = runningTab
    runningTab = tab
    local ok, result = coroutine.resume(tab.co, event, unpack(eventData))
    tab.terminal = term.current()
    if ok then
      tab.filter = result
    else
      printError(result)
    end

    runningTab = previousTab

    return ok, result
  end
end

local function selectTab(tab)
  if not tab then
    for _,ftab in pairs(tabs) do
      if not ftab.hidden then
        tab = ftab
        break
      end
    end
  end

  if not tab then
    tab = tabs[overviewId]
  end

  if currentTab and currentTab ~= tab then
    currentTab.window.setVisible(false)
    if coroutine.status(currentTab.co) == 'suspended' then
      -- the process that opens a new tab won't get the lose focus event
      -- os.queueEvent('multishell_notifyfocus', currentTab.tabId)
      --resumeTab(currentTab, 'multishell_losefocus')
    end
    if tab and not currentTab.hidden then
      tab.previousTabId = currentTab.tabId
    end
  end

  if tab ~= currentTab then
    currentTab = tab
    tab.window.setVisible(true)
    resumeTab(tab, 'multishell_focus')
  end
end

local function nextTabId()
  _tabId = _tabId + 1
  return _tabId
end

local function launchProcess(tab)
  tab.tabId = nextTabId()
  tab.timestamp = os.clock()
  tab.window = window.create(parentTerm, 1, 2, w, h - 1, false)
  tab.terminal = tab.window
  tab.env = Util.shallowCopy(tab.env or sandboxEnv)

  tab.co = coroutine.create(function()

    local result, err

    if tab.fn then
      result, err = Util.runFunction(tab.env, tab.fn, table.unpack(tab.args or { } ))
    elseif tab.path then
      result, err = Util.run(tab.env, tab.path, table.unpack(tab.args or { } ))
    else
      err = 'multishell: invalid tab'
    end

    if not result and err and err ~= 'Terminated' then
      if err then
        printError(tostring(err))
      end
      printError('Press enter to close')
      tab.isDead = true
      while true do
        local e, code = os.pullEventRaw('key')
        if e == 'terminate' or e == 'key' and code == keys.enter then
          break
        end
      end
    end
    tabs[tab.tabId] = nil
    if tab == currentTab then
      local previousTab
      if tab.previousTabId then
        previousTab = tabs[tab.previousTabId]
        if previousTab and previousTab.hidden then
          previousTab = nil
        end
      end
      selectTab(previousTab)
    end
    redrawMenu()
  end)

  tabs[tab.tabId] = tab
  resumeTab(tab)
  return tab
end

function multishell.addHotkey(code, fn)
  hotkeys[code] = fn
end

function multishell.removeHotkey(code)
  hotkeys[code] = nil
end

function multishell.getFocus()
  return currentTab.tabId
end

function multishell.setFocus(tabId)
  local tab = tabs[tabId]
  if tab then
    selectTab(tab)
    redrawMenu()
    return true
  end
  return false
end

function multishell.getTitle(tabId)
  local tab = tabs[tabId]
  if tab then
    return tab.title
  end
end

function multishell.setTitle(tabId, title)
  local tab = tabs[tabId]
  if tab then
    if not tab.isOverview then
      tab.title = title or ''
    end
    redrawMenu()
  end
end

function multishell.getCurrent()
  if runningTab then
    return runningTab.tabId
  end
end

function multishell.getTab(tabId)
  return tabs[tabId]
end

function multishell.terminate(tabId)
  os.queueEvent('multishell_terminate', tabId)
end

function multishell.getTabs()
  return tabs
end

function multishell.launch( tProgramEnv, sProgramPath, ... )
  -- backwards compatibility
  return multishell.openTab({
    env = tProgramEnv,
    path = sProgramPath,
    args = { ... },
  })
end

function multishell.openTab(tab)
  if not tab.title and tab.path then
    tab.title = fs.getName(tab.path)
  end
  tab.title = tab.title or 'untitled'

  local previousTerm = term.current()
  launchProcess(tab)
  term.redirect(previousTerm)

  if tab.hidden then
    if coroutine.status(tab.co) == 'dead' or tab.isDead then
      tab.hidden = false
    end
  elseif tab.focused then
    multishell.setFocus(tab.tabId)
  else
    redrawMenu()
  end

  return tab.tabId
end

function multishell.hideTab(tabId)
  local tab = tabs[tabId]
  if tab then
    tab.hidden = true
    if currentTab.tabId == tabId then
      selectTab(tabs[currentTab.previousTabId])
    end
    redrawMenu()
  end
end

function multishell.unhideTab(tabId)
  local tab = tabs[tabId]
  if tab then
    tab.hidden = false
    redrawMenu()
  end
end

function multishell.getCount()
  return Util.size(tabs)
end

function multishell.hook(event, fn)
  if type(event) == 'table' then
    for _,v in pairs(event) do
      multishell.hook(v, fn)
    end
  else
    if not hooks[event] then
      hooks[event] = { }
    end
    table.insert(hooks[event], fn)
  end
end

-- you can only unhook from within the function that hooked
function multishell.unhook(event, fn)
  local eventHooks = hooks[event]
  if eventHooks then
    Util.removeByValue(eventHooks, fn)
    if #eventHooks == 0 then
      hooks[event] = nil
    end
  end
end

multishell.hook('multishell_terminate', function(_, eventData)
  local tabId = eventData[1] or -1
  local tab = tabs[tabId]

  if tab and not tab.isOverview then
    if coroutine.status(tab.co) ~= 'dead' then
      resumeTab(tab, "terminate")
    end
  end
  return true
end)

multishell.hook('multishell_redraw', function()
  tabsDirty = false

  local function write(x, text, bg, fg)
    parentTerm.setBackgroundColor(bg)
    parentTerm.setTextColor(fg)
    parentTerm.setCursorPos(x, 1)
    parentTerm.write(text)
  end

  local bg = _colors.tabBarBackgroundColor
  parentTerm.setBackgroundColor(bg)
  parentTerm.setCursorPos(1, 1)
  parentTerm.clearLine()

  local function compareTab(a, b)
    return a.tabId < b.tabId
  end

  for _,tab in pairs(tabs) do
    if tab.hidden and tab ~= currentTab then
      tab.width = 0
    else
      tab.width = #tab.title + 1
    end
  end

  local function width()
    local tw = 0
    Util.each(tabs, function(t) tw = tw + t.width end)
    return tw
  end

  while width() > w - 3 do
    local tab = select(2,
      Util.spairs(tabs, function(a, b) return a.width > b.width end)())
    tab.width = tab.width - 1
  end

  local tabX = 0
  for _,tab in Util.spairs(tabs, compareTab) do
    if tab.width > 0 then
      tab.sx = tabX + 1
      tab.ex = tabX + tab.width
      tabX = tabX + tab.width
      if tab ~= currentTab then
        write(tab.sx, tab.title:sub(1, tab.width - 1),
          _colors.backgroundColor, _colors.textColor)
      end
    end
  end

  if currentTab then
    write(currentTab.sx - 1,
      ' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ',
      _colors.focusBackgroundColor, _colors.focusTextColor)
    if not currentTab.isOverview then
      write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor)
    end
  end

  if currentTab then
    currentTab.window.restoreCursor()
  end

  return true
end)

multishell.hook('term_resize', function(_, eventData)
  if not eventData[1] then                            --- TEST
    w,h = parentTerm.getSize()

    local windowHeight = h-1

    for _,key in pairs(Util.keys(tabs)) do
      local tab = tabs[key]
      local x,y = tab.window.getCursorPos()
      if y > windowHeight then
        tab.window.scroll(y - windowHeight)
        tab.window.setCursorPos(x, windowHeight)
      end
      tab.window.reposition(1, 2, w, windowHeight)
    end

    redrawMenu()
  end
end)

-- downstate should be stored in the tab (maybe)
multishell.hook('key_up', function(_, eventData)
  local code = eventData[1]

  if downState[code] ~= currentTab then
    downState[code] = nil
    return true
  end
  downState[code] = nil
end)

multishell.hook('key', function(_, eventData)
  local code = eventData[1]
  local firstPress = not eventData[2]

  if firstPress then
    downState[code] = currentTab
  else
    --key was pressed initially in a previous window
    if downState[code] ~= currentTab then
      return true
    end
  end
end)

multishell.hook({ 'key', 'key_up', 'char', 'paste' }, function(event, eventData)
  local code = Input:translate(event, eventData[1], eventData[2])

  if code and hotkeys[code] then
    hotkeys[code](event, eventData)
  end
end)

multishell.hook('mouse_click', function(_, eventData)
  local x, y = eventData[2], eventData[3]
  if y == 1 then
    if x == 1 then
      multishell.setFocus(overviewId)
    elseif x == w then
      if currentTab then
        multishell.terminate(currentTab.tabId)
      end
    else
      for _,tab in pairs(tabs) do
        if not tab.hidden and tab.sx then
          if x >= tab.sx and x <= tab.ex then
            multishell.setFocus(tab.tabId)
            break
          end
        end
      end
    end
    downState.mouse = nil
    return true
  end
  downState.mouse = currentTab
  eventData[3] = eventData[3] - 1
end)

multishell.hook({ 'mouse_up', 'mouse_drag' }, function(event, eventData)
  if downState.mouse ~= currentTab then
    -- don't send mouse up as the mouse click event was on another window
    if event == 'mouse_up' then
      downState.mouse = nil
    end

    return true -- stop propagation
  end
  eventData[3] = eventData[3] - 1
end)

multishell.hook('mouse_scroll', function(_, eventData)
  local dir, y = eventData[1], eventData[3]

  if y == 1 then
    return true
  end

  if currentTab.terminal.scrollUp then
    if dir == -1 then
      currentTab.terminal.scrollUp()
    else
      currentTab.terminal.scrollDown()
    end
  end

  eventData[3] = y - 1
end)

local function startup()
  local success = true

  local function runDir(directory, open)
    if not fs.exists(directory) then
      return true
    end

    local files = fs.list(directory)
    table.sort(files)

    for _,file in ipairs(files) do
      os.sleep(0)
      local result, err = open(directory .. '/' .. file)
      if result then
        if term.isColor() then
          term.setTextColor(colors.green)
        end
        term.write('[PASS] ')
        term.setTextColor(colors.white)
        term.write(fs.combine(directory, file))
      else
        if term.isColor() then
          term.setTextColor(colors.red)
        end
        term.write('[FAIL] ')
        term.setTextColor(colors.white)
        term.write(fs.combine(directory, file))
        if err then
          _G.printError(err)
        end
        success = false
      end
      print()
    end
  end

  runDir('sys/services', shell.openHiddenTab)
  runDir('sys/autorun', shell.run)
  runDir('usr/autorun', shell.run)

  if not success then
    print()
    error('An autorun program has errored')
  end
end

overviewId = multishell.openTab({
  path = 'sys/apps/Overview.lua',
  isOverview = true,
})
tabs[overviewId].title = '+'

multishell.openTab({
  focused = true,
  fn = startup,
  title = 'Autorun',
})

local currentTabEvents = Util.transpose {
  'char', 'key', 'key_up',
  'mouse_click', 'mouse_drag', 'mouse_scroll', 'mouse_up',
  'paste', 'terminate',
}

while true do
  local tEventData = { os.pullEventRaw() }
  local sEvent = table.remove(tEventData, 1)
  local stopPropagation

  local eventHooks = hooks[sEvent]
  if eventHooks then
    for i = #eventHooks, 1, -1 do
      stopPropagation = eventHooks[i](sEvent, tEventData)
      if stopPropagation then
        break
      end
    end
  end

  if not stopPropagation then
    if currentTabEvents[sEvent] then
      resumeTab(currentTab, sEvent, tEventData)

    else
      -- Passthrough to all processes
      for _,key in pairs(Util.keys(tabs)) do
        resumeTab(tabs[key], sEvent, tEventData)
      end
    end
  end
end