{-------------------------------------------------------------------------
 Module:    OSM Tile LRU Cache (Shared)

 Comment:   LRU tile cache for decoded OSM tile pixels with built-in
            file loading from BaseDir/DownloadDir paths.
            Shared between TGlobe4, TGlobe5, and other consumers.
-------------------------------------------------------------------------}
{$IFDEF FPC}{$MODE DELPHI}{$ENDIF}
{$WEAKPACKAGEUNIT ON}
unit uCICOSMTileStore;

interface

uses
  SysUtils, uCICOSMTileCommon, uCICOSMTileDecode;

const
  OSM_DEFAULT_CAPACITY = 1024;

type
  TOSMTileCacheSlot = record
    Z, X, Y  : Integer;
    Pixels   : POSMTilePixels;
    LRUTick  : UInt64;
    InUse    : Boolean;
  end;

  TGOSMTileCache = class
  private
    FSlots      : array of TOSMTileCacheSlot;
    FHash       : array of Integer;   // open-addressing; -1 = empty
    FHashMask   : Integer;            // power-of-2 minus 1
    FCapacity   : Integer;
    FCount      : Integer;
    FLRUClock   : UInt64;

    FBaseDir     : string;
    FDownloadDir : string;
    FURLTemplate : string;
    FMaxZoom     : Integer;

    function  HashKey(Z, X, Y: Integer): Integer;
    function  FindSlot(Z, X, Y: Integer): Integer;
    function  AllocateSlot(Z, X, Y: Integer): Integer;
    procedure RebuildHash;
    procedure SetCapacity(Value: Integer);
    function  LoadTileFromFile(Z, X, Y: Integer;
                Dest: POSMTilePixels): Boolean;
    procedure SetBaseDir(const Value: string);
    procedure SetDownloadDir(const Value: string);
    procedure SetURLTemplate(const Value: string);
    procedure SetMaxZoom(Value: Integer);
  public
    constructor Create(ACapacity: Integer = OSM_DEFAULT_CAPACITY);
    destructor  Destroy; override;

    function  GetTile(Z, X, Y: Integer): POSMTilePixels;
    function  GetTilePixel(Z, X, Y, PixelIndex: Integer): Cardinal;
    procedure FlushCache;
    function  IsActive: Boolean;

    property Capacity    : Integer read FCapacity    write SetCapacity;
    property Count       : Integer read FCount;
    property BaseDir     : string  read FBaseDir     write SetBaseDir;
    property DownloadDir : string  read FDownloadDir write SetDownloadDir;
    property URLTemplate : string  read FURLTemplate write SetURLTemplate;
    property MaxZoom     : Integer read FMaxZoom     write SetMaxZoom;
  end;

implementation

//==============================================================================
// TGOSMTileCache — private helpers
//==============================================================================

function TGOSMTileCache.HashKey(Z, X, Y: Integer): Integer;
var
  H: UInt64;
begin
  H := (UInt64(Cardinal(Z)) * UInt64(73856093)) xor
       (UInt64(Cardinal(X)) * UInt64(19349663)) xor
       (UInt64(Cardinal(Y)) * UInt64(83492791));
  Result := Integer(H and UInt64(FHashMask));
end;

function TGOSMTileCache.FindSlot(Z, X, Y: Integer): Integer;
var
  Idx, SlotIdx: Integer;
begin
  Idx := HashKey(Z, X, Y);
  while True do
  begin
    SlotIdx := FHash[Idx];
    if SlotIdx = -1 then
    begin
      Result := -1;
      Exit;
    end;
    if FSlots[SlotIdx].InUse and
       (FSlots[SlotIdx].Z = Z) and
       (FSlots[SlotIdx].X = X) and
       (FSlots[SlotIdx].Y = Y) then
    begin
      Result := SlotIdx;
      Exit;
    end;
    Idx := (Idx + 1) and FHashMask;
  end;
end;

function TGOSMTileCache.AllocateSlot(Z, X, Y: Integer): Integer;
var
  I, EvictIdx: Integer;
  MinTick: UInt64;
  Idx: Integer;
begin
  // If there is a free slot, use it
  if FCount < FCapacity then
  begin
    for I := 0 to FCapacity - 1 do
    begin
      if not FSlots[I].InUse then
      begin
        if FSlots[I].Pixels = nil then
          GetMem(FSlots[I].Pixels, OSM_TILE_BGRA_BYTES);

        FSlots[I].Z      := Z;
        FSlots[I].X      := X;
        FSlots[I].Y      := Y;
        FSlots[I].InUse  := True;
        FSlots[I].LRUTick := FLRUClock;

        Inc(FCount);

        // Insert into hash table
        Idx := HashKey(Z, X, Y);
        while FHash[Idx] <> -1 do
          Idx := (Idx + 1) and FHashMask;
        FHash[Idx] := I;

        Result := I;
        Exit;
      end;
    end;
  end;

  // Cache is full — evict the slot with the minimum LRUTick
  EvictIdx := 0;
  MinTick  := FSlots[0].LRUTick;
  for I := 1 to FCapacity - 1 do
  begin
    if FSlots[I].InUse and (FSlots[I].LRUTick < MinTick) then
    begin
      MinTick  := FSlots[I].LRUTick;
      EvictIdx := I;
    end;
  end;

  FSlots[EvictIdx].Z     := Z;
  FSlots[EvictIdx].X     := X;
  FSlots[EvictIdx].Y     := Y;
  FSlots[EvictIdx].InUse := True;

  RebuildHash;

  Result := EvictIdx;
end;

procedure TGOSMTileCache.RebuildHash;
var
  I, Idx: Integer;
