
import {ATTRIBUTE} from "../data/attribute.mjs";
import {spoModData, leadingAttribute} from "../data/spellData/spellData.mjs";
import {evaluateRoll} from "../globals/DSARoll.mjs";
import {displayRoll} from "../globals/displayRoll.js";

const {
    ApplicationV2,
    HandlebarsApplicationMixin
} = foundry.applications.api

export class SpellDialog extends HandlebarsApplicationMixin(ApplicationV2) {

    static DEFAULT_OPTIONS = {
        classes: ['dsa41', 'dialog', 'spell'],
        tag: "form",
        position: {
            width: 480,
            height: 800
        },
        window: {
            resizable: false,
            title: "Zauber wirken"
        },
        form: {
            submitOnChange: true,
            closeOnSubmit: false,
            handler: SpellDialog.#onSubmitForm
        },
        actions: {
            cast: SpellDialog.#cast,
            diceRoll: SpellDialog.#diceRoll,
            cancel: SpellDialog.#cancel
        }
    }


    static PARTS = {
        form: {
            template: 'systems/DSA_4-1/templates/dialog/spell-dialog.hbs',
        }
    }

    static data = {}

    /**
     *
     * @type {Actor}
     * @private
     */
    _actor = null
    _costMutators = {}
    _castTimeMutators = {}
    _variants = {}
    _costModel = {}
    _castTimeModel = {}
    _spoMods = {}
    _spell = null
    displayModResult = 0

    constructor(actor, spellId) {
        super()
        this._actor = actor
        this._spell = this._actor.itemTypes["Spell"].find(p => p._id === spellId)
        this._circumstance = 0
        this._mods = []
        this.mod = 0
        this._costMutators = {}
        this._castTimeMutators = {}
        this._selectedRepresentation = this._spell.getFlag("DSA_4-1", "representation")
        this._spellDie = null
        this._variants = {}
        this._costModel = this._spell.system.kosten.find(c => c.repräsentation === context.selectedRepresentation) ?? this._spell.system.kosten.find(c => c.repräsentation === "")
        this._castTimeModel = this._spell.system.zauberdauer
        this._castTimeMutators = {}
        this._costMutators = {}
        this._costModel.variables.forEach(v => this._costMutators[v] = 0)
        this._castTimeModel.variables.forEach(v => this._castTimeMutators[v] = 0)
        this.cost = this.normalizeCastingCost() ?? 0
        this.castingTime = this.#normalizeCastingTime(this._spell)
        this.zfp = null
        this.zfpDetermined = false
        if (this._selectedRepresentation) {
            this._costModel = this._spell.system.kosten.find(c => c.repräsentation === context.selectedRepresentation) ?? this._spell.system.kosten.find(c => c.repräsentation === "")
            this._castTimeModel = this._spell.system.zauberdauer
            this._castTimeMutators = {}
            this._costMutators = {}
            this._costModel.variables.forEach(v => this._costMutators[v] = 0)
            this._castTimeModel.variables.forEach(v => this._castTimeMutators[v] = 0)
        }
    }

    /**
     * @typedef FormulaData
     * @property {String} additionalFormula mathematical expression that can be eval'd by replacing the variables with the user added input
     * @property {[String]} variables contains all tokens which will be replaceable inside the formula
     * @property {[String: Number]} substitutions user input with which the variables with the same key will be replaced in the formula text
     * @property {"Aktionen"|"SR"} unit gives the evaluated formula its appropriate time unit
     */

    static async #onSubmitForm(event, form, formData) {
        event.preventDefault()
        // handle changes in variable Inputs
        this._selectedRepresentation = formData.object.representation ?? this._selectedRepresentation
        this._variants = foundry.utils.expandObject(formData.object)["variants"] ?? this._variants
        if (this._spell.system.probe.includes("*")) { // ATTRIBUTO
            if (this._variants["Mut"]) {
                this._spellDie = "MU"
            } else if (this._variants["Klugheit"]) {
                this._spellDie = "KL"
            } else if (this._variants["Intuition"]) {
                this._spellDie = "IN"
            } else if (this._variants["Charisma"]) {
                this._spellDie = "CH"
            } else if (this._variants["Fingerfertigkeit"]) {
                this._spellDie = "FF"
            } else if (this._variants["Gewandtheit"]) {
                this._spellDie = "GE"
            } else if (this._variants["Konstitution"]) {
                this._spellDie = "KO"
            } else if (this._variants["Konstitution"]) {
                this._spellDie = "KK"
            } else {
                this._spellDie = null
            }
        }

