unit uHCLAutosaveCleanup;

{$mode objfpc}{$H+}

/// <summary>
/// uHCLAutosaveCleanup — the "Clean up autosaves" window.
///
/// Lists the GE's iterative autosaves (<c>ITER_SAVE_GAMETIME</c>) one row per
/// base name, with count, size, dates and the scenario it belongs to, and
/// offers: keep the newest N, delete those older than N days, delete, and
/// move to the scenario's <c>SavedGames\AutoSaves</c>.  Every removal is
/// confirmed with what it will take, and goes to the Recycle Bin.  Nothing
/// happens on its own.  The logic is in <c>uHCLAutosaves</c>; this is only
/// the window, built in code.
/// </summary>

interface

uses
  Classes, SysUtils, StrUtils, Forms, Controls, StdCtrls, ComCtrls, ExtCtrls, Spin, Dialogs,
  uHCLFileOrganization, uHCLAutosaves;

/// <summary>Shows the cleanup window modally.</summary>
/// <param name="AGEDir">The GE's folder; autosaves are in its ITER_SAVE_GAMETIME.</param>
/// <param name="AHCDir">The HC directory, for battleset names.</param>
/// <param name="AFileOrg">The managed tree, for matching saves to installed scenarios; may be nil.</param>
procedure HCLShowAutosaveCleanup(const AGEDir, AHCDir: String;
  const AFileOrg: IHCLFileOrganization);

implementation

uses
  uHCLBattleSets;

type
  TfrmAutosaveCleanup = class(TForm)
  private
    FDir, FHCDir: String;
    FFileOrg: IHCLFileOrganization;
    FGroups: THCLAutosaveGroupArray;
    FScenarioDirs: array of String;
    lblFolder, lblSummary: TLabel;
    lvGroups: TListView;
    edtKeep, edtDays: TSpinEdit;
    btnKeep, btnOlder, btnDelete, btnMove, btnSelectAll, btnRefresh, btnClose: TButton;
    procedure Rescan;
    function CheckedGroups: TList;
    function Confirm(const AWhat: String; const AFiles: THCLAutosaveArray): Boolean;
    procedure Remove(const AFiles: THCLAutosaveArray; const AWhat: String);
    procedure KeepClick(Sender: TObject);
    procedure OlderClick(Sender: TObject);
    procedure DeleteClick(Sender: TObject);
    procedure MoveClick(Sender: TObject);
    procedure SelectAllClick(Sender: TObject);
    procedure RefreshClick(Sender: TObject);
    function Describe(const ABase: String; out AScenarioDir: String): String;
  public
    constructor CreateFor(const AGEDir, AHCDir: String; const AFileOrg: IHCLFileOrganization);
  end;

function FormatMB(ABytes: Int64): String;
begin
  Result := Format('%.1f MB', [ABytes / (1024 * 1024)]);
end;

procedure Append(var ATo: THCLAutosaveArray; const AFrom: THCLAutosaveArray);
var
  I: Integer;
begin
  for I := 0 to High(AFrom) do
  begin
    SetLength(ATo, Length(ATo) + 1);
    ATo[High(ATo)] := AFrom[I];
  end;
end;

constructor TfrmAutosaveCleanup.CreateFor(const AGEDir, AHCDir: String;
  const AFileOrg: IHCLFileOrganization);

  function MakeButton(const ACaption: String; ALeft, ATop, AWidth: Integer;
    AClick: TNotifyEvent): TButton;
  begin
    Result := TButton.Create(Self);
    Result.Parent := Self;
    Result.Caption := ACaption;
    Result.SetBounds(ALeft, ATop, AWidth, 25);
    Result.Anchors := [akLeft, akBottom];
    Result.OnClick := AClick;
  end;

  procedure AddColumn(const ACaption: String; AWidth: Integer);
  begin
    with lvGroups.Columns.Add do
    begin
      Caption := ACaption;
      Width := AWidth;
    end;
  end;

var
  L: TLabel;
