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

import { DS4Actor } from "../documents/actor/actor";
import { DS4Item } from "../documents/item/item";
import { logger } from "../utils/logger";
import { getGame } from "../utils/utils";

type ItemUpdateDataGetter = (
    itemData: Partial<foundry.data.ItemData["_source"]>,
) => DeepPartial<foundry.data.ItemData["_source"]> | Record<string, unknown> | undefined;

export async function migrateItems(getItemUpdateData: ItemUpdateDataGetter): Promise<void> {
    for (const item of getGame().items ?? []) {
        try {
            const updateData = getItemUpdateData(item.toObject());
            if (updateData) {
                logger.info(`Migrating Item document ${item.name} (${item.id})`);
                await item.update(updateData), { enforceTypes: false };
            }
        } catch (err) {
            logger.error(`Error during migration of Item document ${item.name} (${item.id}), continuing anyways.`, err);
        }
    }
}

type ActorUpdateDataGetter = (
    itemData: Partial<foundry.data.ActorData["_source"]>,
) => DeepPartial<foundry.data.ActorData["_source"]> | undefined;

export async function migrateActors(getActorUpdateData: ActorUpdateDataGetter): Promise<void> {
    for (const actor of getGame().actors ?? []) {
        try {
            const updateData = getActorUpdateData(actor.toObject());
            if (updateData) {
                logger.info(`Migrating Actor document ${actor.name} (${actor.id})`);
                await actor.update(updateData);
            }
        } catch (err) {
            logger.error(
                `Error during migration of Actor document ${actor.name} (${actor.id}), continuing anyways.`,
                err,
            );
        }
    }
}

type SceneUpdateDataGetter = (sceneData: foundry.data.SceneData) => DeepPartial<foundry.data.SceneData["_source"]>;

export async function migrateScenes(getSceneUpdateData: SceneUpdateDataGetter): Promise<void> {
    for (const scene of getGame().scenes ?? []) {
        try {
            const updateData = getSceneUpdateData(scene.data);
            if (updateData) {
                logger.info(`Migrating Scene document ${scene.name} (${scene.id})`);
                await scene.update(
                    updateData as DeepPartial<Parameters<foundry.data.SceneData["_initializeSource"]>[0]>,
                );
            }
        } catch (err) {
            logger.error(
                `Error during migration of Scene document ${scene.name} (${scene.id}), continuing anyways.`,
                err,
            );
        }
    }
}

type CompendiumMigrator = (compendium: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>;

export async function migrateCompendiums(migrateCompendium: CompendiumMigrator): Promise<void> {
    for (const compendium of getGame().packs ?? []) {
        if (compendium.metadata.package !== "world") continue;
        if (!["Actor", "Item", "Scene"].includes(compendium.metadata.type)) continue;
        await migrateCompendium(compendium);
    }
}

export function getActorUpdateDataGetter(getItemUpdateData: ItemUpdateDataGetter): ActorUpdateDataGetter {
    return (
        actorData: Partial<foundry.data.ActorData["_source"]>,
    ): DeepPartial<foundry.data.ActorData["_source"]> | undefined => {
        let hasItemUpdates = false;
        const items = actorData.items?.map((itemData) => {
            const update = getItemUpdateData(itemData);
            if (update) {
                hasItemUpdates = true;
                return { ...itemData, ...update };
            } else {
                return itemData;
            }
        });
        return hasItemUpdates ? { items } : undefined;
    };
}

export function getSceneUpdateDataGetter(getActorUpdateData: ActorUpdateDataGetter): SceneUpdateDataGetter {
    return (sceneData: foundry.data.SceneData) => {
        const tokens = sceneData.tokens.map((token: TokenDocument) => {
            const t = token.toObject();
            if (!t.actorId || t.actorLink) {
                t.actorData = {};
            } else if (!getGame().actors?.has(t.actorId)) {
                t.actorId = null;
                t.actorData = {};
            } else if (!t.actorLink) {
                const actorData = foundry.utils.deepClone(t.actorData);
                actorData.type = token.actor?.type;
                const update = getActorUpdateData(actorData);
                if (update !== undefined) {
                    ["items" as const, "effects" as const].forEach((embeddedName) => {
                        const embeddedUpdates = update[embeddedName];
                        if (embeddedUpdates === undefined || !embeddedUpdates.length) return;
                        const updates = new Map(embeddedUpdates.flatMap((u) => (u && u._id ? [[u._id, u]] : [])));
                        const originals = t.actorData[embeddedName];
                        if (!originals) return;
                        originals.forEach((original) => {
                            if (!original._id) return;
                            const update = updates.get(original._id);
                            if (update) foundry.utils.mergeObject(original, update);
                        });
                        delete update[embeddedName];
                    });
                    foundry.utils.mergeObject(t.actorData, update);
                }
            }
            return t;
        });
        return { tokens };
    };
}

export function getCompendiumMigrator(
    {
        getItemUpdateData,
        getActorUpdateData,
        getSceneUpdateData,
    }: {
        getItemUpdateData?: ItemUpdateDataGetter;
        getActorUpdateData?: ActorUpdateDataGetter;
        getSceneUpdateData?: SceneUpdateDataGetter;
    } = {},
    { migrateToTemplateEarly = true } = {},
) {
    return async (compendium: CompendiumCollection<CompendiumCollection.Metadata>): Promise<void> => {
        const type = compendium.metadata.type;
        if (!["Actor", "Item", "Scene"].includes(type)) return;
        const wasLocked = compendium.locked;
        await compendium.configure({ locked: false });
        if (migrateToTemplateEarly) {
            await compendium.migrate();
        }

        const documents = await compendium.getDocuments();

        for (const doc of documents) {
            try {
                logger.info(`Migrating document ${doc.name} (${doc.id}) in compendium ${compendium.collection}`);
                if (doc instanceof DS4Item && getItemUpdateData) {
                    const updateData = getItemUpdateData(doc.toObject());
                    updateData && (await doc.update(updateData));
                } else if (doc instanceof DS4Actor && getActorUpdateData) {
                    const updateData = getActorUpdateData(doc.toObject());
                    updateData && (await doc.update(updateData));
                } else if (doc instanceof Scene && getSceneUpdateData) {
                    const updateData = getSceneUpdateData(doc.data as foundry.data.SceneData);
                    updateData &&
                        (await doc.update(
                            updateData as DeepPartial<Parameters<foundry.data.SceneData["_initializeSource"]>[0]>,
                        ));
                }
            } catch (err) {
                logger.error(
                    `Error during migration of document ${doc.name} (${doc.id}) in compendium ${compendium.collection}, continuing anyways.`,
                    err,
                );
            }
        }

        if (!migrateToTemplateEarly) {
            await compendium.migrate();
        }
        await compendium.configure({ locked: wasLocked });
    };
}