import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, FeatureItem, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponType } from "~/types/character";
import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json';
import proses, { preview } from "#shared/proses";
import { button, buttongroup, floater, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
import { div, dom, icon, span, text } from "#shared/dom.util";
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
import { clamp } from "#shared/general.util";
import markdown from "#shared/markdown.util";
import { getText } from "./i18n";
import type { User } from "~/types/auth";
import { MarkdownEditor } from "./editor.util";

const config = characterConfig as CharacterConfig;

export const MAIN_STATS = ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] as const;
export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const;
export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const;
export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] as const;
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const;
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const;
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_neutral', 'neutral_neutral', 'chaotic_neutral', 'loyal_evil', 'neutral_evil', 'chaotic_evil'] as const;
export const RESISTANCES = ['stun','bleed','poison','fear','influence','charm','possesion','precision','knowledge','instinct'] as const;
export const DAMAGE_TYPES = ['slashing', 'piercing', 'bludgening', 'magic', 'fire', 'thunder', 'cold'] as const;
export const WEAPON_TYPES = ["light", "shield", "heavy", "classic", "throw", "natural", "twohanded", "finesse", "reach", "projectile"] as const;

export const defaultCharacter: Character = {
    id: -1,

    name: "",
    people: undefined,
    level: 1,

    training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 }; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
    leveling: { 1: 0 },
    abilities: {},
    choices: {},
    variables: {
        health: 0,
        mana: 0,
        spells: [],
        items: [],
        exhaustion: 0,
        sickness: [],
        poisons: [],
    },

    owner: -1,
    visibility: "private",
};
const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (character: Character) => ({
    id: character.id,
    owner: character.owner,
    username: character.username,
    name: character.name,
    health: 0,
    mana: 0,
    race: character.people!,
    modifier: MAIN_STATS.reduce((p, v) => { p[v] = 0; return p; }, {} as Record<MainStat, number>),
    level: character.level,
    variables: character.variables,
    action: 0,
    reaction: 0,
    exhaust: 0,
    itempower: 0,
    features: {
        action: [],
        reaction: [],
        freeaction: [],
        passive: [],
    },
    abilities: {
        athletics: 0,
        acrobatics: 0,
        intimidation: 0,
        sleightofhand: 0,
        stealth: 0,
        survival: 0,
        investigation: 0,
        history: 0,
        religion: 0,
        arcana: 0,
        understanding: 0,
        perception: 0,
        performance: 0,
        medecine: 0,
        persuasion: 0,
        animalhandling: 0,
        deception: 0
    },
    spellslots: 0,
    artslots: 0,
    spellranks: {
        instinct: 0,
        knowledge: 0,
        precision: 0,
        arts: 0,
    },
    speed: false,
    defense: {
        hardcap: Infinity,
        static: 6,
        activeparry: 0,
        activedodge: 0,
        passiveparry: 0,
        passivedodge: 0,
    },
    mastery: {
        strength: 0,
        dexterity: 0,
        shield: 0,
        armor: 0,
        multiattack: 1,
        magicpower: 0,
        magicspeed: 0,
        magicelement: 0,
        magicinstinct: 0,
    },
    bonus: {
        abilities: {},
        defense: {},
    },
    resistance: {},
    initiative: 0,
    capacity: 0,
    lists: {
        action: [],
        freeaction: [],
        reaction: [],
        passive: [],
        spells: [],
    },
    aspect: "",
    notes: Object.assign({ public: '', private: '' }, character.notes),
});

export const mainStatTexts: Record<MainStat, string> = {
    "strength": "Force",
    "dexterity": "Dextérité",
    "constitution": "Constitution",
    "intelligence": "Intelligence",
    "curiosity": "Curiosité",
    "charisma": "Charisme",
    "psyche": "Psyché",
};
export const mainStatShortTexts: Record<MainStat, string> = {
    "strength": "FOR",
    "dexterity": "DEX",
    "constitution": "CON",
    "intelligence": "INT",
    "curiosity": "CUR",
    "charisma": "CHA",
    "psyche": "PSY",
};

export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
    fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
    ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
    thunder: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Foudre' },
    earth: { class: 'text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange', text: 'Terre' },
    arcana: { class: 'text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo', text: 'Arcane' },
    air: { class: 'text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime', text: 'Air' },
    nature: { class: 'text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-dark-green', text: 'Nature' },
    light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
    psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
};
export const elementDom = (element: SpellElement) => dom("span", { 
    class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class], 
    text: elementTexts[element].text
});

export const alignmentTexts: Record<Alignment, string> = {
    'loyal_good': 'Loyal bon',
    'neutral_good': 'Neutre bon',
    'chaotic_good': 'Chaotique bon',
    'loyal_neutral': 'Loyal neutre',
    'neutral_neutral': 'Neutre',
    'chaotic_neutral': 'Chaotique neutre',
    'loyal_evil': 'Loyal mauvais',
    'neutral_evil': 'Neutre mauvais',
    'chaotic_evil': 'Chaotique mauvais',
};
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };

export const abilityTexts: Record<Ability, string> = {
    "athletics": "Athlétisme",
    "acrobatics": "Acrobatique",
    "intimidation": "Intimidation",
    "sleightofhand": "Doigté",
    "stealth": "Discrétion",
    "survival": "Survie",
    "investigation": "Enquête",
    "history": "Histoire",
    "religion": "Religion",
    "arcana": "Arcanes",
    "understanding": "Compréhension",
    "perception": "Perception",
    "performance": "Représentation",
    "medecine": "Médicine",
    "persuasion": "Persuasion",
    "animalhandling": "Dressage",
    "deception": "Mensonge"
};

export const resistanceTexts: Record<Resistance, string> = {
    'stun': 'Hébètement',
    'bleed': 'Saignement',
    'poison': 'Empoisonement',
    'fear': 'Peur',
    'influence': 'Influence',
    'charm': 'Charme',
    'possesion': 'Possession',
    'precision': 'Sorts de précision',
    'knowledge': 'Sorts de savoir',
    'instinct': 'Sorts d\'instinct',
};
export const damageTypeTexts: Record<DamageType, string> = {
    'bludgening': 'Contondant',
    'cold': 'Froid',
    'fire': 'Feu',
    'magic': 'Magique',
    'piercing': 'Perçant',
    'slashing': 'Tranchant',
    'thunder': 'Foudre',
};
export const weaponTypeTexts: Record<WeaponType, string> = {
    "light": "Arme légère", 
    "shield": "Bouclier", 
    "heavy": "Arme lourde", 
    "classic": "Arme", 
    "throw": "Arme de jet", 
    "natural": "Arme naturelle", 
    "twohanded": "Deux mains", 
    "finesse": "Arme maniable", 
    "reach": "Arme longue", 
    "projectile": "Arme à projectile", 
};