begin
  inherited CreateNew(nil);
  FDir := IncludeTrailingPathDelimiter(AGEDir) + HCL_ITER_SAVE_DIR;
  FHCDir := AHCDir;
  FFileOrg := AFileOrg;

  Caption := 'Clean up autosaves';
  Position := poOwnerFormCenter;
  SetBounds(0, 0, 820, 480);
  Constraints.MinWidth := 700;
  Constraints.MinHeight := 320;

  lblFolder := TLabel.Create(Self);
  lblFolder.Parent := Self;
  lblFolder.SetBounds(8, 8, 800, 16);
  lblFolder.Caption := 'Iterative autosaves in ' + FDir + '  (deleted files go to the Recycle Bin)';

  lvGroups := TListView.Create(Self);
  lvGroups.Parent := Self;
  lvGroups.SetBounds(8, 28, 804, 330);
  lvGroups.Anchors := [akLeft, akTop, akRight, akBottom];
  lvGroups.ViewStyle := vsReport;
  lvGroups.Checkboxes := True;
  lvGroups.RowSelect := True;
  lvGroups.ReadOnly := True;
  AddColumn('Saves of', 110);
  AddColumn('Belongs to', 330);
  AddColumn('Files', 50);
  AddColumn('Size', 70);
  AddColumn('Oldest', 110);
  AddColumn('Newest', 110);

  lblSummary := TLabel.Create(Self);
  lblSummary.Parent := Self;
  lblSummary.SetBounds(8, 364, 800, 16);
  lblSummary.Anchors := [akLeft, akBottom];

  // For the checked rows:
  L := TLabel.Create(Self);
  L.Parent := Self;
  L.SetBounds(8, 392, 120, 16);
  L.Anchors := [akLeft, akBottom];
  L.Caption := 'Checked rows:';

  edtKeep := TSpinEdit.Create(Self);
  edtKeep.Parent := Self;
  edtKeep.SetBounds(130, 388, 60, 23);
  edtKeep.Anchors := [akLeft, akBottom];
  edtKeep.MinValue := 0;
  edtKeep.MaxValue := 9999;
  edtKeep.Value := 10;
  btnKeep := MakeButton('Keep newest', 194, 387, 100, @KeepClick);

  edtDays := TSpinEdit.Create(Self);
  edtDays.Parent := Self;
  edtDays.SetBounds(310, 388, 60, 23);
  edtDays.Anchors := [akLeft, akBottom];
  edtDays.MinValue := 0;
  edtDays.MaxValue := 36500;
  edtDays.Value := 30;
  btnOlder := MakeButton('Delete older than (days)', 374, 387, 170, @OlderClick);

  btnDelete := MakeButton('Delete all', 550, 387, 90, @DeleteClick);
  btnMove := MakeButton('Move to scenario''s AutoSaves', 8, 420, 210, @MoveClick);
  btnMove.Hint := 'Only rows that belong to an installed scenario: moves their files into its SavedGames\AutoSaves';
  btnMove.ShowHint := True;
  btnSelectAll := MakeButton('Check all', 224, 420, 90, @SelectAllClick);
  btnRefresh := MakeButton('Refresh', 320, 420, 90, @RefreshClick);
  btnClose := MakeButton('Close', 722, 420, 90, nil);
  btnClose.Anchors := [akRight, akBottom];
  btnClose.ModalResult := mrClose;
  btnClose.Cancel := True;

  Rescan;
end;

function TfrmAutosaveCleanup.Describe(const ABase: String; out AScenarioDir: String): String;
var
  BS, Scen: Integer;
  Info: THCLBattleSetInfo;
