unit uHCLFileOrganization;

{$mode objfpc}{$H+}

/// <summary>
/// uHCLFileOrganization — directory management for HC game artifacts.
///
/// Provides a single managed root at <c>&lt;basedir&gt;\HCLauncher\</c> with
/// well-known sub-trees for BattleSets, PlatformDBs, ExportDLLs, Sounds,
/// Graphics, Maps, and Logs.  Each category contains a <c>cache\</c>
/// sub-folder, and the BattleSet / PlatformDB branches carry a conventional
/// Scenarios → SavedGames → AutoSaves hierarchy.
///
/// Zero UI dependency — only <c>SysUtils</c> and <c>Classes</c> are required.
/// Paths are naturally cross-platform because all separators go through
/// <c>PathDelim</c>.
/// </summary>

interface

uses
  SysUtils, Classes;

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const
  /// <summary>
  /// Name of the top-level managed directory created under the caller's base
  /// directory.
  /// </summary>
  HCL_MANAGED_DIR = 'HCLauncher';

  /// <summary>Name of the per-category cache sub-folder.</summary>
  HCL_CACHE_DIR = 'cache';

  /// <summary>Name of the scenarios sub-folder inside a BattleSet or DB entry.</summary>
  HCL_SCENARIOS_DIR = 'Scenarios';

  /// <summary>Name of the saved-games sub-folder inside a scenario entry.</summary>
  HCL_SAVEDGAMES_DIR = 'SavedGames';

  /// <summary>Name of the auto-saves sub-folder inside SavedGames.</summary>
  HCL_AUTOSAVES_DIR = 'AutoSaves';

// ---------------------------------------------------------------------------
// THCLCategory enum
// ---------------------------------------------------------------------------

type
  /// <summary>
  /// Identifies a top-level artifact category managed under the HCLauncher
  /// root.  The ordinal is used as the index into
  /// <see cref="HCL_CATEGORY_NAMES"/>.
  /// </summary>
  THCLCategory = (
    hclBattleSets,   ///< BattleSets\
    hclPlatformDBs,  ///< PlatformDBs\
    hclExportDLLs,   ///< ExportDLLs\
    hclSounds,       ///< Sounds\
    hclGraphics,     ///< Graphics\
    hclMaps,         ///< Maps\
    hclLogs          ///< Logs\
  );

const
  /// <summary>
  /// Maps each <see cref="THCLCategory"/> value to its directory name on disk.
  /// </summary>
  HCL_CATEGORY_NAMES: array[THCLCategory] of String = (
    'BattleSets', 'PlatformDBs', 'ExportDLLs', 'Sounds', 'Graphics', 'Maps', 'Logs'
  );

// ---------------------------------------------------------------------------
// IHCLFileOrganization interface
// ---------------------------------------------------------------------------

