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

// CC 20260924 non-modal Formation Editor for the focus group. Phase 1 is a
//  read-only view: ring sizes (disabled spin edits, editable in phase 2) and
//  the GE editor's list of units, patrols in progress and deck assets.
//  See plans/Plan-FormationEdit-20260924.md.

interface

uses
  Classes, SysUtils, Forms, Controls, StdCtrls, ComCtrls, ExtCtrls, Spin, ImgList,
  uHCTypeDefs, uHCDefines, uGroupSnapshot, uFormationMath;

type
  // CC 20260924 the user picked a row; aUUID is TUnitSnapshot.UUID,
  //  TPatrolSnapshot.EventUUID or TPlaneAssetSnapshot.UUID per aKind
  TFormationSelectEvent = procedure(Sender: TObject; aKind: TFormationItemKind;
    const aUUID: string) of object;
  // CC 20260924 phase 4
  TAddPatrolEvent = procedure(Sender: TObject; const aAssetUUID: string;
    aPatrolType: ShortInt; aCount: Integer) of object;
  TPatrolEvent = procedure(Sender: TObject; const aPatrolUUID: string) of object;

  // CC 20260924 identity of one list row, kept parallel to lvItems.Items
  TFormationRow = record
    Kind: TFormationItemKind;
    UUID: string;
    Cells: array[0..5] of string;   // Name, Kind, Ring, Sector, State, Count
    ImageIndex: Integer;            // CC 20260924 unit icon, -1 for none (landed aircraft)
  end;
  TFormationRows = array of TFormationRow;

  TfrmFormationEditor = class(TForm)
    gbRings: TGroupBox;
    btnAddRing: TButton;         // CC 20260924 phase 2
    btnRemoveRing: TButton;
    chkSlowReform: TCheckBox;
    sbExtRings: TScrollBox;
    btnApply: TButton;
    btnRevert: TButton;
    pnlPatrolTools: TPanel;      // CC 20260924 phase 4
    lblPatrolType: TLabel;
    cbPatrolType: TComboBox;
    lblPatrolCount: TLabel;
    sePatrolCount: TSpinEdit;
    btnAddPatrol: TButton;
    btnCancelPatrol: TButton;
    lblMainBody: TLabel;
    lblAAW: TLabel;
    lblASW: TLabel;
    lblPicket1: TLabel;
    lblPicket2: TLabel;
    lblPicket3: TLabel;
    seMainBody: TSpinEdit;
    seAAW: TSpinEdit;
    seASW: TSpinEdit;
    sePicket1: TSpinEdit;
    sePicket2: TSpinEdit;
    sePicket3: TSpinEdit;
    gbItems: TGroupBox;
    lvItems: TListView;
    pnlBottom: TPanel;
    lblStatus: TLabel;
    btnClose: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure btnCloseClick(Sender: TObject);
    procedure lvItemsSelectItem(Sender: TObject; Item: TListItem; Selected: Boolean);
    procedure btnAddRingClick(Sender: TObject);      // CC 20260924 phase 2
    procedure btnRemoveRingClick(Sender: TObject);
    procedure btnApplyClick(Sender: TObject);
    procedure btnRevertClick(Sender: TObject);
    procedure btnAddPatrolClick(Sender: TObject);     // CC 20260924 phase 4
    procedure btnCancelPatrolClick(Sender: TObject);
  private
    FZoneEdits: array[0..UI_FORMATION_ZONES-1] of TSpinEdit;
    FRows: TFormationRows;           // row identity, index = list item index
    FDispSide: uchar;                // CC 20260924
    // CC 20260924 phase 2: ring size staging
    FExtLabels: array of TLabel;     // rings 7.. (created in code)
    FExtEdits: array of TSpinEdit;
    FHCZones: TFormationZones;       // what HC has now
    FStaged: TFormationZones;        // clamped staged sizes
    FStagedCount: Integer;           // rings in FStaged
    FZonesDirty: Boolean;            // staged sizes differ from HC, not applied yet
    FStationsStaged: Boolean;        // CC 20260924 phase 3: the owner has staged station moves
    FOnRevert: TNotifyEvent;
    FPending: Boolean;               // applied, HC hasn't caught up yet
    FPaused: Boolean;
    FLoadingZones: Boolean;          // suppress change events while filling edits
    FOnStagedChanged: TNotifyEvent;
    FOnApply: TNotifyEvent;
    FView: TGroupSnapshot;           // CC 20260924 phase 4: the staged view last shown
    FToolsFor: string;               // asset the patrol tools were last set up for
    FOnAddPatrol: TAddPatrolEvent;
    FOnCancelPatrol: TPatrolEvent;
    FSelKind: TFormationItemKind;    // current selection (survives refreshes)
    FSelUUID: string;
    FUpdating: Boolean;              // suppress OnSelectItem on programmatic changes
    FOnSelectItem: TFormationSelectEvent;
    FOnEditorClosed: TNotifyEvent;
    function UnitIconIndex(aUType: HCMT): Integer;  // CC 20260924
    function RingCountShown: Integer;
    function ZoneEdit(aRing: Integer): TSpinEdit;
    procedure SetExtRingCount(aCount: Integer);
    procedure LoadZones(const aZones: TFormationZones; aRingCount: Integer);
    procedure ZoneEditChange(Sender: TObject);
    procedure ZoneEditDone(Sender: TObject);
    procedure RestageFromEdits;
    procedure UpdateStagingUI;
    procedure UpdatePatrolTools;
    procedure BuildRows(const aSnap: TGroupSnapshot; out aRows: TFormationRows);
    procedure ApplyRows(const aRows: TFormationRows);
    procedure ApplySelection;
    function FindRow(aKind: TFormationItemKind; const aUUID: string): Integer;
  public
    // refresh everything, keeping the selected row; never fires OnSelectItem
    // CC 20260924 aDispSide is the side colour the group is shown in (unit icons)
    // CC 20260924 phase 3: aView is the staged view (new stations/areas) used
    //  for the list; aSnap is HC's, used for the ring sizes
    procedure UpdateFromSnapshot(const aSnap, aView: TGroupSnapshot; aPaused: Boolean;
      aDispSide: uchar);
    procedure SetIcons(aIcons: TCustomImageList);  // CC 20260924 group icon strip
    // CC 20260924 phase 2: staged ring sizes (valid while HasStaging)
    function HasStaging: Boolean;
    function StagedZones: TFormationZones;
    function StagedRingCount: Integer;
    function SlowForReform: Boolean;
    procedure MarkApplied(aZonesSent: Boolean); // owner queued the Apply; show it as pending
    function ZonesDirty: Boolean;      // CC 20260924 review: ring sizes edited, not applied
    procedure SetStationsStaged(aStaged: Boolean);  // CC 20260924 phase 3
    procedure DiscardStaging;  // CC 20260924 another group: forget staged sizes (no events)
    property OnRevert: TNotifyEvent read FOnRevert write FOnRevert;
    property OnAddPatrol: TAddPatrolEvent read FOnAddPatrol write FOnAddPatrol;
    property OnCancelPatrol: TPatrolEvent read FOnCancelPatrol write FOnCancelPatrol;
    property OnStagedChanged: TNotifyEvent read FOnStagedChanged write FOnStagedChanged;
    property OnApply: TNotifyEvent read FOnApply write FOnApply;
    // select a row from outside (globe click); never fires OnSelectItem
    procedure SelectItem(aKind: TFormationItemKind; const aUUID: string);
    property OnSelectItem: TFormationSelectEvent read FOnSelectItem write FOnSelectItem;
    property OnEditorClosed: TNotifyEvent read FOnEditorClosed write FOnEditorClosed;
  end;

