local DL = require("design.DesignerLibrary")
local thunk = require("core.thunk")
local gDebugVFS, gDebugPrintVFS
thunk.Install("OnAIPostSpawn", function()
  gDebugVFS = engine.VFSBool.New("Debug Position Submission")
  gDebugPrintVFS = engine.VFSBool.New("Print Position Submission (one frame)")
end)
local function addRows(t, k, v)
  if type(v) == "table" then
    for k2, v2 in pairs(v) do
      local fk
      if type(k2) == "number" then
        fk = string.format("%s[%s]", k, k2)
      else
        fk = string.format("%s.%s", k, k2)
      end
      addRows(t, fk, v2)
    end
  else
    table.insert(t, {k, v})
  end
end
local gGeneratePosition = false
local gSubmittedLastFrame = false
local calculateGeneratePositionThisFrame = function()
  return gGeneratePosition
end
thunk.Install("OnAIUpdate", function(ai)
  gGeneratePosition = gSubmittedLastFrame
  gSubmittedLastFrame = false
end)
local SubmitInputs = function(ai, args)
  local aggressiveSet = ai:IsAggressive() and "Aggressive" or "NonAggressive"
  local preferCenter = false
  local goReferenceCreature = args.RefCreature
  if goReferenceCreature == nil then
    goReferenceCreature = game.Player.FindPlayer()
  end
  local constants = args.Constants
  local currentZoneSet = args.CurrentZoneSet
  local inputs = {
    OccupancyRadius = constants.OccupancyRadius,
    OccupancyPriority = constants.OccupancyPriority,
    GeneratePosition = calculateGeneratePositionThisFrame(),
    Zones = {},
    Separation = {}
  }
  if currentZoneSet == nil then
    currentZoneSet = 1
  end
  local zones = args.Zones[currentZoneSet][aggressiveSet]
  if zones == nil then
  end
  for zi, zone in ipairs(zones) do
    local adjustedZone = {
      Constraints = {},
      MinimumAvailableRadius = zone.MinimumAvailableRadius,
      PreferHighGround = false,
      HeightWeightModifier = 2
    }
    local minDistance, maxDistance, minAngle, maxAngle
    if zi == 1 then
      for _, constraint in ipairs(zone.Constraints) do
        if constraint.Constraint == "Distance" then
          minDistance = constraint.Min
          maxDistance = constraint.Max
        end
        if constraint.Constraint == "Angle" then
          minAngle = constraint.Min
          maxAngle = constraint.Max
        end
      end
    end
    for ci, constraint in ipairs(zone.Constraints) do
      local adjustedConstraint
      adjustedConstraint = {
        Position = goReferenceCreature:GetWorldPosition(),
        Forward = goReferenceCreature:GetWorldForward(),
        Constraint = constraint.Constraint,
        Min = constraint.Min,
        Max = constraint.Max,
        PreferCenter = preferCenter,
        InvalidateOffNav = constraint.InvalidateOffNav,
        UseQuadrants = constraint.UseQuadrants,
        FrontOnly = constraint.FrontOnly or false
      }
      if zi and minAngle and maxAngle and aggressiveSet == "Aggressive" then
        if math.abs(DL.FrontAngle(goReferenceCreature, ai)) < 82.5 and zi == 1 and -90 < minAngle and maxAngle < 90 then
          adjustedConstraint = {
            Position = goReferenceCreature:GetWorldPosition() + goReferenceCreature:GetWorldForward() * (minDistance + maxDistance),
            Forward = -goReferenceCreature:GetWorldForward(),
            Constraint = constraint.Constraint,
            Min = constraint.Min,
            Max = constraint.Max,
            PreferCenter = preferCenter,
            InvalidateOffNav = constraint.InvalidateOffNav,
            UseQuadrants = false,
            FrontOnly = constraint.FrontOnly or false
          }
        elseif math.abs(DL.FrontAngle(goReferenceCreature, ai)) >= 82.5 then
          adjustedConstraint.Forward = -adjustedConstraint.Forward
          if constraint.Constraint == "Angle" then
            adjustedConstraint.Min = -120
            adjustedConstraint.Max = 120
          end
        end
      end
      if aggressiveSet == "NonAggressive" and constraint.SonOnly ~= true and constraint.Constraint == "Angle" then
        if constraint.UseQuadrants == true then
          adjustedConstraint.Forward = -adjustedConstraint.Forward
          adjustedConstraint.Min = -180
          adjustedConstraint.Max = 180
        elseif constraint.PreserveOnScreen and math.abs(DL.FrontAngle(goReferenceCreature, ai)) < 75 then
          adjustedConstraint.Min = -45 - game.AIUtil.Distance(goReferenceCreature, ai)
          adjustedConstraint.Max = 45 + game.AIUtil.Distance(goReferenceCreature, ai)
        elseif math.abs(DL.FrontAngle(goReferenceCreature, ai)) >= 75 then
          adjustedConstraint.Forward = -adjustedConstraint.Forward
          adjustedConstraint.Max = 75
        end
      end
      if constraint.SonOnly then
        local goSonCreature = game.AI.FindSon()
        if goSonCreature ~= nil then
          adjustedConstraint.Position = goSonCreature:GetWorldPosition()
          adjustedConstraint.Forward = (goSonCreature:GetWorldPosition() - goReferenceCreature:GetWorldPosition()):Normalized()
        end
      end
      if constraint.AdjustForward then
        adjustedConstraint.Forward = adjustedConstraint.Forward:RotateXZ(constraint.AdjustForward)
      elseif constraint.DefendPosition then
        adjustedConstraint.Forward = (constraint.DefendPosition - adjustedConstraint.Position):Normalized()
      end
      if constraint.Invert and constraint.Constraint == "Angle" and constraint.UseQuadrants ~= true then
        adjustedConstraint.Forward = -adjustedConstraint.Forward
        adjustedConstraint.Min = -180 + constraint.Max
        adjustedConstraint.Max = 180 + constraint.Min
      end
      adjustedZone.Constraints[ci] = adjustedConstraint
    end
    inputs.Zones[zi] = adjustedZone
  end
  local haveFeatureV1 = game.CheckLibraryFeature and game.CheckLibraryFeature("QUADRANT_CONSTRAINT")
  local haveFeatureV2 = game.CHECK_FEATURE and game.CHECK_FEATURE("QUADRANT_CONSTRAINT")
  if not haveFeatureV1 and not haveFeatureV2 then
    for i = #inputs.Zones, 1, -1 do
      local zone = inputs.Zones[i]
      for j = #zone.Constraints, 1, -1 do
        if zone.Constraints[j].Constraint == "Quadrant" then
          table.remove(zone.Constraints, j)
        end
      end
    end
  end
  local separationConstraints = args.SeparationConstraints[aggressiveSet]
  inputs.Separation.Mass = separationConstraints.Mass
  for ci, constraint in ipairs(separationConstraints) do
    local adjustedConstraint = {
      Position = constraint.Position or goReferenceCreature:GetWorldPosition(),
      Forward = constraint.Forward or goReferenceCreature:GetWorldForward(),
      Constraint = constraint.Constraint,
      Value = constraint.Value,
      OnlyApplyToSon = constraint.OnlyApplyToSon,
      DoNotApplyToSon = constraint.DoNotApplyToSon
    }
    inputs.Separation[ci] = adjustedConstraint
  end
  if gDebugVFS ~= nil and gDebugVFS.value and ai:DebugIsSelectedCreature() then
    local debugTable = {
      X = 2,
      Y = 2,
      Title = "Positioning: " .. ai:GetDebugName(),
      TitleColor = engine.Vector.New(ai:DebugGetColor())
    }
    for k, v in pairs(inputs) do
      addRows(debugTable, k, v)
    end
    engine.DrawDebugTable(debugTable)
  end
  if gDebugPrintVFS ~= nil and gDebugPrintVFS.value then
    local printFn
    function printFn(ind, k, v)
      if type(v) == "table" then
        engine.Print(ind, k, " = {")
        for k2, v2 in pairs(v) do
          printFn(ind .. "  ", k2, v2)
        end
        engine.Print(ind, "},")
      elseif type(v) == "string" then
        engine.Print(ind, k, " = \"", v, "\",")
      else
        engine.Print(ind, k, " = ", v, ",")
      end
    end
    for k, v in pairs(inputs) do
      printFn("", k, v)
    end
    gDebugPrintVFS.value = false
  end
  ai:SetFightKnowledgeInputs(inputs)
  gSubmittedLastFrame = true
