import {PrototypeToken, PrototypeTokenOverrides} from "@common/data/data.mjs";
import ApplicationV2 from "../../api/application.mjs";
import TokenApplicationMixin from "./mixin.mjs";
import FormDataExtended from "../../ux/form-data-extended.mjs";
import PrototypeOverridesConfig from "@client/applications/settings/menus/prototype-overrides.mjs";

/**
 * @import {ApplicationClickAction, ApplicationFormSubmission} from "../../_types.mjs";
 * @import DocumentSheetV2 from "../../api/document-sheet.mjs";
 * @import Actor from "@client/documents/actor.mjs";
 */

/**
 * The Application responsible for configuring an actor's PrototypeToken
 * @extends ApplicationV2
 * @mixes TokenApplication
 */
export default class PrototypeTokenConfig extends TokenApplicationMixin(ApplicationV2) {

  /** @inheritDoc */
  constructor(options) {
    super(options);
    this.#prototype = this.options.prototype;
  }

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

  /** @inheritDoc */
  static DEFAULT_OPTIONS = {
    tag: "form",
    classes: ["prototype-token-config"],
    actions: {
      assignToken: PrototypeTokenConfig.#onAssignToken,
      cycleImage: PrototypeTokenConfig.#onCycleImage,
      openOverridesConfig: PrototypeTokenConfig.#onOpenOverridesConfig
    },
    form: {
      handler: PrototypeTokenConfig.#onSubmit
    }
  };

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

  /**
   * The prototype token being edited
   * @type {PrototypeToken}
   */
  #prototype;

  /**
   * The index of the currently-previewed wildcard image.
   * @type {number}
   */
  #previewImageIndex = 0;

  /**
   * Debounced render of the appearance tab, used for live texture path updates.
   * @type {function}
   */
  #renderAppearance = foundry.utils.debounce(() => this.render({ parts: ["appearance"] }), 300);

  /** @override */
  isPrototype = true;

  /** @override */
  get title() {
    return `${_loc("TOKEN.TitlePrototype")}: ${this.actor.name}`;
  }

  /** @override */
  get token() {
    return this._preview ?? this.#prototype;
  }

  /**
   * The preview.
   * @type {PrototypeToken|null}
   * @protected
   */
  _preview = null;

  /**
   * @type {Actor}
   * @override
   */
  get actor() {
    return this.#prototype.parent;
  }

  /** @override */
  get _fields() {
    return PrototypeToken.schema.fields;
  }

  /**
   * Is this sheet visible to the user?
   * @returns {boolean}
   */
  get isVisible() {
    const ownsActor = this.actor?.isOwner ?? false;
    return ownsActor && game.user.can("TOKEN_CONFIGURE");
  }

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

  /** @inheritDoc */
  _canRender(options) {
    if ( !this.isVisible ) throw new Error(_loc("SHEETS.DocumentSheetPrivate", {type: "DOCUMENT.Token"}));
    return super._canRender(options);
  }

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

  /** @inheritDoc */
  _initializeApplicationOptions(options) {
    const initialized = super._initializeApplicationOptions(options);
    if ( options.prototype.parent?.uuid ) {
      const uuid = options.prototype.parent.uuid;
      initialized.id = `${this.constructor.name}-${uuid.replaceAll(".", "-")}`;
    }
    else throw new Error(`Prototype token ${options.prototype.name} lacks an identifiable parent.`);
    return initialized;
  }

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

  /** @inheritDoc */
  async _prepareContext(options) {
    if ( options.isFirstRender ) this._preview = this.#prototype.clone();
    const context = await super._prepareContext(options);
    const document = this._preview;
    return Object.assign(context, {
      document,
      model: document,
      source: document._source,
      rootId: this.id,
      gridUnits: _loc("MEASUREMENT.GridUnits")
    });
  }

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

