local statemachine = require("ai.statemachine")
local locomotion = require("creature.locomotion")
local UNSPECIFIED_PRIORITY = 10000
local DEFAULT_SEARCH_COOLDOWN_TIME = 0
function _G.LuaHook_GetPreApproachBranch(ai, branch)
  local currentPOI = ai.OwnedPOI
  if not currentPOI then
    return
  end
  local syncing = ai:IsDoingSyncMove()
  if syncing then
    return
  end
  local forward = ai:GetWorldForward()
  local to = currentPOI:GetWorldPosition() - ai:GetWorldPosition()
  local angle = game.AIUtil.AngleBetweenXZ(forward, to)
  if angle < -60 then
    return branch:FindOutcomeBranchesEntry("Left")
  elseif 60 < angle then
    return branch:FindOutcomeBranchesEntry("Right")
  else
    return branch:FindOutcomeBranchesEntry("Forward")
  end
end
function _G.LuaHook_NiflheimExitCubbyHoleCase(ai, branch)
  if ai.OwnedPOI == nil then
    local levelName = ""
    local player = game.Player.FindPlayer()
    local level = player.GroundLevel
    if level ~= nil then
      levelName = level.Name
    end
    if levelName == "WAD_Nid100_Entrance" then
      return branch:FindOutcomeBranchesEntry("Exit")
    end
  end
end
function _G.LuaHook_ShotStarted(ai, branch)
  local currentPOI = ai.OwnedPOI
  if not currentPOI then
    return
  end
  currentPOI:SendEvent("CommandShotStart")
end
function _G.LuaHook_ShotEnded(ai, branch)
  local currentPOI = ai.OwnedPOI
  if not currentPOI then
    return
  end
  currentPOI:SendEvent("CommandShotEnd")
end
local exitPOI = function(ai, global, constants)
  if global.POIInfo.useThisPOI ~= nil then
    global.POIInfo.useThisPOI:Disengage(ai)
  end
  statemachine.DispatchGlobalEvent("OnDisengagePOI", ai, global, constants)
  global.POIInfo.useThisPOI = nil
  global.POIInfo.state = "done"
  global.POIInfo.approach = false
  ai:ModifyCreatureMotion({})
end
local cancelApproach = function(ai, global, constants)
  locomotion.SetActuator(ai, {Stop = true})
  ai:ModifyCreatureMotion({})
