import {ApplicationV2, HandlebarsApplicationMixin} from "../api/_module.mjs";
import Document from "@common/abstract/document.mjs";

/**
 * @import {BaseShapeData} from "@common/data/_module.mjs";
 * @import {FormInputConfig} from "@common/data/_types.mjs";
 * @import {DataField} from "@client/data/fields.mjs";
 */

/**
 * The configuration to edit a shape of a Document.
 * @extends ApplicationV2
 * @mixes HandlebarsApplication
 */
export default class ShapeConfig extends HandlebarsApplicationMixin(ApplicationV2) {

  /**
   * @param {ApplicationConfiguration & {shape: BaseShapeData}} options
   */
  constructor(options) {
    if ( !(options.shape.parent instanceof Document) ) throw new Error("The shape must have a parent Document");
    super(options);
  }

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

  /** @inheritDoc */
  static DEFAULT_OPTIONS = {
    id: "{id}",
    classes: ["shape-config"],
    tag: "form",
    window: {
      contentClasses: ["standard-form"],
      icon: "fa-solid fa-shapes"
    },
    position: {width: 480},
    form: {
      handler: this.#onSubmitForm,
      closeOnSubmit: true
    }
  };

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

  /** @override */
  static PARTS = {
    form: {
      templates: ["templates/apps/shape-config/parts/origin.hbs"]
    },
    footer: {
      template: "templates/generic/form-footer.hbs"
    }
  };

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

  /**
   * The shape that is configured by this application.
   * @type {BaseShapeData}
   */
  get shape() {
    return this.options.shape;
  }

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

  /**
   * The Document whose shape is configured by this application.
   * @type {Document}
   */
  get document() {
    return this.shape.parent;
  }

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

  /** @override */
  get title() {
    return `${_loc("SHAPE.ACTIONS.edit")}: ${_loc(`SHAPE.TYPES.${this.shape.type}.name`)}`;
  }

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

  /** @inheritDoc */
  _initializeApplicationOptions(options) {
    options = super._initializeApplicationOptions(options);
    const suffix = options.shape.parent.uuid ?? foundry.utils.randomID();
    options.uniqueId = `${this.constructor.name}-${suffix.replaceAll(".", "-")}`;
    if ( options.shape._index !== undefined ) options.uniqueId += `-${options.shape._index}`;
    return options;
  }

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

  /** @inheritDoc */
  _configureRenderParts(options) {
    const parts = super._configureRenderParts(options);
    parts.form.template = `templates/apps/shape-config/${this.shape.type}.hbs`;
    if ( this.shape.type === "emanation" ) parts.form.templates.push(`templates/apps/shape-config/${this.shape.base.type}.hbs`);
    return parts;
  }

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