export const CharacterNotesValidation = z.object({
    public: z.string().optional(),
    private: z.string().optional(),
});
export const CharacterVariablesValidation = z.object({
    health: z.number(),
    mana: z.number(),
    exhaustion: z.number(),
    
    sickness: z.array(z.object({
        id: z.string(),
        state: z.number().min(1).max(7).or(z.literal(true)),
    })),
    poisons: z.array(z.object({
        id: z.string(),
        state: z.number().min(1).max(7).or(z.literal(true)),
    })),
    spells: z.array(z.string()),
    items: z.array(z.string()),
});
export const CharacterValidation = z.object({
    id: z.number(),
    name: z.string(),
    people: z.string().nullable(),
    level: z.number().min(1).max(20),
    aspect: z.number().nullable().optional(),
    notes: CharacterNotesValidation,
    training: z.record(z.enum(MAIN_STATS), z.record(z.enum(TRAINING_LEVELS.map(String)), z.number().optional())),
    leveling: z.record(z.enum(LEVELS.map(String)), z.number().optional()),
    abilities: z.record(z.enum(ABILITIES), z.number().optional()),
    choices: z.record(z.string(), z.array(z.number())),
    variables: CharacterVariablesValidation,
    owner: z.number(),
    username: z.string().optional(),
    visibility: z.enum(["public", "private"]),
    thumbnail: z.any(),
});

type Property = { value: number | string | false, id: string, operation: "set" | "add" | "min" };
type PropertySum = { list: Array<Property>, min: number, value: number, _dirty: boolean };
export class CharacterCompiler
{
    protected _character!: Character;
    protected _result!: CompiledCharacter;
    protected _buffer: Record<string, PropertySum> = {
        'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
        'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
        'modifier/constitution': { value: 0, _dirty: false, min: -Infinity, list: [] },
        'modifier/intelligence': { value: 0, _dirty: false, min: -Infinity, list: [] },
        'modifier/curiosity': { value: 0, _dirty: false, min: -Infinity, list: [] },
        'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
        'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
    };
    private _variableDirty: boolean = false;

    constructor(character: Character)
    {
        this.character = character;
    }

    set character(value: Character)
    {
        this._character = value;
        this._result = defaultCompiledCharacter(value);
        this._buffer = {
            'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
            'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
            'modifier/constitution': { value: 0, _dirty: false, min: -Infinity, list: [] },
            'modifier/intelligence': { value: 0, _dirty: false, min: -Infinity, list: [] },
            'modifier/curiosity': { value: 0, _dirty: false, min: -Infinity, list: [] },
            'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
            'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
        };
        
        if(value.people !== undefined)
        {
            Object.entries(value.leveling).forEach(e => this.add(config.peoples[value.people!]!.options[parseInt(e[0]) as Level][e[1]]!));

            MAIN_STATS.forEach(stat => {
                Object.entries(value.training[stat]).forEach(option => this.add(config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]))
            });

            Object.entries(value.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
        }
    }
    get character(): Character
    {
        return this._character;
    }
    get compiled(): CompiledCharacter
    {
        Object.entries(this._character.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
        this.compile(Object.keys(this._buffer));

        return this._result;
    }
    get values(): Record<string, number>
    {
        Object.entries(this._character.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);

        const keys = Object.keys(this._buffer);
        this.compile(keys);

        return keys.reduce((p, v) => {
            p[v] = this._buffer[v]!.value;
            return p;
        }, {} as Record<string, number>);
    }
    
    parse(text: string): string
    {
        return text.replace(/\{(.*?)\}/gmi, (substring: string, group: string) => {
            console.log(substring, group);
            return substring;
        })
    }
    variable<T extends keyof CharacterVariables>(prop: T, value: CharacterVariables[T])
    {
        this._character.variables[prop] = value;
        this._result.variables[prop] = value;
        this._variableDirty = true;
    }
    saveVariables()
    {
        if(this._variableDirty)
        {
            this._variableDirty = false;
            useRequestFetch()(`/api/character/${this.character.id}/variables`, {
                method: 'POST',
                body: this._character.variables,
            }).then(() => {}).catch(() => {
                Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
            })
        }
    }
    saveNotes()
    {
        return useRequestFetch()(`/api/character/${this.character.id}/notes`, {
            method: 'POST',
            body: this._character.notes,
        }).then(() => {}).catch(() => {
            Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
        });
    }
    protected add(feature?: string)
    {
        if(!feature)
            return;

        config.features[feature]?.effect.forEach(this.apply.bind(this));
    }
    protected remove(feature?: string)
    {
        if(!feature)
            return;

        config.features[feature]?.effect.forEach(this.undo.bind(this));
    }
    protected apply(feature?: FeatureItem)
    {
        if(!feature)
            return;

        switch(feature.category)
        {
            case "list":
                if(feature.action === 'add' && !this._result.lists[feature.list]!.includes(feature.item))
                    this._result.lists[feature.list]!.push(feature.item);
                else if(feature.action === 'remove')
                    this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e === feature.item), 1);

                return;
            case "value":
                this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };

                this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });

                this._buffer[feature.property]!.min = -Infinity;
                this._buffer[feature.property]!._dirty = true;

                if(feature.property.startsWith('modifier/'))
                    Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);

                return;
            case "choice":
                const choice = this._character.choices[feature.id];

                if(choice)
                    choice.forEach(e => feature.options[e]!.effects.forEach(this.apply.bind(this)));

                return;
            default:
                return;
        }
    }
    protected undo(feature?: FeatureItem)
    {
        if(!feature)
            return;

        switch(feature.category)
        {
            case "list":
                if(feature.action === 'remove' && !this._result.lists[feature.list]!.includes(feature.item))
                    this._result.lists[feature.list]!.push(feature.item);
                else if(feature.action === 'add')
                    this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e === feature.item), 1)

                return;
            case "value":
                this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };

                this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.id === feature.id), 1);

                this._buffer[feature.property]!.min = -Infinity;
                this._buffer[feature.property]!._dirty = true;

                if(feature.property.startsWith('modifier/'))
                    Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);

                return;
            case "choice":
                const choice = this._character.choices[feature.id];

                if(choice)
                    choice.forEach(e => feature.options[e]!.effects.forEach(this.undo.bind(this)));

                return;
            default:
                return;
        }
    }
    protected compile(queue: string[])
    {
        for(let i = 0; i < queue.length; i++)
        {
            if(queue[i] === undefined || queue[i] === "") continue;

            const property = queue[i]!;
            const buffer = this._buffer[property];

            if(buffer && buffer._dirty === true)
            {
                let sum = 0, shortcut = false;
                for(let j = 0; j < buffer.list.length; j++)
                {
                    const item = buffer.list[j];
                    if(!item)
                        continue;

                    if(typeof item.value === 'string') // Add or set a modifier
                    {
                        const modifier = this._buffer[item.value as string]!;
                        if(modifier._dirty)
                        {
                            //Put it back in queue since its dependencies haven't been resolved yet
                            queue.push(item.value as string);
                            queue.push(property);
                            shortcut = true;
                            break;
                        }
                        else
                        {
                            if(item.operation === 'add')
                                sum += modifier.value;
                            else if(item.operation === 'set')
                                sum = modifier.value;
                            else if(item.operation === 'min')
                                buffer.min = modifier.value;
                        }
                    }
                    else
                    {
                        if(item.operation === 'add')
                            sum += item.value as number;
                        else if(item.operation === 'set')
                            sum = item.value as number;
                        else if(item.operation === 'min')
                            buffer.min = item.value as number;
                    }
                }

                if(shortcut === true)
                    continue;

                const path = property.split("/");
                const object = path.length === 1 ? this._result : path.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, this._result as any);

                if(object.hasOwnProperty(path.slice(-1)[0]!))
                    object[path.slice(-1)[0]!] = Math.max(sum, this._buffer[property]!.min);

                this._buffer[property]!.value = Math.max(sum, this._buffer[property]!.min);
                this._buffer[property]!._dirty = false;
            }
        }
    }
}
export class CharacterBuilder extends CharacterCompiler
{
    private _container: HTMLDivElement;
    private _content?: HTMLDivElement;
    private _stepsHeader: HTMLDivElement[] = [];
    private _steps: Array<BuilderTabConstructor> = [];
    private _helperText!: Text;
    private id?: string;