end
local runApproach = function(ai, global, constants, approachPosition)
  local newApproachPosition
  local activePOI = ai.OwnedPOI
  local poi = global.POIInfo.useThisPOI
  if poi ~= nil and activePOI == poi then
    local poiInfo = ai:UpdatePOI(poi)
    global.POIInfo.approach = poiInfo.Approach
    if poiInfo.Detach then
      exitPOI(ai, global, constants)
    end
    if poiInfo.Approach then
      ai:ModifyCreatureMotion({
        MovementPriority = 1000,
        UnmovableByCreature = true,
        CreatureCollisionRadius = constants.POICollisionRadius
      })
      newApproachPosition = poiInfo.Position
      local stopOverride = poiInfo.StopDistanceOverride
      local ignoreNavmesh = poiInfo.IgnoreNavmesh or false
      if stopOverride == -1 then
        stopOverride = nil
      end
      local pathDestination = ai:GetPathDestination()
      if not approachPosition or approachPosition.x ~= newApproachPosition.x or approachPosition.y ~= newApproachPosition.y or approachPosition.z ~= newApproachPosition.z or pathDestination.x ~= newApproachPosition.x or pathDestination.y ~= newApproachPosition.y or pathDestination.z ~= newApproachPosition.z then
        local destination
        if poiInfo.Direction and not poiInfo.Stop then
          destination = {}
          destination[1] = newApproachPosition - poiInfo.Direction * 2.5
          destination[2] = newApproachPosition + poiInfo.Direction * 1
        else
          destination = newApproachPosition
        end
        if not poiInfo.ApproachAndStopping then
          locomotion.SetActuator(ai, {
            Destination = destination,
            Facing = ai:GetWorldForward(),
            Strafe = false,
            Speed = poiInfo.Speed,
            Stop = poiInfo.Stop,
            StopDistance = stopOverride,
            AllowCloseRangePathfind = true,
            IgnoreNavmesh = ignoreNavmesh,
            DoNotSkipDestinationPoints = true
          })
        else
          locomotion.SetActuator(ai, {
            Destination = poiInfo.Position,
            Facing = ai:GetWorldForward(),
            Speed = 0,
            StopDistance = stopOverride,
            Stop = poiInfo.Stop,
            AllowCloseRangePathfind = true,
            IgnoreNavmesh = ignoreNavmesh
          })
        end
      elseif poiInfo.ApproachAndStopping then
        locomotion.SetActuator(ai, {
          Destination = poiInfo.Position,
          Facing = ai:GetWorldForward(),
          Speed = 0,
          StopDistance = stopOverride,
          Stop = poiInfo.Stop,
          AllowCloseRangePathfind = true,
          IgnoreNavmesh = ignoreNavmesh
        })
      end
    elseif poi.Type ~= "KeepEvaluatingPath" and (poi.Type ~= "ContextAction" or poi:FindLuaTableAttribute("UseSteadyOnExit") ~= false) then
      locomotion.SetActuator(ai, {
        Destination = ai.WorldPosition,
        Facing = ai:GetWorldForward(),
        Speed = 0,
        AllowCloseRangePathfind = false
      })
    end
  elseif poi then
    exitPOI(ai, global, constants)
  end
  return newApproachPosition
end
local findGenericPOI = function(poistate, ai, global, constants)
  local currentPOI = ai.OwnedPOI
  local currentPriority = currentPOI and currentPOI:FindLuaTableAttribute("Priority")
  if currentPOI ~= nil and currentPriority == nil then
    return
  end
  local searchCooldownTime = constants.GenericPOISearchCooldown or DEFAULT_SEARCH_COOLDOWN_TIME
  local searchCooldownComplete = searchCooldownTime < global.POIInfo.timeSinceLastEngage
  local lastSearchPriority = global.POIInfo.lastEngagedPriority or UNSPECIFIED_PRIORITY
  local searchPosition = ai:GetWorldPosition()
  local player = game.Player.FindPlayer()
  local adsToCheck = {
    string.format("GENERIC_%s", poistate.characterType),
    "GENERIC_ANY"
  }
  local inCombat
  if ai == game.AI.FindSon() then
    inCombat = global.bInCombat or not global.bowSheathed or global.currentState ~= "Idle" or ai:IsPlayingMove("MOV_CombatExit")
  else
    inCombat = global.bInCombat
  end
  local closestPOI, closestPriority = currentPOI, currentPriority
  for _, advertisingString in ipairs(adsToCheck) do
    local searchArgs = {advertisingString, MatchAll = true}
    local foundPOIs = game.POI.FindAdvertising(searchPosition, 50, searchArgs)
    for _, potentialPOI in ipairs(foundPOIs) do
      local available = false
      local priority = potentialPOI:FindLuaTableAttribute("Priority") or 0
      if currentPOI ~= potentialPOI and (not (0 <= priority) or (not currentPriority or not (currentPriority < 0)) and (not currentPriority or not (currentPriority >= priority)) and (not closestPriority or not (closestPriority > priority)) and (not (lastSearchPriority >= priority) or searchCooldownComplete)) and (not (potentialPOI.Type == "ContextAction" and currentPOI) or currentPOI.Type ~= "ContextAction" or currentPOI:GetStageName() == "Loop" or currentPOI:GetStageName() == "LoopAction" or currentPOI:GetStageName() == "Exit") and (not inCombat or potentialPOI:FindLuaTableAttribute("AvailableDuringCombat")) and (inCombat or potentialPOI:FindLuaTableAttribute("AvailableOutsideCombat")) then
        local availableRange = potentialPOI:FindLuaTableAttribute("AvailableRange") or 0
        local distance = game.AIUtil.Distance(potentialPOI, searchPosition)
        if not (0 < availableRange) or not (availableRange < distance) then
          local disengageRange = potentialPOI:FindLuaTableAttribute("DisengageIfPlayerFurtherThan") or 0
          local distanceToPlayer = game.AIUtil.Distance(potentialPOI, player)
          if not (0 < disengageRange) or not (disengageRange < distanceToPlayer) then
            available = true
          end
        end
      end
      if available and closestPOI ~= nil then
        if priority == closestPriority or priority < 0 and closestPriority < 0 then
          local closestDistance = game.AIUtil.Distance(searchPosition, closestPOI)
          local potentialDistance = game.AIUtil.Distance(searchPosition, potentialPOI)
          if closestDistance <= potentialDistance then
            available = false
          end
        elseif priority < 0 then
          available = true
        elseif closestPriority < priority then
          available = true
        else
          available = false
        end
      end
      if available then
        closestPOI, closestPriority = potentialPOI, priority
      end
    end
  end
  return closestPOI
