// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
//
// SPDX-License-Identifier: MIT

import { DialogWithListeners } from "../apps/dialog-with-listeners";
import { DS4 } from "../config";
import { getGame } from "../utils/utils";

/** @typedef {"publicroll" | "gmroll" | "gmroll" | "selfroll"} RollModes */

/**
 * Most basic class responsible for generating the chat formula and passing it to the chat as roll.
 */
class CheckFactory {
  /**
   * @param {number} checkTargetNumber                    The check target number for this check factory
   * @param {number} checkModifier                        The check modifier for this check factory
   * @param {Partial<DS4CheckFactoryOptions>} [options]   Options for this check factory
   */
  constructor(checkTargetNumber, checkModifier, options = {}) {
    this.#checkTargetNumber = checkTargetNumber;
    this.#checkModifier = checkModifier;
    this.#options = foundry.utils.mergeObject(this.constructor.defaultOptions, options);
  }

  /**
   * The check target number for this check factory.
   * @type {number}
   */
  #checkTargetNumber;

  /**
   * The check modifier for this check factory.
   * @type {number}
   */
  #checkModifier;

  /**
   * The options for this check factory.
   * @type {DS4CheckFactoryOptions}
   */
  #options;

  /**
   * The default options of thos CheckFactory class. Upon instantiation, they are merged with the explicitly provided options.
   * @type {DS4CheckFactoryOptions}
   */
  static get defaultOptions() {
    return {
      maximumCoupResult: 1,
      minimumFumbleResult: 20,
      useSlayingDice: false,
      rollMode: "publicroll",
    };
  }

  /**
   * Execute this check factory.
   * @returns {Promise<ChatMessage | undefined>} A promise that resolves to the created chat message for the roll */
  async execute() {
    const innerFormula = ["ds", this.createCheckTargetNumberModifier(), this.createCoupFumbleModifier()].filterJoin("");
    const formula = this.#options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
    const roll = Roll.create(formula);
    const speaker = this.#options.speaker ?? ChatMessage.getSpeaker();

    return roll.toMessage(
      {
        speaker,
        flavor: this.#options.flavor,
        flags: this.#options.flavorData ? { ds4: { flavorData: this.#options.flavorData } } : undefined,
      },
      { rollMode: this.#options.rollMode, create: true },
    );
  }

  /**
   * Create the check target number modifier for this roll.
   * @returns string
   */
  createCheckTargetNumberModifier() {
    const totalCheckTargetNumber = this.#checkTargetNumber + this.#checkModifier;
    return totalCheckTargetNumber >= 0 ? `v${this.#checkTargetNumber + this.#checkModifier}` : "v0";
  }

  /**
   * Create the coup fumble modifier for this roll.
   * @returns {string | null}
   */
  createCoupFumbleModifier() {
    const isMinimumFumbleResultRequired =
      this.#options.minimumFumbleResult !== this.constructor.defaultOptions.minimumFumbleResult;
    const isMaximumCoupResultRequired =
      this.#options.maximumCoupResult !== this.constructor.defaultOptions.maximumCoupResult;

    if (isMinimumFumbleResultRequired || isMaximumCoupResultRequired) {
      return `c${this.#options.maximumCoupResult ?? ""}:${this.#options.minimumFumbleResult ?? ""}`;
    } else {
      return null;
    }
  }
}

/**
 * Asks the user for all unknown/necessary information and passes them on to perform a roll.
 * @param {number} checkTargetNumber                        The Check Target Number ("CTN")
 * @param {Partial<DS4CheckFactoryOptions>} [options={}]    Options changing the behavior of the roll and message.
 * @returns {Promise<ChateMessage|undefined>} A promise that resolves to the chat message created by the roll
 */
export async function createCheckRoll(checkTargetNumber, options = {}) {
  // Ask for additional required data;
  const interactiveRollData = await askForInteractiveRollData(checkTargetNumber, options);

  const newTargetValue = interactiveRollData.checkTargetNumber ?? checkTargetNumber;
  const checkModifier = interactiveRollData.checkModifier ?? 0;
  /** @type {Partial<DS4CheckFactoryOptions>} */
  const newOptions = {
    maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult,
    minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult,
    useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
    rollMode: interactiveRollData.rollMode ?? options.rollMode,
    flavor: options.flavor,
    flavorData: options.flavorData,
    speaker: options.speaker,
  };

  // Create Factory
  const cf = new CheckFactory(newTargetValue, checkModifier, newOptions);

  // Possibly additional processing

  // Execute roll
  return cf.execute();
}

/**
 * Responsible for rendering the modal interface asking for the check modifier and (currently) additional
 * data.
 *
 * @notes
 * At the moment, this asks for more data than it will do after some iterations.
 *
 * @param {number} checkTargetNumber                                                            The check target number
 * @param {Partial<DS4CheckFactoryOptions>} [options={}]                                        Predefined roll options
 * @param {template?: string | undefined; title?: string | undefined} [additionalOptions={}]    Additional options to use for the dialog
 * @returns {Promise<Partial<IntermediateInteractiveRollData>>} A promise that resolves to the data given by the user.
 */
async function askForInteractiveRollData(checkTargetNumber, options = {}, { template, title } = {}) {
  const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs";
  const usedTitle = title ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle");
  const id = foundry.utils.randomID();
  const templateData = {
    title: usedTitle,
    checkTargetNumber: checkTargetNumber,
    maximumCoupResult: options.maximumCoupResult ?? this.constructor.defaultOptions.maximumCoupResult,
    minimumFumbleResult: options.minimumFumbleResult ?? this.constructor.defaultOptions.minimumFumbleResult,
    rollMode: options.rollMode ?? getGame().settings.get("core", "rollMode"),
    rollModes: CONFIG.Dice.rollModes,
    checkModifiers: Object.entries(DS4.i18n.checkModifiers).map(([key, translation]) => {
      if (key in DS4.checkModifiers) {
        const value = DS4.checkModifiers[key];
        const label = `${translation} (${value >= 0 ? `+${value}` : value})`;
        return { value, label };
      }
      return { value: key, label: translation };
    }),
    id,
  };
  const renderedHtml = await renderTemplate(usedTemplate, templateData);

  const dialogPromise = new Promise((resolve) => {
    new DialogWithListeners(
      {
        title: usedTitle,
        content: renderedHtml,
        buttons: {
          ok: {
            icon: '<i class="fas fa-check"></i>',
            label: getGame().i18n.localize("DS4.GenericOkButton"),
            callback: (html) => {
              if (!("jquery" in html)) {
                throw new Error(
                  getGame().i18n.format("DS4.ErrorUnexpectedHtmlType", {
                    exType: "JQuery",
                    realType: "HTMLElement",
                  }),
                );
              } else {
                const innerForm = html[0]?.querySelector("form");
                if (!innerForm) {
                  throw new Error(
                    getGame().i18n.format("DS4.ErrorCouldNotFindHtmlElement", {
                      htmlElement: "form",
                    }),
                  );
                }
                resolve(innerForm);
              }
            },
          },
          cancel: {
            icon: '<i class="fas fa-times"></i>',
            label: getGame().i18n.localize("DS4.GenericCancelButton"),
          },
        },
        default: "ok",
      },
      {
        activateAdditionalListeners: (html, app) => {
          const checkModifierCustomFormGroup = html.find(`#check-modifier-custom-${id}`).parent(".form-group");
          html.find(`#check-modifier-${id}`).on("change", (event) => {
            if (event.currentTarget.value === "custom" && checkModifierCustomFormGroup.hasClass("ds4-hidden")) {
              checkModifierCustomFormGroup.removeClass("ds4-hidden");
              app.setPosition({ height: "auto" });
            } else if (!checkModifierCustomFormGroup.hasClass("ds4-hidden")) {
              checkModifierCustomFormGroup.addClass("ds4-hidden");
              app.setPosition({ height: "auto" });
            }
          });
        },
        id,
      },
    ).render(true);
  });
  const dialogForm = await dialogPromise;
  return parseDialogFormData(dialogForm);
}

/**
 * Extracts Dialog data from the returned DOM element.
 * @param {HTMLFormElement} formData    The filed dialog
 * @returns {Partial<IntermediateInteractiveRollData>}
 */
function parseDialogFormData(formData) {
  const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value);
  const chosenCheckModifier = formData["check-modifier"]?.value;