type
  /// <summary>
  /// Manages the canonical directory layout for HC game artifacts under a
  /// caller-supplied base directory.  All path methods return strings
  /// WITHOUT a trailing path delimiter.  Directory-creation methods are
  /// idempotent — they succeed when the directory already exists.
  /// </summary>
  IHCLFileOrganization = interface
    ['{C939ECCD-EDC5-434E-A1C2-7720DA14DE77}']

    // -----------------------------------------------------------------------
    // Root
    // -----------------------------------------------------------------------

    /// <summary>
    /// Returns the managed root path: <c>&lt;BaseDir&gt;\HCLauncher</c>.
    /// </summary>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetManagedRoot: String;

    // -----------------------------------------------------------------------
    // Top-level categories
    // -----------------------------------------------------------------------

    /// <summary>
    /// Returns the path for a top-level category folder, e.g.
    /// <c>&lt;ManagedRoot&gt;\BattleSets</c>.
    /// </summary>
    /// <param name="ACategory">The category whose path to return.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetCategoryPath(ACategory: THCLCategory): String;

    /// <summary>
    /// Returns the cache sub-folder path for a category, e.g.
    /// <c>&lt;CategoryPath&gt;\cache</c>.
    /// </summary>
    /// <param name="ACategory">The category whose cache path to return.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetCachePath(ACategory: THCLCategory): String;

    // -----------------------------------------------------------------------
    // Nested paths — BattleSets branch
    // -----------------------------------------------------------------------

    /// <summary>
    /// Returns the path for a named BattleSet entry under
    /// <c>BattleSets\</c>.
    /// </summary>
    /// <param name="ABattleSetName">BattleSet directory name (leaf only).</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetBattleSetPath(const ABattleSetName: String): String;

    /// <summary>
    /// Returns the <c>Scenarios</c> sub-folder for a BattleSet.
    /// </summary>
    /// <param name="ABattleSetName">BattleSet directory name.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetBattleSetScenariosPath(const ABattleSetName: String): String;

    /// <summary>
    /// Returns the path for a specific scenario inside a BattleSet.
    /// </summary>
    /// <param name="ABattleSetName">BattleSet directory name.</param>
    /// <param name="AScenarioName">Scenario directory name.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetBattleSetScenarioPath(const ABattleSetName, AScenarioName: String): String;

    /// <summary>
    /// Returns the <c>SavedGames</c> path for a BattleSet scenario.
    /// </summary>
    /// <param name="ABattleSetName">BattleSet directory name.</param>
    /// <param name="AScenarioName">Scenario directory name.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetBattleSetSavedGamesPath(const ABattleSetName, AScenarioName: String): String;

    /// <summary>
    /// Returns the <c>AutoSaves</c> path for a BattleSet scenario.
    /// </summary>
    /// <param name="ABattleSetName">BattleSet directory name.</param>
    /// <param name="AScenarioName">Scenario directory name.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetBattleSetAutoSavesPath(const ABattleSetName, AScenarioName: String): String;

    // -----------------------------------------------------------------------
    // Nested paths — PlatformDBs branch
    // -----------------------------------------------------------------------

    /// <summary>
    /// Returns the path for a versioned PlatformDB entry under
    /// <c>PlatformDBs\</c>.
    /// </summary>
    /// <param name="ADBVersion">Platform-DB version string used as the
    /// sub-directory name.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetPlatformDBPath(const ADBVersion: String): String;

    /// <summary>
    /// Returns the <c>Scenarios</c> sub-folder for a PlatformDB version.
    /// </summary>
    /// <param name="ADBVersion">Platform-DB version string.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetPlatformDBScenariosPath(const ADBVersion: String): String;

    /// <summary>
    /// Returns the path for a specific scenario inside a PlatformDB version.
    /// </summary>
    /// <param name="ADBVersion">Platform-DB version string.</param>
    /// <param name="AScenarioName">Scenario directory name.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetPlatformDBScenarioPath(const ADBVersion, AScenarioName: String): String;

    /// <summary>
    /// Returns the <c>SavedGames</c> path for a PlatformDB scenario.
    /// </summary>
    /// <param name="ADBVersion">Platform-DB version string.</param>
    /// <param name="AScenarioName">Scenario directory name.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetPlatformDBSavedGamesPath(const ADBVersion, AScenarioName: String): String;

    /// <summary>
    /// Returns the <c>AutoSaves</c> path for a PlatformDB scenario.
    /// </summary>
    /// <param name="ADBVersion">Platform-DB version string.</param>
    /// <param name="AScenarioName">Scenario directory name.</param>
    /// <returns>Absolute path without trailing delimiter.</returns>
    function GetPlatformDBAutoSavesPath(const ADBVersion, AScenarioName: String): String;

    // -----------------------------------------------------------------------
    // Directory operations
    // -----------------------------------------------------------------------

    /// <summary>
    /// Ensures the category folder and its <c>cache</c> sub-folder exist,
    /// creating them if necessary.
    /// </summary>
    /// <param name="ACategory">The category to ensure.</param>
    /// <returns><c>True</c> if both directories exist after the call.</returns>
    function EnsureCategoryExists(ACategory: THCLCategory): Boolean;

    /// <summary>
    /// Ensures all category folders (and their <c>cache</c> sub-folders)
    /// exist, creating them if necessary.
    /// </summary>
    /// <returns><c>True</c> if every category was created/verified
    /// successfully.</returns>
    function EnsureAllCategoriesExist: Boolean;

    /// <summary>
    /// Creates the full directory chain down to <c>SavedGames</c> and
    /// <c>AutoSaves</c> for the given scenario.
    /// </summary>
    /// <param name="ABattleSetOrDBVersion">BattleSet name or PlatformDB
    /// version string depending on <paramref name="AIsCommonDB"/>.</param>
    /// <param name="AScenarioName">Scenario directory name.</param>
    /// <param name="AIsCommonDB">When <c>True</c> the PlatformDB branch is
    /// used; otherwise the BattleSet branch is used.</param>
    /// <returns><c>True</c> if all required directories exist after the
    /// call.</returns>
    function EnsureScenarioPath(const ABattleSetOrDBVersion, AScenarioName: String;
      AIsCommonDB: Boolean): Boolean;

    // -----------------------------------------------------------------------
    // Listing
    // -----------------------------------------------------------------------

    /// <summary>
    /// Returns the names (not full paths) of immediate sub-directories found
    /// at <paramref name="APath"/>.  Returns an empty array if the path does
    /// not exist.
    /// </summary>
    /// <param name="APath">Directory to enumerate.</param>
    /// <returns>Array of sub-directory names.</returns>
    function ListSubdirectories(const APath: String): TStringArray;

    /// <summary>
    /// Returns the names (not full paths) of files found at
    /// <paramref name="APath"/> whose names match the extension filter.
    /// <paramref name="AExtFilter"/> may be a plain extension
    /// (<c>'.res'</c>) or contain a wildcard (<c>'.sc?'</c>).  Returns an
    /// empty array if the path does not exist.
    /// </summary>
    /// <param name="APath">Directory to enumerate.</param>
    /// <param name="AExtFilter">Extension/wildcard filter appended to the
    /// search mask (e.g. <c>'.res'</c> or <c>'.sc?'</c>).</param>
    /// <returns>Array of file names.</returns>
    function ListFiles(const APath: String; const AExtFilter: String): TStringArray;
  end;

