unit Myc.Core.FileCache;

interface

uses
    System.SysUtils,
    System.IOUtils,
    System.Generics.Collections,
    System.Generics.Defaults,
    System.SyncObjs,
    Myc.Futures;

type
    TLoaderFunc<T> = reference to function(const Filename: String): TFuture<T>;

    IDataFileCache<T> = interface
        procedure Clear;
        function GetOrAdd(const AFileName: string; const Loader: TLoaderFunc<T>): TFuture<T>;
    end;

    // Manages a cache for time-series data files loaded asynchronously.
    // The cache is activated by assigning a function to the IsMemoryLow property.
    // It evicts least-recently-used items when memory is low.
    TDataFileCache<T> = class(TInterfacedObject, IDataFileCache<T>)
    private
        type
            // Holds metadata for a cached file.
            TCachedFile = class
            public
                Name: String;
                Age: TDateTime;
                LastUsed: TDateTime;
                Data: TFuture<T>;
            end;
    private
        FCachedFiles: TDictionary<String, TCachedFile>;
        FLock: TCriticalSection;
    public
        constructor Create;
        destructor Destroy; override;

        // Removes all items from the cache, waiting for any pending operations to complete.
        procedure Clear;

        // Retrieves a data future from the cache or uses the provided loader function to create and cache it.
        // Caching is only active if the IsMemoryLow property is assigned.
        function GetOrAdd(const AFileName: string; const Loader: TLoaderFunc<T>): TFuture<T>;
    end;

var
    IsMemoryLow: TFunc<Boolean>;

implementation

uses
{$IFDEF MSWINDOWS}
    Winapi.Windows,
{$endif}
    System.Math;

{ TDataFileCache<T> }

constructor TDataFileCache<T>.Create;
begin
    inherited Create;
    FLock := TCriticalSection.Create;
    FCachedFiles := TObjectDictionary<String, TCachedFile>.Create([doOwnsValues]);
end;

destructor TDataFileCache<T>.Destroy;
begin
    Clear;
    FCachedFiles.Free;
    FLock.Free;
    inherited;
end;

procedure TDataFileCache<T>.Clear;
begin
    FLock.Enter;
    try
        // Wait for any pending load operations to finish before clearing.
        for var cachedFile in FCachedFiles.Values do
            cachedFile.Data.WaitFor;
        FCachedFiles.Clear;
    finally
        FLock.Leave;
    end;
end;

function TDataFileCache<T>.GetOrAdd(const AFileName: string; const Loader: TLoaderFunc<T>): TFuture<T>;
var
    fileList: TArray<TCachedFile>;
    i: Integer;
    cachedFile: TCachedFile;
    currentFileAge: TDateTime;
begin
    // If no memory check function is assigned, caching is disabled.
    // Simply execute the loader function and return the result.
    if not Assigned(IsMemoryLow) then
    begin
        Result := Loader(AFilename);
        exit;
    end;

    FLock.Enter;
    try
        if IsMemoryLow() then
        begin
            // Caching is active. First, try to free memory if needed.
            fileList := FCachedFiles.Values.ToArray;
            TArray.Sort<TCachedFile>(
                fileList,
                TComparer<TCachedFile>.Construct(
                    function(const Left, Right: TCachedFile): Integer
                    begin
                        // Sort by LastUsed timestamp, oldest first.
                        Result := Sign(Left.LastUsed - Right.LastUsed);
                    end
                )
            );

            for i := 0 to High(fileList) do
            begin
                if not IsMemoryLow() then
                    break; // Stop evicting if memory pressure is relieved.

                if fileList[i].Name <> AFileName then
                begin
                    if fileList[i].Data.Done.IsSet then
                    begin
                        FCachedFiles.Remove(fileList[i].Name);
                        fileList[i].Data := TFuture<T>.Null;
                    end;
                end;
            end;
        end;

        // Check if a valid entry exists in the cache.
        if FCachedFiles.TryGetValue(AFileName, cachedFile) then
        begin
            if FileAge(AFileName, currentFileAge, true) and (currentFileAge = cachedFile.Age) then
            begin
                // Cache hit and file is unchanged. Update usage timestamp and return cached future.
                cachedFile.LastUsed := Now;
                exit(cachedFile.Data);
            end
            else
            begin
                // File is stale or no longer exists. Remove from cache.
                FCachedFiles.Remove(AFileName);
            end;
        end;

        // Cache miss or stale entry. Execute the loader function.
        Result := Loader(AFilename);

        // Create a new cache entry for the loaded future.
        cachedFile := TCachedFile.Create;
        cachedFile.Name := AFileName;
        if FileAge(AFileName, cachedFile.Age, true) then
        begin
            cachedFile.Data := Result;
            cachedFile.LastUsed := Now;
            FCachedFiles.Add(AFileName, cachedFile);
        end
        else
        begin
            // If we cannot get the file's age, we cannot cache it reliably.
            cachedFile.Free;
        end;
    finally
        FLock.Leave;
    end;
end;

initialization
{$IFDEF MSWINDOWS}
    IsMemoryLow :=
        function: Boolean
        var
            memStatusEx: TMemoryStatusEx;
        begin
            Result := false;
            memStatusEx.dwLength := SizeOf(TMemoryStatusEx); // Initialize the structure size
            if GlobalMemoryStatusEx(memStatusEx) then
            begin
                // memStatusEx.dwMemoryLoad indicates the approximate percentage of physical memory that is in use.
                Result := memStatusEx.dwMemoryLoad >= 80;
            end;
        end;
{$endif}

end.
