// A bounded response by original hunter crews. Enemy positions are immutable
// radio-delivered snapshots; native perception still owns aiming and firing.
class ERR_HunterAttackRules
{
 static float ManeuverBudget(float metres) { return Math.Clamp(30 + metres / 2, 90, 240) * 1000; }
 static bool Fresh(float observed, float now) { return observed >= 0 && now >= observed && now - observed <= 60000; }
 static vector Offset(vector contact, vector forward, int role, int phase)
 {
  vector left = Vector(-forward[2], 0, forward[0]);
  if (role == 0) return contact - forward * 210;
  float side = 1; if (role == 2) side = -1;
  if (phase == 0) return contact - forward * 240 + left * (side * 155);
  if (phase == 1) return contact - forward * 85 + left * (side * 170);
  return contact + forward * 35 + left * (side * 105);
 }
 static bool Stage(vector point, vector contact, vector forward, array<vector> occupied)
 {
  float distance = vector.DistanceXZ(point, contact);
  if (distance < 140 || distance > 500 || vector.Dot(point - contact, forward) > -90) return false;
  foreach (vector other : occupied) if (vector.DistanceXZ(point, other) < 85) return false;
  return true;
 }
 static bool Foot(vector point, vector contact, vector forward, int role)
 {
  vector left = Vector(-forward[2], 0, forward[0]);
  float side = vector.Dot(point - contact, left);
  if (role == 2) side = -side;
  return side >= 65 && vector.DistanceXZ(point, contact) >= 85;
 }
}

class ERR_HunterAttack
{
 vector ReportPosition;
 vector Forward;
 float Observed;
 float Started;
 int Message;
 ref array<ref ERR_HunterTactics> Teams = {};
 bool CoverAlive()
 {
  foreach (ERR_HunterTactics team : Teams)
   if (team.Role == 0 && team.Delivered && team.Living()) return true;
  return false;
 }
 bool CoverReady()
 {
  foreach (ERR_HunterTactics team : Teams)
   if (team.Role == 0 && team.Covering()) return true;
  return false;
 }
}