// ---------------------------------------------------------------------------
// THCLFileOrganization class
// ---------------------------------------------------------------------------

  /// <summary>
  /// Concrete implementation of <see cref="IHCLFileOrganization"/>.
  /// Inherit from <c>TInterfacedObject</c> so that interface consumers get
  /// automatic reference-counted lifetime management.
  /// </summary>
  THCLFileOrganization = class(TInterfacedObject, IHCLFileOrganization)
  private
    /// <summary>Base directory with trailing PathDelim stripped.</summary>
    FBaseDir: String;

    /// <summary>
    /// Strips any trailing path delimiter from <paramref name="APath"/> and
    /// returns the result.
    /// </summary>
    /// <param name="APath">Input path string.</param>
    /// <returns>Path without trailing delimiter.</returns>
    class function StripTrailingDelim(const APath: String): String; static;

    /// <summary>
    /// Returns <c>True</c> when <paramref name="APath"/> exists as a
    /// directory on disk.
    /// </summary>
    /// <param name="APath">Path to test.</param>
    /// <returns><c>True</c> if the path is an existing directory.</returns>
    class function DirExists(const APath: String): Boolean; static; inline;

  public
    /// <summary>
    /// Creates a new <c>THCLFileOrganization</c> rooted at
    /// <paramref name="ABaseDir"/>.  Any trailing path delimiter in
    /// <paramref name="ABaseDir"/> is stripped.
    /// </summary>
    /// <param name="ABaseDir">Absolute base directory.  The managed root
    /// <c>HCLauncher\</c> will be placed immediately beneath it.</param>
    constructor Create(const ABaseDir: String);

    // IHCLFileOrganization — Root
    function GetManagedRoot: String;

    // IHCLFileOrganization — Top-level categories
    function GetCategoryPath(ACategory: THCLCategory): String;
    function GetCachePath(ACategory: THCLCategory): String;

    // IHCLFileOrganization — BattleSets branch
    function GetBattleSetPath(const ABattleSetName: String): String;
    function GetBattleSetScenariosPath(const ABattleSetName: String): String;
    function GetBattleSetScenarioPath(const ABattleSetName, AScenarioName: String): String;
    function GetBattleSetSavedGamesPath(const ABattleSetName, AScenarioName: String): String;
    function GetBattleSetAutoSavesPath(const ABattleSetName, AScenarioName: String): String;

    // IHCLFileOrganization — PlatformDBs branch
    function GetPlatformDBPath(const ADBVersion: String): String;
    function GetPlatformDBScenariosPath(const ADBVersion: String): String;
    function GetPlatformDBScenarioPath(const ADBVersion, AScenarioName: String): String;
    function GetPlatformDBSavedGamesPath(const ADBVersion, AScenarioName: String): String;
    function GetPlatformDBAutoSavesPath(const ADBVersion, AScenarioName: String): String;

    // IHCLFileOrganization — Directory operations
    function EnsureCategoryExists(ACategory: THCLCategory): Boolean;
    function EnsureAllCategoriesExist: Boolean;
    function EnsureScenarioPath(const ABattleSetOrDBVersion, AScenarioName: String;
      AIsCommonDB: Boolean): Boolean;

    // IHCLFileOrganization — Listing
    function ListSubdirectories(const APath: String): TStringArray;
    function ListFiles(const APath: String; const AExtFilter: String): TStringArray;
  end;

// ---------------------------------------------------------------------------
// Factory function
// ---------------------------------------------------------------------------

