/**
 * Document Sheet Configuration Application
 */
class DocumentSheetConfig extends FormApplication {

  /** @inheritdoc */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["form", "sheet-config"],
      template: "templates/sheets/sheet-config.html",
      width: 400
    });
  }

  /**
   * An array of pending sheet assignments which are submitted before other elements of the framework are ready.
   * @type {object[]}
   * @private
   */
  static #pending = [];

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

  /** @inheritdoc */
  get title() {
    const name = this.object.name ?? game.i18n.localize(this.object.constructor.metadata.label);
    return `${name}: Sheet Configuration`;
  }

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

  /** @inheritDoc */
  getData(options={}) {
    const {sheetClasses, defaultClasses, defaultClass} = this.constructor.getSheetClassesForSubType(
      this.object.documentName,
      this.object.type || CONST.BASE_DOCUMENT_TYPE
    );

    // Return data
    return {
      isGM: game.user.isGM,
      object: this.object.toObject(),
      options: this.options,
      sheetClass: this.object.getFlag("core", "sheetClass") ?? "",
      blankLabel: game.i18n.localize("SHEETS.DefaultSheet"),
      sheetClasses, defaultClass, defaultClasses
    };
  }

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

  /** @inheritdoc */
  async _updateObject(event, formData) {
    event.preventDefault();
    const original = this.getData({});
    const defaultSheetChanged = formData.defaultClass !== original.defaultClass;
    const documentSheetChanged = formData.sheetClass !== original.sheetClass;

    // Update world settings
    if ( game.user.isGM && defaultSheetChanged ) {
      const setting = game.settings.get("core", "sheetClasses") || {};
      const type = this.object.type || CONST.BASE_DOCUMENT_TYPE;
      foundry.utils.mergeObject(setting, {[`${this.object.documentName}.${type}`]: formData.defaultClass});
      await game.settings.set("core", "sheetClasses", setting);

      // Trigger a sheet change manually if it wouldn't be triggered by the normal ClientDocument#_onUpdate workflow.
      if ( !documentSheetChanged ) return this.object._onSheetChange({ sheetOpen: true });
    }

    // Update the document-specific override
    if ( documentSheetChanged ) return this.object.setFlag("core", "sheetClass", formData.sheetClass);
  }

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

  /**
   * Marshal information on the available sheet classes for a given document type and sub-type, and format it for
   * display.
   * @param {string} documentName  The Document type.
   * @param {string} subType       The Document sub-type.
   * @returns {{sheetClasses: object, defaultClasses: object, defaultClass: string}}
   */
  static getSheetClassesForSubType(documentName, subType) {
    const config = CONFIG[documentName];
    const defaultClasses = {};
    let defaultClass = null;
    const sheetClasses = Object.values(config.sheetClasses[subType]).reduce((obj, cfg) => {
      if ( cfg.canConfigure ) obj[cfg.id] = cfg.label;
      if ( cfg.default && !defaultClass ) defaultClass = cfg.id;
      if ( cfg.canConfigure && cfg.canBeDefault ) defaultClasses[cfg.id] = cfg.label;
      return obj;
    }, {});
    return {sheetClasses, defaultClasses, defaultClass};
  }

  /* -------------------------------------------- */
  /*  Configuration Methods
  /* -------------------------------------------- */

  /**
   * Initialize the configured Sheet preferences for Documents which support dynamic Sheet assignment
   * Create the configuration structure for supported documents
   * Process any pending sheet registrations
   * Update the default values from settings data
   */
  static initializeSheets() {
    for ( let cls of Object.values(foundry.documents) ) {
      const types = this._getDocumentTypes(cls);
      CONFIG[cls.documentName].sheetClasses = types.reduce((obj, type) => {
        obj[type] = {};
        return obj;
      }, {});
    }

    // Register any pending sheets
    this.#pending.forEach(p => {
      if ( p.action === "register" ) this.#registerSheet(p);
      else if ( p.action === "unregister" ) this.#unregisterSheet(p);
    });
    this.#pending = [];

    // Update default sheet preferences
    const defaults = game.settings.get("core", "sheetClasses");
    this.updateDefaultSheets(defaults);
  }

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

  static _getDocumentTypes(cls, types=[]) {
    if ( types.length ) return types;
    return game.documentTypes[cls.documentName];
  }

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

  /**
   * Register a sheet class as a candidate which can be used to display documents of a given type
   * @param {typeof ClientDocument} documentClass  The Document class for which to register a new Sheet option
   * @param {string} scope                         Provide a unique namespace scope for this sheet
   * @param {typeof DocumentSheet} sheetClass      A defined Application class used to render the sheet
   * @param {object} [config]                      Additional options used for sheet registration
   * @param {string|Function} [config.label]       A human-readable label for the sheet name, which will be localized
   * @param {string[]} [config.types]              An array of document types for which this sheet should be used
   * @param {boolean} [config.makeDefault=false]   Whether to make this sheet the default for provided types
   * @param {boolean} [config.canBeDefault=true]   Whether this sheet is available to be selected as a default sheet for
   *                                               all Documents of that type.
   * @param {boolean} [config.canConfigure=true]   Whether this sheet appears in the sheet configuration UI for users.
   */
  static registerSheet(documentClass, scope, sheetClass, {
    label, types, makeDefault=false, canBeDefault=true, canConfigure=true
  }={}) {
    const id = `${scope}.${sheetClass.name}`;
    const config = {documentClass, id, label, sheetClass, types, makeDefault, canBeDefault, canConfigure};
    if ( game.ready ) this.#registerSheet(config);
    else {
      config.action = "register";
      this.#pending.push(config);
    }
  }

  /**
   * Perform the sheet registration.
   * @param {object} config                               Configuration for how the sheet should be registered
   * @param {typeof ClientDocument} config.documentClass  The Document class being registered
   * @param {string} config.id                            The sheet ID being registered
   * @param {string} config.label                         The human-readable sheet label
   * @param {typeof DocumentSheet} config.sheetClass      The sheet class definition being registered
   * @param {object[]} config.types                       An array of types for which this sheet is added
   * @param {boolean} config.makeDefault                  Make this sheet the default for provided types?
   * @param {boolean} config.canBeDefault                 Whether this sheet is available to be selected as a default
   *                                                      sheet for all Documents of that type.
   * @param {boolean} config.canConfigure                 Whether the sheet appears in the sheet configuration UI for
   *                                                      users.
   */
  static #registerSheet({documentClass, id, label, sheetClass, types, makeDefault, canBeDefault, canConfigure}={}) {
    types = this._getDocumentTypes(documentClass, types);
    const classes = CONFIG[documentClass.documentName]?.sheetClasses;
    const defaults = game.ready ? game.settings.get("core", "sheetClasses") : {};
    if ( typeof classes !== "object" ) return;
    for ( const t of types ) {
      classes[t] ||= {};
      const existingDefault = defaults[documentClass.documentName]?.[t];
      const isDefault = existingDefault ? (existingDefault === id) : makeDefault;
      if ( isDefault ) Object.values(classes[t]).forEach(s => s.default = false);
      if ( label instanceof Function ) label = label();
      else if ( label ) label = game.i18n.localize(label);
      else label = id;
      classes[t][id] = {
        id, label, canBeDefault, canConfigure,
        cls: sheetClass,
        default: isDefault
      };
    }
  }

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

  /**
   * Unregister a sheet class, removing it from the list of available Applications to use for a Document type
   * @param {typeof ClientDocument} documentClass  The Document class for which to register a new Sheet option
   * @param {string} scope                         Provide a unique namespace scope for this sheet
   * @param {typeof DocumentSheet} sheetClass      A defined DocumentSheet subclass used to render the sheet
   * @param {object} [config]
   * @param {object[]} [config.types]              An Array of types for which this sheet should be removed
   */
  static unregisterSheet(documentClass, scope, sheetClass, {types}={}) {
    const id = `${scope}.${sheetClass.name}`;
    const config = {documentClass, id, types};
    if ( game.ready ) this.#unregisterSheet(config);
    else {
      config.action = "unregister";
      this.#pending.push(config);
    }
  }

  /**
   * Perform the sheet de-registration.
   * @param {object} config                               Configuration for how the sheet should be un-registered
   * @param {typeof ClientDocument} config.documentClass  The Document class being unregistered
   * @param {string} config.id                            The sheet ID being unregistered
   * @param {object[]} config.types                       An array of types for which this sheet is removed
   */
  static #unregisterSheet({documentClass, id, types}={}) {
    types = this._getDocumentTypes(documentClass, types);
    const classes = CONFIG[documentClass.documentName]?.sheetClasses;
    if ( typeof classes !== "object" ) return;
    for ( let t of types ) {
      delete classes[t][id];
    }
  }

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

  /**
   * Update the current default Sheets using a new core world setting.
   * @param {object} setting
   */
  static updateDefaultSheets(setting={}) {
    if ( !Object.keys(setting).length ) return;
    for ( let cls of Object.values(foundry.documents) ) {
      const documentName = cls.documentName;
      const cfg = CONFIG[documentName];
      const classes = cfg.sheetClasses;
      const collection = cfg.collection?.instance ?? [];
      const defaults = setting[documentName];
      if ( !defaults ) continue;

      // Update default preference for registered sheets
      for ( let [type, sheetId] of Object.entries(defaults) ) {
        const sheets = Object.values(classes[type] || {});
        let requested = sheets.find(s => s.id === sheetId);
        if ( requested ) sheets.forEach(s => s.default = s.id === sheetId);
      }

      // Close and de-register any existing sheets
      for ( let document of collection ) {
        for ( const [id, app] of Object.entries(document.apps) ) {
          app.close();
          delete document.apps[id];
        }
        document._sheet = null;
      }
    }
  }

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

  /**
   * Initialize default sheet configurations for all document types.
   * @private
   */
  static _registerDefaultSheets() {
    const defaultSheets = {
      // Documents
      Actor: ActorSheet,
      Adventure: AdventureImporter,
      Folder: FolderConfig,
      Item: ItemSheet,
      JournalEntry: JournalSheet,
      Macro: MacroConfig,
      Playlist: PlaylistConfig,
      RollTable: RollTableConfig,
      Scene: SceneConfig,
      User: foundry.applications.sheets.UserConfig,
      // Embedded Documents
      ActiveEffect: ActiveEffectConfig,
      AmbientLight: foundry.applications.sheets.AmbientLightConfig,
      AmbientSound: foundry.applications.sheets.AmbientSoundConfig,
      Card: CardConfig,
      Combatant: CombatantConfig,
      Drawing: DrawingConfig,
      MeasuredTemplate: MeasuredTemplateConfig,
      Note: NoteConfig,
      PlaylistSound: PlaylistSoundConfig,
      Region: foundry.applications.sheets.RegionConfig,
      RegionBehavior: foundry.applications.sheets.RegionBehaviorConfig,
      Tile: TileConfig,
      Token: TokenConfig,
      Wall: WallConfig
    };

    Object.values(foundry.documents).forEach(base => {
      const type = base.documentName;
      const cfg = CONFIG[type];
      cfg.sheetClasses = {};
      const defaultSheet = defaultSheets[type];
      if ( !defaultSheet ) return;
      DocumentSheetConfig.registerSheet(cfg.documentClass, "core", defaultSheet, {
        makeDefault: true,
        label: () => game.i18n.format("SHEETS.DefaultDocumentSheet", {document: game.i18n.localize(`DOCUMENT.${type}`)})
      });
    });
    DocumentSheetConfig.registerSheet(Cards, "core", CardsConfig, {
      label: "CARDS.CardsDeck",
      types: ["deck"],
      makeDefault: true
    });
    DocumentSheetConfig.registerSheet(Cards, "core", CardsHand, {
      label: "CARDS.CardsHand",
      types: ["hand"],
      makeDefault: true
    });
    DocumentSheetConfig.registerSheet(Cards, "core", CardsPile, {
      label: "CARDS.CardsPile",
      types: ["pile"],
      makeDefault: true
    });
    DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalTextTinyMCESheet, {
      types: ["text"],
      label: () => game.i18n.localize("EDITOR.TinyMCE")
    });
    DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalImagePageSheet, {
      types: ["image"],
      makeDefault: true,
      label: () =>
        game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypeImage")})
    });
    DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalVideoPageSheet, {
      types: ["video"],
      makeDefault: true,
      label: () =>
        game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypeVideo")})
    });
    DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalPDFPageSheet, {
      types: ["pdf"],
      makeDefault: true,
      label: () =>
        game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypePDF")})
    });
    DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalTextPageSheet, {
      types: ["text"],
      makeDefault: true,
      label: () => {
        return game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {
          page: game.i18n.localize("JOURNALENTRYPAGE.TypeText")
        });
      }
    });
    DocumentSheetConfig.registerSheet(JournalEntryPage, "core", MarkdownJournalPageSheet, {
      types: ["text"],
      label: () => game.i18n.localize("EDITOR.Markdown")
    });
  }
}
