/**
 * @license
 * Copyright (c) 2021 - 2026 Vaadin Ltd.
 * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
 */

/**
 * A class that stores items with their associated sub-caches.
 */
export class Cache {
  /**
   * A context object.
   *
   * @type {{ isExpanded: (item: unknown) => boolean }}
   */
  context;

  /**
   * The number of items to display per page.
   *
   * @type {number}
   */
  pageSize;

  /**
   * An array of cached items.
   *
   * @type {object[]}
   */
  items = [];

  /**
   * A map where the key is a requested page and the value is a callback
   * that will be called with data once the request is complete.
   *
   * @type {Record<number, Function>}
   */
  pendingRequests = {};

  /**
   * A map where the key is the index of an item in the `items` array
   * and the value is a sub-cache associated with that item.
   *
   * Note, it's intentionally defined as an object instead of a Map
   * to ensure that Object.entries() returns an array with keys sorted
   * in alphabetical order, rather than the order they were added.
   *
   * @type {Record<number, Cache>}
   * @private
   */
  #subCacheByIndex = {};

  /**
   * The number of items.
   *
   * @type {number}
   * @private
   */
  #size = 0;

  /**
   * The total number of items, including items from expanded sub-caches.
   *
   * @type {number}
   * @private
   */
  #flatSize = 0;

  /**
   * @param {Cache['context']} context
   * @param {number} pageSize
   * @param {number | undefined} size
   * @param {Cache | undefined} parentCache
   * @param {number | undefined} parentCacheIndex
   */
  constructor(context, pageSize, size, parentCache, parentCacheIndex) {
    this.context = context;
    this.pageSize = pageSize;
    this.size = size;
    this.parentCache = parentCache;
    this.parentCacheIndex = parentCacheIndex;
    this.#flatSize = size || 0;
  }

  /**
   * An item in the parent cache that the current cache is associated with.
   *
   * @return {object | undefined}
   */
  get parentItem() {
    return this.parentCache && this.parentCache.items[this.parentCacheIndex];
  }

  /**
   * An array of sub-caches sorted in the same order as their associated items
   * appear in the `items` array.
   *
   * @return {Cache[]}
   */
  get subCaches() {
    return Object.values(this.#subCacheByIndex);
  }

  /**
   * Whether the cache or any of its descendant caches have pending requests.
   *
   * @return {boolean}
   */
  get isLoading() {
    if (Object.keys(this.pendingRequests).length > 0) {
      return true;
    }

    return this.subCaches.some((subCache) => subCache.isLoading);
  }

  /**
   * The total number of items, including items from expanded sub-caches.
   *
   * @return {number}
   */
  get flatSize() {
    return this.#flatSize;
  }

  /**
   * The number of items.
   *
   * @return {number}
   */
  get size() {
    return this.#size;
  }

  /**
   * Sets the number of items.
   *
   * @param {number} size
   */
  set size(size) {
    const oldSize = this.#size;
    if (oldSize === size) {
      return;
    }

    this.#size = size;

    if (this.context.placeholder !== undefined) {
      this.items.length = size || 0;
      for (let i = 0; i < size || 0; i++) {
        this.items[i] ||= this.context.placeholder;
      }
    }

    if (this.items.length > size) {
      this.items.length = size || 0;
    }

    Object.keys(this.pendingRequests).forEach((page) => {
      const startIndex = parseInt(page) * this.pageSize;
      if (startIndex >= this.size || 0) {
        delete this.pendingRequests[page];
      }
    });
  }

  /**
   * Recalculates the flattened size for the cache and its descendant caches recursively.
   */
  recalculateFlatSize() {
    this.#flatSize =
      !this.parentItem || this.context.isExpanded(this.parentItem)
        ? this.size +
          this.subCaches.reduce((total, subCache) => {
            subCache.recalculateFlatSize();
            return total + subCache.flatSize;
          }, 0)
        : 0;
  }

  /**
   * Adds an array of items corresponding to the given page
   * to the `items` array.
   *
   * @param {number} page
   * @param {object[]} items
   */
  setPage(page, items) {
    const startIndex = page * this.pageSize;
    items.forEach((item, i) => {
      const itemIndex = startIndex + i;
      if (this.size === undefined || itemIndex < this.size) {
        this.items[itemIndex] = item;
      }
    });
  }

  /**
   * Retrieves the sub-cache associated with the item at the given index
   * in the `items` array.
   *
   * @param {number} index
   * @return {Cache | undefined}
   */
  getSubCache(index) {
    return this.#subCacheByIndex[index];
  }

  /**
   * Removes the sub-cache associated with the item at the given index
   * in the `items` array.
   *
   * @param {number} index
   */
  removeSubCache(index) {
    delete this.#subCacheByIndex[index];
  }

  /**
   * Removes all sub-caches.
   */
  removeSubCaches() {
    this.#subCacheByIndex = {};
  }

  /**
   * Creates and associates a sub-cache for the item at the given index
   * in the `items` array.
   *
   * @param {number} index
   * @return {Cache}
   */
  createSubCache(index) {
    const subCache = new Cache(this.context, this.pageSize, 0, this, index);
    this.#subCacheByIndex[index] = subCache;
    return subCache;
  }

  /**
   * Retrieves the flattened index corresponding to the given index
   * of an item in the `items` array.
   *
   * @param {number} index
   * @return {number}
   */
  getFlatIndex(index) {
    const clampedIndex = Math.max(0, Math.min(this.size - 1, index));

    return this.subCaches.reduce((prev, subCache) => {
      const index = subCache.parentCacheIndex;
      return clampedIndex > index ? prev + subCache.flatSize : prev;
    }, clampedIndex);
  }
}