begin
  FillChar(FHash[0], (FHashMask + 1) * SizeOf(Integer), $FF);

  for I := 0 to FCapacity - 1 do
  begin
    if FSlots[I].InUse then
    begin
      Idx := HashKey(FSlots[I].Z, FSlots[I].X, FSlots[I].Y);
      while FHash[Idx] <> -1 do
        Idx := (Idx + 1) and FHashMask;
      FHash[Idx] := I;
    end;
  end;
end;

procedure TGOSMTileCache.SetCapacity(Value: Integer);
var
  HashSize: Integer;
begin
  if Value < 16 then Value := 16;
  if Value <> FCapacity then
  begin
    FlushCache;
    FCapacity := Value;
    SetLength(FSlots, FCapacity);

    HashSize := 1;
    while HashSize < FCapacity * 2 do
      HashSize := HashSize shl 1;
    FHashMask := HashSize - 1;
    SetLength(FHash, HashSize);
    FillChar(FHash[0], HashSize * SizeOf(Integer), $FF);
  end;
end;

function TGOSMTileCache.LoadTileFromFile(Z, X, Y: Integer;
  Dest: POSMTilePixels): Boolean;
var
  Path    : string;
  RelPath : string;
begin
  Result := False;
  RelPath := PathDelim + IntToStr(Z) + PathDelim + IntToStr(X) + PathDelim +
             IntToStr(Y) + '.png';

  // Try local/custom tile directory first
  Path := FBaseDir + RelPath;
  if not FileExists(Path) then
  begin
    // Try download directory
    if FDownloadDir <> '' then
      Path := FDownloadDir + RelPath;
    if (FDownloadDir = '') or not FileExists(Path) then
      Exit;
  end;

  Result := OSMDecodePngToBGRA(Path, Dest);
end;

procedure TGOSMTileCache.SetBaseDir(const Value: string);
begin
  if Value <> FBaseDir then
  begin
    FlushCache;
    FBaseDir := Value;
  end;
end;

procedure TGOSMTileCache.SetDownloadDir(const Value: string);
begin
  if Value <> FDownloadDir then
  begin
    FlushCache;
    FDownloadDir := Value;
  end;
end;

procedure TGOSMTileCache.SetURLTemplate(const Value: string);
begin
  if Value <> FURLTemplate then
  begin
    FlushCache;
    FURLTemplate := Value;
  end;
end;

procedure TGOSMTileCache.SetMaxZoom(Value: Integer);
begin
  if Value <> FMaxZoom then
  begin
    FlushCache;
    FMaxZoom := Value;
  end;
end;

//==============================================================================
// TGOSMTileCache — public
//==============================================================================

constructor TGOSMTileCache.Create(ACapacity: Integer);
var
  HashSize, I: Integer;
begin
  inherited Create;
  FCapacity := ACapacity;
  FCount    := 0;
  FLRUClock := 0;
  FMaxZoom  := OSM_DEFAULT_MAX_ZOOM;

  SetLength(FSlots, FCapacity);
  for I := 0 to FCapacity - 1 do
  begin
    FSlots[I].InUse  := False;
    FSlots[I].Pixels := nil;
  end;

  // Hash table: next power of 2 >= Capacity * 2  (load factor <= 0.5)
  HashSize := 1;
  while HashSize < FCapacity * 2 do
    HashSize := HashSize shl 1;
  FHashMask := HashSize - 1;
  SetLength(FHash, HashSize);
  FillChar(FHash[0], HashSize * SizeOf(Integer), $FF);
end;

destructor TGOSMTileCache.Destroy;
begin
  FlushCache;
  inherited;
end;

function TGOSMTileCache.GetTile(Z, X, Y: Integer): POSMTilePixels;
var
  SlotIdx: Integer;
begin
  Result := nil;

  // Cache hit
  SlotIdx := FindSlot(Z, X, Y);
  if SlotIdx >= 0 then
  begin
    Inc(FLRUClock);
    FSlots[SlotIdx].LRUTick := FLRUClock;
    Result := FSlots[SlotIdx].Pixels;
    Exit;
  end;

  // Cache miss — load from file
  SlotIdx := AllocateSlot(Z, X, Y);
  if SlotIdx < 0 then
    Exit;

  if not LoadTileFromFile(Z, X, Y, FSlots[SlotIdx].Pixels) then
  begin
    // Load failed — mark slot as unused so we don't cache garbage
    FSlots[SlotIdx].InUse := False;
    Dec(FCount);
    RebuildHash;
    Exit;
  end;

  Inc(FLRUClock);
  FSlots[SlotIdx].LRUTick := FLRUClock;
  Result := FSlots[SlotIdx].Pixels;
end;

function TGOSMTileCache.GetTilePixel(Z, X, Y, PixelIndex: Integer): Cardinal;
var
  SlotIdx: Integer;
begin
  Result  := 0;
  SlotIdx := FindSlot(Z, X, Y);
  if (SlotIdx >= 0) and (PixelIndex >= 0) and
     (PixelIndex < OSM_TILE_PIXEL_COUNT) then
    Result := FSlots[SlotIdx].Pixels^[PixelIndex];
end;

procedure TGOSMTileCache.FlushCache;
var
  I: Integer;
begin
  for I := 0 to FCapacity - 1 do
  begin
    if FSlots[I].Pixels <> nil then
    begin
      FreeMem(FSlots[I].Pixels);
      FSlots[I].Pixels := nil;
    end;
    FSlots[I].InUse := False;
  end;
  FillChar(FHash[0], (FHashMask + 1) * SizeOf(Integer), $FF);
  FCount    := 0;
  FLRUClock := 0;
end;

function TGOSMTileCache.IsActive: Boolean;
begin
  Result := ((FBaseDir <> '') and DirectoryExists(FBaseDir))
         or ((FDownloadDir <> '') and DirectoryExists(FDownloadDir))
         or (FURLTemplate <> '');
end;

end.