end
local findBestPOI = function(poistate, ai, global, constants)
  local searchParameterList = poistate.GetSearchParameters and poistate:GetSearchParameters(ai, global, constants)
  local closestPOI
  if not searchParameterList then
    return
  end
  for _, v in ipairs(searchParameterList) do
    local advertisingPOIList = game.POI.FindAdvertising((v.SearchObject or ai):GetWorldPosition(), v.Radius or 20, v.FindArgs)
    for _, poi in ipairs(advertisingPOIList) do
      local ads = {}
      for k in string.gmatch(poi.Advertisement, ":") do
        ads[k] = true
      end
      local ok = true
      local range = poi:FindLuaTableAttribute("broadcastRange")
      if range ~= nil and range < game.AIUtil.Distance(ai, poi) then
        ok = false
      end
      if ok and v.Filter then
        ok = v.Filter(poi, ads, ai, global, constants)
      end
      if ok then
        if closestPOI ~= nil then
          if game.AIUtil.Distance(ai, poi) < game.AIUtil.Distance(ai, closestPOI) then
            closestPOI = poi
          end
        else
          closestPOI = poi
        end
      end
    end
  end
  if closestPOI ~= nil then
    return closestPOI
  elseif poistate.characterType ~= nil then
    local genericPOI = findGenericPOI(poistate, ai, global, constants)
    if genericPOI ~= nil then
      return genericPOI
    end
  end
