import MGT2ActorSheet from "./base-actor-sheet.mjs";
import { MGT2 } from "../../config.js";
import { MGT2Helper } from "../../helper.js";
import { RollPromptHelper } from "../../roll-prompt.js";
import { CharacterPrompts } from "../../actors/character-prompts.js";

export default class TravellerCharacterSheet extends MGT2ActorSheet {

    /** @override */
    static DEFAULT_OPTIONS = {
        ...super.DEFAULT_OPTIONS,
        classes: [...super.DEFAULT_OPTIONS.classes, "character", "nopad"],
        window: {
            ...super.DEFAULT_OPTIONS.window,
            title: "TYPES.Actor.character",
        },
        actions: {
            ...super.DEFAULT_OPTIONS.actions,
            createItem: TravellerCharacterSheet.#onCreateItem,
            editItem: TravellerCharacterSheet.#onEditItem,
            deleteItem: TravellerCharacterSheet.#onDeleteItem,
            equipItem: TravellerCharacterSheet.#onEquipItem,
            itemStorageIn: TravellerCharacterSheet.#onItemStorageIn,
            itemStorageOut: TravellerCharacterSheet.#onItemStorageOut,
            softwareEject: TravellerCharacterSheet.#onSoftwareEject,
            createContainer: TravellerCharacterSheet.#onContainerCreate,
            editContainer: TravellerCharacterSheet.#onContainerEdit,
            deleteContainer: TravellerCharacterSheet.#onContainerDelete,
            roll: TravellerCharacterSheet.#onRoll,
            openConfig: TravellerCharacterSheet.#onOpenConfig,
            openCharacteristic: TravellerCharacterSheet.#onOpenCharacteristic,
            traitCreate: TravellerCharacterSheet.#onTraitCreate,
            traitEdit: TravellerCharacterSheet.#onTraitEdit,
            traitDelete: TravellerCharacterSheet.#onTraitDelete,
            openEditor: TravellerCharacterSheet.#onOpenEditor,
        },
    }

    /** @override */
    static PARTS = {
        sheet: {
            template: "systems/mgt2/templates/actors/actor-sheet.html",
        },
    }

    /** @override */
    tabGroups = {
        sidebar: "inventory",
        characteristics: "core",
        inventory: "onhand",
    }

    /** @override */
    async _prepareContext() {
        const context = await super._prepareContext();
        const actor = this.document;

        context.settings = {
            weightUnit: "kg",
            usePronouns: game.settings.get("mgt2", "usePronouns"),
            useGender: game.settings.get("mgt2", "useGender"),
            showLife: game.settings.get("mgt2", "showLife"),
        };
        context.isGM = game.user.isGM;
        context.showTrash = false;
        context.initiative = actor.getInitiative();

        this._prepareCharacterItems(context);

        return context;
    }

