local consts = require("ui.consts")
local tablex = require("core.tablex")
local thunk = require("core.thunk")
local coreUtil = require("core.util")
local EngineEvents = _G.EngineEvents
local hook2callbacks = {}
local hookToken = 0
local hookDepth = 0
local callbacks = {
  {},
  {},
  {}
}
local removedCallbacks = {
  {},
  {},
  {}
}
local maxHookDepth = tablex.Length(callbacks)
assert(#callbacks == #removedCallbacks)
local InvokeHooks = function(hookName, ...)
  assert(hookDepth < maxHookDepth)
  hookDepth = hookDepth + 1
  local origCallbacks = tablex.Table(hook2callbacks[hookName])
  local copyOfCallbacks = callbacks[hookDepth]
  local curRemovedCallbacks = removedCallbacks[hookDepth]
  assert(#copyOfCallbacks == 0)
  assert(#curRemovedCallbacks == 0)
  for token, callback in pairs(origCallbacks) do
    copyOfCallbacks[token] = callback
  end
  for token, callback in pairs(copyOfCallbacks) do
    if curRemovedCallbacks[token] == nil then
      local func = callback[1]
      local args = callback[2]
      if args then
        engine.PushProfileMarker("hookPrep")
        local numStaticArgs = #args
        local n = numStaticArgs
        for argIndex = 1, select("#", ...) do
          local element = select(argIndex, ...)
          n = n + 1
          args[n] = element
        end
        engine.PopProfileMarker()
        func(unpack(args))
        args[numStaticArgs + 1] = nil
      else
        func(...)
      end
    end
  end
  tablex.Clear(copyOfCallbacks)
  tablex.Clear(curRemovedCallbacks)
  hookDepth = hookDepth - 1
end
local SimulateHook = function(hookName, ...)
  InvokeHooks(hookName, unpack({
    ...
  }))
end
local PadEvents = {
  EVT_L2_Press = true,
  EVT_L2_Release = true,
  EVT_R2_Press = true,
  EVT_R2_Release = true,
  EVT_L1_Press = true,
  EVT_L1_Release = true,
  EVT_R1_Press = true,
  EVT_R1_Release = true,
  EVT_Triangle_Press = true,
  EVT_Triangle_Release = true,
  EVT_Circle_Press = true,
  EVT_Circle_Release = true,
  EVT_X_Press = true,
  EVT_X_Release = true,
  EVT_Square_Press = true,
  EVT_Square_Release = true,
  EVT_Select_Press = true,
  EVT_Select_Release = true,
  EVT_L3_Press = true,
  EVT_L3_Release = true,
  EVT_R3_Press = true,
  EVT_R3_Release = true,
  EVT_Options_Press = true,
  EVT_Options_Release = true,
  EVT_Start_Press = true,
  EVT_Start_Release = true,
  EVT_Up_Press = true,
  EVT_Up_Release = true,
  EVT_Right_Press = true,
  EVT_Right_Release = true,
  EVT_Down_Press = true,
  EVT_Down_Release = true,
  EVT_Left_Press = true,
  EVT_Left_Release = true,
  EVT_TouchPad_Press = true,
  EVT_TouchPad_Release = true,
  EVT_Advance_Press = true,
  EVT_Advance_Release = true,
  EVT_Back_Press = true,
  EVT_Back_Release = true,
  EVT_LeftStick_Up = true,
  EVT_LeftStick_Up_Release = true,
  EVT_LeftStick_Right = true,
  EVT_LeftStick_Right_Release = true,
  EVT_LeftStick_Down = true,
  EVT_LeftStick_Down_Release = true,
  EVT_LeftStick_Left = true,
  EVT_LeftStick_Left_Release = true,
  EVT_RightStick_Up = true,
  EVT_RightStick_Up_Release = true,
  EVT_RightStick_Right = true,
  EVT_RightStick_Right_Release = true,
  EVT_RightStick_Down = true,
  EVT_RightStick_Down_Release = true,
  EVT_RightStick_Left = true,
  EVT_RightStick_Left_Release = true
}
local HookIsPadEvent = function(hookName)
  return nil ~= PadEvents[hookName]
end
local GetTranslatedHookName, GetTranslatedHookNameWithMachineIndex
if not game.UI.EnumerateScripterDefinedEventName then
  function GetTranslatedHookName(hookName)
    local translatedHookName = hookName
    local engineEventId
    if nil ~= EngineEvents then
      engineEventId = EngineEvents[hookName]
    end
    if nil ~= engineEventId then
      translatedHookName = tostring(engineEventId)
    else
      local customHookId = consts.scripterDefinedEvents[hookName]
      if customHookId then
        translatedHookName = tostring(customHookId)
      end
    end
    return translatedHookName
  end
  function GetTranslatedHookNameWithMachineIndex(hookName, machineIndex)
    return machineIndex .. "." .. GetTranslatedHookName(hookName)
  end
end
local InstallImp = function(token, hookName, callback, ...)
  if nil == hook2callbacks[hookName] then
    thunk.Install(hookName, function(self, ...)
      InvokeHooks(hookName, ...)
    end)
    hook2callbacks[hookName] = {}
  end
  local token2callbacks = hook2callbacks[hookName]
  local varargs = {
    ...
  }
  if #varargs == 0 then
    varargs = nil
  end
  assert(nil == token2callbacks[token])
  token2callbacks[token] = {callback, varargs}
end
local Install = function(hookName, callback, ...)
  assert(type(callback) == "function", "hooks.Install: callback is not a function!")
  local token = hookToken
  hookToken = (hookToken + 1) % coreUtil.MaxLuaInt
  InstallImp(token, hookName, callback, ...)
  return token
end
local Uninstall = function(hookName, token)
  local token2callbacks = hook2callbacks[hookName]
  if token2callbacks then
    token2callbacks[token] = nil
    if 0 < hookDepth then
      local curRemovedCallbacks = removedCallbacks[hookDepth]
      curRemovedCallbacks[token] = true
    end
  end
end
local HandleScripterDefinedEvent = function(self, eventId)
  for name, value in pairs(consts.scripterDefinedEvents) do
    if value == eventId then
      InvokeHooks(name)
    end
  end
end
thunk.Install("HandleScripterDefinedEvent", HandleScripterDefinedEvent)
local MachineIndexToFSM = {}
local AllocMachineIndex = function(fsm)
  local i = 10
  while MachineIndexToFSM[i] ~= nil do
    i = i + 1
  end
  MachineIndexToFSM[i] = fsm
  return i
end
local GetFSMAtMachineIndex = function(index)
  return MachineIndexToFSM[index]
end
local FreeMachineIndex = function(index)
  assert(MachineIndexToFSM[index] ~= nil)
  MachineIndexToFSM[index] = nil
end
local GetHookNameForFsmAtIndex = function(hookName, machineIndex)
  if HookIsPadEvent(hookName) then
    return hookName .. "." .. machineIndex
  end
  return hookName
end
local InstallWithMachineIndex = function(hookName, machineIndex, callback, ...)
  local hook = GetHookNameForFsmAtIndex(hookName, machineIndex)
  return Install(hook, callback, ...)
end
local UninstallWithMachineIndex = function(hookName, machineIndex, callback)
  local hook = GetHookNameForFsmAtIndex(hookName, machineIndex)
  return Uninstall(hook, callback)
end
if not game.UI.EnumerateScripterDefinedEventName then
  local GetFullyTranslatedHookName = function(hookName, machineIndex)
    if HookIsPadEvent(hookName) then
      return GetTranslatedHookNameWithMachineIndex(hookName, machineIndex)
    end
    return GetTranslatedHookName(hookName)
  end
  function InstallWithMachineIndex(hookName, machineIndex, callback)
    local hook = GetFullyTranslatedHookName(hookName, machineIndex)
    return Install(hook, callback)
  end
  function UninstallWithMachineIndex(hookName, machineIndex, callback)
    local hook = GetFullyTranslatedHookName(hookName, machineIndex)
    return Uninstall(hook, callback)
  end
end
return {
  HookIsPadEvent = HookIsPadEvent,
  Install = Install,
  Uninstall = Uninstall,
  AllocMachineIndex = AllocMachineIndex,
  GetFSMAtMachineIndex = GetFSMAtMachineIndex,
  FreeMachineIndex = FreeMachineIndex,
  InstallWithMachineIndex = InstallWithMachineIndex,
  UninstallWithMachineIndex = UninstallWithMachineIndex,
  SimulateHook = SimulateHook
}