end
local makePOIBrain = function(poistate)
  local POI_Brain = statemachine.StateMachine.New("POI_Brain")
  local POI_Search = POI_Brain:State("POI_Search")
  local POI_Auto = POI_Brain:State("POI_Auto")
  statemachine.AddTags(POI_Auto, "POIActive")
  statemachine.AddTags(POI_Search, "POISearch")
  POI_Brain.CustomTypeHandlers = {}
  local highPriorityPOICheck = function(ai, global, constants)
    local newPOI = findBestPOI(poistate, ai, global, constants)
    if newPOI ~= nil and ai.OwnedPOI ~= newPOI then
      local currentPOI = global.POIInfo.useThisPOI
      if currentPOI ~= nil and currentPOI ~= newPOI and (not ai.OwnedPOI.GetStageName or ai.OwnedPOI:GetStageName() ~= "QueuedEnd" and ai.OwnedPOI:GetStageName() ~= "Disengage" and ai.OwnedPOI:GetStageName() ~= "Enter" and ai.OwnedPOI:GetStageName() ~= "Exit" and ai.OwnedPOI:GetStageName() ~= "EnterPreempt") then
        currentPOI:SendEvent("Preempt")
      end
    end
  end
  function POI_Brain:SelectNextState(ai, global, constants)
    local ActiveTags = statemachine.ActiveTags()
    local fullSteadyExit = global.POIInfo.POIStageName ~= "ApproachOrMovingBreakOut"
    if global.POIInfo.state ~= "off" and fullSteadyExit then
      local activePOI = ai.OwnedPOI
      if activePOI ~= global.POIInfo.useThisPOI then
        exitPOI(ai, global, constants)
        return
      end
      if not ActiveTags.DoNotSearchPOI then
        highPriorityPOICheck(ai, global, constants)
      end
      for _, state in ipairs(self.CustomTypeHandlers) do
        if state:IsAvailable(global.POIInfo.useThisPOI, ai, global, constants) then
          return state
        end
      end
      return POI_Auto
    end
    if not fullSteadyExit then
      if global.POIInfo.useThisPOI ~= nil then
        global.POIInfo.useThisPOI:Disengage(ai)
        global.POIInfo.useThisPOI = nil
      end
      global.POIInfo.state = "done"
    end
    if ActiveTags.AllowPOIEngage then
      return POI_Search
    end
  end
  function POI_Brain:UpdateData(ai, global, constants)
    if global.POIInfo.state == "done" then
      global.POIInfo.state = "off"
      global.POIInfo.useThisPOI = nil
    end
  end
  function POI_Brain:OnBrainInit(ai, global, constants)
    global.POIInfo.useThisPOI = nil
    global.POIInfo.state = "off"
    global.usePOICooldown = 0
  end
  function POI_Search:UpdateData(ai, global, constants)
    if ai.OwnedPOI ~= nil then
      global.POIInfo.timeSinceLastEngage = 0
      global.POIInfo.lastEngagedPriority = ai.OwnedPOI:FindLuaTableAttribute("Priority") or UNSPECIFIED_PRIORITY
    else
      global.POIInfo.timeSinceLastEngage = (global.POIInfo.timeSinceLastEngage or 0) + ai:GetFrameTime()
    end
  end
  function POI_Search:Update(ai, global, constants)
    if ai:IsDead() then
      exitPOI(ai, global, constants)
      return
    end
    if global.usePOICooldown > 0 then
      global.usePOICooldown = global.usePOICooldown - ai:GetFrameTime()
      return
    end
    local activePOI = ai.OwnedPOI
    if activePOI == nil then
      activePOI = findBestPOI(poistate, ai, global, constants)
      if activePOI and not activePOI:Engage(ai) then
        activePOI = nil
      end
    end
    if activePOI ~= nil then
      global.POIInfo.useThisPOI = activePOI
      global.POIInfo.state = "on"
      global.interactWithThisPOI = nil
      global.POIInfo.POIType = activePOI.Type
    end
  end
  function POI_Search:Exit(ai, global, constants)
    if ai:HasMarker("ContinuousSandBowl") then
      return
    end
    ai:TriggerMoveEvent("kLE_Disengage")
  end
  local poi_auto_userflags_update = function(self, ai, global, constants)
    local poi = global.POIInfo.useThisPOI
    local userFlags = poi ~= nil and poi:GetUserFlags()
    local ForcePushSetting = 0
    if userFlags then
      if userFlags.ForcePushAwayCloseRange then
        ForcePushSetting = 1
      elseif userFlags.ForcePushAway then
        ForcePushSetting = 2
      end
    end
    if ForcePushSetting == 0 then
      return
    end
    if ai == game.AI.FindSon() and ForcePushSetting == 1 then
      local locomotionInfo = ai:GetLocomotionInfo()
      if locomotionInfo.PathLength > 3 then
        return
      end
    end
    if not self.isForcePushAway and ForcePushSetting == 2 then
      ai:ModifyCreatureMotion({MovementPriority = 1000})
      self.isForcePushAway = true
    elseif self.isForcePushAway and not ForcePushSetting == 0 then
      ai:ModifyCreatureMotion({})
      self.isForcePushAway = false
    end
  end
  local poi_auto_userflags_exit = function(self, ai, global, constants)
    if self.isForcePushAway then
      ai:ModifyCreatureMotion({})
      self.isForcePushAway = false
    end
  end
  function POI_Auto:Enter(ai, global, constants)
    ai:PauseForcedPath(true)
  end
  function POI_Auto:Update(ai, global, constants)
    poi_auto_userflags_update(self, ai, global, constants)
    if not ai.OwnedPOI or ai.OwnedPOI.Type ~= "ContextAction" and ai.OwnedPOI.Type ~= "CubbyHole" then
      ai:RemoveAvailabilityRequest("ContextAction")
    end
    self.currentDestination = runApproach(ai, global, constants, self.currentDestination)
  end
  function POI_Auto:Exit(ai, global, constants)
    cancelApproach(ai, global, constants)
    ai:PauseForcedPath(false)
    self.currentDestination = nil
    poi_auto_userflags_exit(self, ai, global, constants)
  end
  function POI_Auto.Events:OnHitReaction(event, ai, global, constants)
    if global.POIInfo.useThisPOI ~= nil and global.POIInfo.useThisPOI.Type == "" then
      if ai == game.AI.FindSon() and event.source == game.Player.FindPlayer() then
        return
      end
      global.POIInfo.useThisPOI:SendEvent("kEHitReaction")
    end
  end
  return POI_Brain