    constructor(container: HTMLDivElement, id?: string)
    {
        super(Object.assign({}, defaultCharacter));
        this.id = id;
        this._container = container;

        if(id)
        {
            const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
            container.replaceChildren(load);
            useRequestFetch()(`/api/character/${id}`).then(character => {
                if(character)
                {
                    this.character = character;

                    document.title = `d[any] - Edition de ${character.name ?? 'nouveau personnage'}`;
                    load.remove();

                    this.render();
                    this.display(0);
                }
            });
        }
        else
        {
            document.title = `d[any] - Edition de nouveau personnage`;

            this.render();
            this.display(0);
        }
    }
    private render()
    {
        const publicNotes = new MarkdownEditor(), privateNotes = new MarkdownEditor();
        this._character.notes ??= { public: '', private: '' };

        publicNotes.onChange = (v) => this._character.notes!.public = this._result.notes.public = v;
        privateNotes.onChange = (v) => this._character.notes!.private = this._result.notes.private = v;
        publicNotes.content = this._character.notes.public!;
        privateNotes.content = this._character.notes.private!;

        this._steps = [
            PeoplePicker,
            LevelPicker,
            TrainingPicker,
            AbilityPicker,
            AspectPicker,
        ];
        this._stepsHeader = this._steps.map((e, i) => 
            dom("div", { class: "group flex items-center", }, [
                i !== 0 ? icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }) : undefined,
                dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.header)]),
            ])
        );
        this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.")
        this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
        this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
            div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [
                div('flex flex-row gap-2', [ floater(tooltip(button(icon('radix-icons:pencil-2', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes publics', 'left'), [ publicNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, title: 'Notes publics', position: 'bottom-start' }), floater(tooltip(button(icon('radix-icons:eye-none', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes privés', 'right'), [ privateNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, title: 'Notes privés', position: 'bottom-start' }) ]), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div(undefined, [ tooltip(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), this._helperText, "bottom-end") ]),
            ]),
            this._content,
        ]));
    }
    display(step: number)
    {
        if(step < 0 || step >= this._stepsHeader.length)
            return;

        for(let i = 0; i < step; i++)
        {
            if(!this._steps[i]?.validate(this))
            {
                Toaster.add({ title: 'Erreur de validation', content: this._steps[i]!.errorMessage, type: 'error', duration: 25000, timer: true })
                return;
            }
            else
            {}
        }
        if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this)))
            return;

        this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
        this._stepsHeader[step]!.setAttribute('data-state', 'active');

        this._content?.replaceChildren(...(new this._steps[step]!(this)).dom);

        this._helperText.textContent = this._steps[step]!.description;
    }
    async save(leave: boolean = true)
    {
        if(this.id === 'new' || this.id === '-1')
        {
            //@ts-ignore
            this.id = this._character.id = this._result.id = await useRequestFetch()(`/api/character`, {
                method: 'post',
                body: this._character,
                onResponseError: (e) => {
                    Toaster.add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', closeable: true, duration: 25000, timer: true });
                }
            });
            Toaster.add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
            useRouter().replace({ name: 'character-id-edit', params: { id: this.id } })
            if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
        }
        else
        {
            //@ts-ignore
            await useRequestFetch()(`/api/character/${this._character.id}`, {
                method: 'post',
                body: this._character,
                onResponseError: (e) => {
                    Toaster.add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', closeable: true, duration: 25000, timer: true });
                }
            });
            Toaster.add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
            if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
        }
    }

    updateLevel(level: Level)
    {
        this._character.level = level;

        if(this._character.leveling) //Invalidate higher levels
        {
            for(let _level = 20; _level > this._character.level; _level--)
            {
                const level = _level as Level;
                if(this._character.leveling.hasOwnProperty(level))
                {
                    const option = this._character.leveling[level]!;
                    const feature = config.peoples[this._character.people!]!.options[level][option]!;
                    delete this._character.leveling[level];
                    if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];

                    this.remove(feature);
                }
            }
        }
    }
    toggleLevelOption(level: Level, choice: number)
    {
        if(level > this._character.level) //Cannot add more level options than the current level
            return;

        if(this._character.leveling === undefined) //Add level 1 if missing
        {
            this._character.leveling = { 1: 0 };
            this.add(config.peoples[this._character.people!]!.options[1][0]!);
        }

        if(level == 1) //Cannot remove level 1
            return;

        for(let i = 1; i < level; i++) //Check previous levels as a requirement
        {
            if(!this._character.leveling.hasOwnProperty(i))
                return;
        }

        if(this._character.leveling.hasOwnProperty(level) && this._character.leveling[level] !== choice) //If the given level is already selected, switch to the new choice
        {
            const feature = config.peoples[this._character.people!]!.options[level][this._character.leveling[level]!]!;
            this.remove(feature);
            if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];

            this.add(config.peoples[this._character.people!]!.options[level][choice]);
            this._character.leveling[level] = choice;
        }
        else if(!this._character.leveling.hasOwnProperty(level)) 
        {
            this._character.leveling[level] = choice;
            
            this.add(config.peoples[this._character.people!]!.options[level][choice]!);
        }
    }
    toggleTrainingOption(stat: MainStat, level: TrainingLevel, choice: number)
    {
        if(level == 0) //Cannot remove the initial level
            return;

        for(let i = 1; i < level; i++) //Check previous levels as a requirement
        {
            if(!this._character.training[stat].hasOwnProperty(i as TrainingLevel))
                return;
        }

        if(this._character.training[stat].hasOwnProperty(level))
        {
            if(this._character.training[stat][level] === choice)
            {
                for(let i = 15; i >= level; i --) //Invalidate higher levels
                {
                    if(this._character.training[stat].hasOwnProperty(i))
                    {
                        const feature = config.training[stat][i as TrainingLevel][this._character.training[stat][i as TrainingLevel]!]!;
                        this.remove(feature);
                        if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];
                        delete this._character.training[stat][i as TrainingLevel];
                    }
                }
            }
            else
            {
                const feature = config.training[stat][level][this._character.training[stat][level]!]!;
                this.remove(feature);
                if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];
                this._character.training[stat][level] = choice;
                this.add(config.training[stat][level][choice]);
            }
        }
        else
        {
            this._character.training[stat][level] = choice;
            this.add(config.training[stat][level][choice]);
        }
    }
    handleChoice(element: HTMLElement, feature: string)
    {
        const choices = config.features[feature]!.effect.filter(e => e.category === 'choice');
        if(choices.length === 0)
            return;

        const menu = followermenu(element, [ div('px-24 py-6 flex flex-col items-center text-light-100 dark:text-dark-100', choices.map(e => div('flex flex-row items-center', [ text(e.text), div('flex flex-col', Array(e.settings?.amount ?? 1).fill(0).map((_, i) => ( 
            select(e.options.map((_e, _i) => ({ text: _e.text, value: _i })), { defaultValue: this._character.choices![e.id] !== undefined ? this._character.choices![e.id]![i] : undefined, change: (value) => { 
                this._character.choices![e.id] ??= []; 
                this._character.choices![e.id]![i] = value;
            }, class: { container: 'w-32' } }) 
        ))) ]))) ], { arrow: true, offset: { mainAxis: 8 }, cover: 'width', placement: 'bottom', priority: false, viewport: document.getElementById('characterEditorContainer') ?? undefined, });
    }
}