    _prepareCharacterItems(context) {
        const actor = this.document;
        const settings = context.settings;
        const items = actor.items;

        const weapons = [], armors = [], augments = [], computers = [], softwares = [];
        const miscItems = [], equipments = [], containerItems = [], careers = [];
        const skills = [], psionics = [], diseases = [], wounds = [], contacts = [];
        const actorContainers = [];

        for (let i of items) {
            if (i.type === "container") {
                actorContainers.push(i);
            } else if (i.type === "computer") {
                computers.push(i);
                i._subItems = [];
                if (i.system.overload === true)
                    i._overloadClass = "computer-overload";
            }
        }

        actorContainers.sort(MGT2Helper.compareByName);

        const containers = [{ name: "(tous)", _id: "" }].concat(actorContainers);
        const containerIndex = new Map();

        for (let c of actorContainers) {
            containerIndex.set(c._id, c);

            if (c.system.weight > 0) {
                const w = MGT2Helper.convertWeightForDisplay(c.system.weight) + " " + settings.weightUnit;
                c._display = c.name.length > 12 ? `${c.name.substring(0, 12)}... (${w})` : `${c.name} (${w})`;
            } else {
                c._display = c.name.length > 12 ? c.name.substring(0, 12) + "..." : c.name;
            }

            if (c.system.onHand === true)
                c._subItems = [];
        }

        const containerView = actor.system.containerView;
        let currentContainerView = containerView !== "" ? containerIndex.get(containerView) : null;

        context.containerView = currentContainerView || null;
        context.containerWeight = currentContainerView
            ? MGT2Helper.convertWeightForDisplay(currentContainerView.system.weight)
            : MGT2Helper.convertWeightForDisplay(0);
        context.containerShowAll = containerView === "";

        for (let i of items) {
            const item = i.system;

            if (item.hasOwnProperty("weight") && item.weight > 0) {
                i._weight = isNaN(item.quantity)
                    ? MGT2Helper.convertWeightForDisplay(item.weight) + " " + settings.weightUnit
                    : MGT2Helper.convertWeightForDisplay(item.weight * item.quantity) + " " + settings.weightUnit;
            }

            if (item.hasOwnProperty("container") && item.container.id !== "" && item.container.id !== undefined) {
                const container = containerIndex.get(item.container.id);
                if (container === undefined) {
                    if (context.containerShowAll) {
                        i._containerName = "#deleted#";
                        containerItems.push(i);
                    }
                    continue;
                }

                if (container.system.locked && !game.user.isGM) continue;

                if (container.system.onHand === true)
                    container._subItems.push(i);

                if (context.containerShowAll || actor.system.containerView === item.container.id) {
                    i._containerName = container.name;
                    containerItems.push(i);
                }
                continue;
            }

            if (item.hasOwnProperty("equipped")) {
                i._canEquip = true;
                i._toggleClass = item.equipped ? "active" : "";
            } else {
                i._canEquip = false;
            }

            switch (i.type) {
                case "equipment":
                    (i.system.subType === "augment" ? augments : equipments).push(i);
                    break;

                case "armor":
                    if (i.system.options?.length > 0)
                        i._subInfo = i.system.options.map(x => x.name).join(", ");
                    armors.push(i);
                    break;

                case "computer":
                    if (i.system.options?.length > 0)
                        i._subInfo = i.system.options.map(x => x.name).join(", ");
                    break;

                case "item":
                    if (i.system.subType === "software") {
                        if (i.system.software.computerId && i.system.software.computerId !== "") {
                            const computer = computers.find(x => x._id === i.system.software.computerId);
                            if (computer !== undefined) computer._subItems.push(i);
                            else softwares.push(i);
                        } else {
                            i._display = i.system.software.bandwidth > 0
                                ? `${i.name} (${i.system.software.bandwidth})`
                                : i.name;
                            softwares.push(i);
                        }
                    } else {
                        miscItems.push(i);
                    }
                    break;

                case "weapon":
                    i._range = i.system.range.isMelee
                        ? game.i18n.localize("MGT2.Melee")
                        : MGT2Helper.getRangeDisplay(i.system.range);
                    if (i.system.traits?.length > 0)
                        i._subInfo = i.system.traits.map(x => x.name).join(", ");
                    weapons.push(i);
                    break;

                case "career":
                    careers.push(i);
                    break;

                case "contact":
                    contacts.push(i);
                    break;

                case "disease":
                    (i.system.subType === "wound" ? wounds : diseases).push(i);
                    break;

                case "talent":
                    if (i.system.subType === "skill") {
                        skills.push(i);
                    } else {
                        if (MGT2Helper.hasValue(i.system.psionic, "reach"))
                            i._reach = game.i18n.localize(`MGT2.PsionicReach.${i.system.psionic.reach}`);
                        if (MGT2Helper.hasValue(i.system.roll, "difficulty"))
                            i._difficulty = game.i18n.localize(`MGT2.Difficulty.${i.system.roll.difficulty}`);
                        psionics.push(i);
                    }
                    break;

                case "container":
                    if (i.system.onHand === true)
                        miscItems.push(i);
                    break;
            }
        }

        const byName = MGT2Helper.compareByName;
        const byEquipName = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase());

        context.encumbranceNormal = MGT2Helper.convertWeightForDisplay(actor.system.inventory.encumbrance.normal);
        context.encumbranceHeavy = MGT2Helper.convertWeightForDisplay(actor.system.inventory.encumbrance.heavy);