  const chosenCheckModifierValue =
    chosenCheckModifier === "custom"
      ? parseInt(formData["check-modifier-custom"]?.value)
      : parseInt(chosenCheckModifier);

  const chosenMaximumCoupResult = parseInt(formData["maximum-coup-result"]?.value);
  const chosenMinimumFumbleResult = parseInt(formData["minimum-fumble-result"]?.value);
  const chosenRollMode = formData["roll-mode"]?.value;

  return {
    checkTargetNumber: Number.isSafeInteger(chosenCheckTargetNumber) ? chosenCheckTargetNumber : undefined,
    checkModifier: Number.isSafeInteger(chosenCheckModifierValue) ? chosenCheckModifierValue : undefined,
    maximumCoupResult: Number.isSafeInteger(chosenMaximumCoupResult) ? chosenMaximumCoupResult : undefined,
    minimumFumbleResult: Number.isSafeInteger(chosenMinimumFumbleResult) ? chosenMinimumFumbleResult : undefined,
    rollMode: Object.keys(CONFIG.Dice.rollModes).includes(chosenRollMode) ? chosenRollMode : undefined,
  };
}

/**
 * Contains data that needs retrieval from an interactive Dialog.
 * @typedef {object} InteractiveRollData
 * @property {number} checkModifier
 * @property {RollModes} rollMode
 */

/**
 * Contains *CURRENTLY* necessary Data for drafting a roll.
 *
 * @deprecated
 * Quite a lot of this information is requested due to a lack of automation:
 *  - maximumCoupResult
 *  - minimumFumbleResult
 *  - checkTargetNumber
 *
 * They will and should be removed once effects and data retrieval is in place.
 * If a "raw" roll dialog is necessary, create another pre-processing Dialog
 * class asking for the required information.
 * This interface should then be replaced with the {@link InteractiveRollData}.
 * @typedef {object} IntermediateInteractiveRollData
 * @property {number} checkTargetNumber
 * @property {number} maximumCoupResult
 * @property {number} minimumFumbleResult
 */

/**
 * The minimum behavioral options that need to be passed to the factory.
 * @typedef {object} DS4CheckFactoryOptions
 * @property {number} maximumCoupResult
 * @property {number} minimumFumbleResult
 * @property {boolean} useSlayingDice
 * @property {RollModes} rollMode
 * @property {string} [flavor]
 * @property {Record<string, string | number | null>} [flavorData]
 * @property {ChatSpeakerData} [speaker]
 */

/**
 * @typedef {object} ChatSpeakerData
 * @property {string | null} [scene]
 * @property {string | null} [actor]
 * @property {string | null} [token]
 * @property {string | null} [alias]
 */