var
  frmFormationEditor: TfrmFormationEditor;  // nil when not open

implementation

uses
  uGroupIcon;

{$R *.lfm}

const
  COL_COUNT = 6;
  // CC 20260924 ring grid geometry (matches the designer's first six rings)
  RING_COLS = 3;
  RING_COL_W = 192;
  RING_ROW_H = 30;
  STATUS_IDLE = 'Edit ring sizes, or drag a unit''s station on the map';
  STATUS_STAGED = 'Changes staged - Apply sends them to HC';
  STATUS_PENDING = 'Applied - waiting for HC';

// CC 20260924 index of the unit with this UUID in aSnap.Units, -1 if absent
function FindUnit(const aSnap: TGroupSnapshot; const aUUID: string): Integer;
var
  i: Integer;
begin
  Result := -1;
  if aUUID = '' then Exit;
  for i := 0 to System.High(aSnap.Units) do
    if aSnap.Units[i].UUID = aUUID then
      Exit(i);
end;

// CC 20260924 True when this air unit is the aircraft of one of the group's patrols
function FliesPatrol(const aSnap: TGroupSnapshot; const aUnitUUID: string): Boolean;
var
  i: Integer;
begin
  Result := False;
  if aUnitUUID = '' then Exit;
  for i := 0 to System.High(aSnap.Patrols) do
    if aSnap.Patrols[i].AircraftUnitUUID = aUnitUUID then
      Exit(True);
