local helper = require("son.helper")
local thunk = require("core.thunk")
local poilib = require("behavior.poi")
local goPlayer = game.Player.FindPlayer()
local DL = require("design.DesignerLibrary")
local bboardUtil = require("game.BlackboardUtil")
local locomotion = require("creature.locomotion")
function SetupPOIStates(POIState)
  local SonPOI_Enter = function(self, poi, ai, global, constants)
    constants.forceBehaviorChanged = false
    constants.awarenessState = poi.Type
    constants.focusDuration = 0
  end
  local poiSearchParameters_Auto = {
    FindArgs = {
      "SON",
      "AUTO",
      MatchAll = true
    },
    Radius = 1000,
    Filter = function(poi, ads, ai, global, constants)
      return true
    end
  }
  local poiSearchParameters_SonInteractable = {
    FindArgs = {
      "SON",
      "SONINTERACTABLE",
      MatchAll = true
    },
    Radius = 1000,
    Filter = function(poi, ads, ai, global, constants)
      if global.interactWithThisPOI == poi then
        return true
      end
    end
  }
  function POIState:GetSearchParameters(ai, global, constants)
    local parameters = {}
    table.insert(parameters, poiSearchParameters_SonInteractable)
    table.insert(parameters, poiSearchParameters_Auto)
    return parameters
  end
  local POI_PreCombatCrouch = POIState:TypeHandler("POI_PreCombatCrouch")
  function POI_PreCombatCrouch:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "Crouch"
  end
  function POI_PreCombatCrouch:Enter(poi, ai, global, constants)
    self.poiPos = poi:GetWorldPosition()
  end
  function POI_PreCombatCrouch:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
    local distToPlayer = (self.poiPos - goPlayer:GetWorldPosition()).length
    if 8 < distToPlayer then
      poi:SendEvent("EndCrouch")
    end
  end
  local POI_CommandShotEnabled = POIState:TypeHandler("POI_CommandShotEnabled")
  function POI_CommandShotEnabled:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "CommandShotEnabled"
  end
  function POI_CommandShotEnabled:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
  end
  local POI_SyncState = POIState:TypeHandler("POI_SyncState")
  function POI_SyncState:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "Sync"
  end
  function POI_SyncState:Update(poi, ai, global, constants)
    if not ai:IsDoingSyncMove() then
      poi:SendEvent("EndSync")
    end
  end
  local POI_TurretMode = POIState:TypeHandler("POI_TurretMode")
  function POI_TurretMode.Events:OnBroadcastReaction(event, ai, global, constants)
    if _G.global.POIInfo.useThisPOI ~= nil and _G.global.POIInfo.useThisPOI.Type == "TurretMode" and DL.CheckCreatureContext(event.broadcastContext, "COMMAND_SAVE") then
      _G.global.POIInfo.useThisPOI:SendEvent("Interrupt")
    end
  end
  function POI_TurretMode:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "TurretMode"
  end
  function POI_TurretMode:Enter(poi, ai, global, constants)
    self.TurretTimer = 0
    self.TurretShootInterval = 8
    self.TurretTimerLength = 30
    self.thisPOI = poi
  end
  function POI_TurretMode:Exit(poi, ai, global, constants)
    global.inCombatPromptPOI = false
    global.bCommand_FireArrow = false
  end
  function POI_TurretMode:Update(poi, ai, global, constants)
    global.inCombatPromptPOI = true
    self.TurretTimer = self.TurretTimer + ai:GetFrameTime()
    local targetOfPlayer = helper.returnBestPlayerTargetCreature(global, true)
    ai:SetCombatTarget(targetOfPlayer)
    local target = goPlayer
    if targetOfPlayer ~= nil then
      target = game.Player.FindPlayer()
    end
    local motionParams = {}
    motionParams.Facing = (target.WorldPosition - ai:GetWorldPosition()):Normalized()
    motionParams.Strafe = true
    motionParams.ApproachSpeed = 0
    motionParams.Position = ai:GetWorldPosition()
    locomotion.SetActuator(ai, {
      Destination = motionParams.Position,
      Facing = motionParams.Facing,
      Strafe = motionParams.Strafe,
      Speed = motionParams.ApproachSpeed,
      StopDistance = motionParams.StopDistance,
      StartDistance = motionParams.StartDistance
    })
  end
  local POI_Toggle = POIState:TypeHandler("POI_Toggle")
  function POI_Toggle:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and (poi.Type == "ToggleActivate" or poi.Type == "ToggleDeactivate")
  end
  function POI_Toggle:OnBrainInit(ai, global, constants)
    global.poistates_poitoggle = {}
    global.poistates_poitoggle.toggleTimer = 0
    global.poistates_poitoggle.toggleTimerMax = 1.5
    global.poistates_poitoggle.usedOnce = false
  end
  function POI_Toggle:Enter(poi, ai, global, constants)
    global.poistates_poitoggle.poiPos = poi:GetWorldPosition()
  end
  function POI_Toggle:Exit(poi, ai, global, constants)
    global.poistates_poitoggle.usedOnce = false
  end
  function POI_Toggle:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
    if global.poistates_poitoggle.usedOnce == false and DL.CheckCreatureContext(ai:GetContext(), "TOGGLE_ENTER") then
      global.poistates_poitoggle.usedOnce = true
      if poi.Type == "ToggleActivate" then
        ai:TriggerMoveEvent("activateLoop")
        _G.global.POIInfo.useThisPOI:SendEvent("activated")
      else
        ai:TriggerMoveEvent("deactivateLoop")
        _G.global.POIInfo.useThisPOI:SendEvent("deactivated")
      end
    end
    local distToPlayer = (global.poistates_poitoggle.poiPos - goPlayer:GetWorldPosition()).length
    if 30 < distToPlayer then
      ai:TriggerMoveEvent("sonDisengage")
      poi:SendEvent("disengage")
    end
    global.poistates_poitoggle.toggleTimer = global.poistates_poitoggle.toggleTimer + ai:GetFrameTime()
  end
  local POI_MoveScripted = POIState:TypeHandler("POI_MoveScripted")
  function POI_MoveScripted:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and (poi.Type == "GoToPoint" or poi.Type == "ForcedGoToPoint")
  end
  function POI_MoveScripted:Enter(poi, ai, global, constants)
    SonPOI_Enter(self, poi, ai, global, constants)
    constants.focusDuration = 0
    constants.allowedPOIAction = poi:FindLuaTableAttribute("SonAttackMode")
    if constants.allowedPOIAction == nil then
      constants.allowedPOIAction = "NoAttack"
    end
  end
  function POI_MoveScripted:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
    if (ai.WorldPosition - global.Player.WorldPosition):Length() > 50 then
      poi:SendEvent("BreakOut")
    end
  end
  function POI_MoveScripted:Exit(poi, ai, global, constants)
    constants.focusDuration = 8
    ai:SetFocus(game.Player.FindPlayer())
    global.POIInfo.timeSinceLastEngage = 100
  end
  local POI_CrystalThrow = POIState:TypeHandler("POI_CrystalThrow")
  function POI_CrystalThrow:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "CrystalThrow"
  end
  function POI_CrystalThrow:Enter(poi, ai, global, constants)
  end
  function POI_CrystalThrow:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
  end
  function POI_CrystalThrow:Exit(poi, ai, global, constants)
  end
  local POI_ObserveAndWait = POIState:TypeHandler("POI_ObserveAndWait")
  function POI_ObserveAndWait:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "ObserveAndWait"
  end
  function POI_ObserveAndWait:Enter(poi, ai, global, constants)
  end
  function POI_ObserveAndWait:Update(poi, ai, global, constants)
    if poi ~= nil then
      ai:UpdatePOI(poi)
    end
    if (ai.WorldPosition - global.Player.WorldPosition):Length() > constants.disengageDistance_Scripted then
      poi:SendEvent("BreakOut")
    end
  end
  function POI_ObserveAndWait:Exit(poi, ai, global, constants)
    constants.forceBehaviorChanged = false
    constants.awarenessState = _G.constants.Wait
  end
  local POI_DispelObject = POIState:TypeHandler("POI_DispelObject")
  function POI_DispelObject:OnBrainInit(poi, ai, global, constants)
  end
  function POI_DispelObject:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "DispellDoor"
  end
  function POI_DispelObject:Enter(poi, ai, global, constants)
    self.interrupted = false
    self.evadeMove = nil
    SonPOI_Enter(self, poi, ai, global, constants)
    constants.awarenessState = "ForcedGoToPoint"
  end
  local CheckCollision = function(ai, direction)
    local dirToCheck
    local evadeDistance = 2.5
    if direction == "Back" then
      dirToCheck = ai:GetWorldForward()
      dirToCheck = dirToCheck * -evadeDistance
    elseif direction == "Left" then
      dirToCheck = ai:GetWorldLeft()
      dirToCheck = dirToCheck * evadeDistance
    elseif direction == "Right" then
      dirToCheck = ai:GetWorldLeft()
      dirToCheck = dirToCheck * -evadeDistance
    end
    local offset = ai:GetWorldPosition() + dirToCheck
    local hit = game.World.RaycastCollision(ai:GetWorldPosition(), offset, {SourceGameObject = ai, FindAnything = true})
    if hit == nil then
      return true
    end
    return false
  end
  function POI_DispelObject:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
    local locoinfo = ai:GetLocomotionInfo()
    local radius = 0.75
    if locoinfo.NextSpeed > 1.5 then
      radius = 2
    end
    local enemies = DL.FindLivingEnemies(ai, radius, true)
    for _, enemy in pairs(enemies) do
      local _, dodgeTime = game.AIUtil.DetectCreatureIntersection(ai, enemy, radius)
      if not enemy:HasMarker("React") and 0 <= dodgeTime and dodgeTime < 1.5 then
        self.interrupted = true
        global.POIInfo.useThisPOI:SendEvent("kEHitReaction")
        local dir_table = {
          "Back",
          "Left",
          "Right"
        }
        while 0 < #dir_table do
          local dir = math.random(1, #dir_table)
          if CheckCollision(ai, dir_table[dir]) then
            self.evadeMove = string.format("BRA_Evade%s", dir_table[dir])
            break
          else
            table.remove(dir_table, dir)
          end
        end
        break
      end
    end
  end
  function POI_DispelObject:Exit(poi, ai, global, constants)
    if self.interrupted then
      self.evadeMove = "kLEEvadeBack"
      ai:CallScript("LuaHook_SetSurvivalReposition")
      ai:TriggerMoveEvent(self.evadeMove)
    end
    constants.moveToPointActuator.Destination = ai.WorldPosition
    constants.moveToPointActuator.Speed = 3
    constants.moveToPointActuator.StopDistance = 0.5
  end
  function POI_DispelObject.Events:OnHitReaction(event, ai, global, constants)
    if global.POIInfo.useThisPOI ~= nil then
      if ai == game.AI.FindSon() and event.source == game.Player.FindPlayer() then
        return
      end
      self.interrupted = true
      global.POIInfo.useThisPOI:SendEvent("kEHitReaction")
    end
  end
  local POI_ProximityEngage = POIState:TypeHandler("POI_ProximityEngage")
  function POI_ProximityEngage:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "ProximityEngage"
  end
  function POI_ProximityEngage:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
  end
  local POI_ActivateOneOff = POIState:TypeHandler("POI_ActivateOneOff")
  function POI_ActivateOneOff:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "ActivateOneOff"
  end
  function POI_ActivateOneOff:OnBrainInit(ai, global, constants)
    self.usedOnce = false
  end
  function POI_ActivateOneOff:Enter(poi, ai, global, constants)
    self.poiPos = poi:GetWorldPosition()
    self.usedOnce = false
  end
  function POI_ActivateOneOff:Exit(poi, ai, global, constants)
    self.usedOnce = false
  end
  function POI_ActivateOneOff:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
  end
  local POI_TakeDown = POIState:TypeHandler("POI_TakeDown")
  function POI_TakeDown:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "TakeDown"
  end
  function POI_TakeDown:OnBrainInit(ai, global, constants)
    self.usedOnce = false
    thunk.Install("OnKillTarget", function(go, killTarget)
      self.Target = killTarget
      _G.global.KillTarget = killTarget
      _G.global.sTargetMode = "KillTarget"
      ai:SetCombatTarget(killTarget)
    end)
  end
  function POI_TakeDown:Enter(poi, ai, global, constants)
    self.poiPos = poi:GetWorldPosition()
    self.usedOnce = false
    self.DoneShooting = false
  end
  function POI_TakeDown:Exit(poi, ai, global, constants)
    self.usedOnce = false
  end
  function POI_TakeDown:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
    if self.usedOnce == false then
      if ai:GetTargetCreature() == self.Target then
        local arrowData = {}
        ai:ForceMove("BRA_ShootBowE3_Kill")
        _G.global.POIInfo.useThisPOI:SendEvent("done")
        self.DoneShooting = true
      end
      if self.DoneShooting == true and (ai:HasMarker("DoneShooting") or ai:IsInNavigationMove()) then
        _G.global.POIInfo.useThisPOI:SendEvent("done")
      end
    end
  end
  local POI_Sandbowl = POIState:TypeHandler("POI_Sandbowl")
  function POI_Sandbowl:IsAvailable(poi, ai, global, constants)
    return poi ~= nil and poi.Type == "SonSandBowl"
  end
  function POI_Sandbowl:Enter(poi, ai, global, constants)
    SonPOI_Enter(self, poi, ai, global, constants)
    ai:ModifyCreatureMotion({
      MovementPriority = 1000,
      UnmovableByCreature = true,
      CreatureCollisionRadius = constants.POICollisionRadius
    })
    self.breakoutRange = poi:FindLuaTableAttribute("breakoutRange")
  end
  function POI_Sandbowl:Update(poi, ai, global, constants)
    self:ApproachPOI(ai, global, constants)
    if game.AIUtil.Distance(ai, game.Player.FindPlayer()) > (self.breakoutRange or 50) and poi.WorldPosition:Distance(ai.WorldPosition) < poi.WorldPosition:Distance(game.Player.FindPlayer().WorldPosition) then
      poi:SendEvent("kESonAbort")
    end
  end
  function POI_Sandbowl:Exit(poi, ai, global, constants)
    constants.focusDuration = 8
    ai:ModifyCreatureMotion({})
    global.POIInfo.timeSinceLastEngage = 100
  end
end
return {
  SetupPOIStates = SetupPOIStates
}