abstract class BuilderTab {
    protected _builder: CharacterBuilder;
    protected _content!: Array<Node | string>;
    static header: string;
    static description: string;
    static errorMessage: string;

    constructor(builder: CharacterBuilder) { this._builder = builder; }
    update() { }
    static validate(builder: CharacterBuilder): boolean { return false; }
    get dom() { return this._content; }
};
type BuilderTabConstructor = {
    new (builder: CharacterBuilder): BuilderTab;
    header: string;
    description: string;
    errorMessage: string;
    validate(builder: CharacterBuilder): boolean;
} 
class PeoplePicker extends BuilderTab
{
    private _nameInput: HTMLInputElement;
    private _visibilityInput: HTMLDivElement;
    private _options: HTMLDivElement[];

    static override header = 'Peuple';
    static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.';
    static override errorMessage = 'Veuillez choisir un peuple pour continuer.';

    constructor(builder: CharacterBuilder)
    {
        super(builder);

        this._nameInput = input('text', {
            input: (value) => {
                this._builder.character.name = value ?? '';
                document.title = `d[any] - Edition de ${this._builder.character.name || 'nouveau personnage'}`;
            }, defaultValue: this._builder.character.name
        });
        this._visibilityInput = toggle({ defaultValue: this._builder.character.visibility === "private", change: (value) => this._builder.character.visibility = value ? "private" : "public" });
        
        this._options = Object.values(config.peoples).map(
            (people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => { 
                    this._builder.character.people = people.id;
                    this._builder.character = { ...this._builder.character, people: people.id };
                    "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.forEach(f => f?.classList.toggle(e, false)));
                    "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options[i]?.classList.toggle(e, true));
                }
            }, attributes: { 'data-people': people.id } }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
        );
        
        this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "pb-1 md:p-0", text: "Nom" }),
                this._nameInput,
            ]),
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "md:text-base text-sm", text: "Privé ?" }),
                this._visibilityInput,
            ]),
            button(text('Suivant'), () => this._builder.display(1), 'h-[35px] px-[15px]'),
        ]), div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options)];

        this.update();
    }
    override update()
    {
        this._nameInput.value = this._builder.character.name;
        this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");

        if(this._builder.character.people !== undefined)
        {
            "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.find(e => e.getAttribute('data-people') === this._builder.character.people)?.classList.toggle(e, true));
        }
    }
    static override validate(builder: CharacterBuilder): boolean
    {
        return builder.character.people !== undefined;
    }
}
class LevelPicker extends BuilderTab
{
    private _levelInput: HTMLInputElement;
    private _pointsInput: HTMLInputElement;
    private _healthText: Text;
    private _manaText: Text;

    private _options: HTMLElement[][];

    static override header = 'Niveaux';
    static override description = 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.';
    static override errorMessage = 'Vous avez attribué trop de niveaux.';

    constructor(builder: CharacterBuilder)
    {
        super(builder);

        this._levelInput = numberpicker({ defaultValue: this._builder.character.level, min: 1, max: 20, input: (value) => {
            this._builder.character.level = value;
            this.updateLevel();
        } });
        this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
        this._healthText = text("0"), this._manaText = text("0");

        this._options = Object.entries(config.peoples[this._builder.character.people!]!.options).map(
            (level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative left-4" }, [ text(level[0]) ])]),
            div("flex flex-row gap-4 justify-center", level[1].map((option, j) => {
                const choice = config.features[option]!.effect.some(e => e.category === 'choice') ? dom('div', { class: 'absolute -bottom-px -right-px border border-light-50 dark:border-dark-50 bg-light-10 dark:bg-dark-10 hover:border-light-70 dark:hover:border-dark-70 flex p-1 justify-center items-center', listeners: { click: (e) => {
                    e.stopImmediatePropagation();
                    this._builder.character.leveling[level[0] as any as Level] === j && this._builder.handleChoice(choice!, config.features[option]!.id);
                } } }, [ icon('radix-icons:gear') ]) : undefined;
                return dom("div", { class: ["flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px] relative", { 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': (level[0] as any as Level) <= this._builder.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': this._builder.character.leveling[level[0] as any as Level] === j }], listeners: { click: e => { 
                    this._builder.toggleLevelOption(parseInt(level[0]) as Level, j);
                    this.update();
                }}}, [ dom('span', { class: "text-wrap whitespace-pre", text: config.features[option]!.description }), choice ]);
            }))
        ]);
        
        this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "pb-1 md:p-0", text: "Niveau" }),
                this._levelInput,
            ]),
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "md:text-base text-sm", text: "Points restantes" }),
                this._pointsInput,
            ]),
            div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
                dom("span", { text: "Vie" }),
                this._healthText,
            ]),
            div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
                dom("span", { text: "Mana" }),
                this._manaText,
            ]),
            button(text('Suivant'), () => this._builder.display(2), 'h-[35px] px-[15px]'),
        ]), div('flex flex-col flex-1 gap-4 mx-8 my-4', this._options.flatMap(e => [...e]))];

        this.update();
    }
    override update()
    {
        const values = this._builder.values;

        this._levelInput.value = this._builder.character.level.toString();
        this._pointsInput.value = (this._builder.character.level - Object.keys(this._builder.character.leveling).length).toString();
        this._healthText.textContent = values.health?.toString() ?? '0';
        this._manaText.textContent = values.mana?.toString() ?? '0';

        this.updateLevel();
    }
    private updateLevel()
    {
        this._builder.updateLevel(this._builder.character.level as Level);

        this._pointsInput.value = (this._builder.character.level - Object.keys(this._builder.character.leveling).length).toString();
        this._options.forEach((e, i) => {
            e[0]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
            e[1]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
            e[1]?.childNodes.forEach((option, j) => {
                'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level));
                '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, this._builder.character.leveling[((i + 1) as Level)] === j));
            });
        });
    }
    static override validate(builder: CharacterBuilder): boolean
    {
        return builder.character.level - Object.keys(builder.character.leveling).length >= 0;
    }
}
class TrainingPicker extends BuilderTab
{
    private _pointsInput: HTMLInputElement;
    private _healthText: Text;
    private _manaText: Text;
    private _options: Record<MainStat, HTMLDivElement[][]>;