        this._costModel = this._spell.system.kosten.find(c => c.repräsentation.toLowerCase() === this._selectedRepresentation.toLowerCase()) ?? this._spell.system.kosten.find(c => c.repräsentation === "")

        let costMutators = foundry.utils.expandObject(formData.object)["costMutators"] ?? this._costMutators

        if (costMutators) {
            this._costMutators = costMutators
        }


        this.cost = this.normalizeCastingCost()

        let castTimeMutators = foundry.utils.expandObject(formData.object)["castTimeMutators"] ?? this._castTimeMutators

        this._castTimeMutators = castTimeMutators

        this.mod = 0
        this._activeVariants = Object.entries(this._variants)
            .filter(([key, truthiness]) => truthiness)
            .map(([key, truthiness]) => this._spell.system.varianten.find(v => v.name === key))
        this._activeVariants.forEach(variant => {

            if (variant.mod) {
                this.mod += Number(variant.mod)
            }

        })

        this.castingTime = this.#normalizeCastingTime(this._spell, this._castTimeMutators)

        // eval probeMod

        if (formData.object["checkMod"]) {
            this.mod -= formData.object["checkMod"]
            this._checkModValue = formData.object["checkMod"]
        }

        // eval spomods

        this._spoMods = foundry.utils.expandObject(formData.object)["spoMods"] ?? {}

        let totalMod = this.mod
        let totalCost = this.cost
        let totalCastingTime = Number(this.castingTime)

        Object.entries(this._spoMods).forEach(([modName, times]) => {

            const actualMod = spoModData[modName]

            for (let i = 0; i < times; i++) {
                const ctfn = new Function("castTime", "return " + actualMod.castTimeModFn)
                totalCastingTime = ctfn(totalCastingTime)

                const cfn = new Function("cost", "return " + actualMod.costModFn)
                totalCost = cfn(totalCost)

                const zmfn = new Function("mod", "return " + actualMod.modFn)
                totalMod = zmfn(totalMod)
            }

        })

        this.mod = totalMod
        this.cost = totalCost
        this.castingTime = totalCastingTime

