local thunk = require("core.thunk")
local motionWarp = require("creature.motionWarp")
local color = require("core.color")
local SetActuator = function(ai, params)
  engine.SendHook("SetActuator", ai, params)
end
local SetFocus = function(ai, params)
  engine.SendHook("SetFocus", ai, params)
end
local FollowNavCurve = function(creature, curve, settings)
  engine.SendHook("FollowNavCurve", creature, curve, settings)
end
local Install = function()
  thunk.Install("SetActuator", function(ai, params)
    ai:SetActuator(params)
  end)
  thunk.Install("SetFocus", function(ai, params)
    ai:SetFocus(params)
  end)
  thunk.Install("FollowNavCurve", function(creature, curve, settings)
    creature:SetNavCurve(curve, settings)
  end)
  motionWarp.Install()
end
local CreateDrivers = function(crt)
  motionWarp.CreateDrivers(crt)
end
local SetWarp = function(rotation, translation)
  motionWarp.SetWarp(rotation, translation)
end
local DisableWarp = function(crt)
  motionWarp.DisableWarp(crt)
end
local Distance = function(data, position)
  local returnValue = math.huge
  local curve = data.curve
  if curve.GetDistance then
    local dist = curve:GetDistance(position, data.params.RangeTest_MaxY)
    if 0 <= dist then
      returnValue = dist
    end
  else
    local curvePosition = curve:GetPosition(curve:GetProgression(position))
    local offset = curvePosition - position
    if math.abs(offset.y) > data.params.RangeTest_MaxY then
      return math.huge
    end
    returnValue = math.sqrt(offset.x * offset.x + offset.z * offset.z)
  end
  return returnValue
end
local Closest = function(curveData, position, exclude)
  local closest, closestDistance
  for _, data in ipairs(curveData) do
    local distance = Distance(data, position)
    if data.curve ~= exclude and (not closest or closestDistance > distance) then
      closest = data
      closestDistance = distance
    end
  end
  return closest, closestDistance
end
local SubmitNavCurve = function(constants, data)
  for _, curve in ipairs(data.curves) do
    if #constants.navCurveData > 25 then
      table.remove(constants.navCurveData, 1)
    end
    table.insert(constants.navCurveData, {
      curve = curve,
      rangeTest = data.rangeTest,
      params = data.params
    })
  end
end
local ClearNavCurveData = function(constants)
  constants.navCurveData = {}
end
local SetNavCurve = function(global, constants, data, crt)
  if not data then
    crt:SetNavCurve(nil)
    global.leadTheWayParams.Role = nil
    global.leadTheWayParams.TargetCreature = nil
    global.leadTheWayParams.TargetCreatureDirBufferPos = nil
    ClearNavCurveData(constants)
    return
  end
  crt:SetNavCurve(data.curve)
  local p = data.params
  if p.Role then
    global.leadTheWayParams.Role = p.Role
  end
  if p.TargetCreature then
    global.leadTheWayParams.TargetCreature = p.TargetCreature
  end
  if p.Speed then
    global.leadTheWayParams.Speed = p.Speed
  end
  if p.LeaderDistance_Wait then
    global.leadTheWayParams.LeaderDistance_Wait = p.LeaderDistance_Wait
  end
  if p.LeaderDistance_Continue then
    global.leadTheWayParams.LeaderDistance_Continue = p.LeaderDistance_Continue
  end
  if p.LeaderDistance_Backtrack then
    global.leadTheWayParams.LeaderDistance_Backtrack = p.LeaderDistance_Backtrack
  end
  if p.LeaderDistance_BacktrackDone then
    global.leadTheWayParams.LeaderDistance_BacktrackDone = p.LeaderDistance_BacktrackDone
  end
  if p.FollowerDistance_CaughtUp then
    global.leadTheWayParams.FollowerDistance_CaughtUp = p.FollowerDistance_CaughtUp
  end
  if p.FollowerDistance_Continue then
    global.leadTheWayParams.FollowerDistance_Continue = p.FollowerDistance_Continue
  end
  if p.TargetCreatureDirBufferRad then
    global.leadTheWayParams.TargetCreatureDirBufferRad = p.TargetCreatureDirBufferRad
  end
  if p.AvoidDistance_Start then
    global.leadTheWayParams.AvoidDistance_Start = p.AvoidDistance_Start
  end
  if p.AvoidDistance_Wait then
    global.leadTheWayParams.AvoidDistance_Wait = p.AvoidDistance_Wait
  end
  if p.RangeTest_Max then
    global.leadTheWayParams.RangeTest_Max = p.RangeTest_Max
  end
  if p.RangeTest_MaxY then
    global.leadTheWayParams.RangeTest_MaxY = p.RangeTest_MaxY
  end
  if p.RangeTest_AllowSwitch then
    global.leadTheWayParams.RangeTest_AllowSwitch = p.RangeTest_AllowSwitch
  end
  if p.Loop then
    global.leadTheWayParams.Loop = p.Loop
  end
  ClearNavCurveData(constants)
