import CONFIGURATION from "./adventure.mjs";

/**
 * @typedef {Object} LocalizationData
 * @property {Set<string>} html       HTML files which provide Journal Entry page translations
 * @property {object} i18n            An object of localization keys and translation strings
 */

/**
 * A subclass of the core AdventureImporter which performs some special functions for Pathfinder premium content.
 */
export default class PF2EAdventureImporter extends AdventureImporter {
  constructor(adventure, options) {
    super(adventure, options);
    this.config = CONFIGURATION.adventure;
    this.options.classes.push(CONFIGURATION.cssClass);
  }

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

  /** @inheritDoc */
  async getData(options={}) {
    return foundry.utils.mergeObject(await super.getData(), {
      importOptions: this.config.importOptions || {}
    });
  }

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

  /** @inheritDoc */
  async _renderInner(data) {
    const html = await super._renderInner(data);
    if ( !this.config.importOptions ) return html;

    // Insert import controls.
    const imported = game.settings.get(CONFIGURATION.moduleId, "imported");
    if ( imported ) this.#addImportControls(html.find(".adventure-contents")[0]);

    // Insert options and return
    html.find(".adventure-contents").append(this.#formatOptions());
    return html;
  }

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

  /**
   * Format adventure import options block.
   * @returns {string}
   */
  #formatOptions() {
    let options = `<section class="import-form"><h2>Importer Options</h2>`;
    for ( const [name, option] of Object.entries(this.config.importOptions) ) {
      options += `<div class="form-group">
        <label class="checkbox">
            <input type="checkbox" name="${name}" title="${option.label}" ${option.default ? "checked" : ""}/>
            ${option.label}
        </label>
      </div>`;
    }
    options += `</section>`;
    return options;
  }

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