begin
  AScenarioDir := HCLFindScenarioForBase(FFileOrg, ABase);
  if AScenarioDir <> '' then
    Exit('Scenario ' + ExtractFileName(AScenarioDir) + ' (' +
      ExtractFileName(ExtractFileDir(ExtractFileDir(AScenarioDir))) + ')');
  if HCLParseBuiltinBase(ABase, BS, Scen) then
  begin
    Info := HCLBattleSetInfo(FHCDir, BS);
    if Info.FileStem <> '' then
      Exit(Format('Battleset %s, scenario %d: %s', [Info.Abbrev, Scen, Info.Name]))
    else
      Exit(Format('Battleset %d, scenario %d', [BS, Scen]));
  end;
  Result := '(a scenario that is not installed here, or a -k base name)';
end;

procedure TfrmAutosaveCleanup.Rescan;
var
  I, Total: Integer;
  Bytes: Int64;
  Item: TListItem;
  ScenDir: String;
begin
  FGroups := HCLScanAutosaves(FDir);
  SetLength(FScenarioDirs, Length(FGroups));
  lvGroups.Items.BeginUpdate;
  try
    lvGroups.Items.Clear;
    Total := 0;
    Bytes := 0;
    for I := 0 to High(FGroups) do
    begin
      Item := lvGroups.Items.Add;
      Item.Caption := FGroups[I].Base;
      Item.SubItems.Add(Describe(FGroups[I].Base, ScenDir));
      FScenarioDirs[I] := ScenDir;
      Item.SubItems.Add(IntToStr(Length(FGroups[I].Files)));
      Item.SubItems.Add(FormatMB(FGroups[I].TotalSize));
      Item.SubItems.Add(FormatDateTime('yyyy-mm-dd hh:nn', FGroups[I].Oldest));
      Item.SubItems.Add(FormatDateTime('yyyy-mm-dd hh:nn', FGroups[I].Newest));
      Item.Data := Pointer(PtrInt(I));
      Inc(Total, Length(FGroups[I].Files));
      Inc(Bytes, FGroups[I].TotalSize);
    end;
  finally
    lvGroups.Items.EndUpdate;
  end;
  if Length(FGroups) = 0 then
    lblSummary.Caption := 'No iterative autosaves.'
  else
    lblSummary.Caption := Format('%d autosave(s), %s, in %d group(s).',
      [Total, FormatMB(Bytes), Length(FGroups)]);
end;

function TfrmAutosaveCleanup.CheckedGroups: TList;
var
  I: Integer;
begin
  Result := TList.Create;
  for I := 0 to lvGroups.Items.Count - 1 do
    if lvGroups.Items[I].Checked then
      Result.Add(lvGroups.Items[I].Data);
end;

function TfrmAutosaveCleanup.Confirm(const AWhat: String; const AFiles: THCLAutosaveArray): Boolean;
begin
  if Length(AFiles) = 0 then
  begin
    MessageDlg('Clean up autosaves', 'Nothing to do: ' + AWhat + ' matches no files.',
      mtInformation, [mbOK], 0);
    Exit(False);
  end;
  Result := MessageDlg('Clean up autosaves', Format('%s: %d file(s), %s.',
    [AWhat, Length(AFiles), FormatMB(HCLAutosaveBytes(AFiles))]) + LineEnding + LineEnding +
    'Continue?', mtConfirmation, [mbYes, mbNo], 0) = mrYes;
end;

procedure TfrmAutosaveCleanup.Remove(const AFiles: THCLAutosaveArray; const AWhat: String);
var
  Done: Integer;
  Err: String;
begin
  if not Confirm(AWhat + ' to the Recycle Bin', AFiles) then
    Exit;
  Screen.Cursor := crHourGlass;
  try
    Done := HCLRemoveAutosaves(AFiles, True, Err);
  finally
    Screen.Cursor := crDefault;
  end;
  if Err <> '' then
    MessageDlg('Clean up autosaves', Format('Removed %d of %d.', [Done, Length(AFiles)]) +
      LineEnding + Err, mtWarning, [mbOK], 0);
  Rescan;
end;

procedure TfrmAutosaveCleanup.KeepClick(Sender: TObject);
var
  Groups: TList;
  Files: THCLAutosaveArray;
  I: Integer;