end;

function UnitKindText(const u: TUnitSnapshot): string;
begin
  if u.IsAir then
    Result := 'Aircraft'
  else if u.UType = MT_SUB then
    Result := 'Sub'
  else
    Result := 'Ship';
end;

procedure TfrmFormationEditor.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  FZoneEdits[MainBodyZone]    := seMainBody;
  FZoneEdits[AAWRingZone]     := seAAW;
  FZoneEdits[ASWRingZone]     := seASW;
  FZoneEdits[PicketRingZone]  := sePicket1;
  FZoneEdits[PicketRing2Zone] := sePicket2;
  FZoneEdits[PicketRing3Zone] := sePicket3;
  for i := 0 to UI_FORMATION_ZONES - 1 do
  begin
    FZoneEdits[i].MinValue := 0;
    FZoneEdits[i].MaxValue := MAX_FORMATION_RADIUS;
    FZoneEdits[i].OnChange := @ZoneEditChange;     // CC 20260924 phase 2
    FZoneEdits[i].OnEditingDone := @ZoneEditDone;
  end;
  FSelKind := fikNone;
  FSelUUID := '';
  FStagedCount := UI_FORMATION_ZONES;
  UpdateStagingUI;
end;

procedure TfrmFormationEditor.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  frmFormationEditor := nil;
  CloseAction := caFree;
  if Assigned(FOnEditorClosed) then
    FOnEditorClosed(Self);
end;

procedure TfrmFormationEditor.btnCloseClick(Sender: TObject);
begin
  Close;  // CC 20260924 goes through FormClose so OnEditorClosed fires
end;

procedure TfrmFormationEditor.lvItemsSelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
var
  idx: Integer;
begin
  if FUpdating or not Selected or (Item = nil) then Exit;
  idx := Item.Index;
  if (idx < 0) or (idx > System.High(FRows)) then Exit;
  if (FRows[idx].Kind = FSelKind) and (FRows[idx].UUID = FSelUUID) then Exit;
  FSelKind := FRows[idx].Kind;
  FSelUUID := FRows[idx].UUID;
  UpdatePatrolTools;  // CC 20260924 phase 4
  if Assigned(FOnSelectItem) then
    FOnSelectItem(Self, FSelKind, FSelUUID);
end;

// CC 20260924 the GE editor's entries, in its order: units (SHIP_FORM/AIR_FORM),
//  patrols in progress, then READY deck assets (ASSET_FORM)
procedure TfrmFormationEditor.BuildRows(const aSnap: TGroupSnapshot;
  out aRows: TFormationRows);
var
  i, j, n: Integer;
  s: string;
  lXOff, lYOff: LongInt;  // CC 20260924
  lInArea: Boolean;