        const totalWeight = actor.system.inventory.weight;
        if (totalWeight > actor.system.inventory.encumbrance.heavy) {
            context.encumbranceClasses = "encumbrance-heavy";
            context.encumbrance = 2;
        } else if (totalWeight > actor.system.inventory.encumbrance.normal) {
            context.encumbranceClasses = "encumbrance-normal";
            context.encumbrance = 1;
        } else {
            context.encumbrance = 0;
        }

        if (softwares.length > 0) { softwares.sort(byName); context.softwares = softwares; }
        augments.sort(byEquipName);         context.augments = augments;
        armors.sort(byEquipName);           context.armors = armors;
        computers.sort(byEquipName);        context.computers = computers;
        context.careers = careers;
        contacts.sort(byName);              context.contacts = contacts;
        containers.sort(byName);            context.containers = containers;
        diseases.sort(byName);              context.diseases = diseases;
        context.wounds = wounds;
        equipments.sort(byEquipName);       context.equipments = equipments;
        miscItems.sort(byEquipName);        context.items = miscItems;
        actorContainers.sort(byName);       context.actorContainers = actorContainers;
        skills.sort(byName);                context.skills = skills;
        psionics.sort(byName);              context.psionics = psionics;
        weapons.sort(byEquipName);          context.weapons = weapons;

