/**
 * @license
 * Copyright (c) 2021 - 2026 Vaadin Ltd.
 * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
 */
import { Cache } from './cache.js';
import { getFlatIndexByPath, getFlatIndexContext, getItemContext } from './helpers.js';

/**
 * A controller that stores and manages items loaded with a data provider.
 */
export class DataProviderController extends EventTarget {
  /**
   * The controller host element.
   *
   * @param {HTMLElement}
   */
  host;

  /**
   * A callback that returns data based on the passed params such as
   * `page`, `pageSize`, `parentItem`, etc.
   */
  dataProvider;

  /**
   * A callback that returns additional params that need to be passed
   * to the data provider callback with every request.
   */
  dataProviderParams;

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

  /**
   * A callback that returns whether the given item is expanded.
   *
   * @type {(item: unknown) => boolean}
   */
  isExpanded;

  /**
   * A callback that returns the id for the given item and that
   * is used when checking object items for equality.
   *
   * @type { (item: unknown) => unknown}
   */
  getItemId;

  /**
   * A reference to the root cache instance.
   *
   * @param {Cache}
   */
  rootCache;

  /**
   * A placeholder item that is used to indicate that the item is not loaded yet.
   *
   * @type {unknown}
   */
  placeholder;

  /**
   * A callback that returns whether the given item is a placeholder.
   *
   * @type {(item: unknown) => boolean}
   */
  isPlaceholder;

  constructor(
    host,
    { size, pageSize, isExpanded, getItemId, isPlaceholder, placeholder, dataProvider, dataProviderParams },
  ) {
    super();
    this.host = host;
    this.pageSize = pageSize;
    this.getItemId = getItemId;
    this.isExpanded = isExpanded;
    this.placeholder = placeholder;
    this.isPlaceholder = isPlaceholder;
    this.dataProvider = dataProvider;
    this.dataProviderParams = dataProviderParams;
    this.rootCache = this.#createRootCache(size);
  }

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

  /** @private */
  get #cacheContext() {
    return {
      isExpanded: this.isExpanded,
      placeholder: this.placeholder,
    };
  }

  /**
   * Whether the root cache or any of its decendant caches have pending requests.
   *
   * @return {boolean}
   */
  isLoading() {
    return this.rootCache.isLoading;
  }

  /**
   * Sets the page size and clears the cache.
   *
   * @param {number} pageSize
   */
  setPageSize(pageSize) {
    this.pageSize = pageSize;
    this.clearCache();
  }

  /**
   * Sets the data provider callback and clears the cache.
   *
   * @type {Function}
   */
  setDataProvider(dataProvider) {
    this.dataProvider = dataProvider;
    this.clearCache();
  }

  /**
   * Recalculates the flattened size.
   */
  recalculateFlatSize() {
    this.rootCache.recalculateFlatSize();
  }

  /**
   * Clears the cache.
   */
  clearCache() {
    this.rootCache = this.#createRootCache(this.rootCache.size);
  }

  /**
   * Returns context for the given flattened index, including:
   * - the corresponding cache
   * - the cache level
   * - the corresponding item (if loaded)
   * - the item's index in the cache's items array
   * - the page containing the item
   *
   * @param {number} flatIndex
   */
  getFlatIndexContext(flatIndex) {
    return getFlatIndexContext(this.rootCache, flatIndex);
  }

  /**
   * Returns context for the given item, including:
   * - the cache containing the item
   * - the cache level
   * - the item
   * - the item's index in the cache's items array
   * - the item's flattened index
   * - the item's sub-cache (if exists)
   * - the page containing the item
   *
   * If the item isn't found, the method returns undefined.
   */
  getItemContext(item) {
    return getItemContext({ getItemId: this.getItemId }, this.rootCache, item);
  }

  /**
   * Returns the flattened index for the item that the given indexes point to.
   * Each index in the path array points to a sub-item of the previous index.
   * Using `Infinity` as an index will point to the last item on the level.
   *
   * @param {number[]} path
   * @return {number}
   */
  getFlatIndexByPath(path) {
    return getFlatIndexByPath(this.rootCache, path);
  }

  /**
   * Requests the data provider to load the page with the item corresponding
   * to the given flattened index. If the item is already loaded, the method
   * returns immediatelly.
   *
   * @param {number} flatIndex
   */
  ensureFlatIndexLoaded(flatIndex) {
    const { cache, page, item } = this.getFlatIndexContext(flatIndex);

    if (!this.#isItemLoaded(item)) {
      this.#loadCachePage(cache, page);
    }
  }

  /**
   * Creates a sub-cache for the item corresponding to the given flattened index and
   * requests the data provider to load the first page into the created sub-cache.
   * If the sub-cache already exists, the method returns immediatelly.
   *
   * @param {number} flatIndex
   */
  ensureFlatIndexHierarchy(flatIndex) {
    const { cache, item, index } = this.getFlatIndexContext(flatIndex);

    if (this.#isItemLoaded(item) && this.isExpanded(item) && !cache.getSubCache(index)) {
      const subCache = cache.createSubCache(index);
      this.#loadCachePage(subCache, 0);
    }
  }

  /**
   * Loads the first page into the root cache.
   */
  loadFirstPage() {
    this.#loadCachePage(this.rootCache, 0);
  }

  /**
   * Override to prevent loading of the cache page under certain conditions.
   *
   * @param {Cache} cache
   * @param {number} page
   * @protected
   */
  _shouldLoadCachePage(_cache, _page) {
    return true;
  }

  /** @private */
  #createRootCache(size) {
    return new Cache(this.#cacheContext, this.pageSize, size);
  }

  /** @private */
  #loadCachePage(cache, page) {
    if (!this.dataProvider || cache.pendingRequests[page] || !this._shouldLoadCachePage(cache, page)) {
      return;
    }

    let params = {
      page,
      pageSize: this.pageSize,
      parentItem: cache.parentItem,
    };

    if (this.dataProviderParams) {
      params = { ...params, ...this.dataProviderParams() };
    }

    const callback = (items, size) => {
      if (cache.pendingRequests[page] !== callback) {
        return;
      }

      if (size !== undefined) {
        cache.size = size;
      } else if (params.parentItem) {
        cache.size = items.length;
      }

      cache.setPage(page, items);

      this.recalculateFlatSize();

      this.dispatchEvent(new CustomEvent('page-received'));

      delete cache.pendingRequests[page];

      this.dispatchEvent(new CustomEvent('page-loaded'));
    };

    cache.pendingRequests[page] = callback;

    this.dispatchEvent(new CustomEvent('page-requested'));

    this.dataProvider(params, callback);
  }

  /** @private */
  #isItemLoaded(item) {
    if (this.isPlaceholder) {
      return !this.isPlaceholder(item);
    } else if (this.placeholder) {
      return item !== this.placeholder;
    }
    return !!item;
  }
}