class ERR_HunterTactics
{
 ERR_HunterVehicle Unit;
 ERR_HunterAttack Attack;
 int Role; // armed covering vehicle, left infantry, right infantry
 vector Stage;
 vector Approach;
 vector Sector;
 ref ERR_HQRoute Radio;
 ref array<AIWaypoint> Orders = {};
 int State; // awaiting radio, approach, dismount, wait for cover, flank, search, return, finished
 float Deadline;
 float NextRetry;
 int Retries;
 bool Delivered;
 bool Stopped;
 protected bool m_VehicleDetached;
 protected float m_TravelDistance;
 protected ERR_GameMode m_Mode;
 void ERR_HunterTactics(ERR_GameMode mode, ERR_HunterVehicle unit, ERR_HunterAttack attack, int role)
 {
  m_Mode = mode; Unit = unit; Attack = attack; Role = role;
 }
 string Label()
 {
  if (Role == 0) return "cover";
  if (Role == 1) return "left flank";
  return "right flank";
 }
 bool Living()
 {
  if (Stopped || !Unit || Unit.Released || !Unit.Group || !Unit.Entity || !ERR_Actors.Alive(Unit.Entity) || Unit.Group.GetFaction() != m_Mode.EnemyFaction()) return false;
  foreach (IEntity member : Unit.Members)
   if (ERR_Actors.CanFight(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == Unit.Group) return true;
  return false;
 }
 bool Aboard(bool required)
 {
  foreach (IEntity member : Unit.Members)
  {
   if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Unit.Group) continue;
   if ((CompartmentAccessComponent.GetVehicleIn(member) == Unit.Entity) != required) return false;
  }
  return true;
 }
 vector Position()
 {
  IEntity leader = Unit.Group.GetLeaderEntity();
  if (leader) return leader.GetOrigin();
  return Unit.Entity.GetOrigin();
 }
 protected bool VisualCover()
 {
  SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
  if (!utility) return false;
  // Cover is useful when this crew really sees a threat in the reported area,
  // not merely when a trace to an old point happens to pass between the trees.
  array<ref ERR_HQVisualContact> visible = {};
  ERR_HQObservation.Gather(utility, GetGame().GetWorld().GetWorldTime(), visible);
  foreach (ERR_HQVisualContact sighting : visible)
   if (vector.DistanceXZ(sighting.Position, Attack.ReportPosition) <= 200) return true;
  return false;
 }
 bool Covering()
 {
  if (!Living() || !Delivered || Role != 0 || State != 3 || !Unit.CrewValid(m_Mode)) return false;
  bool gunner;
  foreach (BaseCompartmentSlot seat : Unit.Seats)
   if (seat && seat.GetType() == ECompartmentType.TURRET && Unit.Members.Contains(seat.GetOccupant()) && ERR_Actors.CanFight(seat.GetOccupant())) gunner = true;
  return gunner && VisualCover();
 }
 protected void Clear()
 {
  foreach (AIWaypoint waypoint : Orders)
  {
   if (waypoint && Unit && Unit.Group) Unit.Group.RemoveWaypoint(waypoint);
   if (waypoint) SCR_EntityHelper.DeleteEntityAndChildren(waypoint);
  }
  Orders.Clear();
 }
 protected void Add(AIWaypoint waypoint) { Orders.Insert(waypoint); Unit.Group.AddWaypoint(waypoint); }
 protected bool Move(vector position, float radius = 20)
 {
  AIWaypoint move = ERR_SupplyConvoyTactics.Move(position, radius);
  if (!move) return false;
  AIWaypoint corner;
  float travel = vector.DistanceXZ(Position(), position);
  if (Role > 0 && State == 3)
  {
   vector origin = Position();
   vector delta = position - origin; delta[1] = 0;
   float lengthSq = vector.Dot(delta, delta);
   float along;
   if (lengthSq > 1) along = Math.Clamp(vector.Dot(Attack.ReportPosition - origin, delta) / lengthSq, 0, 1);
   if (vector.DistanceXZ(origin + delta * along, Attack.ReportPosition) < 130)
   {
    vector left = Vector(-Attack.Forward[2], 0, Attack.Forward[0]);
    float side = 1; if (Role == 2) side = -1;
    vector desired = Attack.ReportPosition + Attack.Forward * vector.Dot(origin - Attack.ReportPosition, Attack.Forward) + left * (side * 230);
    vector detour;
    if (!FootPosition(desired, detour)) { SCR_EntityHelper.DeleteEntityAndChildren(move); return false; }
    travel = vector.DistanceXZ(origin, detour) + vector.DistanceXZ(detour, position);
    corner = ERR_SupplyConvoyTactics.Move(detour, 20);
    if (!corner) { SCR_EntityHelper.DeleteEntityAndChildren(move); return false; }
   }
  }
  SCR_AIWaypoint commanded = SCR_AIWaypoint.Cast(move);
  if (commanded) commanded.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
  SCR_AIWaypoint first = SCR_AIWaypoint.Cast(corner);
  if (first) first.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
  Clear(); if (corner) Add(corner); Add(move); m_TravelDistance = travel; return true;
 }
 protected void VehicleUse(bool enabled)
 {
  if (!Unit || !Unit.Group || !Unit.Entity) return;
  SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
  SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
  if (!utility || !usage) return;
  if (!enabled && !m_VehicleDetached) { utility.RemoveUsableVehicle(usage); m_VehicleDetached = true; }
  else if (enabled && m_VehicleDetached && Living() && m_Mode.Active() && !ERR_ConvoyRules.Captured(Unit.Entity, Unit.Group))
  { utility.AddUsableVehicle(usage); m_VehicleDetached = false; }
 }
 protected bool Board(bool enter)
 {
  AIWaypoint created = ERR_SupplyConvoyTactics.Board(Unit.Entity, enter);
  SCR_BoardingWaypoint waypoint = SCR_BoardingWaypoint.Cast(created);
  if (!waypoint)
  {
   if (created) SCR_EntityHelper.DeleteEntityAndChildren(created);
   return false;
  }
  if (enter) VehicleUse(true);
  waypoint.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
  waypoint.SetAllowance(true, true, true);
  Clear(); Add(waypoint); return true;
 }
 protected void Note(string detail)
 {
  ERR_HQ.Event("HUNTER TACTICS", Unit.Source + " " + Label() + ": " + detail, Attack.Message);
 }
 bool Plan(array<vector> occupied)
 {
  if (!Living()) return false;
  SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
  if (!utility) return false;
  ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
  float currentRange = vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition);
  if (Role == 0 && currentRange >= 140 && currentRange <= 500 && VisualCover())
  { Stage = Unit.Entity.GetOrigin(); return true; }
  vector desired = ERR_HunterAttackRules.Offset(Attack.ReportPosition, Attack.Forward, Role, 0);
  bool found;
  for (int attempt = 0; attempt < 8; attempt++)
  {
   vector point, angles;
   vector seed = desired - Attack.Forward * (attempt * 25);
   if (!ERR_SupplyConvoySystem.ApproachDirected(seed, point, angles, 100)) continue;
   if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, Attack.Forward, occupied) || !utility.IsPositionAllowed(point) || !placement.Clear(point, angles[0])) continue;
   Stage = point; found = true; break;
  }
  // Sparse roads must not collapse every crew onto the same junction. UAZs
  // may use a checked open-ground staging area; native navigation still owns travel.
  for (int offroad = 0; !found && offroad < 8; offroad++)
  {
   vector point = desired;
   if (offroad > 0) point += Vector(Math.RandomFloat(-40, 40), 0, Math.RandomFloat(-40, 40));
   point = ERR_Actors.Ground(point);
   if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, Attack.Forward, occupied) || !utility.IsPositionAllowed(point)) continue;
   if (!placement.Clear(point, ERR_ForceRules.Heading(point, Attack.ReportPosition, 0))) continue;
   Stage = point; found = true;
  }
  // A crew already at a safe stand-off can dismount where it is instead of
  // driving across the enemy's reported position merely to fit the formation.
  if (!found && Role > 0)
  {
   vector parked = Unit.Entity.GetOrigin();
   float range = vector.DistanceXZ(parked, Attack.ReportPosition);
   bool separate = true;
   foreach (vector reserved : occupied) if (vector.DistanceXZ(parked, reserved) < 85) separate = false;
   if (range >= 140 && range <= 500 && separate) { Stage = parked; found = true; }
  }
  if (!found) { Note("no separated safe approach; keeping current orders"); return false; }
  if (Role == 0) return true;
  return FootPosition(ERR_HunterAttackRules.Offset(Attack.ReportPosition, Attack.Forward, Role, 1), Approach)
   && FootPosition(ERR_HunterAttackRules.Offset(Attack.ReportPosition, Attack.Forward, Role, 2), Sector);
 }
 protected bool FootPosition(vector desired, out vector chosen)
 {
  SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
  if (!utility) return false;
  for (int attempt = 0; attempt < 8; attempt++)
  {
   vector point = desired;
   if (attempt > 0) point += Vector(Math.RandomFloat(-15, 15), 0, Math.RandomFloat(-15, 15));
   point = ERR_Actors.Ground(point);
   if (!ERR_HunterAttackRules.Foot(point, Attack.ReportPosition, Attack.Forward, Role)) continue;
   if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) || !utility.IsPositionAllowed(point)) continue;
   if (SCR_WorldTools.TraceCilinderUtil(point + "0 1 0", 1, 2)) { chosen = point; return true; }
  }
  return false;
 }
 bool Queue(ERR_HQ hq)
 {
  Radio = hq.Downlink(Unit.Group, "HUNTER TEAM", Unit.Source + " " + Label() + " stage=" + Stage.ToString() + " sector=" + Sector.ToString(), Attack.Message);
  return Radio != null;
 }
 bool Tick(ERR_HQ hq, float now)
 {
  if (!Living()) { Stop(); return false; }
  if (!Delivered)
  {
   if (!Radio) { Stop(); return false; }
   int result = Radio.Advance(now, hq.Journal());
   if (result == 0) return false; // Normal local reactions retain ownership until delivery.
   Radio = null;
   if (result < 0 || !Move(Stage, 35)) { Stop(); return false; }
   Delivered = true; Unit.ClearOrder(); Unit.Downlink = null;
   State = 1; Deadline = now + 90000;
   Note("radio delivered; separate approach=" + Stage.ToString());
   if (Role == 0) foreach (IEntity crew : Unit.Members) ERR_ConvoyMountedCrew.Hold(crew, Unit.Entity);
  }
  // Keep the original crew's driving recovery from fighting the dismount or
  // returning to the UAZ fifteen seconds after the last visible target.
  Unit.CombatHeld = true; Unit.CombatQuietUntil = now + 15000;
  if (State == 1)
  {
   bool near = vector.DistanceXZ(Unit.Entity.GetOrigin(), Stage) <= 55;
   bool local = ERR_HunterCombat.Active(Unit.Group) && vector.DistanceXZ(Position(), Attack.ReportPosition) <= 400;
   if (Role == 0 && near) { Clear(); State = 3; Note("in covering position; native gunner owns target acquisition and fire"); }
   else if (Role > 0 && (near || local) && Board(false)) { State = 2; Deadline = now + 45000; NextRetry = now + 15000; Note("dismounting original crew"); }
   else if (now >= Deadline) { Note("approach blocked; release this assignment"); Stop(); return false; }
  }
  if (State == 2)
  {
   if (Aboard(false)) { Clear(); VehicleUse(false); State = 3; Deadline = now + 45000; Note("on foot; waiting for covering team"); }
   else if (now >= Deadline) { Note("dismount incomplete; return surviving crew"); State = 6; Deadline = now + 90000; Board(true); }
   else if (now >= NextRetry && Retries < 2) { NextRetry = now + 15000; Retries++; Board(false); }
  }
  if (State == 3 && Role > 0)
  {
   if (Attack.CoverReady() && Move(Approach, 20)) { State = 4; Deadline = now + ERR_HunterAttackRules.ManeuverBudget(m_TravelDistance); Note("cover established; moving around flank via " + Approach.ToString()); }
   else if (now >= Deadline)
   {
    // No blind rush when the covering vehicle cannot see or reach the fight.
    Note("cover unavailable; holding native combat and returning to vehicle");
    State = 6; Deadline = now + 90000; Board(true);
   }
  }
  if (State == 4 && !Attack.CoverAlive())
  { Clear(); State = 3; Deadline = now + 45000; Note("covering crew lost; manoeuvre paused"); }
  if (State == 4 && vector.DistanceXZ(Position(), Approach) <= 30)
  {
   AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), Sector));
   if (search) { search.SetCompletionRadius(25); Clear(); Add(search); State = 5; Deadline = now + 60000; Note("flank reached; searching reported sector=" + Sector.ToString()); }
  }
  if ((State == 4 || State == 5) && now >= Deadline)
  { State = 6; Deadline = now + 90000; Board(true); Note("bounded search complete; regrouping original survivors"); }
  if (State == 3 && Role == 0 && now - Attack.Started >= 300000)
  { Stop(); return false; }
  if (State == 6 && (Aboard(true) || now >= Deadline)) { Note("regroup ended; ordinary HQ travel may resume"); Stop(); return false; }
  return Delivered && !Stopped;
 }
 void Stop()
 {
  if (Stopped) return;
  VehicleUse(true);
  Stopped = true; Radio = null; Clear();
  if (!Unit) return;
  foreach (IEntity crew : Unit.Members) ERR_ConvoyMountedCrew.Release(crew);
  if (Delivered) { Unit.CombatHeld = false; Unit.NextOrder = 0; }
 }
}

