import { DS4 } from "../../config";
import { DS4Item } from "../../item/item";
import { DS4ItemData } from "../../item/item-data";
import { DS4Actor } from "../actor";

/**
 * The base Sheet class for all DS4 Actors
 */
export class DS4ActorSheet extends ActorSheet<DS4Actor> {
    // TODO(types): Improve mergeObject in upstream so that it isn't necessary to provide all parameters (see https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/272)
    /** @override */
    static get defaultOptions(): BaseEntitySheet.Options {
        const superDefaultOptions = super.defaultOptions;
        return mergeObject(superDefaultOptions, {
            classes: ["ds4", "sheet", "actor"],
            width: 745,
            height: 600,
            scrollY: [".sheet-body"],
            template: superDefaultOptions.template,
            viewPermission: superDefaultOptions.viewPermission,
            closeOnSubmit: superDefaultOptions.closeOnSubmit,
            submitOnChange: superDefaultOptions.submitOnChange,
            submitOnClose: superDefaultOptions.submitOnClose,
            editable: superDefaultOptions.editable,
            baseApplication: superDefaultOptions.baseApplication,
            top: superDefaultOptions.top,
            left: superDefaultOptions.left,
            popOut: superDefaultOptions.popOut,
            minimizable: superDefaultOptions.minimizable,
            resizable: superDefaultOptions.resizable,
            id: superDefaultOptions.id,
            dragDrop: superDefaultOptions.dragDrop,
            filters: superDefaultOptions.filters,
            title: superDefaultOptions.title,
            tabs: superDefaultOptions.tabs,
        });
    }

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

    /**
     * This method returns the data for the template of the actor sheet.
     * It explicitly adds the items of the object sorted by type in the
     * object itemsByType.
     * @returns The data fed to the template of the actor sheet
     */
    getData(): ActorSheet.Data<DS4Actor> | Promise<ActorSheet.Data<DS4Actor>> {
        const data = {
            ...super.getData(),
            // Add the localization config to the data:
            config: DS4,
            // Add the items explicitly sorted by type to the data:
            itemsByType: this.actor.itemTypes,
        };
        return data;
    }

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

        // Everything below here is only needed if the sheet is editable
        if (!this.options.editable) return;

        // Add Inventory Item
        html.find(".item-create").on("click", this._onItemCreate.bind(this));

        // Update Inventory Item
        html.find(".item-edit").on("click", (ev) => {
            const li = $(ev.currentTarget).parents(".item");
            const item = this.actor.getOwnedItem(li.data("itemId"));
            item.sheet.render(true);
        });

        // Delete Inventory Item
        html.find(".item-delete").on("click", (ev) => {
            const li = $(ev.currentTarget).parents(".item");
            this.actor.deleteOwnedItem(li.data("itemId"));
            li.slideUp(200, () => this.render(false));
        });

        html.find(".item-change").on("change", this._onItemChange.bind(this));

        // Rollable abilities.
        html.find(".rollable").click(this._onRoll.bind(this));
    }

    /**
     * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
     * @param event - The originating click event
     */
    protected _onItemCreate(event: JQuery.ClickEvent): Promise<DS4ItemData> {
        event.preventDefault();
        const header = event.currentTarget;
        // Get the type of item to create.
        // Grab any data associated with this control.
        const { type, ...data } = duplicate(header.dataset);
        // Initialize a default name.
        const name = `New ${type.capitalize()}`;
        // Prepare the item object.
        const itemData = {
            name: name,
            type: type,
            data: data,
        };

        // Finally, create the item!
        return this.actor.createOwnedItem(itemData);
    }

    /**
     * Handle changes to properties of an Owned Item from within character sheet.
     * Can currently properly bind: see getValue().
     * Assumes the item property is given as the value of the HTML element property 'data-property'.
     * @param ev - The originating change event
     */
    protected _onItemChange(ev: JQuery.ChangeEvent): void {
        ev.preventDefault();
        console.log("Current target:", $(ev.currentTarget).get(0)["name"]);
        const el: HTMLFormElement = $(ev.currentTarget).get(0);
        const id = $(ev.currentTarget).parents(".item").data("itemId");
        const item = duplicate<DS4Item, "lenient">(this.actor.getOwnedItem(id));
        const property: string | undefined = $(ev.currentTarget).data("property");

        // Early return:
        // Disabled => do nothing
        if (el.disabled || el.getAttribute("disabled")) return;
        // name not given => raise
        if (property === undefined) {
            throw TypeError("HTML element does not provide 'data-property' attribute");
        }

        // Set new value
        const newValue = this.getValue(el);
        setProperty(item, property, newValue);
        this.actor.updateOwnedItem(item);
    }

    /**
     * Collect the value of a form element depending on the element's type
     * The value is parsed to:
     * - Checkbox: boolean
     * - Text input: string
     * - Number: number
     * @param el - The input element to collect the value of
     */
    private getValue(el: HTMLFormElement): boolean | string | number {
        // One needs to differentiate between e.g. checkboxes (value="on") and select boxes etc.
        // Checkbox:
        if (el.type === "checkbox") {
            const value: boolean = el.checked;
            return value;
        }

        // Text input:
        else if (el.type === "text") {
            const value: string = el.value;
            return value;
        }

        // Numbers:
        else if (el.type === "number") {
            const value = Number(el.value.trim());
            return value;
        }

        // // Ranges:
        // else if (el.type === "range") {
        //     const value: string = el.value.trim();
        //     return value;
        // }

        // // Radio Checkboxes (untested, cf. FormDataExtended.process)
        // else if (el.type === "radio") {
        //     const chosen: HTMLFormElement = el.find((r: HTMLFormElement) => r["checked"]);
        //     const value: string = chosen ? chosen.value : null;
        //     return value;
        // }

        // // Multi-Select (untested, cf. FormDataExtended.process)
        // else if (el.type === "select-multiple") {
        //     const value: Array<string> = [];
        //     el.options.array.forEach((opt: HTMLOptionElement) => {
        //         if (opt.selected) value.push(opt.value);
        //     });
        //    return value;

        // unsupported:
        else {
            throw TypeError("Binding of item property to this type of HTML element not supported; given: " + el);
        }
    }

    /**
     * Handle clickable rolls.
     * @param event - The originating click event
     */
    protected _onRoll(event: JQuery.ClickEvent): void {
        event.preventDefault();
        const element = event.currentTarget;
        const dataset = element.dataset;

        if (dataset.roll) {
            const roll = new Roll(dataset.roll, this.actor.data.data);
            const label = dataset.label ? `Rolling ${dataset.label}` : "";
            roll.roll().toMessage({
                speaker: ChatMessage.getSpeaker({ actor: this.actor }),
                flavor: label,
            });
        }
    }

    /** @override */
    protected async _onDropItem(
        event: DragEvent,
        data: { type: "Item" } & (DeepPartial<ActorSheet.OwnedItemData<DS4Actor>> | { pack: string } | { id: string }),
    ): Promise<boolean | undefined | ActorSheet.OwnedItemData<DS4Actor>> {
        const item = ((await Item.fromDropData(data)) as unknown) as DS4Item;
        if (item && !this.actor.canOwnItemType(item.data.type)) {
            ui.notifications?.warn(
                game.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);
    }
}