import defined from "../Core/defined.js";

/**
 * A priority queue of tiles to be replaced, if necessary, to make room for new tiles.  The queue
 * is implemented as a linked list.
 *
 * @alias TileReplacementQueue
 * @private
 */
function TileReplacementQueue() {
  this.head = undefined;
  this.tail = undefined;
  this.count = 0;
  this._lastBeforeStartOfFrame = undefined;
}

/**
 * Marks the start of the render frame.  Tiles before (closer to the head) this tile in the
 * list were used last frame and must not be unloaded.
 */
TileReplacementQueue.prototype.markStartOfRenderFrame = function () {
  this._lastBeforeStartOfFrame = this.head;
};

/**
 * Reduces the size of the queue to a specified size by unloading the least-recently used
 * tiles.  Tiles that were used last frame will not be unloaded, even if that puts the number
 * of tiles above the specified maximum.
 *
 * @param {number} maximumTiles The maximum number of tiles in the queue.
 */
TileReplacementQueue.prototype.trimTiles = function (maximumTiles) {
  let tileToTrim = this.tail;
  let keepTrimming = true;
  while (
    keepTrimming &&
    defined(this._lastBeforeStartOfFrame) &&
    this.count > maximumTiles &&
    defined(tileToTrim)
  ) {
    // Stop trimming after we process the last tile not used in the
    // current frame.
    keepTrimming = tileToTrim !== this._lastBeforeStartOfFrame;

    const previous = tileToTrim.replacementPrevious;

    if (tileToTrim.eligibleForUnloading) {
      tileToTrim.freeResources();
      remove(this, tileToTrim);
    }

    tileToTrim = previous;
  }
};

function remove(tileReplacementQueue, item) {
  const previous = item.replacementPrevious;
  const next = item.replacementNext;

  if (item === tileReplacementQueue._lastBeforeStartOfFrame) {
    tileReplacementQueue._lastBeforeStartOfFrame = next;
  }

  if (item === tileReplacementQueue.head) {
    tileReplacementQueue.head = next;
  } else {
    previous.replacementNext = next;
  }

  if (item === tileReplacementQueue.tail) {
    tileReplacementQueue.tail = previous;
  } else {
    next.replacementPrevious = previous;
  }

  item.replacementPrevious = undefined;
  item.replacementNext = undefined;

  --tileReplacementQueue.count;
}

/**
 * Marks a tile as rendered this frame and moves it before the first tile that was not rendered
 * this frame.
 *
 * @param {TileReplacementQueue} item The tile that was rendered.
 */
TileReplacementQueue.prototype.markTileRendered = function (item) {
  const head = this.head;
  if (head === item) {
    if (item === this._lastBeforeStartOfFrame) {
      this._lastBeforeStartOfFrame = item.replacementNext;
    }
    return;
  }

  ++this.count;

  if (!defined(head)) {
    // no other tiles in the list
    item.replacementPrevious = undefined;
    item.replacementNext = undefined;
    this.head = item;
    this.tail = item;
    return;
  }

  if (defined(item.replacementPrevious) || defined(item.replacementNext)) {
    // tile already in the list, remove from its current location
    remove(this, item);
  }

  item.replacementPrevious = undefined;
  item.replacementNext = head;
  head.replacementPrevious = item;

  this.head = item;
};
export default TileReplacementQueue;