begin
  Files := nil;
  Groups := CheckedGroups;
  try
    for I := 0 to Groups.Count - 1 do
      Append(Files, HCLKeepNewest(FGroups[PtrInt(Groups[I])], edtKeep.Value));
  finally
    Groups.Free;
  end;
  Remove(Files, Format('Keep the newest %d of each checked row, removing the rest', [edtKeep.Value]));
end;

procedure TfrmAutosaveCleanup.OlderClick(Sender: TObject);
var
  Groups: TList;
  Files: THCLAutosaveArray;
  I: Integer;
begin
  Files := nil;
  Groups := CheckedGroups;
  try
    for I := 0 to Groups.Count - 1 do
      Append(Files, HCLOlderThan(FGroups[PtrInt(Groups[I])], edtDays.Value, Now));
  finally
    Groups.Free;
  end;
  Remove(Files, Format('Remove checked rows'' autosaves older than %d day(s)', [edtDays.Value]));
end;

procedure TfrmAutosaveCleanup.DeleteClick(Sender: TObject);
var
  Groups: TList;
  Files: THCLAutosaveArray;
  I: Integer;
begin
  Files := nil;
  Groups := CheckedGroups;
  try
    for I := 0 to Groups.Count - 1 do
      Append(Files, FGroups[PtrInt(Groups[I])].Files);
  finally
    Groups.Free;
  end;
  Remove(Files, 'Remove every autosave in the checked rows');
end;

procedure TfrmAutosaveCleanup.MoveClick(Sender: TObject);
var
  Groups: TList;
  I, G, Moved, Skipped: Integer;
  Files: THCLAutosaveArray;
  Err, FirstErr: String;
begin
  Files := nil;
  Skipped := 0;
  Groups := CheckedGroups;
  try
    for I := 0 to Groups.Count - 1 do
      if FScenarioDirs[PtrInt(Groups[I])] <> '' then
        Append(Files, FGroups[PtrInt(Groups[I])].Files)
      else
        Inc(Skipped);
    if not Confirm('Move checked rows'' autosaves into their scenario''s SavedGames\AutoSaves' +
                   IfThen(Skipped > 0, Format(' (%d row(s) without an installed scenario are left alone)', [Skipped])),
                   Files) then
      Exit;
    Moved := 0;
    FirstErr := '';
    for I := 0 to Groups.Count - 1 do
    begin
      G := PtrInt(Groups[I]);
      if FScenarioDirs[G] = '' then
        Continue;
      Inc(Moved, HCLMoveAutosaves(FGroups[G].Files, IncludeTrailingPathDelimiter(FScenarioDirs[G]) +
        'SavedGames' + PathDelim + 'AutoSaves', Err));
      if (Err <> '') and (FirstErr = '') then
        FirstErr := Err;
    end;
  finally
    Groups.Free;
  end;
  if FirstErr <> '' then
    MessageDlg('Clean up autosaves', Format('Moved %d of %d.', [Moved, Length(Files)]) +
      LineEnding + FirstErr, mtWarning, [mbOK], 0);
  Rescan;
end;

procedure TfrmAutosaveCleanup.SelectAllClick(Sender: TObject);
var
  I: Integer;
  AllChecked: Boolean;
begin
  AllChecked := True;
  for I := 0 to lvGroups.Items.Count - 1 do
    AllChecked := AllChecked and lvGroups.Items[I].Checked;
  for I := 0 to lvGroups.Items.Count - 1 do
    lvGroups.Items[I].Checked := not AllChecked;
end;

procedure TfrmAutosaveCleanup.RefreshClick(Sender: TObject);
begin
  Rescan;
end;

procedure HCLShowAutosaveCleanup(const AGEDir, AHCDir: String;
  const AFileOrg: IHCLFileOrganization);
var
  F: TfrmAutosaveCleanup;
begin
  F := TfrmAutosaveCleanup.CreateFor(AGEDir, AHCDir, AFileOrg);
  try
    F.ShowModal;
  finally
    F.Free;
  end;
end;

end.