begin
  aRows := nil;
  SetLength(aRows, Length(aSnap.Units) + Length(aSnap.Patrols) + Length(aSnap.Assets));
  n := 0;

  // units; aircraft on an attack are hidden, as in the GE
  // CC 20260924 an aircraft flying a patrol is listed once, on its patrol's row
  //  (the GE's AIR_FORM entry), not also as a unit
  for i := 0 to System.High(aSnap.Units) do
    with aSnap.Units[i] do
    begin
      if IsAir and (CombatFlag <> 0) then Continue;
      if IsAir and FliesPatrol(aSnap, UUID) then Continue;
      aRows[n].Kind := fikUnit;
      aRows[n].UUID := UUID;
      aRows[n].Cells[0] := Name;
      aRows[n].Cells[1] := UnitKindText(aSnap.Units[i]);
      aRows[n].Cells[2] := RingsText(Rings);
      aRows[n].Cells[3] := SectorsText(Sectors);
      s := FormationStateText(Independent);
      if HasFormationOrder then
        s := s + ' (moving to ' + RingsText(OrderRings) + ')';
      aRows[n].Cells[4] := s;
      aRows[n].Cells[5] := '';
      aRows[n].ImageIndex := UnitIconIndex(UType);
      Inc(n);
    end;

  // patrols in progress
  for i := 0 to System.High(aSnap.Patrols) do
    with aSnap.Patrols[i] do
    begin
      s := PatrolTypeText(PatrolType) + ' patrol';
      j := FindUnit(aSnap, AircraftUnitUUID);
      if j >= 0 then
        s := s + ' - ' + aSnap.Units[j].Name;
      aRows[n].Kind := fikPatrol;
      aRows[n].UUID := EventUUID;
      aRows[n].Cells[0] := s;
      aRows[n].Cells[1] := 'Patrol';
      aRows[n].Cells[2] := RingsText(Rings);
      aRows[n].Cells[3] := SectorsText(Sectors);
      // CC 20260924 derived state: HC's own status names run a step early
      if j >= 0 then
      begin
        LatLonToFormationOffset(aSnap.sLat, aSnap.sLon,
          aSnap.Units[j].sLat, aSnap.Units[j].sLon, lXOff, lYOff);
        lInArea := InPatrolArea(lXOff, lYOff, Rings, Sectors, aSnap.Zones);
      end
      else
        lInArea := False;
      aRows[n].Cells[4] := PatrolStateText(Status, Number, j >= 0, lInArea);
      // CC 20260924 phase 4: staged create / cancel
      if IsNew then
        aRows[n].Cells[4] := 'New - starts on Apply'
      else if CancelStaged then
        aRows[n].Cells[4] := 'Cancel on Apply';
      aRows[n].Cells[5] := IntToStr(Number);
      // CC 20260924 the airborne aircraft's own icon; aircraft until one launches
      if j >= 0 then
        aRows[n].ImageIndex := UnitIconIndex(aSnap.Units[j].UType)
      else
        aRows[n].ImageIndex := UnitIconIndex(HCMT(MT_AC));
      Inc(n);
    end;

  // aircraft ready on deck (HC Combat.h READY = PS_READY = $08)
  for i := 0 to System.High(aSnap.Assets) do
    with aSnap.Assets[i] do
    begin
      if (Status and PS_READY) = 0 then Continue;
      j := FindUnit(aSnap, OwnerUnitUUID);
      if j >= 0 then
        s := 'On deck (' + aSnap.Units[j].Name + ')'
      else
        s := 'On deck';
      aRows[n].Kind := fikAsset;
      aRows[n].UUID := UUID;
      aRows[n].Cells[0] := Name;
      aRows[n].Cells[1] := s;
      aRows[n].Cells[2] := '';
      aRows[n].Cells[3] := '';
      aRows[n].Cells[4] := 'Ready';
      aRows[n].Cells[5] := IntToStr(PlatNow);
      aRows[n].ImageIndex := -1;  // CC 20260924 landed: no icon, the Kind names its parent
      Inc(n);
    end;

  SetLength(aRows, n);
end;

