import Hooks from "../hooks.mjs";
import {fromUuidSync} from "@client/utils/helpers.mjs";

/**
 * @import Document from "@common/abstract/document.mjs";
 * @import {CompendiumArtDescriptor, CompendiumArtInfo, CompendiumArtMapping} from "../_types.mjs"
 */

/**
 * A class responsible for managing package-provided art and applying it to Documents in compendium packs.
 * @extends {Map<string, CompendiumArtInfo>}
 */
export default class CompendiumArt extends Map {
  /**
   * @param {Iterable<[string, CompendiumArtInfo]>|null} [iterable]
   */
  constructor(iterable) {
    super(iterable);
    if ( game.compendiumArt instanceof this.constructor ) {
      throw new Error("You may not re-initialize the singleton CompendiumArt. Use game.compendiumArt instead.");
    }
  }

  /**
   * The key for the package manifest flag used to store the mapping information.
   * @type {string}
   */
  FLAG = "compendiumArtMappings";

  /**
   * The key for the setting used to store the World's art preferences.
   * @type {string}
   */
  SETTING = "compendiumArtConfiguration";

  /**
   * Whether art application is enabled. This should be switched off when performing client-side compendium migrations
   * in order to avoid persisting injected data.
   * @type {boolean}
   */
  enabled = true;

  /* -------------------------------------------- */

  /**
   * Apply any art configured for a Document to its source data as it is initialized from a compendium pack.
   * @param {typeof Document} documentClass  The class of the Document being initialized.
   * @param {object} source                  The Document's source data.
   * @param {string} [packId]                The ID of the compendium pack the Document is initialized from.
   * @returns {object}                       The Document's source data.
   */
  applyArt(documentClass, source, packId) {
    const pack = game.packs.get(packId);
    if ( !this.enabled || !source._id || (pack?.documentName !== documentClass.documentName) ) return source;
    const art = this.get(pack.getUuid(source._id));
    if ( !art ) return source;
    const img = art.img ?? art.actor;
    let applied = !!img;
    if ( img ) source.img = img;
    if ( documentClass.documentName === "Actor" ) {
      if ( typeof art.token === "string" ) source.prototypeToken.texture.src = art.token;
      else if ( art.token ) foundry.utils.mergeObject(source.prototypeToken, art.token);
      applied ||= !!art.token;
    }
    if ( applied ) Hooks.callAll("applyCompendiumArt", documentClass, source, pack, art);
    return source;
  }

  /* -------------------------------------------- */

  /**
   * Retrieve all active packages that provide art mappings in priority order.
   * @returns {CompendiumArtDescriptor[]}
   */
  getPackages() {
    const settings = game.settings.get("core", this.SETTING);
    const unsorted = [];
    const configs = [];

    for ( const pkg of [game.system, ...game.modules] ) {
      const isActive = (pkg instanceof foundry.packages.System) || pkg.active;
      const flag = pkg.flags?.[this.FLAG]?.[game.system.id];
      if ( !isActive || !flag ) continue;
      const { id: packageId, title } = pkg;
      const { mapping, credit } = flag;
      const config = { packageId, title, mapping, credit };
      configs.push(config);
      const setting = { portraits: true, tokens: true, items: true, ...settings[packageId] };
      foundry.utils.mergeObject(config, setting);
      if ( config.priority === undefined ) unsorted.push(config);
    }

    const maxPriority = Math.max(...configs.map(({ priority }) => priority ?? -Infinity), CONST.SORT_INTEGER_DENSITY);
    unsorted.forEach((config, i) => config.priority = maxPriority + ((i + 1) * CONST.SORT_INTEGER_DENSITY));
    configs.sort((a, b) => a.priority - b.priority);
    return configs;
  }

  /* -------------------------------------------- */

  /**
   * Collate Document art mappings from active packages.
   * @internal
   */
  async _registerArt() {
    this.clear();
    // Load packages in reverse order so that higher-priority packages overwrite lower-priority ones.
    for ( const { packageId, mapping, credit } of this.getPackages().reverse() ) {
      try {
        const json = await foundry.utils.fetchJsonWithTimeout(mapping);
        await this.#parseArtMapping(packageId, json, credit);
      } catch(e) {
        const pkg = packageId === game.system.id ? game.system : game.modules.get(packageId);
        Hooks.onError("CompendiumArt#_registerArt", e, {
          msg: `Failed to parse compendium art mapping for package '${pkg?.title}'`,
          log: "error"
        });
      }
    }
  }

  /* -------------------------------------------- */

  /**
   * Parse a provided art mapping and store it for reference later, and update compendium indices to use the provided
   * art.
   * @param {string} packageId              The ID of the package providing the mapping.
   * @param {CompendiumArtMapping} mapping  The art mapping information provided by the package.
   * @param {string} [credit]               An optional credit string for use by the game system to apply in an
   *                                        appropriate place.
   */
  async #parseArtMapping(packageId, mapping, credit) {
    const stored = game.settings.get("core", this.SETTING)?.[packageId];
    const settings = { portraits: true, tokens: true, items: true, ...stored };
    for ( const [packName, documents] of Object.entries(mapping) ) {
      const pack = game.packs.get(packName);
      if ( !pack ) continue;
      for ( const [documentId, info] of Object.entries(documents) ) {
        const entry = fromUuidSync(pack.getUuid(documentId));
        if ( !entry ) continue;
        switch ( pack.documentName ) {
          case "Actor":
            // The actor key is an alias of img, retained for backwards compatibility, and kept populated for consumers
            // which still rely on it.
            info.img ??= info.actor;
            if ( !(settings.portraits || settings.tokens) ) continue;
            if ( settings.portraits ) info.actor = entry.img = info.img;
            else {
              delete info.img;
              delete info.actor;
            }
            if ( !settings.tokens ) delete info.token;
            break;
          case "Item":
            if ( !settings.items || !info.img ) continue;
            entry.img = info.img;
            break;
          default:
            continue;
        }
        if ( credit ) info.credit = credit;
        this.set(entry.uuid, Object.assign(this.get(entry.uuid) ?? {}, info));
      }
    }
  }
}