  /** @inheritDoc */
  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    context.rootId = this.id;
    ShapeConfig._prepareShapeContext(context, this.shape, this.shape.constructor.schema.fields);
    context.buttons = [{type: "submit", icon: "fa-solid fa-floppy-disk", label: "Update"}];
    return context;
  }

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

  /** @override */
  _onChangeForm(formConfig, event) {
    ShapeConfig._onChangeDimension(event.target, this.shape.grid);
  }

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

  /**
   * Submit a Document update request based on the processed form data.
   * @param {SubmitEvent} event                   The originating form submission event
   * @param {HTMLFormElement} form                The form element that was submitted
   * @param {object} shapeData                    Processed and validated form data to be used for a document update
   * @param {Partial<DatabaseUpdateOperation>} [updateOptions] Additional options altering the request
   * @returns {Promise<void>}
   * @protected
   */
  async _processSubmitData(event, form, shapeData, updateOptions) {
    if ( this.document.schema.has("shapes") ) {
      const shapes = this.document.toObject().shapes;
      shapes[this.shape._index] = shapeData;
      await this.document.update({shapes}, updateOptions);
    } else if ( this.document.schema.has("shape") ) {
      await this.document.update({shape: shapeData}, updateOptions);
    } else {
      throw new Error(`${this.constructor.name} must implement the _processSubmitData method.`);
    }
  }

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

  /**
   * Process form submission for the config.
   * @this {ShapeConfig}                          The handler is called with the application as its bound scope
   * @param {SubmitEvent} event                   The originating form submission event
   * @param {HTMLFormElement} form                The form element that was submitted
   * @param {FormDataExtended} formData           Processed data for the submitted form
   * @param {object} [options]                    Additional options provided by a manual submit call. All options
   *                                              except `options.updateData` are forwarded along to _processSubmitData.
   * @param {object} [options.updateData]         Additional data passed in if this form is submitted manually which
   *                                              should be merged with prepared formData.
   * @returns {Promise<void>}
   */
  static async #onSubmitForm(event, form, formData, options={}) {
    const {updateData, ...updateOptions} = options;
    const shapeData = ShapeConfig._processShapeData(this.shape, foundry.utils.expandObject(formData.object));
    if ( updateData ) foundry.utils.mergeObject(shapeData, updateData);

    // Apply the change
    await this._processSubmitData(event, form, shapeData, updateOptions);
  }

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

  /**
   * Process the shape form data.
   * @param {BaseShapeData} shape
   * @param {object} shapeData
   * @returns {object}
   * @internal
   */
  static _processShapeData(shape, shapeData) {

    // Base shapes of emanation need the same processing
    if ( (shape.type === "emanation") && ("base" in shapeData) ) {

      // The fieldPath doesn't work as the name and therefore must be converted
      const baseData = Object.values(shapeData.base)[0];
      shapeData.base = this._processShapeData(shape.base, baseData);
    }

    // Handle optional origin
    if ( shapeData.origin && !(Number.isFinite(shapeData.origin.x) && Number.isFinite(shapeData.origin.y)) ) {
      shapeData.origin = null;
    }

    return foundry.utils.mergeObject(shape.toObject(), shapeData);
  }

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

  /**
   * Create the dimension field.
   * @param {DataField} field
   * @param {FormInputConfig} inputConfig
   * @returns {HTMLElement[]}
   */
  static #dimensionField(field, inputConfig) {
    const grid = inputConfig.grid;
    const distancePixels = grid.size / grid.distance;
    const pixelsInput = foundry.applications.fields.createNumberInput({
      name: field.fieldPath, id: `${inputConfig.rootId}-${field.fieldPath}-pixels`,
      min: 0, step: "any", dataset: {units: "pixels"}, value: inputConfig.value
    });
    const pixelsLabel = document.createElement("label");
    pixelsLabel.setAttribute("for", pixelsInput.id);
    pixelsLabel.textContent = _loc("MEASUREMENT.Pixels");
    const linkIcon = document.createElement("i");
    linkIcon.classList.add("fa-solid", "fa-fw", "fa-link-simple");
    const gridInput = foundry.applications.fields.createNumberInput({
      id: `${inputConfig.rootId}-${field.fieldPath}-grid`, min: 0, step: "any", dataset: {units: "grid"},
      value: inputConfig.value / distancePixels
    });
    const gridLabel = document.createElement("label");
    gridLabel.setAttribute("for", gridInput.id);
    gridLabel.textContent = _loc("MEASUREMENT.GridUnits");
    if ( grid.units ) gridLabel.textContent += ` (${grid.units})`;
    return [pixelsLabel, pixelsInput, linkIcon, gridInput, gridLabel];
  }

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

  /**
   * Prepare the shape context.
   * @param {object} context
   * @param {BaseShapeData} shape
   * @param {DataField} fields
   * @internal
   */
  static _prepareShapeContext(context, shape, fields) {
    context.shape = shape;
    context.fields = fields;
    context.source = shape._source;
    context.gridUnits = shape.grid.units || _loc("MEASUREMENT.GridUnits");
    context.dimensionInput = ShapeConfig.#dimensionField;

    // Prepare base shape context for emanations
    if ( shape.type === "emanation" ) {
      context.baseContext = {
        rootId: context.rootId,
        shape: shape.base,
        fields: fields.base.types[shape.base.type].fields,
        source: shape.base._source,
        gridUnits: context.gridUnits,
        dimensionInput: context.dimensionInput
      };
    }

    // Prepare token shapes if necessary
    if ( (shape.type === "token") || ((shape.type === "emanation") && (shape.base.type === "token")) ) {
      let tokenShapes;
      if ( shape.grid.isGridless ) {
        tokenShapes = {
          [CONST.TOKEN_SHAPES.ELLIPSE_1]: _loc("TOKEN.SHAPES.ELLIPSE.label"),
          [CONST.TOKEN_SHAPES.RECTANGLE_1]: _loc("TOKEN.SHAPES.RECTANGLE.label")
        };
      } else if ( shape.grid.isSquare ) {
        tokenShapes = {
          [CONST.TOKEN_SHAPES.RECTANGLE_1]: _loc("TOKEN.SHAPES.RECTANGLE.label")
        };
      } else {
        tokenShapes = Object.entries(CONST.TOKEN_SHAPES)
          .reduce((shapes, [key, value]) => {
            shapes[value] = _loc(`TOKEN.SHAPES.${key}.label`);
            return shapes;
          }, {});
      }
      if ( shape.type === "emanation" ) context.baseContext.tokenShapes = tokenShapes;
      else context.tokenShapes = tokenShapes;
    }
  }

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

  /**
   * Handle changes to dimension input.
   * @param {HTMLInputElement} input
   * @param {BaseGrid} grid
   * @internal
   */
  static _onChangeDimension(input, grid) {
    const distancePixels = grid.size / grid.distance;
    if ( input.dataset.units === "pixels" ) {
      const pixels = input.value;
      const gridUnits = pixels / distancePixels;
      input.parentNode.querySelector('input[data-units="grid"]').value = gridUnits;
    } else if ( input.dataset.units === "grid" ) {
      const gridUnits = input.value;
      const pixels = gridUnits * distancePixels;
      input.parentNode.querySelector('input[data-units="pixels"]').value = pixels;
    }
  }
}