  /**
   * Add controls for which content to import.
   * @param {HTMLElement} content  The adventure content container.
   */
  #addImportControls(content) {
    const heading = content.querySelector("h2");
    const list = content.querySelector("ul");
    const section = document.createElement("section");
    section.classList.add("import-controls");
    let html = `
      <div class="form-group">
        <label class="checkbox">
          <input type="checkbox" name="importFields" value="all" title="Import All" checked>
          Import All
        </label>
      </div>
    `;
    for (const [field, cls] of Object.entries(Adventure.contentFields)) {
      const count = this.object[field].size;
      if ( !count ) continue;
      const label = game.i18n.localize(count > 1 ? cls.metadata.labelPlural : cls.metadata.label);
      html += `
        <div class="form-group">
          <label class="checkbox">
            <input type="checkbox" name="importFields" value="${field}" title="Import ${label}"
                   checked disabled>
            <i class="${CONFIG[cls.documentName].sidebarIcon}"></i>
            ${count} ${label}
          </label>
        </div>
      `;
    }
    section.innerHTML = html;
    section.insertAdjacentElement("afterbegin", heading);
    list.before(section);
    list.remove();
    section.querySelector('[value="all"]').addEventListener("change", event => {
      this.#onToggleImportAll(event);
    });
  }

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

  /**
   * Handle toggling the import all checkbox.
   * @param {Event} event  The change event.
   */
  #onToggleImportAll(event) {
    const target = event.currentTarget;
    const section = target.closest(".import-controls");
    const checked = target.checked;
    section.querySelectorAll("input").forEach(input => {
      if ( input.value !== "folders" ) input.disabled = checked;
      if ( checked ) input.checked = true;
    });
    target.disabled = false;
  }

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

  /** @inheritDoc */
  async _prepareImportData(formData) {
    this.submitOptions = formData;
    const {toCreate, toUpdate, documentCount} = await super._prepareImportData(formData);
    this.#applyImportControls(formData, toCreate, toUpdate);
    this.#applyEnhancedMapsPreference(formData.enhancedMaps, toCreate, toUpdate);

    // Prepare localization data
    const localization = await this.#prepareLocalizationData();

    // Merge Compendium Actor data
    if ( "Actor" in toCreate ) await this.#mergeCompendiumActors(toCreate.Actor, formData);
    if ( "Actor" in toUpdate ) await this.#mergeCompendiumActors(toUpdate.Actor, formData);

    // Merge Journal HTML data
    if ( "JournalEntry" in toCreate ) await this.#mergeJournalHTML(toCreate.JournalEntry, localization);
    if ( "JournalEntry" in toUpdate ) await this.#mergeJournalHTML(toUpdate.JournalEntry, localization);

    // Apply localized translations
    await this.#applyTranslations(toCreate, localization);
    await this.#applyTranslations(toUpdate, localization);
    return {toCreate, toUpdate, documentCount};
  }

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

  /** @inheritDoc */
  async _importContent(toCreate, toUpdate, documentCount) {
    const importResult = await super._importContent(toCreate, toUpdate, documentCount);
    for ( let [name, option] of Object.entries(this.config.importOptions || {}) ) {
      if ( !option.handler ) continue;
      await option.handler(this.document, option, this.submitOptions[name]);
    }
    return importResult;
  }

  /* -------------------------------------------- */
  /*  Pre-Import Customizations                   */
  /* -------------------------------------------- */

  /**
   * Get available localization data which can be used during the import process
   * @returns {Promise<LocalizationData>}
   */
  async #prepareLocalizationData() {
    const path = `modules/${CONFIGURATION.moduleId}/lang/${game.i18n.lang}/${this.config.slug}`;
    if ( game.i18n.lang === "en" ) return {path, i18n: {}, html: new Set()};
    const json = `${path}/${this.config.slug}.json`;
    try {
      const files = (await FilePicker.browse("data", path)).files;
      const i18n = files.includes(json) ? await fetch(json).then(r => r.json()) : {};
      const html = new Set(files.filter(f => f.endsWith(".html")));
      return {path, i18n, html};
    } catch(err) {
      return {path, i18n: {}, html: new Set()};
    }
  }

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

  /**
   * Merge Actor data with authoritative source data from system compendium packs
   * @param {Actor[]} actors        Actor documents intended to be imported
   * @param {object} importOptions  Form submission import options
   * @returns {Promise<void>}
   */
  async #mergeCompendiumActors(actors, importOptions) {
    for ( const actor of actors ) {
      const sourceId = actor.flags?.core?.sourceId;
      if ( !sourceId ) {
        console.warn(`[${CONFIGURATION.moduleId}] Actor "${actor.name}" [${actor._id}] had no `
          + "sourceId to retrieve source data from.");
        continue;
      }
      const source = await fromUuid(sourceId);
      if ( source ) {
        const {system, items, effects} = source.toObject();
        const updateData = {
          system, items, effects,
          "flags.core.sourceId": source.uuid
        };
        const overrides = this.config.actorOverrides[actor._id] || [];
        for ( const field of overrides ) delete updateData[field];
        foundry.utils.mergeObject(actor, updateData);
      } else {
        const [, scope, packName] = sourceId?.split(".") ?? [];
        console.warn(`[${CONFIGURATION.moduleId}] Compendium source data for "${actor.name}" `
            + `[${actor._id}] not found in pack ${scope}.${packName}.`);
      }
    }
  }

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

  /**
   * Merge JournalEntry data with localized source HTML.
   * @param {JournalEntry[]} entries                JournalEntry documents intended to be imported
   * @param {LocalizationData} localization         Localization configuration data
   * @returns {Promise<void>}
   */
  async #mergeJournalHTML(entries, localization) {
    for ( const entry of entries ) {
      for ( const page of entry.pages ) {
        const htmlFile = `${localization.path}/${page._id}-${page.name.slugify({strict: true})}.html`;
        if ( localization.html.has(htmlFile) ) {
          const content = await fetch(htmlFile).then(r => r.text()).catch(err => null);
          if ( content ) foundry.utils.mergeObject(page, {"text.content": content});
        }
      }
    }
  }

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

  /**
   * Apply localization translations to documents prior to import.
   * @param {Object<string,Document[]>} group       A group of documents to be created or updated
   * @param {LocalizationData} localization         Localization configuration data
   * @returns {Promise<void>}
   */
  async #applyTranslations(group, localization) {
    for ( const [documentName, documents] of Object.entries(group) ) {
      const translations = localization.i18n[documentName] || [];
      for ( const document of documents ) {
        const translation = translations.find(d => d._id === document._id);
        if ( translation ) foundry.utils.mergeObject(document, translation);
      }
    }
  }

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

  /**
   * Remove adventure content that the user indicated they did not want to import.
   * @param {object} formData  The submitted adventure form data.
   * @param {object} toCreate  An object of document data to create.
   * @param {object} toUpdate  An object of document data to update.
   */
  #applyImportControls(formData, toCreate, toUpdate) {
    if ( !game.settings.get(CONFIGURATION.moduleId, "imported") ) return;
    const fields = formData.importFields.filter(_ => _);
    fields.push("folders");
    if ( !fields || !Array.isArray(fields) || fields.some(field => field === "all") ) return;
    const keep = new Set(fields.map(field => Adventure.contentFields[field].documentName));
    [toCreate, toUpdate].forEach(docs => {
      for ( const type of Object.keys(docs) ) {
        if ( !keep.has(type) ) delete docs[type];
      }
      if ( docs.Folder ) docs.Folder = docs.Folder.filter(f => keep.has(f.type));
    });
  }

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

  /**
   * Remove scenes from the import depending on whether the user wants only the enhanced maps or
   * only the original ones.
   * @param {boolean} useEnhanced  Whether to import enhanced or original maps.
   * @param {object} toCreate      An object of document data to create.
   * @param {object} toUpdate      An object of document data to update.
   */
  #applyEnhancedMapsPreference(useEnhanced, toCreate, toUpdate) {
    const sceneIds = this.config.importOptions.enhancedMaps.sceneIds;
    const affectedScenes = new Set(sceneIds.original.concat(sceneIds.enhanced));
    const original = new Set(sceneIds.original);
    const enhanced = new Set(sceneIds.enhanced);
    [toCreate, toUpdate].forEach(docs => {
      if ( docs.Scene ) docs.Scene = docs.Scene.filter(s => {
        if ( !affectedScenes.has(s._id) ) return true;
        return (useEnhanced && enhanced.has(s._id)) || (!useEnhanced && original.has(s._id));
      });
    });
  }
}