// CC 20260924 update list items in place (only changed cells are written) so a
//  refresh doesn't flicker or lose the scroll position
procedure TfrmFormationEditor.ApplyRows(const aRows: TFormationRows);
var
  i, c: Integer;
  li: TListItem;
  origin: TPoint;
begin
  origin := lvItems.ViewOrigin;
  lvItems.Items.BeginUpdate;
  try
    while lvItems.Items.Count > Length(aRows) do
      lvItems.Items.Delete(lvItems.Items.Count - 1);
    while lvItems.Items.Count < Length(aRows) do
      lvItems.Items.Add;
    for i := 0 to System.High(aRows) do
    begin
      li := lvItems.Items[i];
      if li.Caption <> aRows[i].Cells[0] then
        li.Caption := aRows[i].Cells[0];
      if li.ImageIndex <> aRows[i].ImageIndex then
        li.ImageIndex := aRows[i].ImageIndex;  // CC 20260924
      while li.SubItems.Count < COL_COUNT - 1 do
        li.SubItems.Add('');
      for c := 1 to COL_COUNT - 1 do
        if li.SubItems[c - 1] <> aRows[i].Cells[c] then
          li.SubItems[c - 1] := aRows[i].Cells[c];
    end;
    FRows := aRows;
  finally
    lvItems.Items.EndUpdate;
  end;
  if (lvItems.ViewOrigin.X <> origin.X) or (lvItems.ViewOrigin.Y <> origin.Y) then
    lvItems.ViewOrigin := origin;
end;

function TfrmFormationEditor.FindRow(aKind: TFormationItemKind;
  const aUUID: string): Integer;
var
  i: Integer;
begin
  Result := -1;
  if (aKind = fikNone) or (aUUID = '') then Exit;
  for i := 0 to System.High(FRows) do
    if (FRows[i].Kind = aKind) and (FRows[i].UUID = aUUID) then
      Exit(i);
end;

// CC 20260924 show FSelKind/FSelUUID in the list; the wanted selection is kept
//  even if its row is missing, so it comes back when the row does
procedure TfrmFormationEditor.ApplySelection;
var
  idx: Integer;
begin
  idx := FindRow(FSelKind, FSelUUID);
  FUpdating := True;
  try
    if idx < 0 then
      lvItems.Selected := nil
    else if lvItems.Selected <> lvItems.Items[idx] then
      lvItems.Selected := lvItems.Items[idx];
  finally
    FUpdating := False;
  end;
end;

// CC 20260924 index into the group icon strip for a unit type in this group's
//  side colour, -1 when there are no icons or the type has none
function TfrmFormationEditor.UnitIconIndex(aUType: HCMT): Integer;
begin
  Result := -1;
  if lvItems.SmallImages = nil then Exit;
  Result := GetIconIndex(FDispSide, '', AnsiChar(aUType));
  if (Result < 0) or (Result >= lvItems.SmallImages.Count) then
    Result := -1;
end;

procedure TfrmFormationEditor.SetIcons(aIcons: TCustomImageList);
begin
  lvItems.SmallImages := aIcons;
end;

procedure TfrmFormationEditor.UpdateFromSnapshot(const aSnap, aView: TGroupSnapshot;
  aPaused: Boolean; aDispSide: uchar);
var
  i: Integer;
  rows: TFormationRows;
  s: string;
begin
  FDispSide := aDispSide;  // CC 20260924
  FUpdating := True;
  try
    s := 'Formation Editor - ' + aSnap.id;
    if Caption <> s then
      Caption := s;
    // CC 20260924 phase 2: HC's sizes fill the edits only when nothing is staged;
    //  an applied change stays pending until HC reports the same sizes
    FHCZones := aSnap.Zones;
    if FPending and CompareMem(@FHCZones, @FStaged, SizeOf(FStaged)) then
      FPending := False;
    if not FZonesDirty and not FPending then
    begin
      i := EnabledRingCount(FHCZones);
      if i < UI_FORMATION_ZONES then i := UI_FORMATION_ZONES;
      LoadZones(FHCZones, i);
      FStaged := FHCZones;
      FStagedCount := i;
    end;
    FView := aView;  // CC 20260924 phase 4
    BuildRows(aView, rows);
    ApplyRows(rows);
  finally
    FUpdating := False;
  end;
  ApplySelection;

  FPaused := aPaused;
  UpdateStagingUI;
  UpdatePatrolTools;
