{$mode objfpc}{$H+}
unit uGroupSnapshot;

// CC 20260323 snapshot types and capture procedures for deep-copying HC group
// data from live PHCGroup pointers into DLL-owned records

interface

uses
  uHCTypeDefs, uHCDefines, uHCAnnexDefs, uHCAnnexHelpers,
  uFormationMath;  // CC 20260924 TFormationZones

type
  TTrackNodeSnapshot = record
    DestLon, DestLat: LongInt;
  end;

  // CC 20260924 formation editing (phase 1) - captured only for the focus group.
  //  See plans/Plan-FormationEdit-20260924.md.

  // CC 20260924 one unit of the focus group (ship, sub, or aircraft inside the group)
  TUnitSnapshot = record
    UUID: string;
    Name: string;
    Id: Integer;                 // THCUnit.id
    UType: HCMT;                 // THCUnit.utype
    IsAir: Boolean;
    CombatFlag: Byte;            // non-zero: aircraft on an attack (GE hides these in the editor)
    sLon, sLat: LongInt;         // actual position, slivers
    xOff, yOff: LongInt;         // station offset from group centre, DU (+yOff = south)
    Rings, Sectors: UInt64;      // formation ring/sector bitmasks
    Independent: Byte;           // FM_* formation-keeping state
    HasFormationOrder: Boolean;  // a FORMATION_EVENT is pending for this unit
    OrderXOff, OrderYOff: LongInt;       // that order's target offset, DU
    OrderRings, OrderSectors: UInt64;    // that order's target region
  end;

  // CC 20260924 aircraft aboard a ship/base of the focus group (THCUnit.myPlanes)
  TPlaneAssetSnapshot = record
    UUID: string;                // THCPlaneRec.UUID
    OwnerUnitUUID: string;       // the ship/base carrying it
    PlaneID, LoadID: Word;       // annex (memory) IDs
    Name: string;                // THCPlaneRec.name
    Status: Byte;                // READY etc.
    PlatNow: Byte;               // aircraft in this record not yet claimed by a
                                 //  patrol waiting to launch (CC 20260924 review)
    LoadoutIDX: Byte;            // CC 20260924 review: as an AIR_PATROL_EVENT stores it
    PatrolFlag: Byte;
    DefaultPatrolType: ShortInt; // CC 20260924 phase 4, from the loadout's mission
  end;

  // CC 20260924 one AIR_PATROL_EVENT whose ourGroup is the focus group
  TPatrolSnapshot = record
    EventUUID: string;           // THCHarpoonEvent.uuid
    EventTime: LongInt;          // absolute game time it next runs
    AircraftUnitUUID: string;    // THCHarpoonEvent.unit_uuid ('' until launched)
    PlaneID: Word;
    LoadoutIDX: Byte;
    PatrolType: ShortInt;        // ASuW_PATROL..EW_PATROL (1..5)
    Number: Byte;                // aircraft in the patrol
    Status: ShortInt;            // PATROL_START..PATROL_ONSTATION, PATROL_DELETE
    Rings, Sectors: UInt64;
    // CC 20260924 phase 4, staged view only (never set by the capture)
    IsNew: Boolean;              // created in the editor, not yet applied
    CancelStaged: Boolean;       // cancel staged, not yet applied
  end;

  TGroupSnapshot = record
    UUID: string;
    sLon, sLat: LongInt;
    course: SmallInt;
    kts: SmallInt;
    side: uchar;
    showing: uchar;
    gtype: HCMT;
    id: string;
    BestWeaponsRange: array[0..1] of SmallInt;
    BestSensorRange: array[0..1] of SmallInt;
    TrackNodes: array of TTrackNodeSnapshot;
    AirRangeNM: SmallInt;   // CC 20260403 total airborne range in NM (0 = not air)
    BingoRangeNM: SmallInt; // CC 20260403 bingo-adjusted range in NM (0 = not air)
    BingoPct: SmallInt;     // CC 20260403 bingo fuel percentage (0-100)
    HasUncertainty: Boolean;
    UncertSLon: array[0..3] of LongInt;
    UncertSLat: array[0..3] of LongInt;
    // CC 20260921 formation zone ring radii in NM, indexed MainBodyZone..PicketRing3Zone
    // CC 20260924 all 64 slots (decision 7: extended rings)
    Zones: TFormationZones;
    Jammed: LongInt;  // CC 20260921 non-zero while the group's radar is being jammed
    // CC 20260924 formation editing - filled only when IsFocus
    IsFocus: Boolean;
    MaxSpeed: SmallInt;          // THCGroup.maxspeed, kts
    Units: array of TUnitSnapshot;
    Assets: array of TPlaneAssetSnapshot;
    Patrols: array of TPatrolSnapshot;
  end;

  TGameTimeSnapshot = record
    GameTime: LongInt;
    GameTimeLeft: LongInt;
    PlayerSide: byte;           // gSide at capture time
    Groups: array of TGroupSnapshot;
    Count: Integer;
    Ready: Boolean;
  end;