        if (containerItems.length > 0) {
            containerItems.sort((a, b) => {
                const r = a._containerName.localeCompare(b._containerName);
                return r !== 0 ? r : a.name.toLowerCase().localeCompare(b.name.toLowerCase());
            });
        }
        context.containerItems = containerItems;
    }

    // =========================================================
    // Event Binding (AppV2 _onRender — replaces jQuery activateListeners)
    // Templates still use CSS class selectors, so we bind manually here.
    // =========================================================

    /** @override */
    _onRender(context, options) {
        super._onRender(context, options);
        const html = this.element;
        if (!this.isEditable) return;

        this._bindClassEvent(html, ".roll", "click", TravellerCharacterSheet.#onRoll);
        this._bindClassEvent(html, ".cfg-characteristic", "click", TravellerCharacterSheet.#onOpenCharacteristic);
        this._bindClassEvent(html, ".item-create", "click", TravellerCharacterSheet.#onCreateItem);
        this._bindClassEvent(html, ".item-edit", "click", TravellerCharacterSheet.#onEditItem);
        this._bindClassEvent(html, ".item-delete", "click", TravellerCharacterSheet.#onDeleteItem);
        this._bindClassEvent(html, ".item-equip", "click", TravellerCharacterSheet.#onEquipItem);
        this._bindClassEvent(html, ".item-storage-in", "click", TravellerCharacterSheet.#onItemStorageIn);
        this._bindClassEvent(html, ".item-storage-out", "click", TravellerCharacterSheet.#onItemStorageOut);
        this._bindClassEvent(html, ".software-eject", "click", TravellerCharacterSheet.#onSoftwareEject);
        this._bindClassEvent(html, ".container-create", "click", TravellerCharacterSheet.#onContainerCreate);
        this._bindClassEvent(html, ".container-edit", "click", TravellerCharacterSheet.#onContainerEdit);
        this._bindClassEvent(html, ".container-delete", "click", TravellerCharacterSheet.#onContainerDelete);
        this._bindClassEvent(html, ".traits-create", "click", TravellerCharacterSheet.#onTraitCreate);
        this._bindClassEvent(html, ".traits-edit", "click", TravellerCharacterSheet.#onTraitEdit);
        this._bindClassEvent(html, ".traits-delete", "click", TravellerCharacterSheet.#onTraitDelete);
        this._bindClassEvent(html, "[data-editor='open']", "click", TravellerCharacterSheet.#onOpenEditor);
        html.querySelector("[name='config']")?.addEventListener("click", (ev) => TravellerCharacterSheet.#onOpenConfig.call(this, ev, ev.currentTarget));
    }

    /** Helper: bind a handler to all matching elements, with `this` set to the sheet instance */
    _bindClassEvent(html, selector, event, handler) {
        for (const el of html.querySelectorAll(selector)) {
            el.addEventListener(event, (ev) => handler.call(this, ev, ev.currentTarget));
        }
    }

    // =========================================================
    // Drag & Drop
    // =========================================================

    /** @override */
    async _onDrop(event) {
        event.preventDefault();
        event.stopImmediatePropagation();
        const dropData = MGT2Helper.getDataFromDropEvent(event);
        if (!dropData) return false;

        const sourceItemData = await MGT2Helper.getItemDataFromDropData(dropData);

        if (sourceItemData.type === "species") {
            const update = {
                system: {
                    personal: {
                        species: sourceItemData.name,
                        speciesText: {
                            description: sourceItemData.system.description,
                            descriptionLong: sourceItemData.system.descriptionLong,
                        },
                    },
                },
            };
            update.system.personal.traits = this.actor.system.personal.traits.concat(sourceItemData.system.traits);

            if (sourceItemData.system.modifiers?.length > 0) {
                update.system.characteristics = {};
                for (let modifier of sourceItemData.system.modifiers) {
                    if (MGT2Helper.hasValue(modifier, "characteristic") && MGT2Helper.hasValue(modifier, "value")) {
                        const c = this.actor.system.characteristics[modifier.characteristic];
                        const updateValue = { value: c.value + modifier.value };
                        if (c.showMax) updateValue.max = c.max + modifier.value;
                        update.system.characteristics[modifier.characteristic] = updateValue;
                    }
                }
            }

            this.actor.update(update);
            return true;
        }

        if (["contact", "disease", "career", "talent"].includes(sourceItemData.type)) {
            let transferData = {};
            try { transferData = sourceItemData.toJSON(); } catch (e) { transferData = sourceItemData; }
            delete transferData._id;
            delete transferData.id;
            await this.actor.createEmbeddedDocuments("Item", [transferData]);
            return true;
        }

        if (!["armor", "weapon", "computer", "container", "item", "equipment"].includes(sourceItemData.type)) return false;

        const target = event.target.closest(".table-row");
        let targetId = null;
        let targetItem = null;
        if (target !== null) {
            targetId = target.dataset.itemId;
            targetItem = this.actor.getEmbeddedDocument("Item", targetId);
        }

        let sourceItem = this.actor.getEmbeddedDocument("Item", sourceItemData.id);
        if (sourceItem) {
            if (!targetItem) return false;
            sourceItem = foundry.utils.deepClone(sourceItem);
            if (sourceItem._id === targetId) return false;

            if (targetItem.type === "item" || targetItem.type === "equipment") {
                if (targetItem.system.subType === "software")
                    sourceItem.system.software.computerId = targetItem.system.software.computerId;
                else
                    sourceItem.system.container.id = targetItem.system.container.id;
                this.actor.updateEmbeddedDocuments("Item", [sourceItem]);
                return true;
            } else if (targetItem.type === "computer") {
                sourceItem.system.software.computerId = targetId;
                this.actor.updateEmbeddedDocuments("Item", [sourceItem]);
                return true;
            } else if (targetItem.type === "container") {
                if (targetItem.system.locked && !game.user.isGM) {
                    ui.notifications.error("Verrouillé");
                } else {
                    sourceItem.system.container.id = targetId;
                    this.actor.updateEmbeddedDocuments("Item", [sourceItem]);
                    return true;
                }
            }
        } else {
            let transferData = {};
            try { transferData = sourceItemData.toJSON(); } catch (e) { transferData = sourceItemData; }
            delete transferData._id;
            delete transferData.id;

            const recalcWeight = transferData.system.hasOwnProperty("weight");

            if (transferData.system.hasOwnProperty("container")) transferData.system.container.id = "";
            if (transferData.type === "item" && transferData.system.subType === "software") transferData.system.software.computerId = "";
            if (transferData.type === "container") transferData.system.onHand = true;
            if (transferData.system.hasOwnProperty("equipment")) transferData.system.equipped = false;

            if (targetItem !== null) {
                if (transferData.type === "item" && transferData.system.subType === "software") {
                    if (targetItem.type === "item" && targetItem.system.subType === "software")
                        transferData.system.software.computerId = targetItem.system.software.computerId;
                    else if (targetItem.type === "computer")
                        transferData.system.software.computerId = targetItem._id;
                } else if (["armor", "computer", "equipment", "item", "weapon"].includes(transferData.type)) {
                    if (targetItem.type === "container") {
                        if (!targetItem.system.locked || game.user.isGM)
                            transferData.system.container.id = targetId;
                    } else {
                        transferData.system.container.id = targetItem.system.container.id;
                    }
                }
            }

            await this.actor.createEmbeddedDocuments("Item", [transferData]);
            if (recalcWeight) await this.actor.recalculateWeight();
        }
        return true;
    }

    // =========================================================
    // Actions (static private methods)
    // =========================================================

    static async #onCreateItem(event, target) {
        event.preventDefault();
        const data = {
            name: target.dataset.createName,
            type: target.dataset.typeItem,
        };
        if (target.dataset.subtype) {
            data.system = { subType: target.dataset.subtype };
        }
        const cls = getDocumentClass("Item");
        return cls.create(data, { parent: this.actor });
    }

    static async #onEditItem(event, target) {
        event.preventDefault();
        const li = target.closest("[data-item-id]");
        const item = this.actor.getEmbeddedDocument("Item", li?.dataset.itemId);
        if (item) item.sheet.render(true);
    }

    static async #onDeleteItem(event, target) {
        event.preventDefault();
        const li = target.closest("[data-item-id]");
        if (!li?.dataset.itemId) return;
        this.actor.deleteEmbeddedDocuments("Item", [li.dataset.itemId]);
    }

    static async #onEquipItem(event, target) {
        event.preventDefault();
        const li = target.closest("[data-item-id]");
        const item = foundry.utils.deepClone(this.actor.getEmbeddedDocument("Item", li?.dataset.itemId));
        if (!item) return;
        item.system.equipped = !item.system.equipped;
        this.actor.updateEmbeddedDocuments("Item", [item]);
    }

    static async #onItemStorageIn(event, target) {
        event.preventDefault();
        const li = target.closest("[data-item-id]");
        const item = foundry.utils.deepClone(this.actor.getEmbeddedDocument("Item", li?.dataset.itemId));
        if (!item) return;

        if (item.type === "container") {
            item.system.onHand = false;
        } else {
            const containers = this.actor.getContainers();
            let container;
            const dropInId = this.actor.system.containerDropIn;

            if (!dropInId) {
                container = containers.length === 0
                    ? await getDocumentClass("Item").create({ name: "New container", type: "container" }, { parent: this.actor })
                    : containers[0];
            } else {
                container = containers.find(x => x._id === dropInId);
            }

            if (container?.system.locked && !game.user.isGM) {
                ui.notifications.error("Objet verrouillé");
                return;
            }
            item.system.container.id = container._id;
        }
        this.actor.updateEmbeddedDocuments("Item", [item]);
    }

    static async #onItemStorageOut(event, target) {
        event.preventDefault();
        const li = target.closest("[data-item-id]");
        const item = foundry.utils.deepClone(this.actor.getEmbeddedDocument("Item", li?.dataset.itemId));
        if (!item) return;
        item.system.container.id = "";
        this.actor.updateEmbeddedDocuments("Item", [item]);
    }

    static async #onSoftwareEject(event, target) {
        event.preventDefault();
        const li = target.closest("[data-item-id]");
        const item = foundry.utils.deepClone(this.actor.getEmbeddedDocument("Item", li?.dataset.itemId));
        if (!item) return;
        item.system.software.computerId = "";
        this.actor.updateEmbeddedDocuments("Item", [item]);
    }

    static async #onContainerCreate(event) {
        event.preventDefault();
        const cls = getDocumentClass("Item");
        return cls.create({ name: "New container", type: "container" }, { parent: this.actor });
    }

    static async #onContainerEdit(event) {
        event.preventDefault();
        const container = this.actor.getEmbeddedDocument("Item", this.actor.system.containerView);
        if (container) container.sheet.render(true);
    }

    static async #onContainerDelete(event) {
        event.preventDefault();
        const containers = this.actor.getContainers();
        const container = containers.find(x => x._id === this.actor.system.containerView);
        if (!container) return;

        const containerItems = this.actor.items.filter(
            x => x.system.hasOwnProperty("container") && x.system.container.id === container._id
        );

        if (containerItems.length > 0) {
            for (let item of containerItems) {
                let clone = foundry.utils.deepClone(item);
                clone.system.container.id = "";
                this.actor.updateEmbeddedDocuments("Item", [clone]);
            }
        }

        const cloneActor = foundry.utils.deepClone(this.actor);
        cloneActor.system.containerView = "";
        if (cloneActor.system.containerDropIn === container._id) {
            cloneActor.system.containerDropIn = "";
            const remaining = containers.filter(x => x._id !== container._id);
            if (remaining.length > 0) cloneActor.system.containerDropIn = remaining[0]._id;
        }

        this.actor.deleteEmbeddedDocuments("Item", [container._id]);
        this.actor.update(cloneActor);
    }

    static async #onRoll(event, target) {
        event.preventDefault();

        const rollOptions = {
            rollTypeName: game.i18n.localize("MGT2.RollPrompt.Roll"),
            rollObjectName: "",
            characteristics: [{ _id: "", name: "" }],
            characteristic: "",
            skills: [],
            skill: "",
            fatigue: this.actor.system.states.fatigue,
            encumbrance: this.actor.system.states.encumbrance,
            difficulty: null,
            damageFormula: null,
        };

        const cardButtons = [];
        for (const [key, label] of Object.entries(MGT2.Characteristics)) {
            const c = this.actor.system.characteristics[key];
            if (c.show) {
                rollOptions.characteristics.push({
                    _id: key,
                    name: game.i18n.localize(label) + MGT2Helper.getDisplayDM(c.dm),
                });
            }
        }

        for (let item of this.actor.items) {
            if (item.type === "talent" && item.system.subType === "skill")
                rollOptions.skills.push({ _id: item._id, name: item.getRollDisplay() });
        }

        rollOptions.skills.sort(MGT2Helper.compareByName);
        rollOptions.skills = [{ _id: "NP", name: game.i18n.localize("MGT2.Items.NotProficient") }].concat(rollOptions.skills);

        let itemObj = null;
        let isInitiative = false;
        const rollType = target.dataset.roll;

        if (rollType === "initiative") {
            rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.InitiativeRoll");
            rollOptions.characteristic = this.actor.system.config.initiative;
            isInitiative = true;
        } else if (rollType === "characteristic") {
            rollOptions.characteristic = target.dataset.rollCharacteristic;
            rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.CharacteristicRoll");
            rollOptions.rollObjectName = game.i18n.localize(`MGT2.Characteristics.${rollOptions.characteristic}.name`);
        } else {
            if (rollType === "skill") {
                rollOptions.skill = target.dataset.rollSkill;
                itemObj = this.actor.getEmbeddedDocument("Item", rollOptions.skill);
                rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.SkillRoll");
                rollOptions.rollObjectName = itemObj.name;
            } else if (rollType === "psionic") {
                rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.PsionicRoll");
            }

            if (itemObj === null && target.dataset.itemId) {
                itemObj = this.actor.getEmbeddedDocument("Item", target.dataset.itemId);
                rollOptions.rollObjectName = itemObj.name;
                if (itemObj.type === "weapon") rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.weapon");
                else if (itemObj.type === "armor") rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.armor");
                else if (itemObj.type === "computer") rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.computer");
            }

            if (rollType === "psionic" && itemObj) {
                rollOptions.rollObjectName = itemObj.name;
                if (MGT2Helper.hasValue(itemObj.system.psionic, "duration")) {
                    cardButtons.push({
                        label: game.i18n.localize("MGT2.Items.Duration"),
                        formula: itemObj.system.psionic.duration,
                        message: {
                            objectName: itemObj.name,
                            flavor: "{0} ".concat(game.i18n.localize(`MGT2.Durations.${itemObj.system.psionic.durationUnit}`)),
                        },
                    });
                }
            }

            if (itemObj?.system.hasOwnProperty("damage")) {
                rollOptions.damageFormula = itemObj.system.damage;
                if (itemObj.type === "disease") {
                    if (itemObj.system.subType === "disease")
                        rollOptions.rollTypeName = game.i18n.localize("MGT2.DiseaseSubType.disease");
                    else if (itemObj.system.subType === "poison")
                        rollOptions.rollTypeName = game.i18n.localize("MGT2.DiseaseSubType.poison");
                }
            }

            if (itemObj?.system.hasOwnProperty("roll")) {
                if (MGT2Helper.hasValue(itemObj.system.roll, "characteristic")) rollOptions.characteristic = itemObj.system.roll.characteristic;
                if (MGT2Helper.hasValue(itemObj.system.roll, "skill")) rollOptions.skill = itemObj.system.roll.skill;
                if (MGT2Helper.hasValue(itemObj.system.roll, "difficulty")) rollOptions.difficulty = itemObj.system.roll.difficulty;
            }
        }

        const userRollData = await RollPromptHelper.roll(rollOptions);

        const rollModifiers = [];
        const rollFormulaParts = [];

        if (userRollData.diceModifier) {
            rollFormulaParts.push("3d6", userRollData.diceModifier);
        } else {
            rollFormulaParts.push("2d6");
        }

        if (userRollData.characteristic) {
            const c = this.actor.system.characteristics[userRollData.characteristic];
            rollFormulaParts.push(MGT2Helper.getFormulaDM(c.dm));
            rollModifiers.push(game.i18n.localize(`MGT2.Characteristics.${userRollData.characteristic}.name`) + MGT2Helper.getDisplayDM(c.dm));
        }

        if (userRollData.skill) {
            if (userRollData.skill === "NP") {
                rollFormulaParts.push("-3");
                rollModifiers.push(game.i18n.localize("MGT2.Items.NotProficient"));
            } else {
                const skillObj = this.actor.getEmbeddedDocument("Item", userRollData.skill);
                rollFormulaParts.push(MGT2Helper.getFormulaDM(skillObj.system.level));
                rollModifiers.push(skillObj.getRollDisplay());
            }
        }

        if (userRollData.psionic) {
            const psionicObj = this.actor.getEmbeddedDocument("Item", userRollData.psionic);
            rollFormulaParts.push(MGT2Helper.getFormulaDM(psionicObj.system.level));
            rollModifiers.push(psionicObj.getRollDisplay());
        }

        if (userRollData.timeframes && userRollData.timeframes !== "" && userRollData.timeframes !== "Normal") {
            rollModifiers.push(game.i18n.localize(`MGT2.Timeframes.${userRollData.timeframes}`));
            rollFormulaParts.push(userRollData.timeframes === "Slower" ? "+2" : "-2");
        }

        if (userRollData.encumbrance === true) {
            rollFormulaParts.push("-2");
            rollModifiers.push(game.i18n.localize("MGT2.Actor.Encumbrance") + " -2");
        }

        if (userRollData.fatigue === true) {
            rollFormulaParts.push("-2");
            rollModifiers.push(game.i18n.localize("MGT2.Actor.Fatigue") + " -2");
        }

        if (userRollData.customDM) {
            const s = userRollData.customDM.trim();
            if (/^[0-9]/.test(s)) rollFormulaParts.push("+");
            rollFormulaParts.push(s);
        }

        if (MGT2Helper.hasValue(userRollData, "difficulty")) rollOptions.difficulty = userRollData.difficulty;

        const rollFormula = rollFormulaParts.join("");
        if (!Roll.validate(rollFormula)) {
            ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula"));
            return;
        }

        let roll = await new Roll(rollFormula, this.actor.getRollData()).roll({ rollMode: userRollData.rollMode });

        if (isInitiative && this.token?.combatant) {
            await this.token.combatant.update({ initiative: roll.total });
        }

        const chatData = {
            user: game.user.id,
            speaker: this.actor ? ChatMessage.getSpeaker({ actor: this.actor }) : null,
            formula: roll._formula,
            tooltip: await roll.getTooltip(),
            total: Math.round(roll.total * 100) / 100,
            showButtons: true,
            showLifeButtons: false,
            showRollRequest: false,
            rollTypeName: rollOptions.rollTypeName,
            rollObjectName: rollOptions.rollObjectName,
            rollModifiers: rollModifiers,
            showRollDamage: rollOptions.damageFormula !== null && rollOptions.damageFormula !== "",
            cardButtons: cardButtons,
        };

        if (MGT2Helper.hasValue(rollOptions, "difficulty")) {
            chatData.rollDifficulty = rollOptions.difficulty;
            chatData.rollDifficultyLabel = MGT2Helper.getDifficultyDisplay(rollOptions.difficulty);
            if (roll.total >= MGT2Helper.getDifficultyValue(rollOptions.difficulty))
                chatData.rollSuccess = true;
            else
                chatData.rollFailure = true;
        }

        const html = await foundry.applications.handlebars.renderTemplate("systems/mgt2/templates/chat/roll.html", chatData);
        chatData.content = html;

        let flags = null;
        if (rollOptions.damageFormula) {
            flags = { mgt2: { damage: { formula: rollOptions.damageFormula, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } };
        }
        if (cardButtons.length > 0) {
            if (!flags) flags = { mgt2: {} };
            flags.mgt2.buttons = cardButtons;
        }
        if (flags) chatData.flags = flags;

        return roll.toMessage(chatData);
    }

    static async #onOpenConfig(event) {
        event.preventDefault();
        const userConfig = await CharacterPrompts.openConfig(this.actor.system);
        if (userConfig) this.actor.update({ "system.config": userConfig });
    }

    static async #onOpenCharacteristic(event, target) {
        event.preventDefault();
        const name = target.dataset.cfgCharacteristic;
        const c = this.actor.system.characteristics[name];

        let showAll = false;
        for (const value of Object.values(this.actor.system.characteristics)) {
            if (!value.show) { showAll = true; break; }
        }

        const userConfig = await CharacterPrompts.openCharacteristic(
            game.i18n.localize(`MGT2.Characteristics.${name}.name`),
            c.show, c.showMax, showAll
        );

        if (userConfig) {
            const data = { system: { characteristics: {} } };
            data.system.characteristics[name] = { show: userConfig.show, showMax: userConfig.showMax };

            if (userConfig.showAll === true) {
                for (const [key, value] of Object.entries(this.actor.system.characteristics)) {
                    if (key !== name && !value.show)
                        data.system.characteristics[key] = { show: true };
                }
            }
            this.actor.update(data);
        }
    }

    static async #onTraitCreate(event) {
        event.preventDefault();
        let traits = this.actor.system.personal.traits;
        let newTraits;
        if (traits.length === 0) {
            newTraits = [{ name: "", description: "" }];
        } else {
            newTraits = [...traits, { name: "", description: "" }];
        }
        return this.actor.update({ system: { personal: { traits: newTraits } } });
    }

    static async #onTraitEdit(event, target) {
        event.preventDefault();
        const element = target.closest("[data-traits-part]");
        const index = Number(element.dataset.traitsPart);
        const trait = this.actor.system.personal.traits[index];
        const result = await CharacterPrompts.openTraitEdit(trait);
        const traits = [...this.actor.system.personal.traits];
        traits[index] = { ...traits[index], name: result.name, description: result.description };
        return this.actor.update({ system: { personal: { traits: traits } } });
    }

    static async #onTraitDelete(event, target) {
        event.preventDefault();
        const element = target.closest("[data-traits-part]");
        const index = Number(element.dataset.traitsPart);
        const traits = foundry.utils.deepClone(this.actor.system.personal.traits);
        const newTraits = Object.entries(traits)
            .filter(([key]) => Number(key) !== index)
            .map(([, value]) => value);
        return this.actor.update({ system: { personal: { traits: newTraits } } });
    }

    static async #onOpenEditor(event) {
        event.preventDefault();
        await CharacterPrompts.openEditorFullView(
            this.actor.system.personal.species,
            this.actor.system.personal.speciesText.descriptionLong
        );
    }
}