    private _tab: number = 0;
    private _statIndicator: HTMLSpanElement;
    private _statContainer: HTMLDivElement;

    static override header = 'Entrainement';
    static override description = 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.';
    static override errorMessage = 'Vous avez dépensé trop de points d\'entrainement.';

    constructor(builder: CharacterBuilder)
    {
        super(builder);
        const statRenderBlock = (stat: MainStat) => {
            return Object.entries(config.training[stat]).map(
                (level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]),
                div("flex flex-row gap-4 justify-center", level[1].map((option, j) => {
                    const choice = config.features[option]!.effect.some(e => e.category === 'choice') ? dom('div', { class: 'absolute -bottom-px -right-px border border-light-50 dark:border-dark-50 bg-light-10 dark:bg-dark-10 hover:border-light-70 dark:hover:border-dark-70 flex p-1 justify-center items-center', listeners: { click: (e) => {
                        e.stopImmediatePropagation();
                        this._builder.character.training[stat as MainStat][parseInt(level[0], 10) as TrainingLevel] === j && this._builder.handleChoice(choice!, config.features[option]!.id);
                    } } }, [ icon('radix-icons:gear') ]) : undefined;
                    return dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50 relative"], listeners: { click: e => { 
                        this._builder.toggleTrainingOption(stat, parseInt(level[0]) as TrainingLevel, j);
                        this.update();
                    }}}, [ markdown(config.features[option]!.description, undefined, { tags: { a: preview } }), choice ]);
                }))
            ]);
        }

        this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
        this._healthText = text("0"), this._manaText = text("0");
        
        this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record<MainStat, HTMLDivElement[][]>);
        
        this._statIndicator = dom('span', { class: 'rounded-full w-3 h-3 bg-accent-blue absolute transition-[left] after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center' });
        this._statContainer = div('relative  select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(this._options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e]))));
        this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20", [
            div('flex flex-shrink gap-3 items-center relative w-48 ms-12', [
                ...MAIN_STATS.map((e, i) => dom('span', { listeners: { click: () => this.switchTab(i) },  class: 'block w-2.5 h-2.5 m-px outline outline-1 outline-transparent hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer' })),
                this._statIndicator,
            ]),
            div('flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10', [
                dom("label", { class: "flex justify-center items-center my-2" }, [
                    dom("span", { class: "md:text-base text-sm", text: "Points restantes" }),
                    this._pointsInput,
                ]),
                div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
                    dom("span", { text: "Vie" }),
                    this._healthText,
                ]),
                div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
                    dom("span", { text: "Mana" }),
                    this._manaText,
                ]),
                button(text('Suivant'), () => this._builder.display(3), 'h-[35px] px-[15px]'),
            ]), dom('span')
        ]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])];

        this.switchTab(0);

        this.update();
    }
    switchTab(tab: number)
    {
        this._tab = tab;
        
        this._statIndicator.setAttribute('data-text', mainStatTexts[MAIN_STATS[tab] as MainStat]);
        this._statIndicator.style.left = `${tab * 1.5}em`;

        this._statContainer.style.left = `-${tab * 100}%`;
    }
    override update()
    {
        const values = this._builder.values;
        const training = Object.values(this._builder.character.training).reduce((p, v) => p + Object.values(v).filter(e => e !== undefined).length, 0);

        this._pointsInput.value = ((values.training ?? 0) - training).toString();
        this._healthText.textContent = values.health?.toString() ?? '0';
        this._manaText.textContent = values.mana?.toString() ?? '0';

        Object.keys(this._options).forEach(stat => {
            const max = Object.keys(this._builder.character.training[stat as MainStat]).length;
            this._options[stat as MainStat].forEach((e, i) => {
                e[0]?.classList.toggle("opacity-30", (i as TrainingLevel) > max);
                e[1]?.classList.toggle("opacity-30", (i as TrainingLevel) > max);
                e[1]?.childNodes.forEach((option, j) => {
                    '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, i == 0 || (this._builder.character.training[stat as MainStat][i as TrainingLevel] === j)));
                })
            })
        });
    }
    static override validate(builder: CharacterBuilder): boolean
    {
        const values = builder.values;
        const training = Object.values(builder.character.training).reduce((p, v) => p + Object.values(v).filter(e => e !== undefined).length, 0);

        return (values.training ?? 0) - training >= 0;
    }
}
class AbilityPicker extends BuilderTab
{
    private _pointsInput: HTMLInputElement;
    private _options: HTMLDivElement[];

    private _maxs: HTMLElement[] = [];

    static override header = 'Compétences';
    static override description = 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.';
    static override errorMessage = 'Une compétence est incorrectement saisie ou vous avez dépassé le nombre de points à attribuer.';