  /** @inheritDoc */
  async _prepareAppearanceTab() {
    const context = await super._prepareAppearanceTab();
    const source = this.token._source;
    if ( source.randomImg ) {
      const images = await PrototypeTokenConfig.#getWildcardImages(source.texture.src);
      if ( images.length ) {
        this.#previewImageIndex = Math.clamp(this.#previewImageIndex, 0, images.length - 1);
        const src = images[this.#previewImageIndex];
        context.imagePreview = {
          src,
          cls: "token-image-preview wildcard",
          isVideo: foundry.helpers.media.VideoHelper.hasVideoExtension(src),
          hasPrev: this.#previewImageIndex > 0,
          hasNext: this.#previewImageIndex < (images.length - 1),
          current: this.#previewImageIndex + 1,
          total: images.length
        };
      }
    } else if ( source.texture.src ) {
      const { src } = source.texture;
      context.imagePreview = {
        src, cls: "token-image-preview", isVideo: foundry.helpers.media.VideoHelper.hasVideoExtension(src)
      };
    }
    return context;
  }

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

  /** @inheritDoc */
  _onChangeForm(formConfig, event) {
    super._onChangeForm(formConfig, event);
    const name = event.target.name || event.target.closest("[name]")?.name;
    const formData = new FormDataExtended(this.form);
    const submitData = this._processFormData(event, this.form, formData);
    this._previewChanges(submitData);

    // Re-render the appearance tab when wildcard mode is toggled.
    if ( name === "randomImg" ) this.render({ parts: ["appearance"] });

    // Debounced re-render when the image path changes to update the preview thumbnail.
    else if ( name === "texture.src" ) this.#renderAppearance();
  }

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

  /** @inheritDoc */
  _prepareButtons() {
    const buttons = super._prepareButtons();
    buttons.unshift({type: "button", icon: "fa-solid fa-user", label: "TOKEN.Assign", action: "assignToken"});
    return buttons;
  }

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

  /** @inheritDoc */
  async _onFirstRender(context, options) {
    await super._onFirstRender(context, options);
    this.#prototype.actor.apps[this.id] = this;
    game.settings.get("core", "prototypeTokenOverrides", {document: true}).apps[this.id] = this;
  }

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

  /**
   * Create a warning button informing the user why a field is disabled.
   * @returns {HTMLButtonElement}
   */
  #createOverridesWarning() {
    if ( this.#overridesWarning ) return this.#overridesWarning.cloneNode();
    const button = document.createElement("button");
    button.type = "button";
    button.className = "overrides-warning inline-control icon fa-solid fa-warning";
    button.dataset.action = "openOverridesConfig";
    button.dataset.tooltip = "";
    button.ariaLabel = _loc("TOKEN.PROTOTYPE.OverridesWarning");
    return (this.#overridesWarning = button);
  }

  /**
   * @type {HTMLButtonElement}
   */
  #overridesWarning;

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

  /** @inheritDoc */
  async _onRender(context, options) {
    await super._onRender(context, options);

    // Disable fields with PrototypeToken overrides applied
    let overrides = {};
    PrototypeTokenOverrides.applyOverrides(overrides, this.actor.type);
    if ( foundry.utils.isEmpty(overrides) ) return;
    overrides = foundry.utils.flattenObject(overrides);
    for ( const [path, value] of Object.entries(overrides) ) {
      if ( value === undefined ) continue;
      const input = this.form.elements[path];
      input.disabled = true;
      const formGroup = input.closest(".form-group");
      formGroup.classList.add("overridden");
      const label = formGroup.querySelector(":scope > label, :scope > span.label");
      label.append(this.#createOverridesWarning());
    }
  }

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

  /** @inheritDoc */
  _onClose(options) {
    super._onClose(options);
    this._preview = null;
  }

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

  /**
   * Preview changes.
   * @param {object} changes  The changes to preview.
   * @protected
   */
  _previewChanges(changes) {
    this._preview.updateSource(changes);
  }

  /* -------------------------------------------- */
  /*  Event Listeners and Handlers                */
  /* -------------------------------------------- */

  /**
   * Handle Token assignment requests to update the default prototype Token.
   * @this {PrototypeTokenConfig}
   * @type {ApplicationClickAction}
   */
  static async #onAssignToken() {
    // Get controlled Token data
    const tokens = canvas.ready ? canvas.tokens.controlled : [];
    if ( tokens.length !== 1 ) {
      ui.notifications.warn("TOKEN.AssignWarn", {localize: true});
      return;
    }
    const token = tokens.pop().document.toObject();
    token.tokenId = token.x = token.y = null;
    token.randomImg = this.form.elements.randomImg.checked;
    if ( token.randomImg ) delete token.texture.src;

    // Update the prototype token for the actor using the existing Token instance
    await this.actor.update({prototypeToken: token}, {diff: false, recursive: false, noHook: true});
    ui.notifications.info("TOKEN.AssignSuccess", {format: {name: this.actor.name}});
  }

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

  /**
   * Cycle through available wildcard images for the token image preview.
   * @this {PrototypeTokenConfig}
   * @type {ApplicationClickAction}
   */
  static async #onCycleImage(event, target) {
    const images = await PrototypeTokenConfig.#getWildcardImages(this.token._source.texture.src);
    this.#previewImageIndex = Math.clamp(this.#previewImageIndex + Number(target.dataset.delta), 0, images.length - 1);
    await this.render({ parts: ["appearance"] });
  }

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

  /**
   * Open the {@link PrototypeOverridesConfig} application to allow the user to review what overrides are causing some
   * fields in this sheet to be disabled.
   * @this {PrototypeTokenConfig}
   * @type {ApplicationClickAction}
   */
  static async #onOpenOverridesConfig() {
    const existing = foundry.applications.instances.get("prototype-token-overrides");
    if ( existing?.rendered ) existing.bringToFront();
    else new PrototypeOverridesConfig().render({force: true});
  }

