local action = require("ai.action")
local statelib = require("ai.state")
local statelogic = require("ai.statelogic")
local timerstate = require("ai.timerstate")
local class = require("core.class")
local clone = require("core.clone")
local profile = require("core.profile")
local thunk = require("core.thunk")
local StateMachine = class.Class("StateMachine")
function StateMachine:init(name)
  self.Name = name
  self.StateClasses = {}
  self.StateProxies = {}
end
function StateMachine:GetAI()
  return self.ai
end
local instantiateClass = function(self, name, CLASS)
  local proxy = self.StateProxies[name]
  setmetatable(proxy, nil)
  return class.InPlaceNew(proxy, CLASS)
end
local storeClassForInstantiation = function(self, CLASS)
  local CLASSNAME = CLASS.CLASSNAME
  local proxy = setmetatable({}, {
    __index = CLASS,
    __newindex = CLASS,
    __concat = function(proxy, t)
      class.extend(CLASS, t)
      return proxy
    end
  })
  self.StateClasses[CLASSNAME] = CLASS
  self.StateProxies[CLASSNAME] = proxy
  return proxy
end
function StateMachine:InitStates(state_machine_list, ...)
  local CS = statelogic.call_start(self, nil, nil, ...)
  self.States = {}
  self.state_machine_list = state_machine_list
  for name, CLASS in pairs(self.StateClasses) do
    self.States[name] = instantiateClass(self, name, CLASS)
  end
  statelogic.dispatch_multiple_function(self.States, "OnBrainInit", ...)
  statelogic.dispatch_multiple_function(self.States, "state_init", ...)
  statelogic.dispatch_multiple_function(self.States, "state_append_statemachine", state_machine_list, ...)
  statelogic.call_end(CS)
end
function StateMachine:State(name, parent)
  local CLASS = class.Class({
    self.Name,
    name
  }, parent or statelib.State)
  return storeClassForInstantiation(self, CLASS)
end
function StateMachine:Action(name, parent)
  local CLASS = class.Class({
    self.Name,
    name
  }, parent or action.Action)
  return storeClassForInstantiation(self, CLASS)
end
function StateMachine:Timer(name)
  local CLASS = class.Class({
    self.Name,
    name
  }, timerstate.TimerState)
  return storeClassForInstantiation(self, CLASS)
end
local DispatchEventToActiveMachine = function(event)
  local CS = statelogic.call_state()
  statelogic.active_dispatch_event(CS.M, CS.active_state, "Events", event, unpack(CS.args))
end
local DispatchEventToAllMachines = function(funcName, ...)
  local CS = statelogic.call_state()
  local state_machine_list = CS.M.state_machine_list
  local handled = false
  for _, M in ipairs(state_machine_list) do
    local active_state_name = CS.active_state and CS.active_state.Name or 0
    local active_state = M.States[active_state_name] == CS.active_state and CS.active_state or nil
    handled = statelogic.active_dispatch_event(M, active_state, "Events", funcName, ...)
    if handled then
      break
    end
  end
  return handled