    constructor(builder: CharacterBuilder)
    {
        super(builder);

        this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
        
        this._options = ABILITIES.map((e, i) => { 
            const max = dom('span', { class: 'text-lg text-end font-bold' });
            this._maxs.push(max);
            return div('flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative', [
                div('flex justify-between', [ numberpicker({ defaultValue: this._builder.character.abilities[e], input: (value) => { this._builder.character.abilities[e] = value; this.update(); }}), max ]),
                dom('span', { class: "text-xl text-center font-bold", text: abilityTexts[e] }),
                dom('span', { class: "absolute -bottom-px -left-px h-[3px] bg-accent-blue" }),
            ])
        });
        
        this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10", [
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "md:text-base text-sm", text: "Points restantes" }),
                this._pointsInput,
            ]),
            button(text('Suivant'), () => this._builder.display(4), 'h-[35px] px-[15px]'),
        ]), div('flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48', this._options)];

        this.update();
    }
    override update()
    {
        const values = this._builder.values, compiled = this._builder.compiled;
        const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0);

        this._pointsInput.value = ((values.ability ?? 0) - abilities).toString();

        ABILITIES.forEach((e, i) => {
            const max = (values[`bonus/abilities/${e}`] ?? 0);
            
            const load = this._options[i]?.lastElementChild as HTMLSpanElement | undefined;
            const valid = (compiled.abilities[e] ?? 0) <= max;
            if(load)
            {
                Object.assign(load.style ?? {}, { width: `${clamp((max === 0 ? 0 : (this._builder.character.abilities[e] ?? 0) / max) * 100, 0, 100)}%` });
                'bg-accent-blue'.split(' ').forEach(_e => load.classList.toggle(_e, valid));
                'bg-light-red dark:bg-dark-red'.split(' ').forEach(_e => load.classList.toggle(_e, !valid));
            }
            this._maxs[i]!.textContent = `/ ${max ?? 0}`;
        })
    }
    static override validate(builder: CharacterBuilder): boolean
    {
        const values = builder.values, compiled = builder.compiled;
        const abilities = Object.values(builder.character.abilities).reduce((p, v) => p + v, 0);

        return ABILITIES.map(e => (values[`bonus/abilities/${e}`] ?? 0) >= (compiled.abilities[e] ?? 0)).every(e => e) && (values.ability ?? 0) - abilities >= 0;
    }
}
class AspectPicker extends BuilderTab
{
    private _physicInput: HTMLInputElement;
    private _mentalInput: HTMLInputElement;
    private _personalityInput: HTMLInputElement;

    private _filter: boolean = true;

    private _options: HTMLDivElement[];

    static override header = 'Aspect';
    static override description = 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.';
    static override errorMessage = 'Veuillez choisir un Aspect.';