class ERR_HunterCoordination
{
 protected ERR_GameMode m_Mode;
 protected ref array<ref ERR_HunterAttack> m_Attacks = {};
 protected float m_NextPlan;
 void ERR_HunterCoordination(ERR_GameMode mode) { m_Mode = mode; }
 bool Covered(vector position)
 {
  foreach (ERR_HunterAttack attack : m_Attacks)
   if (vector.DistanceXZ(position, attack.ReportPosition) < 300) return true;
  return false;
 }
 void Tick(ERR_HQ hq, array<ref ERR_HunterVehicle> hunters, float now)
 {
  for (int i = m_Attacks.Count() - 1; i >= 0; i--)
  {
   bool running;
   foreach (ERR_HunterTactics old : m_Attacks[i].Teams) if (!old.Stopped) running = true;
   if (!running) m_Attacks.Remove(i);
  }
  if (m_Attacks.Count() >= 5) return;
  if (now < m_NextPlan) return;
  m_NextPlan = now + 10000;
  array<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
  ERR_HQBelief best;
  foreach (ERR_HQReport report : reports)
  {
   ERR_HQBelief belief = hq.Memory().Get(report.Target);
   if (!belief || !belief.Actionable() || !ERR_HunterAttackRules.Fresh(belief.ObservedAt, now) || Covered(belief.PredictedPosition)) continue;
   int nearby;
   foreach (ERR_HunterVehicle candidate : hunters)
    if (candidate.Registered && !candidate.Released && candidate.CrewValid(m_Mode) && (!candidate.Tactics || candidate.Tactics.Stopped) && vector.DistanceXZ(candidate.Entity.GetOrigin(), belief.PredictedPosition) <= 1800) nearby++;
   if (nearby >= 2 && (!best || belief.ObservedAt > best.ObservedAt)) best = belief;
  }
  if (!best) return;
  array<ERR_HunterVehicle> available = {};
  foreach (ERR_HunterVehicle unit : hunters)
   if (unit.Registered && !unit.Released && unit.CrewValid(m_Mode) && (!unit.Tactics || unit.Tactics.Stopped) && vector.DistanceXZ(unit.Entity.GetOrigin(), best.PredictedPosition) <= 1800) available.Insert(unit);
  if (available.Count() < 2) return;
  ERR_HQ.Event("HUNTER COORDINATION", string.Format("%1 nearby crews available for delivered contact=%2", available.Count(), best.PredictedPosition), best.MessageId);
  ERR_HunterAttack attack = new ERR_HunterAttack();
  m_Attacks.Insert(attack);
  attack.ReportPosition = best.PredictedPosition; attack.Observed = best.ObservedAt; attack.Message = best.MessageId; attack.Started = now;
  array<vector> occupied = {};
  for (int role = 0; role < 3 && !available.IsEmpty(); role++)
  {
   int nearest; float distance = 1e20;
   vector desired = attack.ReportPosition;
   if (role > 0) desired = ERR_HunterAttackRules.Offset(attack.ReportPosition, attack.Forward, role, 0);
   for (int index = 0; index < available.Count(); index++)
   {
    float candidate = vector.DistanceXZ(available[index].Entity.GetOrigin(), desired);
    if (candidate < distance) { distance = candidate; nearest = index; }
   }
   ERR_HunterVehicle selected = available[nearest]; available.Remove(nearest);
   if (role == 0)
   {
    attack.Forward = attack.ReportPosition - selected.Entity.GetOrigin(); attack.Forward[1] = 0;
    if (attack.Forward.Length() < 1) attack.Forward = "0 0 1";
    attack.Forward.Normalize();
   }
   ERR_HunterTactics team = new ERR_HunterTactics(m_Mode, selected, attack, role);
   if (!team.Plan(occupied) || !team.Queue(hq)) { team.Stop(); if (role == 0) break; continue; }
   occupied.Insert(team.Stage); attack.Teams.Insert(team); selected.Tactics = team;
  }
  if (attack.Teams.Count() < 2)
  {
   foreach (ERR_HunterTactics incomplete : attack.Teams) incomplete.Stop();
   m_Attacks.RemoveItem(attack);
  }
 }
 void Stop()
 {
  foreach (ERR_HunterAttack attack : m_Attacks)
   foreach (ERR_HunterTactics team : attack.Teams) team.Stop();
  m_Attacks.Clear();
 }
}