procedure CaptureGroupSnapshot(const pGroup: PHCGroup; const pShowing: uchar;
  pStartGroup: PHCGroup; pAnnexHelper: IHCAnnexHelper;
  out snap: TGroupSnapshot);

procedure CaptureGameTimeSnapshot(pStartGroup: PHCGroup;
  aGameTime, aGameTimeLeft: LongInt;
  aPlayerSide: byte;
  pAnnexHelper: IHCAnnexHelper;
  var snap: TGameTimeSnapshot);

implementation

uses
  uGroupTypes, uHCUtils, uThreadComm;

const
  // CC 20260924 sanity cap on the HarpoonEvent walk, in case the stored head is
  //  stale (see HarpoonEventHead in MapWindow.lpr) and the links are garbage
  MAX_EVENT_WALK = 200000;

// CC 20260924 formation editing: deep-copy the focus group's units, the plane
//  records aboard its non-air units, and its AIR_PATROL_EVENTs / pending
//  FORMATION_EVENTs. HC thread only (reads live HC pointers).
procedure CaptureFocusGroupDetail(const pGroup: PHCGroup; var snap: TGroupSnapshot;
  pAnnexHelper: IHCAnnexHelper);
var
  pUnit: PHCUnit;
  pPlane: PHCPlaneRec;
  pEvent: PHCHarpoonEvent;
  pPatrol: PHCAirPatrolEvent;
  pForm: PHCFormationEvent;
  pY1: PHCAnnexY1_disk;           // CC 20260924 review
  lUnitUUID: string;
  count, i, lWalk: Integer;
  lLeft: Integer;
