/**
 * A collection of world-level Document objects with a singleton instance per primary Document type.
 * Each primary Document type has an associated subclass of WorldCollection which contains them.
 * @extends {DocumentCollection}
 * @abstract
 * @see {Game#collections}
 *
 * @param {object[]} data      An array of data objects from which to create Document instances
 */
class WorldCollection extends DirectoryCollectionMixin(DocumentCollection) {
  /* -------------------------------------------- */
  /*  Collection Properties                       */
  /* -------------------------------------------- */

  /**
   * Reference the set of Folders which contain documents in this collection
   * @type {Collection<string, Folder>}
   */
  get folders() {
    return game.folders.reduce((collection, folder) => {
      if (folder.type === this.documentName) {
        collection.set(folder.id, folder);
      }
      return collection;
    }, new foundry.utils.Collection());
  }

  /**
   * Return a reference to the SidebarDirectory application for this WorldCollection.
   * @type {DocumentDirectory}
   */
  get directory() {
    const doc = getDocumentClass(this.constructor.documentName);
    return ui[doc.metadata.collection];
  }

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

  /**
   * Return a reference to the singleton instance of this WorldCollection, or null if it has not yet been created.
   * @type {WorldCollection}
   */
  static get instance() {
    return game.collections.get(this.documentName);
  }

  /* -------------------------------------------- */
  /*  Collection Methods                          */
  /* -------------------------------------------- */

  /** @override */
  _getVisibleTreeContents(entry) {
    return this.contents.filter(c => c.visible);
  }

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

  /**
   * Import a Document from a Compendium collection, adding it to the current World.
   * @param {CompendiumCollection} pack The CompendiumCollection instance from which to import
   * @param {string} id             The ID of the compendium entry to import
   * @param {object} [updateData]   Optional additional data used to modify the imported Document before it is created
   * @param {object} [options]      Optional arguments passed to the {@link WorldCollection#fromCompendium} and
   *                                {@link Document.create} methods
   * @returns {Promise<Document>}   The imported Document instance
   */
  async importFromCompendium(pack, id, updateData={}, options={}) {
    const cls = this.documentClass;
    if (pack.documentName !== cls.documentName) {
      throw new Error(`The ${pack.documentName} Document type provided by Compendium ${pack.collection} is incorrect for this Collection`);
    }

    // Prepare the source data from which to create the Document
    const document = await pack.getDocument(id);
    const sourceData = this.fromCompendium(document, options);
    const createData = foundry.utils.mergeObject(sourceData, updateData);

    // Create the Document
    console.log(`${vtt} | Importing ${cls.documentName} ${document.name} from ${pack.collection}`);
    this.directory.activate();
    options.fromCompendium = true;
    return this.documentClass.create(createData, options);
  }

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

  /**
   * @typedef {object} FromCompendiumOptions
   * @property {boolean} [options.clearFolder=false]    Clear the currently assigned folder.
   * @property {boolean} [options.clearSort=true]       Clear the current sort order.
   * @property {boolean} [options.clearOwnership=true]  Clear Document ownership.
   * @property {boolean} [options.keepId=false]         Retain the Document ID from the source Compendium.
   */

  /**
   * Apply data transformations when importing a Document from a Compendium pack
   * @param {Document|object} document         The source Document, or a plain data object
   * @param {FromCompendiumOptions} [options]  Additional options which modify how the document is imported
   * @returns {object}                         The processed data ready for world Document creation
   */
  fromCompendium(document, {clearFolder=false, clearSort=true, clearOwnership=true, keepId=false, ...rest}={}) {
    /** @deprecated since v12 */
    if ( "addFlags" in rest ) {
      foundry.utils.logCompatibilityWarning("The addFlags option for WorldCompendium#fromCompendium has been removed. ",
        { since: 12, until: 14 });
    }

    // Prepare the data structure
    let data = document;
    if (document instanceof foundry.abstract.Document) {
      data = document.toObject();
      if ( document.pack ) foundry.utils.setProperty(data, "_stats.compendiumSource", document.uuid);
    }

    // Eliminate certain fields
    if ( !keepId ) delete data._id;
    if ( clearFolder ) delete data.folder;
    if ( clearSort ) delete data.sort;
    if ( clearOwnership && ("ownership" in data) ) {
      data.ownership = {
        default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE,
        [game.user.id]: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
      };
    }
    return data;
  }

  /* -------------------------------------------- */
  /*  Sheet Registration Methods                  */
  /* -------------------------------------------- */

  /**
   * Register a Document sheet class as a candidate which can be used to display Documents of a given type.
   * See {@link DocumentSheetConfig.registerSheet} for details.
   * @static
   * @param {Array<*>} args      Arguments forwarded to the DocumentSheetConfig.registerSheet method
   *
   * @example Register a new ActorSheet subclass for use with certain Actor types.
   * ```js
   * Actors.registerSheet("dnd5e", ActorSheet5eCharacter, { types: ["character], makeDefault: true });
   * ```
   */
  static registerSheet(...args) {
    DocumentSheetConfig.registerSheet(getDocumentClass(this.documentName), ...args);
  }

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

  /**
   * Unregister a Document sheet class, removing it from the list of available sheet Applications to use.
   * See {@link DocumentSheetConfig.unregisterSheet} for detauls.
   * @static
   * @param {Array<*>} args      Arguments forwarded to the DocumentSheetConfig.unregisterSheet method
   *
   * @example Deregister the default ActorSheet subclass to replace it with others.
   * ```js
   * Actors.unregisterSheet("core", ActorSheet);
   * ```
   */
  static unregisterSheet(...args) {
    DocumentSheetConfig.unregisterSheet(getDocumentClass(this.documentName), ...args);
  }

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

  /**
   * Return an array of currently registered sheet classes for this Document type.
   * @static
   * @type {DocumentSheet[]}
   */
  static get registeredSheets() {
    const sheets = new Set();
    for ( let t of Object.values(CONFIG[this.documentName].sheetClasses) ) {
      for ( let s of Object.values(t) ) {
        sheets.add(s.cls);
      }
    }
    return Array.from(sheets);
  }
}