/// <summary>
/// Creates and returns a new <see cref="IHCLFileOrganization"/> instance
/// rooted at <paramref name="ABaseDir"/>.
///
/// Callers should hold the result in an <c>IHCLFileOrganization</c> variable
/// so that the object is freed automatically when the variable goes out of
/// scope.
/// </summary>
/// <param name="ABaseDir">Absolute base directory passed to
/// <see cref="THCLFileOrganization.Create"/>.</param>
/// <returns>A reference-counted <see cref="IHCLFileOrganization"/>
/// implementation.</returns>
function CreateFileOrganization(const ABaseDir: String): IHCLFileOrganization;

implementation

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

class function THCLFileOrganization.StripTrailingDelim(const APath: String): String;
begin
  Result := APath;
  if (Result <> '') and (Result[Length(Result)] = PathDelim) then
    SetLength(Result, Length(Result) - 1);
end;

class function THCLFileOrganization.DirExists(const APath: String): Boolean;
begin
  Result := DirectoryExists(APath);
end;

// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------

constructor THCLFileOrganization.Create(const ABaseDir: String);
begin
  inherited Create;
  FBaseDir := StripTrailingDelim(ABaseDir);
end;

// ---------------------------------------------------------------------------
// Root
// ---------------------------------------------------------------------------

function THCLFileOrganization.GetManagedRoot: String;
begin
  Result := FBaseDir + PathDelim + HCL_MANAGED_DIR;
end;

// ---------------------------------------------------------------------------
// Top-level categories
// ---------------------------------------------------------------------------

function THCLFileOrganization.GetCategoryPath(ACategory: THCLCategory): String;
begin
  Result := GetManagedRoot + PathDelim + HCL_CATEGORY_NAMES[ACategory];
end;

function THCLFileOrganization.GetCachePath(ACategory: THCLCategory): String;
begin
  Result := GetCategoryPath(ACategory) + PathDelim + HCL_CACHE_DIR;
end;

// ---------------------------------------------------------------------------
// BattleSets branch
// ---------------------------------------------------------------------------

function THCLFileOrganization.GetBattleSetPath(const ABattleSetName: String): String;
begin
  Result := GetCategoryPath(hclBattleSets) + PathDelim + ABattleSetName;
end;

function THCLFileOrganization.GetBattleSetScenariosPath(const ABattleSetName: String): String;
begin
  Result := GetBattleSetPath(ABattleSetName) + PathDelim + HCL_SCENARIOS_DIR;
end;

function THCLFileOrganization.GetBattleSetScenarioPath(
  const ABattleSetName, AScenarioName: String): String;
begin
  Result := GetBattleSetScenariosPath(ABattleSetName) + PathDelim + AScenarioName;
end;

function THCLFileOrganization.GetBattleSetSavedGamesPath(
  const ABattleSetName, AScenarioName: String): String;
begin
  Result := GetBattleSetScenarioPath(ABattleSetName, AScenarioName) + PathDelim + HCL_SAVEDGAMES_DIR;
end;

function THCLFileOrganization.GetBattleSetAutoSavesPath(
  const ABattleSetName, AScenarioName: String): String;
begin
  Result := GetBattleSetSavedGamesPath(ABattleSetName, AScenarioName) + PathDelim + HCL_AUTOSAVES_DIR;
end;

// ---------------------------------------------------------------------------
// PlatformDBs branch
// ---------------------------------------------------------------------------

function THCLFileOrganization.GetPlatformDBPath(const ADBVersion: String): String;
begin
  Result := GetCategoryPath(hclPlatformDBs) + PathDelim + ADBVersion;
end;

function THCLFileOrganization.GetPlatformDBScenariosPath(const ADBVersion: String): String;
begin
  Result := GetPlatformDBPath(ADBVersion) + PathDelim + HCL_SCENARIOS_DIR;
end;

function THCLFileOrganization.GetPlatformDBScenarioPath(
  const ADBVersion, AScenarioName: String): String;
begin
  Result := GetPlatformDBScenariosPath(ADBVersion) + PathDelim + AScenarioName;
end;

function THCLFileOrganization.GetPlatformDBSavedGamesPath(
  const ADBVersion, AScenarioName: String): String;
begin
  Result := GetPlatformDBScenarioPath(ADBVersion, AScenarioName) + PathDelim + HCL_SAVEDGAMES_DIR;
end;

function THCLFileOrganization.GetPlatformDBAutoSavesPath(
  const ADBVersion, AScenarioName: String): String;
begin
  Result := GetPlatformDBSavedGamesPath(ADBVersion, AScenarioName) + PathDelim + HCL_AUTOSAVES_DIR;
end;