end
local AssignNavCurve = function(global, constants, crt)
  local currentCurve = crt:GetNavCurve()
  local player = game.Player.FindPlayer()
  local playerPosition = player.WorldPosition
  local creaturePosition = crt.WorldPosition
  for _, data in ipairs(constants.navCurveData) do
    if data.curve == currentCurve and not data.rangeTest then
      SetNavCurve(global, constants, data, crt)
      return
    end
  end
  for _, data in ipairs(constants.navCurveData) do
    if not data.rangeTest then
      SetNavCurve(global, constants, data, crt)
      return
    end
  end
  local currentCurveData
  for _, data in ipairs(constants.navCurveData) do
    if data.curve == currentCurve then
      currentCurveData = data
    end
  end
  if not currentCurveData then
    currentCurve = nil
  end
  local nearbyCurveData = {}
  for _, data in ipairs(constants.navCurveData) do
    if data.curve ~= nil and data.rangeTest and Distance(data, playerPosition) < data.params.RangeTest_Max then
      table.insert(nearbyCurveData, data)
    end
  end
  if engine.IsDebug() then
    local offset = engine.Vector.New(0, 0.05, 0)
    for _, data in ipairs(nearbyCurveData) do
      local curvePosition = data.curve:GetPosition(data.curve:GetProgression(playerPosition))
      local curveDistance = curvePosition:Distance(playerPosition)
      local textPosition = (playerPosition + curvePosition) * 0.5
      local c = data.curve == currentCurve and curveDistance < data.params.RangeTest_AllowSwitch and color.green or color.gray
      engine.DrawLine(playerPosition + offset, curvePosition + offset, c)
      engine.DrawSphere(curvePosition + offset, 0.1, c)
      engine.DrawTextInWorld(textPosition, data.curve:GetName(), color.white)
      engine.DrawTextInWorld(textPosition, string.format("%0.2fm", curveDistance), color.white, 1)
    end
  end
  if not currentCurve then
    local closest, _ = Closest(nearbyCurveData, playerPosition)
    SetNavCurve(global, constants, closest, crt)
    return
  end
  local advancing = not global.leadTheWayParams.Backtracking
  local currentProgression = currentCurve:GetProgression(creaturePosition)
  if advancing and 0.9 < currentProgression then
    local closest, dist = Closest(nearbyCurveData, creaturePosition, currentCurve)
    if closest and dist < 2 and Distance(closest, playerPosition) < closest.params.RangeTest_AllowSwitch then
      SetNavCurve(global, constants, closest, crt)
      return
    end
  end
  local currentDistance = Distance(currentCurveData, playerPosition)
  if currentDistance < currentCurveData.params.RangeTest_AllowSwitch then
    SetNavCurve(global, constants, currentCurveData, crt)
    return
  end
  local switchRange = 5
  local closest, closestDistance = Closest(nearbyCurveData, playerPosition, currentCurve)
  if closest and currentDistance > closestDistance and switchRange > closestDistance then
    SetNavCurve(global, constants, closest, crt)
    return
  end
  if currentDistance >= currentCurveData.params.RangeTest_Max then
    SetNavCurve(global, constants, nil, crt)
    return
  end
  SetNavCurve(global, constants, currentCurveData, crt)
end
return {
  SetActuator = SetActuator,
  SetFocus = SetFocus,
  FollowNavCurve = FollowNavCurve,
  Install = Install,
  CreateDrivers = CreateDrivers,
  SetWarp = SetWarp,
  DisableWarp = DisableWarp,
  SubmitNavCurve = SubmitNavCurve,
  AssignNavCurve = AssignNavCurve,
  ClearNavCurveData = ClearNavCurveData
}