        this.render({parts: ["form"]})
    }

    static async #cast(event, target) {
        ChatMessage.create({
            user: game.user._id,
            speaker: {actor: this._actor},
            content: `beginnt ${this._spell.name} zu wirken`,
            type: CONST.CHAT_MESSAGE_TYPES.IC
        })
        const cooldowns = this._actor.system.cooldowns
        let m = (queue, data) => {

            ChatMessage.create({
                user: game.user._id,
                speaker: {actor: this._actor},
                content: data.message,
                type: CONST.CHAT_MESSAGE_TYPES.IC
            })
        }

        let message = `<details><summary>${this._spell.name}</summary>${this._spell.system.wirkung}</details>`
        if (this._activeVariants?.length > 0 ?? false) {
            message += "<hr/>"
            message += this._activeVariants.map(v => v.name).join(", ")
        }
        if (Object.keys(this._spoMods).length > 0) {
            message += "<hr/>"
            Object.entries(this._spoMods).forEach(([modName, times]) => {
                if (times > 0) {
                    message += times + "x" + modName + "<br/>"
                }
            })
        }

        if (Object.keys({...this._castTimeMutators, ...this._costMutators}).length > 0) {
            message += "<hr/>"
            Object.entries({...this._castTimeMutators, ...this._costMutators}).forEach(([mutatorName, mutatorValue]) => {
                message += mutatorName + ": " + mutatorValue + "<br/>"
            })
        }

        if (this.cost) {
            message += `Kosten: [[/roll ${this.cost}]]{${this.cost}}`
        } else {
            if (this._costModel.complex) {
                message += `Kosten: ${this._costModel.complex}`
            }
        }
        message += "<hr/>" + this.zfp + " ZfP*<br/>" + this._spell.system.zfw + " ZfW"

        if (Number.isNaN(this.castingTime)) {
            this.castingTime = 0
        }

        let currentCooldown = 0
        if (game.combats.size === 0) { // no combat started
            currentCooldown = this.castingTime
        }

        cooldowns.push({
            start: this.castingTime,
            current: currentCooldown,
            data: {
                cssClass: "Magisch",
                title: this._spell.name,
                taw: this.zfp,
                mod: 0,
                actorId: this._actor._id,
                spellId: this._spell._id,
                message,
                castingTime: this.castingTime,
                maneuver: m.toString()
            }

        })
        await this._actor.update({"system.cooldowns": cooldowns})
        this.close()
    }

    static async #diceRoll(event, target) {
        const result = await evaluateRoll(
            "3d20",
            {
                value: this._spell.system.zfw + this.mod,
                werte: this.#getProbenWerte(),
                owner: this._actor
            }
        )

        const taw = this._spell.system.zfw

        let spellDie3 = this._spell.system.probe[2]

        if (spellDie3 === "*") {
            spellDie3 = this._spellDie
        }

        const context = {
            spell: this._spell,
            castingTime : this.castingTime,
            zfp: result.tap,

            ergebnis: [
                {
                    eigenschaft: this._spell.system.probe[0],
                    eigenschaftWert: this._actor.system.attribute[this._spell.system.probe[0].toLowerCase()].aktuell,
                    wuerfelErgebnis: result.evaluated.terms[0].results[0].result,

                },
                {
                    eigenschaft: this._spell.system.probe[1],
                    eigenschaftWert: this._actor.system.attribute[this._spell.system.probe[1].toLowerCase()].aktuell,
                    wuerfelErgebnis: result.evaluated.terms[0].results[1].result,

                },
                {
                    eigenschaft: this._spell.system.probe[2],
                    eigenschaftWert: this._actor.system.attribute[spellDie3.toLowerCase()].aktuell,
                    wuerfelErgebnis: result.evaluated.terms[0].results[2].result,
                }
            ],
            patzer: result.patzer,
            meisterlich: result.meisterlich,
        }


        if(result.tap>0) {
            context.remaining = Math.abs(result.tap)
        } else if (result.tap===0) {
            context.remaining = 1
        } else {
            context.missing = Math.abs(result.tap)
        }

        displayRoll(result.evaluated, game.user, this._actor, true, true, 'systems/DSA_4-1/templates/chat/spell-chat-message.hbs', context)
