/**
 * Provides default values for all arguments the `CheckFactory` expects.
 */
class DefaultCheckOptions implements DS4CheckFactoryOptions {
    readonly maximumCoupResult = 1;
    readonly minimumFumbleResult = 20;
    readonly useSlayingDice = false;
    readonly rollMode: Const.DiceRollMode = "roll";
    readonly flavor: undefined;

    mergeWith(other: Partial<DS4CheckFactoryOptions>): DS4CheckFactoryOptions {
        return { ...this, ...other };
    }
}

/**
 * Singleton reference for default value extraction.
 */
const defaultCheckOptions = new DefaultCheckOptions();

/**
 * Most basic class responsible for generating the chat formula and passing it to the chat as roll.
 */
class CheckFactory {
    constructor(
        private checkTargetNumber: number,
        private gmModifier: number,
        options: Partial<DS4CheckFactoryOptions> = {},
    ) {
        this.options = defaultCheckOptions.mergeWith(options);
    }

    private options: DS4CheckFactoryOptions;

    async execute(): Promise<ChatMessage> {
        const innerFormula = ["ds", this.createCheckTargetNumberModifier(), this.createCoupFumbleModifier()].filterJoin(
            "",
        );
        const formula = this.options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
        const roll = Roll.create(formula);

        return roll.toMessage(
            { speaker: ChatMessage.getSpeaker(), flavor: this.options.flavor },
            { rollMode: this.options.rollMode, create: true },
        );
    }

    createCheckTargetNumberModifier(): string | null {
        return "v" + (this.checkTargetNumber + this.gmModifier);
    }

    createCoupFumbleModifier(): string | null {
        const isMinimumFumbleResultRequired =
            this.options.minimumFumbleResult !== defaultCheckOptions.minimumFumbleResult;
        const isMaximumCoupResultRequired = this.options.maximumCoupResult !== defaultCheckOptions.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 checkTargetNumber - The Check Target Number ("CTN")
 * @param options           - Options changing the behavior of the roll and message.
 */
export async function createCheckRoll(
    checkTargetNumber: number,
    options: Partial<DS4CheckFactoryOptions> = {},
): Promise<ChatMessage | unknown> {
    // Ask for additional required data;
    const gmModifierData = await askGmModifier(checkTargetNumber, options);

    const newTargetValue = gmModifierData.checkTargetNumber ?? checkTargetNumber;
    const gmModifier = gmModifierData.gmModifier ?? 0;
    const newOptions: Partial<DS4CheckFactoryOptions> = {
        maximumCoupResult: gmModifierData.maximumCoupResult ?? options.maximumCoupResult,
        minimumFumbleResult: gmModifierData.minimumFumbleResult ?? options.minimumFumbleResult,
        useSlayingDice: game.settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
        rollMode: gmModifierData.rollMode ?? options.rollMode,
        flavor: options.flavor,
    };

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

    // Possibly additional processing

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

/**
 * Responsible for rendering the modal interface asking for the modifier specified by GM and (currently) additional data.
 *
 * @notes
 * At the moment, this asks for more data than it will do after some iterations.
 *
 * @returns The data given by the user.
 */
async function askGmModifier(
    checkTargetNumber: number,
    options: Partial<DS4CheckFactoryOptions> = {},
    { template, title }: { template?: string; title?: string } = {},
): Promise<Partial<IntermediateGmModifierData>> {
    const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs";
    const usedTitle = title ?? game.i18n.localize("DS4.DialogRollOptionsDefaultTitle");
    const templateData = {
        title: usedTitle,
        checkTargetNumber: checkTargetNumber,
        maximumCoupResult: options.maximumCoupResult ?? defaultCheckOptions.maximumCoupResult,
        minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult,
        rollMode: options.rollMode ?? game.settings.get("core", "rollMode"),
        rollModes: CONFIG.Dice.rollModes,
    };
    const renderedHtml = await renderTemplate(usedTemplate, templateData);

    const dialogPromise = new Promise<HTMLFormElement>((resolve) => {
        new Dialog({
            title: usedTitle,
            content: renderedHtml,
            buttons: {
                ok: {
                    icon: '<i class="fas fa-check"></i>',
                    label: game.i18n.localize("DS4.GenericOkButton"),
                    callback: (html) => {
                        if (!("jquery" in html)) {
                            throw new Error(
                                game.i18n.format("DS4.ErrorUnexpectedHtmlType", {
                                    exType: "JQuery",
                                    realType: "HTMLElement",
                                }),
                            );
                        } else {
                            const innerForm = html[0].querySelector("form");
                            if (!innerForm) {
                                throw new Error(
                                    game.i18n.format("DS4.ErrorCouldNotFindHtmlElement", { htmlElement: "form" }),
                                );
                            }
                            resolve(innerForm);
                        }
                    },
                },
                cancel: {
                    icon: '<i class="fas fa-times"></i>',
                    label: game.i18n.localize("DS4.GenericCancelButton"),
                },
            },
            default: "ok",
        }).render(true);
    });
    const dialogForm = await dialogPromise;
    return parseDialogFormData(dialogForm);
}

/**
 * Extracts Dialog data from the returned DOM element.
 * @param formData - The filed dialog
 */
function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateGmModifierData> {
    return {
        checkTargetNumber: parseInt(formData["check-target-number"]?.value),
        gmModifier: parseInt(formData["gm-modifier"]?.value),
        maximumCoupResult: parseInt(formData["maximum-coup-result"]?.value),
        minimumFumbleResult: parseInt(formData["minimum-fumble-result"]?.value),
        rollMode: formData["roll-mode"]?.value,
    };
}

/**
 * Contains data that needs retrieval from an interactive Dialog.
 */
interface GmModifierData {
    gmModifier: number;
    rollMode: Const.DiceRollMode;
}

/**
 * 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
 *  - useSlayingDice
 *  - 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 `GmModifierData`.
 */
interface IntermediateGmModifierData extends GmModifierData {
    checkTargetNumber: number;
    maximumCoupResult: number;
    minimumFumbleResult: number;
}

/**
 * The minimum behavioral options that need to be passed to the factory.
 */
export interface DS4CheckFactoryOptions {
    maximumCoupResult: number;
    minimumFumbleResult: number;
    useSlayingDice: boolean;
    rollMode: Const.DiceRollMode;
    flavor?: string;
}