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

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

import type { DS4Actor } from "../actor/actor";
import type { DS4Item } from "../item/item";

declare global {
    interface DocumentClassConfig {
        ActiveEffect: typeof DS4ActiveEffect;
    }
    interface FlagConfig {
        ActiveEffect: {
            ds4?: {
                itemEffectConfig?: {
                    applyToItems?: boolean;
                    itemName?: string;
                    condition?: string;
                };
            };
        };
    }
}

type PromisedType<T> = T extends Promise<infer U> ? U : T;

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
     */
    protected source: PromisedType<ReturnType<typeof fromUuid>> | undefined = undefined;

    /**
     * Whether or not this effect is currently surpressed.
     */
    get isSurpressed(): boolean {
        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.
     */
    get originatingItem(): DS4Item | undefined {
        if (!(this.parent instanceof Actor)) {
            return;
        }
        const itemIdRegex = /Item\.([a-zA-Z0-9]+)/;
        const itemId = this.data.origin?.match(itemIdRegex)?.[1];
        if (!itemId) {
            return;
        }
        return this.parent.items.get(itemId);
    }

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

    override apply(document: DS4Actor | DS4Item, change: EffectChangeData): unknown {
        change.value = Roll.replaceFormulaData(change.value, document.data);
        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
        }
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-expect-error In the types and foundry's documentation, only actors are allowed, but the implementation actually works for all kinds of documents
        return super.apply(document, change);
    }

    /**
     * Gets the current source name based on the cached source object.
     */
    async getCurrentSourceName(): Promise<string> {
        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 async getSource(): ReturnType<typeof fromUuid> {
        if (this.source === undefined) {
            this.source = this.data.origin !== undefined ? await fromUuid(this.data.origin) : null;
        }
        return this.source;
    }

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

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

    static safeEval(expression: string): number | `${number | boolean}` {
        const result = mathEvaluator.evaluate(expression);
        if (!Number.isNumeric(result)) {
            throw new Error(`mathEvaluator.evaluate produced a non-numeric result from expression "${expression}"`);
        }
        return result as number | `${number | boolean}`;
    }

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

        // 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 predicate An optional predicate to filter which changes should be considered
     * @returns The array of changes from this effect, considering the factor.
     */
    protected getFactoredChangesWithEffect(
        predicate: (change: EffectChangeData) => boolean = () => true,
    ): EffectChangeDataWithEffect[] {
        if (this.data.disabled || this.isSurpressed) {
            return [];
        }

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

type EffectChangeData = foundry.data.ActiveEffectData["changes"][number];
type EffectChangeDataWithEffect = { effect: DS4ActiveEffect; change: EffectChangeData };