// ---------------------------------------------------------------------------
// Directory operations
// ---------------------------------------------------------------------------

function THCLFileOrganization.EnsureCategoryExists(ACategory: THCLCategory): Boolean;
var
  LCatPath, LCachePath: String;
begin
  LCatPath   := GetCategoryPath(ACategory);
  LCachePath := GetCachePath(ACategory);
  ForceDirectories(LCatPath);
  ForceDirectories(LCachePath);
  Result := DirExists(LCatPath) and DirExists(LCachePath);
end;

function THCLFileOrganization.EnsureAllCategoriesExist: Boolean;
var
  LCat: THCLCategory;
begin
  Result := True;
  for LCat := Low(THCLCategory) to High(THCLCategory) do
    if not EnsureCategoryExists(LCat) then
      Result := False;
end;

function THCLFileOrganization.EnsureScenarioPath(
  const ABattleSetOrDBVersion, AScenarioName: String;
  AIsCommonDB: Boolean): Boolean;
var
  LSavedGamesPath, LAutoSavesPath: String;
begin
  if AIsCommonDB then
  begin
    LSavedGamesPath := GetPlatformDBSavedGamesPath(ABattleSetOrDBVersion, AScenarioName);
    LAutoSavesPath  := GetPlatformDBAutoSavesPath(ABattleSetOrDBVersion, AScenarioName);
  end
  else
  begin
    LSavedGamesPath := GetBattleSetSavedGamesPath(ABattleSetOrDBVersion, AScenarioName);
    LAutoSavesPath  := GetBattleSetAutoSavesPath(ABattleSetOrDBVersion, AScenarioName);
  end;
  // ForceDirectories creates every intermediate level, so one call per leaf
  // is sufficient.
  ForceDirectories(LSavedGamesPath);
  ForceDirectories(LAutoSavesPath);
  Result := DirExists(LSavedGamesPath) and DirExists(LAutoSavesPath);
end;

// ---------------------------------------------------------------------------
// Listing
// ---------------------------------------------------------------------------

function THCLFileOrganization.ListSubdirectories(const APath: String): TStringArray;
var
  LRec:  TSearchRec;
  LList: TStringList;
  I:     Integer;
begin
  Result := nil;   // initialise managed return value
  if not DirExists(APath) then
    Exit;

  LList := TStringList.Create;
  try
    if FindFirst(APath + PathDelim + '*', faDirectory, LRec) = 0 then
    try
      repeat
        if (LRec.Attr and faDirectory) <> 0 then
          if (LRec.Name <> '.') and (LRec.Name <> '..') then
            LList.Add(LRec.Name);
      until FindNext(LRec) <> 0;
    finally
      FindClose(LRec);
    end;

    SetLength(Result, LList.Count);
    for I := 0 to LList.Count - 1 do
      Result[I] := LList[I];
  finally
    LList.Free;
  end;
end;

function THCLFileOrganization.ListFiles(const APath: String;
  const AExtFilter: String): TStringArray;
var
  LRec:  TSearchRec;
  LList: TStringList;
  LMask: String;
  I:     Integer;
begin
  Result := nil;   // initialise managed return value
  if not DirExists(APath) then
    Exit;

  // Build search mask: APath\*<AExtFilter>
  // e.g.  AExtFilter = '.res'  →  C:\foo\*.res
  //       AExtFilter = '.sc?'  →  C:\foo\*.sc?   (FindFirst handles the '?')
  LMask := APath + PathDelim + '*' + AExtFilter;

  LList := TStringList.Create;
  try
    if FindFirst(LMask, faAnyFile and not faDirectory, LRec) = 0 then
    try
      repeat
        if (LRec.Attr and faDirectory) = 0 then
        begin
          // Windows FindFirst '?' can match 0+ chars due to 8.3 compat.
          // Post-filter: if filter contains '?', enforce that the file
          // extension length matches the filter extension length exactly.
          if (Pos('?', AExtFilter) > 0) and
             (Length(ExtractFileExt(LRec.Name)) <> Length(AExtFilter)) then
            Continue;
          LList.Add(LRec.Name);
        end;
      until FindNext(LRec) <> 0;
    finally
      FindClose(LRec);
    end;

    SetLength(Result, LList.Count);
    for I := 0 to LList.Count - 1 do
      Result[I] := LList[I];
  finally
    LList.Free;
  end;
end;

// ---------------------------------------------------------------------------
// Factory function
// ---------------------------------------------------------------------------

function CreateFileOrganization(const ABaseDir: String): IHCLFileOrganization;
begin
  Result := THCLFileOrganization.Create(ABaseDir);
end;

end.