end
local GetResult = function(ai)
  local knowledge = ai.GetFightKnowledge and ai:GetFightKnowledge()
  if knowledge then
    return {
      Position = knowledge.SeparatedPosition
    }
  end
end
local CreateStandardPositioningInputs = function(posData)
  local angle
  if posData.OccupancyRadius > 4.5 then
    angle = 120
  elseif posData.OccupancyRadius > 4 then
    angle = 85
  elseif posData.OccupancyRadius > 3.5 then
    angle = 60
  elseif posData.OccupancyRadius > 3 then
    angle = 45
  elseif posData.OccupancyRadius > 2.5 then
    angle = 40
  elseif posData.OccupancyRadius > 2 then
    angle = 35
  elseif posData.OccupancyRadius > 1.5 then
    angle = 30
  elseif posData.OccupancyRadius > 1 then
    angle = 25
  else
    angle = 20
  end
  local preserveOnScreen = true
  local useQuadrants = true
  if posData.UseQuadrants ~= nil then
    useQuadrants = posData.UseQuadrants
  end
  if posData.PreserveOnScreen ~= nil then
    preserveOnScreen = posData.PreserveOnScreen
  end
  local positioningInputs = {
    CurrentZoneSet = "Default",
    Constants = {
      OccupancyRadius = posData.OccupancyRadius,
      OccupancyPriority = posData.OccupancyPriority
    },
    SeparationConstraints = {
      Aggressive = {
        Mass = posData.AggressiveMass,
        {
          Constraint = "RadialDistance",
          Value = posData.SeperationDistance or posData.OccupancyRadius + 2
        }
      },
      NonAggressive = {
        Mass = posData.NonAggressiveMass,
        {
          Constraint = "Distance",
          Value = posData.SeperationDistance or posData.OccupancyRadius * 3
        },
        {
          Constraint = "Angle",
          Value = posData.SeperationAngle or angle
        }
      }
    },
    Zones = {
      Default = {
        Aggressive = {
          {
            MinimumAvailableRadius = posData.OccupancyRadius,
            Constraints = {
              {
                Constraint = "Distance",
                Min = posData.AggressiveMinDistance,
                Max = posData.AggressiveMaxDistance,
                SonOnly = posData.SonOnly,
                InvalidateOffNav = true,
                FrontOnly = true
              },
              {
                Constraint = "Angle",
                Min = posData.AggressiveMinAngle or -50,
                Max = posData.AggressiveMaxAngle or 50,
                SonOnly = posData.SonOnly,
                Invert = false,
                FrontOnly = true
              }
            }
          },
          {
            MinimumAvailableRadius = posData.OccupancyRadius,
            Constraints = {
              {
                Constraint = "Distance",
                Min = posData.AggressiveMinDistance,
                Max = posData.AggressiveMaxDistance,
                SonOnly = posData.SonOnly
              },
              {
                Constraint = "Angle",
                Min = -180,
                Max = 180,
                Invert = false,
                SonOnly = posData.SonOnly,
                useQuadrants or true
              },
              {
                Constraint = "Quadrant",
                Min = posData.AggressiveMinDistance,
                Max = posData.AggressiveMaxDistance,
                UseQuadrants = useQuadrants or true
              }
            }
          },
          {
            Constraints = {
              {
                Constraint = "Distance",
                Min = posData.AggressiveMinDistance + 1,
                Max = posData.AggressiveMaxDistance + 5,
                SonOnly = posData.SonOnly
              },
              {
                Constraint = "Angle",
                Min = -180,
                Max = 180,
                Invert = false,
                SonOnly = posData.SonOnly
              }
            }
          }
        },
        NonAggressive = {
          {
            MinimumAvailableRadius = posData.OccupancyRadius,
            Constraints = {
              {
                Constraint = "Distance",
                Min = posData.NonAggressiveMinDistance,
                Max = posData.NonAggressiveMaxDistance,
                SonOnly = posData.SonOnly,
                InvalidateOffNav = not useQuadrants
              },
              {
                Constraint = "Angle",
                Min = posData.NonAggressiveMinAngle or -180,
                Max = posData.NonAggressiveMaxAngle or 180,
                Invert = false,
                PreserveOnScreen = preserveOnScreen,
                SonOnly = posData.SonOnly,
                UseQuadrants = useQuadrants or true
              },
              {
                Constraint = "Quadrant",
                UseQuadrants = useQuadrants or true
              }
            }
          },
          {
            Constraints = {
              {
                Constraint = "Distance",
                Min = posData.NonAggressiveMinDistance,
                Max = posData.NonAggressiveMaxDistance,
                SonOnly = posData.SonOnly
              },
              {
                Constraint = "Angle",
                Min = -180,
                Max = 180,
                Invert = false,
                SonOnly = posData.SonOnly
              }
            }
          }
        }
      }
    }
  }
  return positioningInputs
