// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

// This file is derived from the Cesium code base under Apache 2 license
// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md

import type {Tileset3D} from './tileset-3d';
import type {Tile3D} from './tile-3d';
import {DoublyLinkedList} from '../utils/doubly-linked-list';

/**
 * Stores tiles with content loaded.
 * @private
 */
export class TilesetCache {
  private _list: DoublyLinkedList;
  private _sentinel: any;
  private _trimTiles: boolean;

  constructor() {
    // [head, sentinel) -> tiles that weren't selected this frame and may be removed from the cache
    // (sentinel, tail] -> tiles that were selected this frame
    this._list = new DoublyLinkedList();
    this._sentinel = this._list.add('sentinel');
    this._trimTiles = false;
  }

  reset(): void {
    // Move sentinel node to the tail so, at the start of the frame, all tiles
    // may be potentially replaced.  Tiles are moved to the right of the sentinel
    // when they are selected so they will not be replaced.
    this._list.splice(this._list.tail, this._sentinel);
  }

  touch(tile: Tile3D): void {
    const node = tile._cacheNode;
    if (node) {
      this._list.splice(this._sentinel, node);
    }
  }

  add(
    tileset: Tileset3D,
    tile: Tile3D,
    addCallback?: (tileset: Tileset3D, tile: Tile3D) => void
  ): void {
    if (!tile._cacheNode) {
      tile._cacheNode = this._list.add(tile);

      if (addCallback) {
        addCallback(tileset, tile);
      }
    }
  }

  unloadTile(
    tileset: Tileset3D,
    tile: Tile3D,
    unloadCallback?: (tileset: Tileset3D, tile: Tile3D) => void
  ): void {
    const node = tile._cacheNode;
    if (!node) {
      return;
    }

    this._list.remove(node);
    tile._cacheNode = null;
    if (unloadCallback) {
      unloadCallback(tileset, tile);
    }
  }

  unloadTiles(tileset, unloadCallback): void {
    const trimTiles = this._trimTiles;
    this._trimTiles = false;

    const list = this._list;

    const maximumMemoryUsageInBytes = tileset.maximumMemoryUsage * 1024 * 1024;

    // Traverse the list only to the sentinel since tiles/nodes to the
    // right of the sentinel were used this frame.
    // The sub-list to the left of the sentinel is ordered from LRU to MRU.
    const sentinel = this._sentinel;
    let node = list.head;

    while (
      node !== sentinel &&
      (tileset.gpuMemoryUsageInBytes > maximumMemoryUsageInBytes || trimTiles)
    ) {
      // @ts-expect-error
      const tile = node.item;
      // @ts-expect-error
      node = node.next;
      this.unloadTile(tileset, tile, unloadCallback);
    }
  }

  trim(): void {
    this._trimTiles = true;
  }
}