end
local ConnectHookFunctions = function(ai, state_machine_list, activeStates, ...)
  local hook_names = {}
  for _, M in ipairs(state_machine_list) do
    for _, State in pairs(M.States) do
      for HookName in pairs(State.Hooks) do
        hook_names[HookName] = true
      end
    end
  end
  local global_args = {
    ...
  }
  for hook_name in pairs(hook_names) do
    local callback = function(_self, ...)
      local full_arg_list = clone.shallow(global_args)
      local nargs = select("#", ...)
      for i = 1, nargs do
        full_arg_list[#full_arg_list + 1] = select(i, ...)
      end
      for _, M in ipairs(state_machine_list) do
        local activeState = activeStates[M.Name]
        local CS = statelogic.call_start(M, activeState, nil, unpack(global_args))
        statelogic.active_dispatch_event(M, activeState, "Hooks", hook_name, unpack(full_arg_list))
        statelogic.call_end(CS)
      end
    end
    thunk.Install(hook_name, callback)
  end
end
local StartAll = function(ai, state_machine_list, ...)
  _G.__active_states__ = _G.__active_states__ or {}
  local activeStates = _G.__active_states__[state_machine_list] or {}
  _G.__active_states__[state_machine_list] = activeStates
  activeStates.ActiveTags = {}
  for _, M in ipairs(state_machine_list) do
    M.ai = ai
    if M.OnBrainInit then
      engine.ProfileCall(M.OnBrainInit, M, ...)
    end
    engine.ProfileCall(M.InitStates, M, state_machine_list, ...)
    class.lazy_init_profile_markers(M)
  end
  ConnectHookFunctions(ai, state_machine_list, activeStates, ...)
end
local updateTags = function(state_machine_list, activeStates, tags)
  for k in pairs(tags) do
    tags[k] = nil
  end
  for _, M in ipairs(state_machine_list) do
    local activeState = activeStates[M.Name]
    if activeState and activeState.tagset then
      for k in pairs(activeState.tagset) do
        tags[k] = true
      end
    end
  end
end
local UpdateAll = function(state_machine_list, ...)
  statelogic.crash_cleanup()
  local activeStates = _G.__active_states__[state_machine_list]
  assert(activeStates, "No active state list! Did something go wrong during statemachine.StartAll?")
  local tags = activeStates.ActiveTags
  updateTags(state_machine_list, activeStates, tags)
  engine.PushProfileMarker("DispatchReactions")
  local _, firstM = next(state_machine_list)
  local ai = firstM and firstM:GetAI(...)
  if ai then
    for event in ai:IterateCollisionNotifications() do
      for _, M in ipairs(state_machine_list) do
        local activeState = activeStates[M.Name]
        local eventName = event.wasBlocked and "OnBlockedReaction" or "OnHitReaction"
        local CS = statelogic.call_start(M, activeState, nil, ...)
        statelogic.active_dispatch_event(M, activeState, "Events", eventName, event, ...)
        statelogic.call_end(CS)
      end
    end
    for event in ai:IterateBroadcastNotifications() do
      for _, M in ipairs(state_machine_list) do
        local activeState = activeStates[M.Name]
        local CS = statelogic.call_start(M, activeState, nil, ...)
        statelogic.active_dispatch_event(M, activeState, "Events", "OnBroadcastReaction", event, ...)
        statelogic.call_end(CS)
      end
    end
  end
  engine.PopProfileMarker()
  for _, M in ipairs(state_machine_list) do
    local dt = ai.GetFrameTime and ai:GetFrameTime() or ai:GetUnitTime()
    timerstate.UpdateTimers(M, dt, ...)
  end
  for _, M in ipairs(state_machine_list) do
    local activeState = activeStates[M.Name]
    activeStates[M.Name] = engine.ProfileCall(M.Name, statelogic.update, M, activeState, M.SelectNextState, ...)
    updateTags(state_machine_list, activeStates, tags)
  end
  for _, M in ipairs(state_machine_list) do
    local publishStateFunc = M._publishState
    if publishStateFunc then
      publishStateFunc(M, ...)
    end
  end
end
local GetActiveStates = function(state_machine_list)
  local activeStates = _G.__active_states__[state_machine_list]
  assert(activeStates)
  local out = {}
  for k, state in pairs(activeStates) do
    out[k] = state and state.Name or "<no state>"
  end
  return out
end
local AddTags = function(STATE, ...)
  assert(STATE.__identity == nil, "Cannot tag an instantiated state!")
  STATE.tagset = STATE.tagset or {}
  local n = select("#", ...)
  for i = 1, n do
    local tag = select(i, ...)
    STATE.tagset[tag] = true
  end
end
local ActiveTags = function()
  local _, activeStates = next(_G.__active_states__)
  assert(activeStates)
  return activeStates.ActiveTags
end
local OnSaveCheckpoint = function(state_machine_list, root)
  if not _G.__new_pickle__ then
    return
  end
  root.state_machines = {}
  for _, M in ipairs(state_machine_list) do
    local this_machine = {}
    root.state_machines[M.Name] = this_machine
    for Name, State in pairs(M.States) do
      this_machine[Name] = State
    end
  end
end
local GetDebugTable = function(state_machine_list, ai)
  if not engine.IsDebug() then
    return
  end
  if not ai:DebugIsSelectedCreature() then
    return
  end
  local activeStates = _G.__active_states__[state_machine_list]
  assert(activeStates)
  local debugTable = {}
  debugTable[#debugTable + 1] = {
    "State Machine",
    "State"
  }
  for _, M in ipairs(state_machine_list) do
    local state = activeStates[M.Name]
    local row = {}
    debugTable[#debugTable + 1] = row
    row[1] = "- " .. M.Name
    row[2] = "- " .. (state and state.LEAFNAME or "<no state>")
    if state then
      local SI = statelogic.get_state_instance(M, state.Name)
      if class.iskindof(state, action.Action) then
        local sequence = SI.transient.sequence
        local current_index = SI.transient.current_index
        local elapsed_time = SI.transient.elapsed_time or 0
        row[3] = string.format("%d/%d @%.2f", current_index or 0, #sequence, elapsed_time)
        row[4] = current_index and sequence[current_index].debug()
      else
        row[3] = ""
        row[4] = ""
      end
    end
  end
  debugTable[#debugTable + 1] = {"Timer", "State"}
  for _, M in ipairs(state_machine_list) do
    for _, state in pairs(M.States) do
      if class.iskindof(state, timerstate.TimerState) then
        local row = {}
        debugTable[#debugTable + 1] = row
        row[1] = "- " .. state.Name
        local SI = statelogic.get_state_instance(M, state.Name)
        local TimerInstance = SI.TimerInstance
        local timerString = TimerInstance.running and "running" or "stopped"
        row[2] = "- " .. timerString
        row[3] = string.format("@%.2f", TimerInstance.time)
        row[4] = TimerInstance.period
      end
    end
  end
  for _, M in ipairs(state_machine_list) do
    local GI = statelogic.get_global_instance(M)
    local GlobalTimers = GI.GlobalTimers
    if GlobalTimers then
      for T in pairs(GlobalTimers.references) do
        if not T.fromTimerState then
          local row = {}
          debugTable[#debugTable + 1] = row
          row[1] = string.format("- GlobalTimer (%s)", tostring(T))
          row[2] = "- " .. (T.running and "running" or "stopped")
          row[3] = string.format("@%.2f", T.time)
          row[4] = T.period
        end
      end
    end
    for _, state in pairs(M.States) do
      local SI = statelogic.get_state_instance(M, state.Name)
      local transient = SI.transient
      local AttachedTimers = transient.AttachedTimers
      if AttachedTimers then
        for T in pairs(AttachedTimers.references) do
          local row = {}
          debugTable[#debugTable + 1] = row
          row[1] = string.format("- %s timer", state.Name, tostring(T))
          row[2] = "- " .. (T.running and "running" or "stopped")
          row[3] = string.format("@%.2f", T.time)
          row[4] = T.period
        end
      end
    end
  end
  debugTable[#debugTable + 1] = {
    "Active Tags"
  }
  for tag in pairs(activeStates.ActiveTags) do
    local row = {}
    debugTable[#debugTable + 1] = row
    row[1] = "- " .. tag
  end
  debugTable[#debugTable + 1] = {"----", "----"}
  local r, g, b = ai:DebugGetColor()
  local color = engine.Vector.New(r, g, b)
  debugTable.Title = ai:GetDebugName()
  debugTable.TitleColor = color
  debugTable.X, debugTable.Y = -21, 2
  return debugTable
end
return profile.WrapLibrary({
  StateMachine = StateMachine,
  StartAll = StartAll,
  UpdateAll = UpdateAll,
  GetActiveStates = GetActiveStates,
  DispatchEvent = DispatchEventToActiveMachine,
  DispatchGlobalEvent = DispatchEventToAllMachines,
  AttachedTimer = timerstate.AttachedTimer,
  GlobalTimer = timerstate.GlobalTimer,
  GetDebugTable = GetDebugTable,
  ActiveTags = ActiveTags,
  AddTags = AddTags,
  OnSaveCheckpoint = OnSaveCheckpoint
})
