// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT

import { mathEvaluator } from "../expression-evaluation/evaluator";
import { getGame } from "../utils/utils";

/**
 * @typedef {object} ItemEffectConfig
 * @property {boolean} [applyToItems]   Whether or not to apply this effect to owned items instead of the actor
 * @property {string} [itemName]        Only apply this effect to items with this name
 * @property {string} [condition]       Only apply this effect to items where this condition is fullfilled
 */

/**
 * @typedef {object} DS4ActiveEffectFlags
 * @property {ItemEffectConfig} [itemEffectConfig]  Configuration for applying this effect to owned items
 */

/**
 * @typedef {Record<string, unknown>} ActiveEffectFlags
 * @property {DS4ActiveEffectFlags} [ds4]   Flags for DS4
 */

export class DS4ActiveEffect extends ActiveEffect {
    /**
     * A fallback icon that can be used if no icon is defined for the effect.
     */
    static FALLBACK_ICON = "icons/svg/aura.svg";

    /**
     * A cached reference to the source document to avoid recurring database lookups
     * @type {foundry.abstract.Document | undefined | null}
     * @protected
     */
    source = undefined;

    /**
     * Whether or not this effect is currently surpressed.
     * @type {boolean}
     */
    get isSurpressed() {
        const originatingItem = this.originatingItem;
        if (!originatingItem) {
            return false;
        }
        return originatingItem.isNonEquippedEuipable();
    }

    /**
     * The item which this effect originates from if it has been transferred from an item to an actor.
     * @return {import("./item/item").DS4Item | undefined}
     */
    get originatingItem() {
        if (!(this.parent instanceof Actor)) {
            return;
        }
        const itemIdRegex = /Item\.([a-zA-Z0-9]+)/;
        const itemId = this.origin?.match(itemIdRegex)?.[1];
        if (!itemId) {
            return;
        }
        return this.parent.items.get(itemId);
    }

    /**
     * The number of times this effect should be applied.
     * @type {number}
     */
    get factor() {
        return this.originatingItem?.activeEffectFactor ?? 1;
    }

    /** @override */
    apply(document, change) {
        change.value = Roll.replaceFormulaData(change.value, document);
        try {
            change.value = DS4ActiveEffect.safeEval(change.value).toString();
        } catch (e) {
            // this is a valid case, e.g., if the effect change simply is a string
        }
        return super.apply(document, change);
    }

    /**
     * Gets the current source name based on the cached source object.
     * @returns {Promise<string>} The current source name
     */
    async getCurrentSourceName() {
        const game = getGame();
        const origin = await this.getSource();
        if (origin === null) return game.i18n.localize("None");
        return origin.name ?? game.i18n.localize("Unknown");
    }

    /**
     * Gets the source document for this effect. Uses the cached {@link DS4ActiveEffect#source} if it has already been
     * set.
     * @protected
     * @returns {Promise<foundry.abstract.Document | null>}
     */
    async getSource() {
        if (this.source === undefined) {
            this.source = this.origin != null ? await fromUuid(this.origin) : null;
        }
        return this.source;
    }

    /**
     * Create a new {@link DS4ActiveEffect} using default values.
     *
     * @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} parent The parent of the effect.
     * @returns {Promise<DS4ActiveEffect | undefined>} A promise that resolved to the created effect or udifined of the
     *                                                 creation was prevented.
     */
    static async createDefault(parent) {
        const createData = {
            label: getGame().i18n.localize(`DS4.NewEffectLabel`),
            icon: this.FALLBACK_ICON,
        };

        return this.create(createData, { parent, pack: parent.pack ?? undefined });
    }

    /**
     * Safely evaluate a mathematical expression.
     * @param {string} expression   The expression to evaluate
     * @returns {number | `${number | boolean}`} The numeric result of the expression
     * @throws If the expression could not be evaluated or did not produce a numeric resilt
     */
    static safeEval(expression) {
        const result = mathEvaluator.evaluate(expression);
        if (!Number.isNumeric(result)) {
            throw new Error(`mathEvaluator.evaluate produced a non-numeric result from expression "${expression}"`);
        }
        return result;
    }

    /**
     * Apply the given effects to the gicen Actor or item.
     * @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} document   The Actor or Item to which to apply the effects
     * @param {DS4ActiveEffect[]} effetcs                                                   The effects to apply
     * @param {(change: EffectChangeData) => boolean} [predicate=() => true]                Apply only changes that fullfill this predicate
     */
    static applyEffetcs(document, effetcs, predicate = () => true) {
        /** @type {Record<string, unknown>} */
        const overrides = {};

        // Organize non-disabled and -surpressed effects by their application priority
        const changesWithEffect = effetcs.flatMap((e) => e.getFactoredChangesWithEffect(predicate));
        changesWithEffect.sort((a, b) => (a.change.priority ?? 0) - (b.change.priority ?? 0));

        // Apply all changes
        for (const changeWithEffect of changesWithEffect) {
            const result = changeWithEffect.effect.apply(document, changeWithEffect.change);
            if (result !== null) overrides[changeWithEffect.change.key] = result;
        }

        // Expand the set of final overrides
        document.overrides = foundry.utils.expandObject({
            ...foundry.utils.flattenObject(document.overrides),
            ...overrides,
        });
    }

    /**
     * Get the array of changes for this effect, considering the {@link DS4ActiveEffect#factor}.
     * @param {(change: EffectChangeData) => boolean} [predicate=() => true]    An optional predicate to filter which changes should be considered
     * @returns {EffectChangeDataWithEffect[]} The array of changes from this effect, considering the factor.
     * @protected
     */
    getFactoredChangesWithEffect(predicate = () => true) {
        if (this.disabled || this.isSurpressed) {
            return [];
        }

        return this.changes.filter(predicate).flatMap((change) => {
            change.priority = change.priority ?? change.mode * 10;
            return Array(this.factor).fill({ effect: this, change });
        });
    }
}

/**
 * @typedef {object} EffectChangeDataWithEffect
 * @property {DS4ActiveEffect} effect
 * @property {EffectChangeData} change
 */

/**
 * @typedef {object} EffectChangeData
 * @property {string} key                 The attribute path in the Actor or Item data which the change modifies
 * @property {string} value               The value of the change effect
 * @property {number} mode                The modification mode with which the change is applied
 * @property {number} priority            The priority level with which this change is applied
 */