  /* -------------------------------------------- */
  /*  Form Submission                             */
  /* -------------------------------------------- */

  /**
   * Customize how form data is extracted into an expanded object.
   * @param {SubmitEvent|null} event    The originating form submission event
   * @param {HTMLFormElement} form      The form element that was submitted
   * @param {FormDataExtended} formData Processed data for the submitted form
   * @returns {object} An expanded object of processed form data
   * @throws {Error}   Subclasses may throw validation errors here to prevent form submission
   * @protected
   */
  _processFormData(event, form, formData) {
    const submitData = foundry.utils.expandObject(formData.object);
    this._processChanges(submitData);
    return submitData;
  }

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

  /**
   * Process form submission for the sheet
   * @this {PrototypeTokenConfig}
   * @type {ApplicationFormSubmission}
   */
  static async #onSubmit(event, form, formData) {
    const submitData = this._processFormData(event, form, formData);
    const changes = {prototypeToken: submitData};
    this.actor.validate({changes, clean: true, fallback: false});
    await this.actor.update(changes);
  }

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

  /** @inheritDoc */
  _tearDown(options) {
    super._tearDown(options);
    delete this.actor.apps[this.id];
    delete game.settings.get("core", "prototypeTokenOverrides", {document: true}).apps[this.id];
  }

  /* -------------------------------------------- */
  /*  Helpers                                     */
  /* -------------------------------------------- */

  /**
   * Browse for images matching a wildcard texture path.
   * @param {string} src    The wildcard-eligible texture path.
   * @returns {Promise<string[]>}
   */
  static async #getWildcardImages(src) {
    if ( !src ) return [];
    const source = CONFIG.ux.FilePicker.matchS3URL(src) ? "s3" : "data";
    try {
      const result = await CONFIG.ux.FilePicker.browse(source, src, { wildcard: true });
      return result.files ?? [];
    } catch {
      return [];
    }
  }
}