    constructor(builder: CharacterBuilder)
    {
        super(builder);

        this._physicInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
        this._mentalInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
        this._personalityInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
        
        this._options = config.aspects.map((e, i) => dom('div', { attributes: { "data-aspect": i.toString() }, listeners: { click: () => {
            this._builder.character.aspect = i;
            this._options.forEach(_e => _e.setAttribute('data-state', 'inactive'));
            this._options[i]?.setAttribute('data-state', 'active');
        }}, class: 'group flex flex-col w-[360px] border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 cursor-pointer' }, [
            div('bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2  group-data-[state=active]:bg-accent-blue group-data-[state=active]:bg-opacity-10', [
                div('flex flex-row gap-8 ps-4 items-center', [
                    div("flex flex-1 flex-col gap-2 justify-center", [ div('text-lg font-bold', [ text(e.name) ]), dom('span', { class: 'border-b w-full border-light-50 dark:border-dark-50 group-data-[state=active]:border-b-[4px] group-data-[state=active]:border-accent-blue' }) ]),
                    div('rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10')
                ])
            ]),
            div('flex justify-stretch items-stretch py-2 px-4 gap-4', [
                div('flex flex-col flex-1 items-stretch gap-4', [
                    div('flex flex-1 justify-between', [ text('Difficulté'), div('text-sm font-bold', [ text(e.difficulty.toString()) ]) ]),
                    div('flex flex-1 justify-between', [ text('Bonus'), div('text-sm font-bold', [ text(e.stat === 'special' ? 'Special' : mainStatTexts[e.stat]) ]) ])
                ]),
                div('w-px h-full bg-light-50 dark:bg-dark-50'),
                div('flex flex-col items-center justify-between py-2', [
                    div('text-sm italic', [ text(alignmentTexts[e.alignment]) ]),
                    div(['text-sm font-bold', { "text-light-purple dark:text-dark-purple italic": e.magic, "text-light-orange dark:text-dark-orange": !e.magic }], [ text(e.magic ? 'Magie autorisée' : 'Magie interdite') ]),
                ]),
            ])
        ]));

        const filterSwitch = dom("div", { class: `group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, attributes: { "data-state": this._filter ? "checked" : "unchecked" }, listeners: {
            click: (e: Event) => {
                this._filter = !this._filter;
                filterSwitch.setAttribute('data-state', this._filter ? "checked" : "unchecked");
                this.update();
            }
        }}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
        
        this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10", [
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "md:text-base text-sm", text: "Physique" }),
                this._physicInput,
            ]),
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "md:text-base text-sm", text: "Mental" }),
                this._mentalInput,
            ]),
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "md:text-base text-sm", text: "Caractère" }),
                this._personalityInput,
            ]),
            dom("label", { class: "flex justify-center items-center my-2" }, [
                dom("span", { class: "md:text-base text-sm", text: "Filtrer ?" }),
                filterSwitch,
            ]),
            button(text('Enregistrer'), () => this._builder.save(), 'h-[35px] px-[15px]'),
        ]), div('flex flex-row flex-wrap justify-center items-center flex-1 gap-8 mx-8 my-4 px-8', this._options)];

        this.update();
    }
    override update()
    {
        const physic = Object.values(this._builder.character.training['strength']).length + Object.values(this._builder.character.training['dexterity']).length + Object.values(this._builder.character.training['constitution']).length;
        const mental = Object.values(this._builder.character.training['intelligence']).length + Object.values(this._builder.character.training['curiosity']).length;
        const personality = Object.values(this._builder.character.training['charisma']).length + Object.values(this._builder.character.training['psyche']).length;

        this._physicInput.value = physic.toString();
        this._mentalInput.value = mental.toString();
        this._personalityInput.value = personality.toString();

        (this._content[1] as HTMLElement).replaceChildren(...this._options.filter(e => {
            const index = parseInt(e.getAttribute('data-aspect')!);
            const aspect = config.aspects[index]!;

            e.setAttribute('data-state', this._builder.character.aspect === index ? 'active' : 'inactive');

            if(!this._filter)
                return true;

            if(physic > aspect.physic.max || physic < aspect.physic.min)
                return false;
            if(mental > aspect.mental.max || mental < aspect.mental.min)
                return false;
            if(personality > aspect.personality.max || personality < aspect.personality.min)
                return false;

            return true;
        }));
    }
    static override validate(builder: CharacterBuilder): boolean
    {
        if(builder.character.aspect === undefined)
            return false;

        return true;
    }
}

export class CharacterSheet
{
    user: ComputedRef<User | null>;
    character?: CharacterCompiler;
    container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center');
    tabs?: HTMLDivElement & { refresh: () => void };
    constructor(id: string, user: ComputedRef<User | null>)
    {
        this.user = user;
        const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
        this.container.replaceChildren(load);
        useRequestFetch()(`/api/character/${id}`).then(character => {
            if(character)
            {
                this.character = new CharacterCompiler(character);

                document.title = `d[any] - ${character.name}`;
                load.remove();

                this.render();
            }
            else
                throw new Error();
        }).catch((e) => {
            console.error(e);
            this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [
                span('text-2xl font-bold tracking-wider', 'Personnage introuvable'),
                span(undefined, 'Ce personnage n\'existe pas ou est privé.'),
                div('flex flex-row gap-4 justify-center items-center', [ 
                    button(text('Personnages publics'), () => useRouter().push({ name: 'character-list' }), 'px-2 py-1'),
                    button(text('Créer un personange'), () => useRouter().push({ name: 'character-id-edit', params: { id: 'new' } }), 'px-2 py-1')
                ])
            ]))
        });
    }
    render()
    {
        if(!this.character)
            return;

        const character = this.character.compiled;
        
        const publicNotes = new MarkdownEditor();
        const privateNotes = new MarkdownEditor();

        const loadableIcon = icon('radix-icons:paper-plane', { width: 16, height: 16 });
        const saveLoading = loading('small');
        const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.character?.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }

        publicNotes.onChange = (v) => this.character!.character.notes!.public = v;
        privateNotes.onChange = (v) => this.character!.character.notes!.private = v;
        publicNotes.content = this.character!.character.notes!.public!;
        privateNotes.content = this.character!.character.notes!.private!;

        this.tabs = tabgroup([
            { id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) },

            { id: 'abilities', title:  [ text('Aptitudes') ], content: () => this.abilitiesTab(character) },

            { id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },

            { id: 'inventory', title: [ text('Inventaire') ], content: () => [

            ] },

            { id: 'notes', title: [ text('Notes') ], content: () => [
                div('flex flex-col gap-2', [
                    div('flex flex-col gap-2 border-b border-light-35 dark:border-dark-35 pb-4', [ div('flex flex-row w-full items-center justify-between', [ span('text-lg font-bold', 'Notes publics'), tooltip(button(loadableIcon, saveNotes, 'p-1 items-center justify-center'), 'Enregistrer', 'right') ]), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ publicNotes.dom ]) ]),
                    div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]),
                ])
            ] },
        ], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px] h-full', content: 'overflow-auto' } });
        this.container.replaceChildren(div('flex flex-col justify-start gap-1 h-full', [
            div("flex flex-row gap-4 justify-between", [
                div(),
                
                div("flex lg:flex-row flex-col gap-6 items-center justify-center", [
                    div("flex gap-6 items-center", [
                        div('inline-flex select-none items-center justify-center overflow-hidden align-middle h-16', [
                            div('text-light-100 dark:text-dark-100 leading-1 flex p-4 items-center justify-center bg-light-25 dark:bg-dark-25 font-medium', [
                                icon("radix-icons:person", { width: 16, height: 16 }),
                            ])
                        ]),
                        
                        div("flex flex-col", [
                            dom("span", { class: "text-xl font-bold", text: character.name }),
                            dom("span", { class: "text-sm", text: `De ${character.username}` })
                        ]),
                        
                        div("flex flex-col", [
                            dom("span", { class: "font-bold", text: `Niveau ${character.level}` }),
                            dom("span", { text: config.peoples[character.race]?.name ?? 'Peuple inconnu' })
                        ])
                    ]),
                    
                    div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-4 ps-4 gap-8", [
                        dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
                            text("PV: "),
                            dom("span", { 
                                class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
                                text: `${character.health - character.variables.health}`
                            }),
                            text(`/ ${character.health}`)
                        ]),
                        dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
                            text("Mana: "),
                            dom("span", { 
                                class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
                                text: `${character.mana - character.variables.mana}`
                            }),
                            text(`/ ${character.mana}`)
                        ])
                    ])
                ]),
                
                div("self-center", [
                    this.user.value && this.user.value.id === character.owner ? 
                        button(icon("radix-icons:pencil-2"), () => useRouter().push({ name: 'character-id-edit', params: { id: this.character?.character.id } }), "p-1") 
                        : div()
                ])
            ]),
            
            div("flex flex-row justify-center 2xl:gap-4 gap-2 p-4 border-b border-light-35 dark:border-dark-35", [
                div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
                    div("flex flex-col items-center px-2", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.strength}` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Force" })
                    ]),
                    div("flex flex-col items-center px-2", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.dexterity}` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Dextérité" })
                    ]),
                    div("flex flex-col items-center px-2", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.constitution}` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Constitution" })
                    ]),
                    div("flex flex-col items-center px-2", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.intelligence}` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Intelligence" })
                    ]),
                    div("flex flex-col items-center px-2", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.curiosity}` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Curiosité" })
                    ]),
                    div("flex flex-col items-center px-2", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.charisma}` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Charisme" })
                    ]),
                    div("flex flex-col items-center px-2", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.psyche}` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Psyché" })
                    ])
                ]),

                div('border-l border-light-35 dark:border-dark-35'),

                div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
                    div("flex flex-col px-2 items-center", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.initiative}` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Initiative" })
                    ]),
                    div("flex flex-col px-2 items-center", [
                        dom("span", { class: "2xl:text-2xl text-xl font-bold", text: character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Course" })
                    ])
                ]),

                div('border-l border-light-35 dark:border-dark-35'),
                
                div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
                    icon("game-icons:checked-shield", { width: 32, height: 32 }),
                    div("flex flex-col px-2 items-center", [
                        dom("span", { 
                            class: "2xl:text-2xl text-xl font-bold", 
                            text: `${clamp(character.defense.static + character.defense.passivedodge + character.defense.passiveparry, 0, character.defense.hardcap)}`
                        }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Passive" })
                    ]),
                    div("flex flex-col px-2 items-center", [
                        dom("span", { 
                            class: "2xl:text-2xl text-xl font-bold", 
                            text: `${clamp(character.defense.static + character.defense.passivedodge + character.defense.activeparry, 0, character.defense.hardcap)}`
                        }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Blocage" })
                    ]),
                    div("flex flex-col px-2 items-center", [
                        dom("span", { 
                            class: "2xl:text-2xl text-xl font-bold", 
                            text: `${clamp(character.defense.static + character.defense.activedodge + character.defense.passiveparry, 0, character.defense.hardcap)}`
                        }),
                        dom("span", { class: "text-sm 2xl:text-base", text: "Esquive" })
                    ])
                ]),
            ]),

            div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4 h-0", [
                div("flex flex-col gap-4 py-1 w-60", [
                    div("flex flex-col py-1 gap-4", [
                        div("flex flex-row items-center justify-center gap-4", [
                            div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]),
                            div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
                        ]),

                        div("grid grid-cols-2 gap-2", 
                            Object.entries(character.abilities).map(([ability, value]) =>
                                div("flex flex-row px-1 justify-between items-center", [
                                    span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate", abilityTexts[ability as Ability] || ability),
                                    span("font-bold text-base text-light-100 dark:text-dark-100", `+${value}`),
                                ])
                            )
                        ),

                        div("flex flex-row items-center justify-center gap-4", [
                            div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]),
                            div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
                        ]),

                        character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
                            character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', label: 'Arme légère' }) : undefined,
                            character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', label: 'Arme de jet' }) : undefined,
                            character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', label: 'Arme naturelle' }) : undefined,
                            character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', label: 'Arme standard' }) : undefined,
                            character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', label: 'Arme improvisée' }) : undefined,
                            character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', label: 'Arme lourde' }) : undefined,
                            character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', label: 'Arme à deux mains' }) : undefined,
                            character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', label: 'Arme maniable' }) : undefined,
                            character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', label: 'Arme à projectiles' }) : undefined,
                            character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', label: 'Arme longue' }) : undefined,
                            character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', label: 'Bouclier' }) : undefined,
                            character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', label: 'Bouclier à deux mains' }) : undefined,
                        ]) : undefined,
                        
                        character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
                            character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', label: 'Armure légère' }) : undefined,
                            character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', label: 'Armure standard' }) : undefined,
                            character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', label: 'Armure lourde' }) : undefined,
                        ]) : undefined,
                        
                        div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
                            div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', character.spellranks.precision.toString()) ]),
                            div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', character.spellranks.knowledge.toString()) ]),
                            div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', character.spellranks.instinct.toString()) ]),
                            div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', character.spellranks.arts.toString()) ]),
                        ])
                    ])
                ]),
                
                div('border-l border-light-35 dark:border-dark-35'),
                
                this.tabs,
            ])
        ]));
    }
    actionsTab(character: CompiledCharacter)
    {
        return [
            div('flex flex-col gap-8', [
                div('flex flex-col gap-2', [
                    div("flex flex-row items-center justify-center gap-4", [
                        div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
                        div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
                        div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
                    ]),

                    div('flex flex-col gap-2', [
                        div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
                        ...(character.lists.action?.map(e => div('flex flex-col gap-1', [
                            div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
                            markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }),
                        ])) ?? [])
                    ]),
                ]),
                div('flex flex-col gap-2', [
                    div("flex flex-row items-center justify-center gap-4", [
                        div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
                        div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
                        div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
                    ]),

                    div('flex flex-col gap-2', [
                        div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
                        ...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
                            div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
                            markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }),
                        ])) ?? [])
                    ]),
                ]),
                div('flex flex-col gap-2', [
                    div("flex flex-row items-center justify-center gap-4", [
                        div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]),
                        div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
                    ]),

                    div('flex flex-col gap-2', [
                        div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
                        ...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
                            div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]),
                            markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }),
                        ])) ?? [])
                    ]),
                ]),
            ]),
        ]
    }
    abilitiesTab(character: CompiledCharacter)
    {
        return [
            div('flex flex-col gap-2', [
                ...(character.lists.passive?.map(e => div('flex flex-col gap-1', [
                    div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]),
                    markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }),
                ])) ?? []),
            ]),
        ];
    }
    spellTab(character: CompiledCharacter)
    {
        let sortPreference = (localStorage.getItem('character-sort') ?? 'rank') as 'rank' | 'type' | 'element';

        const sort = (spells: Array<{ id: string, spell?: SpellConfig, source: string }>) => {
            spells = spells.filter(e => !!e.spell);
            switch(sortPreference)
            {
                case 'rank': return spells.sort((a, b) => a.spell!.rank - b.spell!.rank || SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!));
                case 'type': return spells.sort((a, b) => a.spell!.type.localeCompare(b.spell!.type) || a.spell!.rank - b.spell!.rank);
                case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!) || a.spell!.rank - b.spell!.rank);
                default: return spells;
            }
        };
        const spells = sort([...(character.lists.spells ?? []).map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'feature' })), ...character.variables.spells.map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'player' }))]).map(e => ({...e, dom: 
            e.spell ? div('flex flex-col gap-2', [ 
                div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${e.spell.cost ?? 0} mana` }) ]),
                div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [ 
                    div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]), 
                    div('flex flex-row gap-2', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ])
                ]),
                div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.description) ]),
            ]) : undefined }));
        return [
            div('flex flex-col gap-2', [
                div('flex flex-row justify-between items-center', [
                    div('flex flex-row gap-2 items-center', [
                        dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
                        buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; this.tabs?.refresh(); } }),
                    ]),
                    div('flex flex-row gap-2 items-center', [
                        dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
                        button(text('Modifier'), () => this.spellPanel(character, spells), 'py-1 px-4'),
                    ])
                ]),
                div('flex flex-col gap-2', spells.map(e => e.dom))
            ])
        ]
    }
    spellPanel(character: CompiledCharacter, spelllist: Array<{ id: string, spell?: SpellConfig, source: string }>)
    {
        const availableSpells = Object.values(config.spells).filter(spell => {
            if (spell.rank === 4) return false;
            if (character.spellranks[spell.type] < spell.rank) return false;
            return true;
        });

        const textAmount = text(character.variables.spells.length.toString()), textMax = text(character.spellslots.toString());
        const container = div("border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [
            div("flex flex-row justify-between items-center mb-4", [
                dom("h2", { class: "text-xl font-bold", text: "Ajouter un sort" }),
                div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ textAmount, text(' / '), textMax, text(' sorts maitrisés') ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
                    setTimeout(blocker.close, 150);
                    container.setAttribute('data-state', 'inactive');
                }, "p-1"), "Fermer", "left") ])
            ]),
            div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', availableSpells.map(spell => {
                let state = character.lists.spells?.includes(spell.id) ? 'given' : character.variables.spells.includes(spell.id) ? 'choosen' : 'empty';
                const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => {
                    if(state === 'choosen')
                    {
                        this.character!.variable('spells', character.variables.spells.filter(e => e !== spell.id));
                        state = 'empty';
                    }
                    else if(state === 'empty')
                    {
                        this.character!.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK
                        state = 'choosen';
                    }
                    toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
                    textAmount.textContent = character.variables.spells.length.toString();
                    this.tabs?.refresh();
                }, "px-2 py-1 text-sm font-normal");
                toggleButton.disabled = state === 'given';
                return foldable(() => [
                    markdown(spell.description),
                ], [ div("flex flex-row justify-between gap-2", [
                        dom("span", { class: "text-lg font-bold", text: spell.name }),
                        div("flex flex-row items-center gap-6", [
                            div("flex flex-row text-sm gap-2", 
                                spell.elements.map(el =>
                                    dom("span", { 
                                        class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[el].class], 
                                        text: elementTexts[el].text
                                    })
                                )
                            ),
                            div("flex flex-row text-sm gap-1", [
                                ...(spell.rank !== 4 ? [
                                    dom("span", { text: `Rang ${spell.rank}` }),
                                    text("/"),
                                    dom("span", { text: spellTypeTexts[spell.type] }),
                                    text("/")
                                ] : []),
                                dom("span", { text: `${spell.cost} mana` }),
                                text("/"),
                                dom("span", { text: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` })
                            ]),
                            toggleButton,
                        ]),
                    ]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } });
            }))
        ]);
        const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
        setTimeout(() => container.setAttribute('data-state', 'active'), 1);
    }
}