end
local IsPOIActive = function()
  return statemachine.ActiveTags().POIActive
end
local IsPOISearch = function()
  return statemachine.ActiveTags().POISearch
end
local AllowPOIFromStates = function(...)
  for _, state in ipairs({
    ...
  }) do
    statemachine.AddTags(state, "AllowPOIEngage")
  end
end
local SetCharacterType = function(POIState, characterType)
  POIState.characterType = characterType
end
local TypeHandler_ExitPOI = function(self, ai, global, constants)
  exitPOI(ai, global, constants)
end
local TypeHandler_ApproachPOI = function(self, ai, global, constants)
  return runApproach(ai, global, constants)
end
local NewPOIState = function(M, stateName)
  local POIState = M:State(stateName)
  function POIState:state_init(ai, global, constants)
    global.POIInfo = {}
    global.POIInfo.useThisPOI = nil
    global.POIInfo.state = "off"
    global.POIInfo.POIStageName = nil
  end
  function POIState:state_append_statemachine(state_machine_list, ai, global, constants)
    table.insert(state_machine_list, self.POIBrainInstance)
  end
  function POIState:TypeHandler(name, base)
    local state = self.POIBrainInstance:State(name, base)
    table.insert(self.POIBrainInstance.CustomTypeHandlers, state)
    statemachine.AddTags(state, "POIActive")
    state.ExitPOI = TypeHandler_ExitPOI
    state.ApproachPOI = TypeHandler_ApproachPOI
    local enter_thunk = state.enter_thunk
    local update_thunk = state.update_thunk
    local exit_thunk = state.exit_thunk
    function state:enter_thunk(ai, global, constants, ...)
      local enterFn = enter_thunk and enter_thunk or state.Enter
      return enterFn and enterFn(self, global.POIInfo.useThisPOI, ai, global, constants, ...)
    end
    function state:update_thunk(ai, global, constants, ...)
      local updateFn = update_thunk and update_thunk or state.Update
      return updateFn and updateFn(self, global.POIInfo.useThisPOI, ai, global, constants, ...)
    end
    function state:exit_thunk(ai, global, constants, ...)
      cancelApproach(ai, global, constants)
      local exitFn = exit_thunk and exit_thunk or state.Exit
      return exitFn and exitFn(self, global.POIInfo.useThisPOI, ai, global, constants, ...)
    end
    return state
  end
  POIState.POIBrainInstance = makePOIBrain(POIState)
  return POIState
end
return {
  NewPOIState = NewPOIState,
  IsPOIActive = IsPOIActive,
  IsPOISearch = IsPOISearch,
  AllowPOIFromStates = AllowPOIFromStates,
  SetCharacterType = SetCharacterType
}