end
local AddStandardPositioningZone = function(zoneName, posData, posInputs)
  local preserveOnScreen = true
  if posData.PreserveOnScreen ~= nil then
    preserveOnScreen = posData.PreserveOnScreen
  end
  local useQuadrants = true
  if posData.UseQuadrants ~= nil then
    useQuadrants = posData.UseQuadrants
  end
  local zone = {
    Aggressive = {
      {
        MinimumAvailableRadius = posData.OccupancyRadius,
        Constraints = {
          {
            Constraint = "Distance",
            Min = posData.AggressiveMinDistance,
            Max = posData.AggressiveMaxDistance,
            SonOnly = posData.SonOnly
          },
          {
            Constraint = "Angle",
            Min = posData.AggressiveMinAngle or -60,
            Max = posData.AggressiveMaxAngle or 60,
            SonOnly = posData.SonOnly,
            Invert = false,
            PreserveOnScreen = preserveOnScreen
          }
        }
      },
      {
        MinimumAvailableRadius = posData.OccupancyRadius,
        Constraints = {
          {
            Constraint = "Distance",
            Min = posData.AggressiveMinDistance,
            Max = posData.AggressiveMaxDistance + 2,
            SonOnly = posData.SonOnly
          },
          {
            Constraint = "Angle",
            Min = -180,
            Max = 180,
            Invert = false,
            SonOnly = posData.SonOnly
          }
        }
      },
      {
        Constraints = {
          {
            Constraint = "Distance",
            Min = posData.AggressiveMinDistance + 1,
            Max = posData.AggressiveMaxDistance + 5,
            SonOnly = posData.SonOnly
          },
          {
            Constraint = "Angle",
            Min = -180,
            Max = 180,
            Invert = false,
            SonOnly = posData.SonOnly
          }
        }
      }
    },
    NonAggressive = {
      {
        MinimumAvailableRadius = posData.OccupancyRadius,
        Constraints = {
          {
            Constraint = "Distance",
            Min = posData.NonAggressiveMinDistance,
            Max = posData.NonAggressiveMaxDistance,
            SonOnly = posData.SonOnly,
            InvalidateOffNav = true
          },
          {
            Constraint = "Angle",
            Min = posData.NonAggressiveMinAngle or -180,
            Max = posData.NonAggressiveMaxAngle or 180,
            SonOnly = posData.SonOnly,
            Invert = false,
            PreserveOnScreen = preserveOnScreen or true,
            UseQuadrants = useQuadrants or true
          }
        }
      },
      {
        Constraints = {
          {
            Constraint = "Distance",
            Min = posData.NonAggressiveMinDistance,
            Max = posData.NonAggressiveMaxDistance,
            SonOnly = posData.SonOnly
          },
          {
            Constraint = "Angle",
            Min = -180,
            Max = 180,
            Invert = false,
            SonOnly = posData.SonOnly
          }
        }
      }
    }
  }
  posInputs.Zones[zoneName] = zone
end
return {
  SubmitInputs = SubmitInputs,
  GetResult = GetResult,
  CreateStandardPositioningInputs = CreateStandardPositioningInputs,
  AddStandardPositioningZone = AddStandardPositioningZone
}
