// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
// SPDX-FileCopyrightText: 2021 Siegfried Krug
//
// SPDX-License-Identifier: MIT

import { DS4ActiveEffect } from "../active-effect/active-effect";
import { disableOverriddenFields } from "../apps/sheet-helpers";
import { DS4 } from "../config";
import { getCanvas, getGame } from "../helpers";
import { getDS4Settings } from "../settings";
import notifications from "../ui/notifications";
import { enforce } from "../utils";
import { isCheck } from "./actor-data-properties-base";

import type { ModifiableDataBaseTotal } from "../common/common-data";
import type { DS4Settings } from "../settings";
import type { DS4Item } from "../item/item";

/**
 * The base sheet class for all {@link DS4Actor}s.
 */
export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetData> {
    static override get defaultOptions(): ActorSheet.Options {
        return foundry.utils.mergeObject(super.defaultOptions, {
            classes: ["sheet", "ds4-actor-sheet"],
            height: 625,
            scrollY: [".ds4-sheet-body"],
            tabs: [{ navSelector: ".ds4-sheet-tab-nav", contentSelector: ".ds4-sheet-body", initial: "values" }],
            dragDrop: [
                { dragSelector: ".item-list .item", dropSelector: null },
                { dragSelector: ".effect-list .effect", dropSelector: null },
                { dragSelector: ".ds4-check", dropSelector: null },
            ],
            width: 650,
        });
    }

    override get template(): string {
        const basePath = "systems/ds4/templates/sheets/actor";
        return `${basePath}/${this.actor.data.type}-sheet.hbs`;
    }

    override async getData(): Promise<DS4ActorSheetData> {
        const itemsByType = Object.fromEntries(
            Object.entries(this.actor.itemTypes).map(([itemType, items]) => {
                return [itemType, items.map((item) => item.data).sort((a, b) => (a.sort || 0) - (b.sort || 0))];
            }),
        );

        const enrichedEffectPromises = this.actor.effects.map(async (effect) => {
            return {
                ...effect.toObject(),
                sourceName: await effect.getCurrentSourceName(),
                factor: effect.factor,
                isEffectivelyEnabled: !effect.data.disabled && !effect.isSurpressed,
            };
        });
        const enrichedEffects = await Promise.all(enrichedEffectPromises);

        const data = {
            ...this.addTooltipsToData(await super.getData()),
            config: DS4,
            itemsByType,
            enrichedEffects,
            settings: getDS4Settings(),
        };
        return data;
    }

    /**
     * Adds tooltips to the attributes, traits, and combatValues of the actor data of the given {@link ActorSheet.Data}.
     */
    protected addTooltipsToData(data: ActorSheet.Data): ActorSheet.Data {
        const valueGroups = [data.data.data.attributes, data.data.data.traits, data.data.data.combatValues];

        valueGroups.forEach((valueGroup) => {
            Object.values(valueGroup).forEach((attribute: ModifiableDataBaseTotal<number> & { tooltip?: string }) => {
                attribute.tooltip = this.getTooltipForValue(attribute);
            });
        });
        return data;
    }

    /**
     * Generates a tooltip for a given attribute, trait, or combatValue.
     */
    protected getTooltipForValue(value: ModifiableDataBaseTotal<number>): string {
        return `${value.base} (${getGame().i18n.localize("DS4.TooltipBaseValue")}) + ${
            value.mod
        } (${getGame().i18n.localize("DS4.TooltipModifier")}) ➞ ${getGame().i18n.localize("DS4.TooltipEffects")} ➞ ${
            value.total
        }`;
    }

    override activateListeners(html: JQuery): void {
        super.activateListeners(html);

        if (!this.options.editable) return;

        html.find(".control-item").on("click", this.onControlItem.bind(this));
        html.find(".change-item").on("change", this.onChangeItem.bind(this));

        html.find(".control-effect").on("click", this.onControlEffect.bind(this));
        html.find(".change-effect").on("change", this.onChangeEffect.bind(this));

        html.find(".rollable-item").on("click", this.onRollItem.bind(this));
        html.find(".rollable-check").on("click", this.onRollCheck.bind(this));

        html.find(".sort-items").on("click", this.onSortItems.bind(this));

        disableOverriddenFields.call(this);
    }

    /**
     * Handles a click on an element of this sheet to control an embedded item of the actor corresponding to this sheet.
     *
     * @param event - The originating click event
     */
    protected onControlItem(event: JQuery.ClickEvent): void {
        event.preventDefault();
        const a = event.currentTarget;
        switch (a.dataset["action"]) {
            case "create":
                return this.onCreateItem(event);
            case "edit":
                return this.onEditItem(event);
            case "delete":
                return this.onDeleteItem(event);
        }
    }

    /**
     * Creates a new embedded item using the initial data defined in the HTML dataset of the clicked element.
     *
     * @param event - The originating click event
     */
    protected onCreateItem(event: JQuery.ClickEvent): void {
        const { type, ...data } = foundry.utils.deepClone(event.currentTarget.dataset);
        const name = getGame().i18n.localize(`DS4.New${type.capitalize()}Name`);
        const itemData = {
            name: name,
            type: type,
            data: data,
        };
        Item.create(itemData, { parent: this.actor, pack: this.actor.pack ?? undefined });
    }

    /**
     * Opens the sheet of the embedded item corresponding to the clicked element.
     *
     * @param event - The originating click event
     */
    protected onEditItem(event: JQuery.ClickEvent): void {
        const id = $(event.currentTarget)
            .parents(embeddedDocumentListEntryProperties.Item.selector)
            .data(embeddedDocumentListEntryProperties.Item.idDataAttribute);
        const item = this.actor.items.get(id);
        enforce(item, getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
        enforce(item.sheet);
        item.sheet.render(true);
    }

    /**
     * Deletes the embedded item corresponding to the clicked element.
     *
     * @param event - The originating click event
     */
    protected onDeleteItem(event: JQuery.ClickEvent): void {
        const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.Item.selector);
        this.actor.deleteEmbeddedDocuments("Item", [li.data(embeddedDocumentListEntryProperties.Item.idDataAttribute)]);
        li.slideUp(200, () => this.render(false));
    }

    /**
     * Applies a change to a property of an embedded item depending on the `data-property` attribute of the
     * {@link HTMLInputElement} that has been changed and its new value.
     *
     * @param event - The originating change event
     */
    protected onChangeItem(event: JQuery.ChangeEvent): void {
        return this.onChangeEmbeddedDocument(event, "Item");
    }

    /**
     * Handles a click on an element of this sheet to control an embedded effect of the actor corresponding to this
     * sheet.
     *
     * @param event - The originating click event
     */
    protected onControlEffect(event: JQuery.ClickEvent): void {
        event.preventDefault();
        const a = event.currentTarget;
        switch (a.dataset["action"]) {
            case "create":
                return this.onCreateEffect();
            case "edit":
                return this.onEditEffect(event);
            case "delete":
                return this.onDeleteEffect(event);
        }
    }

    /**
     * Creates a new embedded effect.
     *
     * @param event - The originating click event
     */
    protected onCreateEffect(): void {
        DS4ActiveEffect.createDefault(this.actor);
    }

    /**
     * Opens the sheet of the embedded effect corresponding to the clicked element.
     *
     * @param event - The originating click event
     */
    protected onEditEffect(event: JQuery.ClickEvent): void {
        const id = $(event.currentTarget)
            .parents(embeddedDocumentListEntryProperties.ActiveEffect.selector)
            .data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
        const effect = this.actor.effects.get(id);
        enforce(effect, getGame().i18n.format("DS4.ErrorActorDoesNotHaveEffect", { id, actor: this.actor.name }));
        effect.sheet?.render(true);
    }

    /**
     * Deletes the embedded item corresponding to the clicked element.
     *
     * @param event - The originating click event
     */
    protected onDeleteEffect(event: JQuery.ClickEvent): void {
        const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.ActiveEffect.selector);
        const id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
        this.actor.deleteEmbeddedDocuments("ActiveEffect", [id]);
        li.slideUp(200, () => this.render(false));
    }

    /**
     * Applies a change to a property of an embedded effect depending on the `data-property` attribute of the
     * {@link HTMLInputElement} that has been changed and its new value.
     *
     * @param event - The originating change event
     */
    protected onChangeEffect(event: JQuery.ChangeEvent): void {
        return this.onChangeEmbeddedDocument(event, "ActiveEffect");
    }

    /**
     * Applies a change to a property of an embedded document of the actor belonging to this sheet. The change depends
     * on the `data-property` attribute of the {@link HTMLInputElement} that has been changed and its new value.
     *
     * @param event - The originating change event
     * @param documentName - The name of the embedded document to be changed.
     */
    protected onChangeEmbeddedDocument(event: JQuery.ChangeEvent, documentName: "Item" | "ActiveEffect"): void {
        event.preventDefault();
        const element = $(event.currentTarget).get(0);
        enforce(element instanceof HTMLInputElement);
        if (element.disabled) return;

        const documentElement = element.closest(embeddedDocumentListEntryProperties[documentName].selector);
        enforce(documentElement instanceof HTMLElement);
        const id = documentElement.dataset[embeddedDocumentListEntryProperties[documentName].idDataAttribute];
        const property = element.dataset["property"];
        enforce(property !== undefined, TypeError("HTML element does not provide 'data-property' attribute"));

        const newValue = this.parseValue(element);
        this.actor.updateEmbeddedDocuments(documentName, [{ _id: id, [property]: newValue }]);
    }

    /**
     * Parses the value of the given {@link HTMLInputElement} depending on the element's type
     * The value is parsed to:
     * - checkbox: `boolean`, if the attribute `data-inverted` is set to a truthy value, the parsed value is inverted
     * - text input: `string`
     * - number: `number`
     *
     * @param element - The input element to parse the value from
     */
    protected parseValue(element: HTMLInputElement): boolean | string | number {
        switch (element.type) {
            case "checkbox": {
                const inverted = Boolean(element.dataset["inverted"]);
                const value: boolean = element.checked;
                return inverted ? !value : value;
            }
            case "text": {
                const value: string = element.value;
                return value;
            }
            case "number": {
                const value = Number(element.value.trim());
                return value;
            }
            default: {
                throw new TypeError(
                    "Binding of item property to this type of HTML element not supported; given: " + element,
                );
            }
        }
    }

    /**
     * Handle clickable item rolls.
     * @param event - The originating click event
     */
    protected onRollItem(event: JQuery.ClickEvent): void {
        event.preventDefault();
        const id = $(event.currentTarget)
            .parents(embeddedDocumentListEntryProperties.Item.selector)
            .data(embeddedDocumentListEntryProperties.Item.idDataAttribute);
        const item = this.actor.items.get(id);
        enforce(item, getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
        item.roll().catch((e) => notifications.error(e, { log: true }));
    }

    /**
     * Handle clickable check rolls.
     * @param event - The originating click event
     */
    protected onRollCheck(event: JQuery.ClickEvent): void {
        event.preventDefault();
        const check = event.currentTarget.dataset["check"];
        this.actor.rollCheck(check).catch((e) => notifications.error(e, { log: true }));
    }

    override _onDragStart(event: DragEvent): void {
        const target = event.currentTarget as HTMLElement;
        if (!(target instanceof HTMLElement)) return super._onDragStart(event);

        const check = target.dataset.check;
        if (!check) return super._onDragStart(event);

        enforce(isCheck(check), getGame().i18n.format("DS4.ErrorCannotDragMissingCheck", { check }));

        const dragData = {
            actorId: this.actor.id,
            sceneId: this.actor.isToken ? getCanvas().scene?.id : null,
            tokenId: this.actor.isToken ? this.actor.token?.id : null,
            type: "Check",
            data: check,
        };

        event.dataTransfer?.setData("text/plain", JSON.stringify(dragData));
    }

    /**
     * Sort items according to the item list header that has been clicked.
     * @param event - The originating click event
     */
    protected onSortItems(event: JQuery.ClickEvent<unknown, unknown, HTMLElement>): void {
        event.preventDefault();
        const target = event.currentTarget;
        const type = target.parentElement?.dataset["type"];
        enforce(type !== undefined, `Could not find property 'type' in the dataset of the parent of ${target}`);
        const dataPath = target.dataset["dataPath"];
        enforce(dataPath !== undefined, `Could not find property 'dataPath' in the dataset of ${target}`);
        const items = this.actor.items.filter((item) => item.type === type);
        items.sort((a, b) => a.data.sort - b.data.sort);

        const sortFunction =
            (invert: boolean) =>
            (a: DS4Item, b: DS4Item): number => {
                const propertyA = getProperty(a.data, dataPath);
                const propertyB = getProperty(b.data, dataPath);
                if (typeof propertyA === "string" || typeof propertyB === "string") {
                    return invert
                        ? (propertyB ?? "").localeCompare(propertyA ?? "")
                        : (propertyA ?? "").localeCompare(propertyB ?? "");
                } else {
                    return invert ? propertyB - propertyA : propertyA - propertyB;
                }
            };

        const sortedItems = [...items].sort(sortFunction(false));
        const wasSortedAlready = !sortedItems.find((item, index) => item !== items[index]);

        if (wasSortedAlready) {
            sortedItems.sort(sortFunction(true));
        }

        const updates = sortedItems.map((item, i) => ({
            _id: item.id,
            sort: (i + 1) * CONST.SORT_INTEGER_DENSITY,
        }));

        this.actor.updateEmbeddedDocuments("Item", updates);
    }

    protected override async _onDropItem(event: DragEvent, data: ActorSheet.DropData.Item): Promise<unknown> {
        const item = await Item.fromDropData(data);
        if (item && !this.actor.canOwnItemType(item.data.type)) {
            notifications.warn(
                getGame().i18n.format("DS4.WarningActorCannotOwnItem", {
                    actorName: this.actor.name,
                    actorType: this.actor.data.type,
                    itemName: item.name,
                    itemType: item.data.type,
                }),
            );
            return false;
        }
        return super._onDropItem(event, data);
    }
}

interface DS4ActorSheetData extends ActorSheet.Data {
    config: typeof DS4;
    itemsByType: Record<string, foundry.data.ItemData[]>;
    enrichedEffects: EnrichedActiveEffectDataSource[];
    settings: DS4Settings;
}

type ActiveEffectDataSource = foundry.data.ActiveEffectData["_source"];

interface EnrichedActiveEffectDataSource extends ActiveEffectDataSource {
    sourceName: string;
}

/**
 * This object contains information about specific properties embedded document list entries for each different type.
 */
const embeddedDocumentListEntryProperties = Object.freeze({
    ActiveEffect: {
        selector: ".effect",
        idDataAttribute: "effectId",
    },
    Item: {
        selector: ".item",
        idDataAttribute: "itemId",
    },
});