/*
        if (result.tap >= 0) { // erfolg
            await result.evaluated.toMessage({
                speaker: ChatMessage.getSpeaker({actor: this._actor}),
                flavor: ` ${result.meisterlich ? 'Meisterlich geschafft' : 'Geschafft'} mit ${result.tap} Punkten übrig`,
            })
        } else { // misserfolg
            await result.evaluated.toMessage({
                speaker: ChatMessage.getSpeaker({actor: this._actor}),
                flavor: ` ${result.meisterlich ? 'Gepatzt' : ''} mit ${Math.abs(result.tap)} Punkten daneben`,
            })
        }*/
        this.zfp = result.tap

        this.zfpDetermined = true
        this.render({parts: ["form"]})
    }

    static async #cancel(event, target) {
        this.close()
    }

    normalizeCastingCost() {
        let costFormula = this._costModel.additionalFormula
        if (costFormula) {

            this._costModel.variables.forEach(v => {

                costFormula = costFormula.replaceAll(v, this._costMutators[v])

            })

            costFormula = Number(eval(costFormula)) + Number(this._costModel.cost)
        } else {
            costFormula = this._costModel.cost
        }
        if (costFormula <= this._costModel.min) {
            costFormula = this._costModel.min
        }
        return costFormula
    }

    /**
     *
     * @param spell
     * @param {FormulaData} additionalFormulaData
     * @returns {number|*}
     */
    #normalizeCastingTime(spell, additionalFormulaData) {

        // min: Wenn ein Zauber eine mindest dauer hat kann diese nachdem diese abgelaufen ist jederzeit abgebrochen werden
        // normal: Standard Zauberzeit eines Zaubers
        // additionalFormulaData: enthält die zur Normalzeit zusätzlichen Zauberdauer

        const castingTime = spell.system.zauberdauer.normal ?? 0
        let baseCastTime = 0
        const minCastingTime = spell.system.zauberdauer.min ?? 0
        let baseMinCastTime = 0
        if (castingTime) { // TODO: what about Stunden
            baseCastTime = castingTime.replace(/(.*) Aktionen/g, (_, aktionen) => {
                return aktionen
            })
            baseCastTime = baseCastTime.replace(/(.*) SR/g, (_, aktionen) => {
                return aktionen * 20
            })
        }
        if (minCastingTime) {
            baseMinCastTime = minCastingTime.replace(/(.*) Aktionen/g, (_, aktionen) => {
                return aktionen
            })
            baseMinCastTime = baseMinCastTime.replace(/(.*) SR/g, (_, aktionen) => {
                return aktionen * 20
            })
        }

        let actualCastingTime = 0
        let formula = spell.system.zauberdauer.additionalFormula
        if (formula) {
            Object.entries(additionalFormulaData).forEach(([variableName, variableValue]) => {
                formula = formula.replaceAll(variableName, variableValue)
            })

            if (spell.system.zauberdauer.additionalFormulaTimeUnit == "Aktionen") {
                actualCastingTime = (Number(baseCastTime) + Number(eval(formula)) ?? 0)
            } else { // TODO: what about Stunden, Spielrunden
                actualCastingTime = (Number(baseCastTime) + (Number(eval(formula)) * 20) ?? 0)
            }

        } else {
            actualCastingTime = baseCastTime
        }

        if (Number(actualCastingTime) <= Number(baseMinCastTime)) {
            actualCastingTime = baseMinCastTime
        }

        return actualCastingTime
    }

    _configureRenderOptions(options) {
        super._configureRenderOptions(options)

        if (options.window) {
            if (this._spell) {
                options.window.title = `${this._spell.name} [${this._spell.system.zfw}]`
            }
        }


        return options
    }

    #getProbenWerte() {
        let dice = []
        this._spell.system.probe.map(p => {
            if (p === "*") {
                return this._spellDie ?? null
            } else {
                return p
            }
        }).forEach(p => {
            if (p !== null) {
                dice.push(
                    this._actor.system.attribute[p.toLowerCase()].aktuell
                )
            } else {
                dice.push(
                    "??"
                )
            }
        })
        return dice
    }


    async _prepareContext(options) {
        const context = await super._prepareContext(options)
        context.actor = this._actor
        context.spell = this._spell
        context.representationOptions = {}
        context.selectedRepresentation = this._selectedRepresentation
        context.text = this._spell.system.wirkung
        context.dice = []
        context.colorfulDice = game.settings.get('DSA_4-1', 'optional_colorfuldice')
        context.modResult = this._spell.system.zfw + this.mod
        context.penalty = (this.mod > 0 ? "+" : "") + this.mod
        context.displayModResult = (context.modResult > 0 ? "+" : "") + context.modResult
        context.castingTime = this.castingTime
        context.ready = true
        context.zfpDetermined = this.zfpDetermined

        // variable probe (should consider Achaz as they can replace one KL in a KL/KL/* spell with IN

        this._spell.system.probe.map(p => {
            if (p === "*") {
                return this._spellDie ?? null
            } else {
                return p
            }
        }).forEach(p => {
            if (p !== null) {
                context.dice.push({
                    wert: this._actor.system.attribute[p.toLowerCase()].aktuell,
                    name: p,
                    tooltip: ATTRIBUTE[p.toLowerCase()],
                })
            } else {
                context.dice.push({
                    wert: "??",
                    name: "??",
                    tooltip: ATTRIBUTE["UNKNOWN"],
                })
            }
        })

        context.variants = this._spell.system.varianten.map(v => {
            return {
                variantText: v.description,
                variantName: v.name,
                variantPenalty: v.mod ?? "0",
                variantChecked: this._variants[v.name]
            }
        })

        // Repräsentation

        context.representationOptions[""] = ""
        Object.entries(this._spell.system.repräsentation).forEach(([key, value]) => {
            context.representationOptions[key] = key
        })
        if (!this._selectedRepresentation) {
            context.ready = false
        }

        // Costs and Mutators

        context.castingCosts = this.cost


        // set probe to current held probe variables or take from _spell
        context.costMutators = this._costMutators

        if (this._costModel) {
            context.costVariables = this._costModel.variables
        } else {
            context.costVariables = []
        }

        // probeMod

        if (this._spell.system.probeMod) {
            context.checkModTest = this._spell.system.probeMod
            context.checkModValue = this._checkModValue
        }

        // SpoMods

        context.spoModCount = Object.values(this._spoMods).reduce((previousValue, currentValue) => previousValue + currentValue, 0)
        context.maxSpoModCount = 0

        if (this._selectedRepresentation) {
            const leadingAttributKey = leadingAttribute[this._selectedRepresentation]

            context.maxSpoModCount = (this._actor.system.attribute[leadingAttributKey.toLowerCase()].aktuell ?? 0) - 12
            if (context.maxSpoModCount < 0) {
                context.maxSpoModCount = 0
            }
        }

        if (context.spoModCount > context.maxSpoModCount) {
            context.ready = false
        }

        const mapper = (spoModName) => {

            let data = spoModData[spoModName]
            let value = this._spoMods[data.name] ?? 0
            let totalModValue = data.mod * value
            return {
                ...data,
                value,
                totalModValue
            }
        }


        context.spoMods = []
        if (this._spell.system.modifikationen) {
            this._spell.system.modifikationen.split(",").forEach(spoMod => {

                switch (spoMod.trim()) {
                    case "Zauberdauer":
                        context.spoMods.push(mapper("Halbierte Zauberdauer"))
                        context.spoMods.push(mapper("Verdoppelte Zauberdauer"))
                        break;
                    case "Kosten":
                        context.spoMods.push(mapper("Kosten einsparen"))
                        break;
                    case "Reichweite":
                        context.spoMods.push(mapper("Verkleinerung von Reichweite oder Wirkungsradius"))
                        context.spoMods.push(mapper("Vergrößerung von Reichweite oder Wirkungsradius"))
                        break;
                }
            })
        }


        // if this.zfp is null then we are in the first step pre dice roll
        if (this.zfp == null) {
            context.ready = false
            context.diceRoll = true
        } else {
            if (this.zfp === 0) {
                this.zfp = 1
            }
            let zfpMod = 0

            Object.entries(this._spoMods).forEach(([modName, times]) => {

                const actualMod = spoModData[modName]

                for (let i = 0; i < times; i++) {
                    const zmfn = new Function("mod", "return " + actualMod.modFn)
                    zfpMod = zmfn(zfpMod)
                }

            })

            if (this.zfp + zfpMod > this._spell.system.zfw) { // cant be higher than the learnt level
                context.zfpModified = this._spell.system.zfw
            } else {
                context.zfpModified = this.zfp + zfpMod
            }

            context.spellName = this._spell.system.name
            context.variant = context.variants.filter(v => v.variantChecked).map(v => `<em>${v.variantName}</em>&mdash;${v.variantText}`).join("<br/><br/>")

            if (context.zfpModified < 0) {
                context.ready = false
            }
        }

        if (!context.ready) { // rules have changed, it cant be cast when zfp - selected mutators is below 0

            context.notReadyReasons = `<em>${game.i18n.format("SPELL_DIALOG.notReadyReason.title")}</em><ul>`

            if (this.zfp == null) {
                context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.noZFPDataAvailable")}</li>`
            }
            if (context.zfpModified < 0) {
                context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.overspentZFP")}</li>`
            }
            if (context.spoModCount > context.maxSpoModCount) {
                context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.tooManySpoMods")}</li>`
            }
            if (!this._selectedRepresentation) {
                context.noSelectedRepresentation = true
                context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.noRepresentation")}</li>`
            }
            context.notReadyReasons += "</ul>"
        }


        return context
    }

    async _onRender(context, options) {

    }
}