begin
  snap.IsFocus  := True;
  snap.MaxSpeed := pGroup^.maxspeed;

  // Units: count, then copy
  count := 0;
  pUnit := pGroup^.Units;
  while pUnit <> nil do
  begin
    Inc(count);
    pUnit := pUnit^.next;
  end;
  SetLength(snap.Units, count);
  pUnit := pGroup^.Units;
  for i := 0 to count - 1 do
  begin
    with snap.Units[i] do
    begin
      UUID        := String(pUnit^.UUID);
      Name        := String(pUnit^.name);
      Id          := pUnit^.id;
      UType       := pUnit^.utype;
      IsAir       := HCMTIsAir(pUnit^.utype);
      CombatFlag  := pUnit^.CombatFlag;
      sLon        := pUnit^.sLon;
      sLat        := pUnit^.sLat;
      xOff        := pUnit^.xOff;
      yOff        := pUnit^.yOff;
      Rings       := pUnit^.rings;
      Sectors     := pUnit^.sectors;
      Independent := pUnit^.independent;
      HasFormationOrder := False;
      OrderXOff    := 0;
      OrderYOff    := 0;
      OrderRings   := 0;
      OrderSectors := 0;
    end;
    pUnit := pUnit^.next;
  end;

  // Deck assets: every plane record aboard a non-air unit (editor filters by Status)
  count := 0;
  pUnit := pGroup^.Units;
  while pUnit <> nil do
  begin
    if not HCMTIsAir(pUnit^.utype) then
    begin
      pPlane := pUnit^.myPlanes;
      while pPlane <> nil do
      begin
        Inc(count);
        pPlane := PHCPlaneRec(pPlane^.next);
      end;
    end;
    pUnit := pUnit^.next;
  end;
  SetLength(snap.Assets, count);
  i := 0;
  pUnit := pGroup^.Units;
  while (pUnit <> nil) and (i < count) do
  begin
    if not HCMTIsAir(pUnit^.utype) then
    begin
      lUnitUUID := String(pUnit^.UUID);
      pPlane := pUnit^.myPlanes;
      while (pPlane <> nil) and (i < count) do
      begin
        with snap.Assets[i] do
        begin
          UUID          := String(pPlane^.UUID);
          OwnerUnitUUID := lUnitUUID;
          PlaneID       := pPlane^.PlaneID;
          LoadID        := pPlane^.LoadID;
          Name          := String(pPlane^.name);
          Status        := pPlane^.Status;
          PlatNow       := pPlane^.PlatNow;
          PatrolFlag    := pPlane^.PatrolFlag;
          LoadoutIDX    := HCLoadoutIndex(pPlane^.PlaneID, pPlane^.LoadID);
          // CC 20260924 phase 4: default patrol type from the loadout's mission
          DefaultPatrolType := AEW_PATROL;
          if (pAnnexHelper <> nil) and (pPlane^.LoadID <> INVALID_ID) then
          try
            pY1 := pAnnexHelper.HCGetAnnexY1(pPlane^.LoadID);
            if pY1 <> nil then
              DefaultPatrolType := DefaultPatrolTypeForMission(pY1^.MissionID);
          except
            DefaultPatrolType := AEW_PATROL;  // an annex ID HC's data doesn't know
          end;
        end;
        Inc(i);
        pPlane := PHCPlaneRec(pPlane^.next);
      end;
    end;
    pUnit := pUnit^.next;
  end;

  // Patrols and pending formation orders: one walk of HC's event list.
  //  Queued events hold absolute times (harpoon.c QueHarpoonEvent sets
  //  Time := EventTime and time_type := TimeAbs), so Time is used as is.
  SetLength(snap.Patrols, 0);
  count := 0;
  lWalk := 0;
  pEvent := gHarpoonEventHead;
  while (pEvent <> nil) and (lWalk < MAX_EVENT_WALK) do
  begin
    Inc(lWalk);
    if (pEvent^.ourGroup = Pointer(pGroup)) and (pEvent^.DataPtr <> nil) then
    begin
      if pEvent^.event_type = AIR_PATROL_EVENT then
      begin
        pPatrol := PHCAirPatrolEvent(pEvent^.DataPtr);
        if count >= Length(snap.Patrols) then
          SetLength(snap.Patrols, count * 2 + 4);
        with snap.Patrols[count] do
        begin
          EventUUID        := String(pEvent^.uuid);
          EventTime        := pEvent^.Time;
          AircraftUnitUUID := String(pEvent^.unit_uuid);
          PlaneID          := pPatrol^.PlaneID;
          LoadoutIDX       := pPatrol^.LoadoutIDX;
          PatrolType       := pPatrol^.PatrolType;
          Number           := pPatrol^.Numer;
          Status           := pPatrol^.Status;
          Rings            := pPatrol^.Rings;
          Sectors          := pPatrol^.Sectors;
        end;
        Inc(count);
      end
      else if pEvent^.event_type = FORMATION_EVENT then
      begin
        // first (soonest) pending order per unit wins
        pForm := PHCFormationEvent(pEvent^.DataPtr);
        lUnitUUID := String(pEvent^.unit_uuid);
        for i := 0 to Length(snap.Units) - 1 do
          if (snap.Units[i].UUID = lUnitUUID) and
            not snap.Units[i].HasFormationOrder then
          begin
            snap.Units[i].HasFormationOrder := True;
            snap.Units[i].OrderXOff    := pForm^.Xoffset;
            snap.Units[i].OrderYOff    := pForm^.Yoffset;
            snap.Units[i].OrderRings   := pForm^.rings;
            snap.Units[i].OrderSectors := pForm^.sectors;
            Break;
          end;
      end;
    end;
    pEvent := PHCHarpoonEvent(pEvent^.next);
  end;
  SetLength(snap.Patrols, count);

  // CC 20260924 review: a patrol that hasn't launched yet (PATROL_START) will take
  //  READY aircraft of its type and loadout from the deck (Effects.c
  //  ProcessAirPatrolEvent); take them off the deck count now so they can't be
  //  given to another patrol meanwhile
  for count := 0 to Length(snap.Patrols) - 1 do
    if snap.Patrols[count].Status = PATROL_START then
    begin
      lLeft := snap.Patrols[count].Number;
      for i := 0 to Length(snap.Assets) - 1 do
      begin
        if lLeft <= 0 then Break;
        with snap.Assets[i] do
          if ((Status and PS_READY) <> 0) and (PlaneID = snap.Patrols[count].PlaneID) and
             (LoadoutIDX = snap.Patrols[count].LoadoutIDX) and (PlatNow > 0) then
            if PlatNow >= lLeft then
            begin
              Dec(PlatNow, lLeft);
              lLeft := 0;
            end
            else
            begin
              Dec(lLeft, PlatNow);
              PlatNow := 0;
            end;
      end;
    end;
end;

procedure CaptureGroupSnapshot(const pGroup: PHCGroup; const pShowing: uchar;
  pStartGroup: PHCGroup; pAnnexHelper: IHCAnnexHelper;
  out snap: TGroupSnapshot);
var
  pRec: PHCPathRec;
  count: Integer;
  i: Integer;
  lEnduranceNM: LongInt;
  lBingo: SmallInt;