end;

// ---------------------------------------------------------------------------
// CC 20260924 phase 2: ring size staging
// ---------------------------------------------------------------------------

function TfrmFormationEditor.RingCountShown: Integer;
begin
  Result := UI_FORMATION_ZONES + Length(FExtEdits);
end;

function TfrmFormationEditor.ZoneEdit(aRing: Integer): TSpinEdit;
begin
  if aRing < UI_FORMATION_ZONES then
    Result := FZoneEdits[aRing]
  else
    Result := FExtEdits[aRing - UI_FORMATION_ZONES];
end;

// rings 7.. live in sbExtRings, three per row like the designer's first six
procedure TfrmFormationEditor.SetExtRingCount(aCount: Integer);
var
  n, k, r: Integer;
begin
  if aCount < 0 then aCount := 0;
  if aCount > MAX_FORMATION_ZONES - UI_FORMATION_ZONES then
    aCount := MAX_FORMATION_ZONES - UI_FORMATION_ZONES;
  n := Length(FExtEdits);
  while n > aCount do
  begin
    Dec(n);
    FExtEdits[n].Free;
    FExtLabels[n].Free;
    SetLength(FExtEdits, n);
    SetLength(FExtLabels, n);
  end;
  while n < aCount do
  begin
    SetLength(FExtEdits, n + 1);
    SetLength(FExtLabels, n + 1);
    r := UI_FORMATION_ZONES + n;  // ring index
    k := n;                       // position in the extended grid
    FExtLabels[n] := TLabel.Create(Self);
    FExtLabels[n].Parent := sbExtRings;
    FExtLabels[n].Left := 4 + (k mod RING_COLS) * RING_COL_W;
    FExtLabels[n].Top := 4 + (k div RING_COLS) * RING_ROW_H + 4;
    FExtLabels[n].Caption := RingName(r);
    FExtEdits[n] := TSpinEdit.Create(Self);
    FExtEdits[n].Parent := sbExtRings;
    FExtEdits[n].Left := 80 + (k mod RING_COLS) * RING_COL_W;
    FExtEdits[n].Top := 4 + (k div RING_COLS) * RING_ROW_H;
    FExtEdits[n].Width := 72;
    FExtEdits[n].MinValue := 0;
    FExtEdits[n].MaxValue := MAX_FORMATION_RADIUS;
    FExtEdits[n].OnChange := @ZoneEditChange;
    FExtEdits[n].OnEditingDone := @ZoneEditDone;
    Inc(n);
  end;
end;

procedure TfrmFormationEditor.LoadZones(const aZones: TFormationZones;
  aRingCount: Integer);
var
  i: Integer;
begin
  FLoadingZones := True;
  try
    SetExtRingCount(aRingCount - UI_FORMATION_ZONES);
    for i := 0 to RingCountShown - 1 do
      if ZoneEdit(i).Value <> aZones[i] then
        ZoneEdit(i).Value := aZones[i];
  finally
    FLoadingZones := False;
  end;
end;

// read the edits, clamp like the GE, compare with HC
procedure TfrmFormationEditor.RestageFromEdits;
var
  i: Integer;
begin
  FillChar(FStaged, SizeOf(FStaged), 0);
  FStagedCount := RingCountShown;
  for i := 0 to FStagedCount - 1 do
    FStaged[i] := ZoneEdit(i).Value;
  ClampFormationZones(FStaged, FStagedCount);
  FZonesDirty := not CompareMem(@FStaged, @FHCZones, SizeOf(FStaged));
  if FZonesDirty then
    FPending := False;
  UpdateStagingUI;
  if Assigned(FOnStagedChanged) then
    FOnStagedChanged(Self);
