import HandlebarsApplicationMixin from "../../api/handlebars-application.mjs";
import FormDataExtended from "../../ux/form-data-extended.mjs";

/**
 * @import ApplicationV2 from "../../api/application.mjs";
 * @import {DataSchema} from "@common/abstract/_types.mjs";
 * @import {PrototypeToken} from "@common/data/data.mjs";
 * @import TokenDocument from "@client/documents/token.mjs";
 * @import {NumberField} from "@common/data/fields.mjs";
 * @import {HTMLMultiCheckboxElement} from "../../elements/multi-select.mjs";
 * @import {ApplicationClickAction, ApplicationTab, FormFooterButton} from "../../_types.mjs";
 */

/**
 * A mixin for UI shared between TokenDocument and PrototypeToken sheets
 * @param {typeof ApplicationV2} Base
 */
export default function TokenApplicationMixin(Base) {
  class TokenApplication extends HandlebarsApplicationMixin(Base) {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      classes: ["token-config"],
      window: {
        contentClasses: ["standard-form"],
        icon: "fa-solid fa-circle-user"
      },
      position: {width: 560},
      form: {closeOnSubmit: true},
      actions: {
        overrideDetectionMode: TokenApplication.#onOverrideDetectionMode,
        removeDetectionMode: TokenApplication.#onRemoveDetectionMode
      }
    };

    /** @override */
    static PARTS = {
      tabs: {template: "templates/generic/tab-navigation.hbs"},
      identity: {template: "templates/scene/token/identity.hbs", scrollable: [""]},
      appearance: {template: "templates/scene/token/appearance.hbs", scrollable: [""]},
      vision: {template: "templates/scene/token/vision.hbs", scrollable: [""]},
      light: {template: "templates/scene/token/light.hbs", scrollable: [""]},
      resources: {template: "templates/scene/token/resources.hbs", scrollable: [""]},
      footer: {template: "templates/generic/form-footer.hbs"}
    };

    /** @override */
    static TABS = {
      sheet: {
        tabs: [
          {id: "identity", icon: "fa-solid fa-memo-pad"},
          {id: "appearance", icon: "fa-solid fa-square-user"},
          {id: "vision", icon: "fa-solid fa-eye"},
          {id: "light", icon: "fa-solid fa-lightbulb"},
          {id: "resources", icon: "fa-solid fa-heart"}
        ],
        initial: "identity",
        labelPrefix: "TOKEN.TABS"
      }
    };

    /**
     * Localized Token Display Modes
     * @returns {Record<string, string>}
     */
    static get DISPLAY_MODES() {
      TokenApplication.#DISPLAY_MODES ??= Object.entries(CONST.TOKEN_DISPLAY_MODES).reduce((modes, [key, value]) => {
        modes[value] = _loc(`TOKEN.DISPLAY_${key}`);
        return modes;
      }, {});
      return TokenApplication.#DISPLAY_MODES;
    }

    static #DISPLAY_MODES;

    /**
     * Localized Token Dispositions
     * @returns {Record<string, string>}
     */
    static get TOKEN_DISPOSITIONS() {
      TokenApplication.#TOKEN_DISPOSITIONS ??= Object.entries(CONST.TOKEN_DISPOSITIONS)
        .reduce((dispositions, [key, value]) => {
          dispositions[value] = _loc(`TOKEN.DISPOSITION.${key}`);
          return dispositions;
        }, {});
      return TokenApplication.#TOKEN_DISPOSITIONS;
    }

    static #TOKEN_DISPOSITIONS;

    /**
     * Localized Token Turn Marker modes
     * @returns {Record<string, string>}
     */
    static get TURN_MARKER_MODES() {
      TokenApplication.#TURN_MARKER_MODES ??= Object.entries(CONST.TOKEN_TURN_MARKER_MODES)
        .reduce((modes, [key, value]) => {
          modes[value] = _loc(`TOKEN.TURNMARKER.MODES.${key}`);
          return modes;
        }, {});
      return TokenApplication.#TURN_MARKER_MODES;
    }

    static #TURN_MARKER_MODES;

    /**
     * Localized Token Shapes
     * @returns {Record<string, string>}
     */
    static get TOKEN_SHAPES() {
      TokenApplication.#TOKEN_SHAPES ??= Object.entries(CONST.TOKEN_SHAPES)
        .reduce((shapes, [key, value]) => {
          shapes[value] = _loc(`TOKEN.SHAPES.${key}.label`);
          return shapes;
        }, {});
      return TokenApplication.#TOKEN_SHAPES;
    }

    static #TOKEN_SHAPES;

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

    /**
     * Maintain a copy of the original to show a real-time preview of changes.
     * @type {TokenDocument|PrototypeToken|null}
     * @protected
     */
    _preview = null;

    /**
     * Is the token a PrototypeToken?
     * @type {boolean}
     * @abstract
     */
    isPrototype;

    /**
     * A reference to the Actor the token depicts
     * @returns {Actor|null}
     * @abstract
     */
    get actor() {
      throw new Error("The Base class must implement this getter.");
    }

    /**
     * The TokenDocument or PrototypeToken
     * @returns {TokenDocument|PrototypeToken}
     * @abstract
     */
    get token() {
      throw new Error("The Base class must implement this getter.");
    }

    /**
     * The schema fields for this token DataModel
     * @returns {DataSchema}
     * @protected
     * @abstract
     */
    get _fields() {
      throw new Error("The Base class must implement this getter.");
    }

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

    /**
     * Get an Object of image paths and filenames to display in the Token sheet
     * @returns {Promise<Record<string, string>>}
     */
    async #getAlternateTokenImages() {
      if ( !this.actor?.prototypeToken.randomImg ) return {};
      const alternates = await this.actor.getTokenImages();
      return alternates.reduce((obj, img) => {
        obj[img] = img.split("/").pop();
        return obj;
      }, {});
    }

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

    /**
     * Render the Token ring effects input using a multi-checkbox element.
     * @param {NumberField} field                   The ring effects field
     * @param {FormInputConfig<string[]>} inputConfig Form input configuration
     * @returns {HTMLMultiCheckboxElement}
     */
    #ringEffectsInput(field, inputConfig) {
      const options = [];
      const value = [];
      for ( const [effectName, effectValue] of Object.entries(CONFIG.Token.ring.ringClass.effects) ) {
        const localization = CONFIG.Token.ring.effects[effectName];
        if ( (effectName === "DISABLED") || (effectName === "ENABLED") || !localization ) continue;
        options.push({value: effectName, label: _loc(localization)});
        if ( (inputConfig.value & effectValue) !== 0 ) value.push(effectName);
      }
      Object.assign(inputConfig, {name: field.fieldPath, options, value, type: "checkboxes"});
      return foundry.applications.fields.createMultiSelectInput(inputConfig);
    }

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

    /** @inheritDoc */
    async _prepareContext(options) {
      const context = await super._prepareContext(options);
      return Object.assign(context, {
        tabClasses: "top-tabs",
        fields: this._fields,
        isPrototype: this.isPrototype,
        displayModes: TokenApplication.DISPLAY_MODES,
        buttons: this._prepareButtons()
      });
    }

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

    /** @inheritDoc */
    async _preparePartContext(partId, context, options) {
      context = await super._preparePartContext(partId, context, options);
      switch ( partId ) {
        case "footer":
          context.buttons = this._prepareButtons();
          break;
        default: {
          const tab = context.tabs[partId];
          if ( tab ) {
            context.tab = tab;
            const tabContext = await this[`_prepare${partId.titleCase()}Tab`]?.();
            Object.assign(context, tabContext ?? {});
          }
        }
      }
      return context;
    }

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

    /**
     * Prepare data to be displayed in the Identity tab.
     * @protected
     */
    _prepareIdentityTab() {
      const currentAction = this.token._source.movementAction;
      const defaultAction = this.isPrototype ? undefined
        : (!currentAction ? this.token.movementAction : this.token._inferMovementAction());
      const defaultLabel = _loc("COMMON.Default");
      return {
        isGM: game.user.isGM,
        actors: game.actors
          .filter(a => a.isOwner)
          .map(a => ({value: a.id, label: a.name}))
          .sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang)),
        defaultMovementActionLabel: defaultAction
          ? `${defaultLabel} (${_loc(CONFIG.Token.movement.actions[defaultAction].label)})`
          : defaultLabel,
        movementActions: Object.entries(CONFIG.Token.movement.actions).reduce(
          (choices, [action, {label, canSelect}]) => {
            if ( canSelect(this.token) || (action === currentAction) ) choices[action] = _loc(label);
            return choices;
          }, {}),
        dispositions: TokenApplication.TOKEN_DISPOSITIONS
      };
    }

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

    /**
     * Prepare data to be displayed in the Appearance tab.
     * @returns {Promise<object>}
     * @protected
     */
    async _prepareAppearanceTab() {
      const token = this.token;
      const source = token._source;
      const alternateImages = await this.#getAlternateTokenImages();
      const canBrowseFiles = game.user.hasPermission("FILES_BROWSE");
      return {
        shapes: this.isPrototype ? undefined : TokenApplication.TOKEN_SHAPES,
        hasAlternates: !foundry.utils.isEmpty(alternateImages),
        alternateImages,
        colorationTechniques: foundry.canvas.rendering.shaders.AdaptiveLightingShader.SHADER_TECHNIQUES,
        randomImgEnabled: this.isPrototype && (canBrowseFiles || source.randomImg),
        scale: Math.abs(source.texture.scaleX),
        mirrorX: source.texture.scaleX < 0,
        mirrorY: source.texture.scaleY < 0,
        textureFitModes: CONST.TEXTURE_DATA_FIT_MODES.reduce((obj, fit) => {
          obj[fit] = _loc(`TEXTURE_DATA.FIT.${fit}`);
          return obj;
        }, {}),
        ringEffectsInput: this.#ringEffectsInput.bind(this),
        usingSubject: source.ring.enabled && source.ring.subject.texture
      };
    }

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

    /**
     * Prepare data to be displayed in the Vision tab.
     * @returns {Promise<object>}
     * @protected
     */
    async _prepareVisionTab() {
      const token = this.token;
      const visionModes = Object.values(CONFIG.Canvas.visionModes)
        .filter(f => f.tokenConfig)
        .reduce((modes, mode) => {
          modes[mode.id] = mode.label;
          return modes;
        }, {});
      const detectionModes = [];
      for ( let {id, label, tokenConfig} of Object.values(CONFIG.Canvas.detectionModes) ) {
        if ( !tokenConfig ) continue;
        label = _loc(label);
        const source = id in token._source.detectionModes;
        let mode = source ? token._source.detectionModes[id] : token.detectionModes[id];
        if ( !mode ) mode = {range: 0, enabled: false};
        let {range, ...rest} = mode;
        if ( range === Infinity ) range = null;
        detectionModes.push({id, range, label, source, ...rest});
      }
      detectionModes.sort((mode1, mode2) => mode1.label.localeCompare(mode2.label, game.i18n.lang));
      return {
        sightFields: this._fields.sight.fields,
        visionModes,
        detectionModes
      };
    }

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

    /**
     * Prepare data to be displayed in the Vision tab.
     * @returns {Promise<object>}
     * @protected
     */
    async _prepareLightTab() {
      return {
        lightFields: this._fields.light.fields,
        lightAnimations: this.token.light.negative ? CONFIG.Canvas.darknessAnimations : CONFIG.Canvas.lightAnimations
      };
    }

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

    /**
     * Prepare data to be displayed in the Resources tab.
     * @returns {Promise<object>}
     * @protected
     */
    async _prepareResourcesTab() {
      const token = this.token;
      const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
      const attributeSource = (this.actor?.system instanceof foundry.abstract.DataModel) && usesTrackableAttributes
        ? this.actor?.type
        : this.actor?.system;
      const TokenDocument = foundry.utils.getDocumentClass("Token");
      const attributes = TokenDocument.getTrackedAttributes(attributeSource);
      return {
        barAttributes: TokenDocument.getTrackedAttributeChoices(attributes),
        bar1: token.getBarAttribute?.("bar1"),
        bar2: token.getBarAttribute?.("bar2"),
        turnMarkerModes: TokenApplication.TURN_MARKER_MODES,
        turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
      };
    }

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

    /**
     * Prepare form submission buttons.
     * @returns {FormFooterButton[]}
     * @protected
     */
    _prepareButtons() {
      return [{type: "submit", icon: "fa-solid fa-floppy-disk", label: "TOKEN.Update"}];
    }

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

    /** @inheritDoc */
    _processFormData(event, form, formData) {
      const submitData = super._processFormData(event, form, formData);
      this._processChanges(submitData);
      return submitData;
    }

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

    /** @inheritDoc */
    _onChangeForm(formConfig, event) {
      super._onChangeForm(formConfig, event);
      const target = event.target;

      // Pre-populate vision mode defaults
      if ( target?.name === "sight.visionMode" ) {
        const defaults = CONFIG.Canvas.visionModes[event.target.value]?.vision?.defaults || {};
        for ( const fieldName of ["color", "attenuation", "brightness", "saturation", "contrast"] ) {
          const value = defaults[fieldName];
          if ( value === undefined ) continue;
          const field = this.form.querySelector(`[name="sight.${fieldName}"]`);
          if ( !field ) continue;
          if ( field.type === "checkbox" ) field.checked = value;
          else field.value = value;
        }
      }

      // Update texture input when alternate image is selected
      else if ( target?.name === "alternateImages" ) {
        event.target.form.elements["texture.src"].value = event.target.value;
      }

      // Show or hide the warning that the base texture is overridden by the ring subject texture.
      else if ( (target?.name === "ring.subject.texture") || (target?.name === "ring.enabled") ) {
        const usingSubject = this.form.elements["ring.enabled"]?.checked
          && this.form.elements["ring.subject.texture"]?.value;
        this.form.querySelector(".texture-src-warning")?.toggleAttribute("hidden", !usingSubject);
      }
    }

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

    /**
     * Add a new detection mode to the Token preview.
     * @this {TokenApplication}
     * @type {ApplicationClickAction}
     */
    static async #onOverrideDetectionMode(_event, target) {
      const id = target.closest("[data-id]").dataset.id;
      const formData = new FormDataExtended(this.form);
      const modes = this._processFormData(event, this.form, formData).detectionModes;
      modes[id] = {range: null, enabled: true};
      this._previewChanges({detectionModes: modes});
      await this.render({parts: ["vision"]});
    }

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

    /**
     * Remove a detection mode from the Token preview.
     * @this {TokenApplication}
     * @type {ApplicationClickAction}
     */
    static async #onRemoveDetectionMode(_event, target) {
      const id = target.closest("[data-id]").dataset.id;
      const formData = new FormDataExtended(this.form);
      const modes = this._processFormData(event, this.form, formData).detectionModes;
      modes[id] = _del;
      this._previewChanges({detectionModes: modes});
      await this.render({parts: ["vision"]});
    }

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

    /**
     * Process several fields from form submission data into proper model changes.
     * @param {object} submitData Form submission data passed through {@link foundry.applications.ux.FormDataExtended}
     * @protected
     */
    _processChanges(submitData) {

      // Empty string bar attributes need to be converted to null, because "" is cleaned to the initial value
      if ( !submitData.bar1.attribute ) submitData.bar1.attribute = null;
      if ( !submitData.bar2.attribute ) submitData.bar2.attribute = null;

      // Convert scale and mirror data from the form submission to TextureData changes
      if ( typeof submitData.scale === "number" ) {
        submitData.texture.scaleX = submitData.scale * (submitData.mirrorX ? -1 : 1);
        submitData.texture.scaleY = submitData.scale * (submitData.mirrorY ? -1 : 1);
      }
      for ( const key of ["scale", "mirrorX", "mirrorY"] ) delete submitData[key];

      // Process token ring effects from the form submission
      if ( Array.isArray(submitData.ring?.effects) ) {
        const TRE = CONFIG.Token.ring.ringClass.effects;
        let effects = submitData.ring.enabled ? TRE.ENABLED : TRE.DISABLED;
        for ( const effectName of submitData.ring.effects ) {
          const v = TRE[effectName] ?? 0;
          effects |= v;
        }
        submitData.ring.effects = effects;
      }

      // Process detection modes
      submitData.detectionModes ??= {};
      for ( const [id, {tokenConfig}] of Object.entries(CONFIG.Canvas.detectionModes) ) {
        if ( tokenConfig ) submitData.detectionModes[id] ??= _del;
      }
    }
  }
  return TokenApplication;
}