begin
  // Copy flat fields
  snap.UUID             := String(pGroup^.UUID);
  snap.sLon             := pGroup^.sLon;
  snap.sLat             := pGroup^.sLat;
  snap.course           := pGroup^.course;
  snap.kts              := pGroup^.kts;
  snap.side             := pGroup^.side;
  snap.showing          := pShowing;
  snap.gtype            := pGroup^.gtype;
  snap.id               := String(pGroup^.id);
  snap.BestWeaponsRange[0] := pGroup^.BestWeaponsRange[0];
  snap.BestWeaponsRange[1] := pGroup^.BestWeaponsRange[1];
  snap.BestSensorRange[0] := pGroup^.BestSensorRange[0];
  snap.BestSensorRange[1] := pGroup^.BestSensorRange[1];

  // CC 20260403 compute airborne endurance for air groups
  snap.AirRangeNM := 0;
  snap.BingoRangeNM := 0;
  snap.BingoPct := 0;
  if (pAnnexHelper <> nil) and HCMTIsAir(pGroup^.gtype) then
  begin
    lEnduranceNM := HCGetAirborneEndurance(pGroup, pStartGroup,
      pAnnexHelper, lBingo);
    snap.AirRangeNM := SmallInt(lEnduranceNM);
    snap.BingoPct := lBingo;

    // CC 20260403 bingo range from percentage
    if (snap.AirRangeNM > 0) and (lBingo > 0) then
      snap.BingoRangeNM := SmallInt((LongInt(snap.AirRangeNM) * lBingo) div 100);
  end;

  // CC 20260921 formation zone radii and jamming state
  for i := 0 to MAX_FORMATION_ZONES - 1 do  // CC 20260924 all 64 (extended rings)
    snap.Zones[i] := pGroup^.zones[i];
  snap.Jammed := pGroup^.jammed;

  // CC 20260924 formation detail is filled later, only for the focus group
  snap.IsFocus  := False;
  snap.MaxSpeed := 0;
  SetLength(snap.Units, 0);
  SetLength(snap.Assets, 0);
  SetLength(snap.Patrols, 0);

  // Deep copy track linked list — count nodes first
  count := 0;
  pRec := PHCPathRec(pGroup^.track);
  while pRec <> nil do
  begin
    Inc(count);
    pRec := PHCPathRec(pRec^.next);
  end;

  SetLength(snap.TrackNodes, count);
  pRec := PHCPathRec(pGroup^.track);
  for i := 0 to count - 1 do
  begin
    snap.TrackNodes[i].DestLon := pRec^.DestLon;
    snap.TrackNodes[i].DestLat := pRec^.DestLat;
    pRec := PHCPathRec(pRec^.next);
  end;

  // Copy uncertainty region if non-nil
  snap.HasUncertainty := pGroup^.UncertaintyRegion <> nil;
  if snap.HasUncertainty then
    for i := 0 to 3 do
    begin
      snap.UncertSLon[i] := pGroup^.UncertaintyRegion^.sLon[i];
      snap.UncertSLat[i] := pGroup^.UncertaintyRegion^.sLat[i];
    end;
end;

procedure CaptureGameTimeSnapshot(pStartGroup: PHCGroup;
  aGameTime, aGameTimeLeft: LongInt;
  aPlayerSide: byte;
  pAnnexHelper: IHCAnnexHelper;
  var snap: TGameTimeSnapshot);
var
  pGroup: PHCGroup;
  count: Integer;
  lVisibility: uchar;
  lFocusUUID: string;
  lFocusDone: Boolean;
begin
  // CC 20260924 read the formation-edit focus group once, under the lock
  EnterCriticalSection(gSnapshotCS);
  try
    lFocusUUID := gFocusGroupUUID;
  finally
    LeaveCriticalSection(gSnapshotCS);
  end;
  lFocusDone := lFocusUUID = '';

  snap.GameTime       := aGameTime;
  snap.GameTimeLeft   := aGameTimeLeft;
  snap.PlayerSide     := aPlayerSide;
  snap.Count          := 0;
  snap.Ready          := False;

  // First pass: count groups
  count := 0;
  pGroup := pStartGroup;
  while pGroup <> nil do
  begin
    Inc(count);
    pGroup := pGroup^.toSouth;
  end;

  SetLength(snap.Groups, count);

  // CC 20260323 Second pass: capture ALL groups with real Player-mode showing value.
  // The form thread filters by visibility mode (God vs Player) when applying.
  pGroup := pStartGroup;
  while pGroup <> nil do
  begin
    lVisibility := HCGroupShowing(aPlayerSide, pGroup);
    CaptureGroupSnapshot(pGroup, lVisibility, pStartGroup, pAnnexHelper,
      snap.Groups[snap.Count]);
    // CC 20260924 units/assets/patrols for the focus group only
    if (not lFocusDone) and (snap.Groups[snap.Count].UUID = lFocusUUID) then
    begin
      CaptureFocusGroupDetail(pGroup, snap.Groups[snap.Count], pAnnexHelper);
      lFocusDone := True;
    end;
    Inc(snap.Count);
    pGroup := pGroup^.toSouth;
  end;

  snap.Ready := True;
end;

end.