end;

procedure TfrmFormationEditor.ZoneEditChange(Sender: TObject);
begin
  if FLoadingZones then Exit;
  RestageFromEdits;   // the edits keep what was typed; ZoneEditDone shows the clamp
end;

procedure TfrmFormationEditor.ZoneEditDone(Sender: TObject);
begin
  if FLoadingZones then Exit;
  RestageFromEdits;
  LoadZones(FStaged, FStagedCount);
end;

procedure TfrmFormationEditor.btnAddRingClick(Sender: TObject);
var
  n, lNew: Integer;
begin
  n := RingCountShown;
  if n >= MAX_FORMATION_ZONES then Exit;
  // new ring as wide as the band inside it
  lNew := ZoneEdit(n - 1).Value + (ZoneEdit(n - 1).Value - ZoneEdit(n - 2).Value);
  if lNew <= ZoneEdit(n - 1).Value then
    lNew := ZoneEdit(n - 1).Value + 1;
  FLoadingZones := True;
  try
    SetExtRingCount(n + 1 - UI_FORMATION_ZONES);
    ZoneEdit(n).Value := lNew;
  finally
    FLoadingZones := False;
  end;
  RestageFromEdits;
  LoadZones(FStaged, FStagedCount);
  sbExtRings.VertScrollBar.Position := sbExtRings.VertScrollBar.Range;
end;

procedure TfrmFormationEditor.btnRemoveRingClick(Sender: TObject);
begin
  if Length(FExtEdits) = 0 then Exit;  // the GE's six rings always stay
  FLoadingZones := True;
  try
    SetExtRingCount(Length(FExtEdits) - 1);
  finally
    FLoadingZones := False;
  end;
  RestageFromEdits;
end;

procedure TfrmFormationEditor.btnApplyClick(Sender: TObject);
begin
  if not (FZonesDirty or FStationsStaged) then Exit;
  RestageFromEdits;
  LoadZones(FStaged, FStagedCount);
  if Assigned(FOnApply) then
    FOnApply(Self);
end;

procedure TfrmFormationEditor.btnRevertClick(Sender: TObject);
var
  n: Integer;
begin
  FZonesDirty := False;
  FStationsStaged := False;
  FPending := False;
  n := EnabledRingCount(FHCZones);
  if n < UI_FORMATION_ZONES then n := UI_FORMATION_ZONES;
  LoadZones(FHCZones, n);
  FStaged := FHCZones;
  FStagedCount := n;
  UpdateStagingUI;
  if Assigned(FOnRevert) then
    FOnRevert(Self);   // CC 20260924 owner drops staged station moves
  if Assigned(FOnStagedChanged) then
    FOnStagedChanged(Self);
end;

procedure TfrmFormationEditor.UpdateStagingUI;
var
  s: string;
begin
  btnApply.Enabled := FZonesDirty or FStationsStaged;
  btnRevert.Enabled := FZonesDirty or FStationsStaged or FPending;
  btnRemoveRing.Enabled := Length(FExtEdits) > 0;
  btnAddRing.Enabled := RingCountShown < MAX_FORMATION_ZONES;
  if FZonesDirty or FStationsStaged then
    s := STATUS_STAGED
  else if FPending then
    s := STATUS_PENDING
  else
    s := STATUS_IDLE;
  if FPaused then
  begin
    if FZonesDirty or FStationsStaged or FPending then
      s := s + ' (game paused: applies when time resumes)'
    else
      s := s + '  -  Game paused';
  end;
  if lblStatus.Caption <> s then
    lblStatus.Caption := s;
end;

function TfrmFormationEditor.HasStaging: Boolean;
begin
  Result := FZonesDirty or FPending;
end;

function TfrmFormationEditor.StagedZones: TFormationZones;
begin
  Result := FStaged;
end;

function TfrmFormationEditor.StagedRingCount: Integer;
begin
  Result := FStagedCount;
end;

function TfrmFormationEditor.SlowForReform: Boolean;
begin
  Result := chkSlowReform.Checked;
end;

// CC 20260924 phase 4: Add patrol works on an "On deck" row (count up to the
//  aircraft still unassigned there, type defaulting from its loadout); Cancel
//  patrol works on a patrol row (a new one is removed, an existing one is
//  cancelled on Apply, and pressing it again undoes that)
procedure TfrmFormationEditor.UpdatePatrolTools;
var
  i: Integer;
  lAsset, lPatrol: Integer;
begin
  lAsset := -1;
  lPatrol := -1;
  if FSelKind = fikAsset then
    for i := 0 to System.High(FView.Assets) do
      if FView.Assets[i].UUID = FSelUUID then lAsset := i;
  if FSelKind = fikPatrol then
    for i := 0 to System.High(FView.Patrols) do
      if FView.Patrols[i].EventUUID = FSelUUID then lPatrol := i;

  if lAsset >= 0 then
  begin
    sePatrolCount.MaxValue := FView.Assets[lAsset].PlatNow;
    if FToolsFor <> FSelUUID then
    begin
      // a newly picked asset: defaults from its loadout
      FToolsFor := FSelUUID;
      cbPatrolType.ItemIndex := FView.Assets[lAsset].DefaultPatrolType - 1;
      sePatrolCount.Value := 1;
    end;
    btnAddPatrol.Enabled := FView.Assets[lAsset].PlatNow > 0;
  end
  else
    btnAddPatrol.Enabled := False;

  btnCancelPatrol.Enabled := lPatrol >= 0;
  if lPatrol < 0 then
    btnCancelPatrol.Caption := 'Cancel patrol'
  else if FView.Patrols[lPatrol].IsNew then
    btnCancelPatrol.Caption := 'Remove patrol'
  else if FView.Patrols[lPatrol].CancelStaged then
    btnCancelPatrol.Caption := 'Keep patrol'
  else
    btnCancelPatrol.Caption := 'Cancel patrol';
end;

procedure TfrmFormationEditor.btnAddPatrolClick(Sender: TObject);
begin
  if (FSelKind <> fikAsset) or (cbPatrolType.ItemIndex < 0) then Exit;
  if Assigned(FOnAddPatrol) then
    FOnAddPatrol(Self, FSelUUID, ShortInt(cbPatrolType.ItemIndex + 1), sePatrolCount.Value);
end;

procedure TfrmFormationEditor.btnCancelPatrolClick(Sender: TObject);
begin
  if FSelKind <> fikPatrol then Exit;
  if Assigned(FOnCancelPatrol) then
    FOnCancelPatrol(Self, FSelUUID);
end;

// CC 20260924 review: pending (waiting for HC to report the sizes) only when
//  the sizes were actually sent
procedure TfrmFormationEditor.MarkApplied(aZonesSent: Boolean);
begin
  FZonesDirty := False;
  FStationsStaged := False;
  if aZonesSent then
    FPending := True;
  UpdateStagingUI;
end;

function TfrmFormationEditor.ZonesDirty: Boolean;
begin
  Result := FZonesDirty;
end;

procedure TfrmFormationEditor.DiscardStaging;
begin
  FZonesDirty := False;
  FStationsStaged := False;
  FPending := False;   // the next UpdateFromSnapshot loads the new group's sizes
  UpdateStagingUI;
end;

procedure TfrmFormationEditor.SetStationsStaged(aStaged: Boolean);
begin
  if FStationsStaged = aStaged then Exit;
  FStationsStaged := aStaged;
  UpdateStagingUI;
end;

procedure TfrmFormationEditor.SelectItem(aKind: TFormationItemKind; const aUUID: string);
var
  idx: Integer;
begin
  FSelKind := aKind;
  FSelUUID := aUUID;
  ApplySelection;
  idx := FindRow(FSelKind, FSelUUID);
  if idx >= 0 then
    lvItems.Items[idx].MakeVisible(False);
  UpdatePatrolTools;  // CC 20260924 phase 4
end;

end.
