import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
import { contextmenu, followermenu, popper, tooltip, type FloatState } from "./floating.util";
import { clamp } from "./general.util";
import { Tree } from "./tree";
import type { Placement } from "@floating-ui/dom";

export function link(children: NodeChildren, properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>)
{
    const router = useRouter();
    const nav = link ? router.resolve(link) : undefined;
    return dom('a', { ...properties, class: [properties?.class, properties?.active && router.currentRoute.value.fullPath === nav?.fullPath ? properties.active : undefined], attributes: { href: nav?.href, 'data-active': properties?.active ? mergeClasses(properties?.active) : undefined }, listeners: link ? {
        click: function(e)
        {
            e.preventDefault();
            router.push(link);
        }
    } : undefined }, children);
}
export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElement
{
    return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
}
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<HTMLElement>)
{
    let state = { current: loading(size) };

    fn.then((element) => {
        state.current.replaceWith(element);
        state.current = element;
    }).catch(e => {
        console.error(e);
        state.current.remove();
    })

    return state;
}
export function button(content: Node, onClick?: (this: HTMLElement) => void, cls?: Class)
{
    /*
    text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
    border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
    disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50
    */
    const btn = dom('button', { class: [`inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
        text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
        hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
        focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50
        disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick.bind(btn)()) } }, [ content ]);
    let disabled = false;
    Object.defineProperty(btn, 'disabled', {
        get: () => disabled,
        set: (v) => { 
            disabled = !!v;
            btn.toggleAttribute('disabled', disabled);
        }
    })
    return btn;
}
export function buttongroup<T extends any>(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean | void })
{
    let currentValue = settings?.value;
    const elements = options.map(e => dom('div', { class: [`cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 dark:hover:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-none
    border border-light-40 dark:border-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[selected]:z-10 data-[selected]:border-light-50 dark:data-[selected]:border-dark-50 
    data-[selected]:shadow-raw transition-[box-shadow] data-[selected]:shadow-light-50 dark:data-[selected]:shadow-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40`, 
    settings?.class?.option], text: e.text, attributes: { 'data-selected': settings?.value === e.value }, listeners: { click: function() {
        if(currentValue !== e.value)
        {
            elements.forEach(e => e.toggleAttribute('data-selected', false));
            this.toggleAttribute('data-selected', true);

            if(!settings?.onChange || settings?.onChange(e.value) !== false)
            {
                currentValue = e.value;
            }
        }
    }}}))
    return div(['flex flex-row', settings?.class?.container], elements);
}
export function optionmenu(options: Array<{ title: string, click: () => void }>, settings?: { position?: Placement, class?: { container?: Class, option?: Class } }): (target?: HTMLElement) => void
{
    let close: () => void;
    const element = div(['flex flex-col divide-y divide-light-30 dark:divide-dark-30 text-light-100 dark:text-dark-100', settings?.class?.container], options.map(e => dom('div', { class: ['flex flex-row px-2 py-1 hover:bg-light-35 dark:hover:bg-dark-35 cursor-pointer', settings?.class?.option], text: e.title, listeners: { click: () => { e.click(); close() } } })));
    return function(this: HTMLElement, target?: HTMLElement) {
        close = followermenu(target ?? this, [ element ], { arrow: true, placement: settings?.position, offset: 8 }).close;
    }
}
export type Option<T> = { text: string, render?: () => HTMLElement, value: T | Option<T>[] } | undefined;
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T>> };
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
{
    let context: { close: Function };
    let focused: number | undefined;

    options = options.filter(e => !!e);

    const focus = (i?: number) => {
        focused !== undefined && optionElements[focused]?.toggleAttribute('data-focused', false);
        i !== undefined && optionElements[i]?.toggleAttribute('data-focused', true) && optionElements[i]?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
        focused = i;
    }

    let disabled = settings?.disabled ?? false;
    const textValue = text(options.find(e => Array.isArray(e) ? false : e?.value === settings?.defaultValue)?.text ?? '');
    const optionElements = options.map((e, i) => {
        if(e === undefined)
            return;

        return dom('div', { listeners: { click: (_e) => { 
            textValue.textContent = e.text;
            settings?.change && settings?.change(e.value);
            context && context.close && !_e.ctrlKey && context.close();
        }, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]);
    });
    const select = dom('div', { listeners: { click: () => { 
        if(disabled)
            return;

        const handleKeys = (e: KeyboardEvent) => {
            switch(e.key.toLocaleLowerCase())
            {
                case 'arrowdown':
                    focus(clamp((focused ?? -1) + 1, 0, options.length - 1));
                    return;
                case 'arrowup':
                    focus(clamp((focused ?? 1) - 1, 0, options.length - 1));
                    return;
                case 'pageup':
                    focus(0);
                    return;
                case 'pagedown':
                    focus(optionElements.length - 1);
                    return;
                case 'enter':
                    focused && optionElements[focused]?.click();
                    return;
                case 'escape':
                    context?.close();
                    return;
                default: return;
            }
        }
        window.addEventListener('keydown', handleKeys);

        const box = select.getBoundingClientRect();
        context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
    } }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);

    Object.defineProperty(select, 'disabled', {
        get: () => disabled,
        set: (v) => {
            disabled = !!v;
            select.toggleAttribute('data-disabled', disabled);
        },
    })
    return select;
}
export function multiselect<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T[], change?: (value: T[]) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
{
    let context: { close: Function };
    let focused: number | undefined;
    let selection: T[] = settings?.defaultValue ?? [];

    options = options.filter(e => !!e);

    const focus = (i?: number) => {
        focused !== undefined && optionElements[focused]?.toggleAttribute('data-focused', false);
        i !== undefined && optionElements[i]?.toggleAttribute('data-focused', true) && optionElements[i]?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
        focused = i;
    }

    let disabled = settings?.disabled ?? false;
    const textValue = text(selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : '');
    const optionElements = options.map((e, i) => {
        if(e === undefined)
            return;

        const element =  dom('div', { listeners: { click: (_e) => { 
            selection = selection.includes(e.value) ? selection.filter(f => f !== e.value) : [...selection, e.value];
            textValue.textContent = selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : '';
            element.toggleAttribute('data-selected', selection.includes(e.value));
            settings?.change && settings?.change(selection);
            context && context.close && !_e.ctrlKey && context.close();
        }, mouseenter: (e) => focus(i) }, class: ['group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], attributes: { 'data-selected': selection.includes(e.value) } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block', noobserver: true }) ]);
        return element;
    });
    const select = dom('div', { listeners: { click: () => { 
        if(disabled)
            return;

        const handleKeys = (e: KeyboardEvent) => {
            switch(e.key.toLocaleLowerCase())
            {
                case 'arrowdown':
                    focus(clamp((focused ?? -1) + 1, 0, options.length - 1));
                    return;
                case 'arrowup':
                    focus(clamp((focused ?? 1) - 1, 0, options.length - 1));
                    return;
                case 'pageup':
                    focus(0);
                    return;
                case 'pagedown':
                    focus(optionElements.length - 1);
                    return;
                case 'enter':
                    focused && optionElements[focused]?.click();
                    return;
                case 'escape':
                    context?.close();
                    return;
                default: return;
            }
        }
        window.addEventListener('keydown', handleKeys);

        const box = select.getBoundingClientRect();
        context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
    } }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);

    Object.defineProperty(select, 'disabled', {
        get: () => disabled,
        set: (v) => {
            disabled = !!v;
            select.toggleAttribute('data-disabled', disabled);
        },
    })
    return select;
}
export function combobox<T extends NonNullable<any>>(options: Option<T>[], settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean, fill?: 'contain' | 'cover' }): HTMLElement
{
    let context: { container: HTMLElement, content: NodeChildren, close: () => void };
    let selected = true, tree: StoredOption<T>[] = [];
    let focused: number | undefined;
    let currentOptions: StoredOption<T>[] = [];

    const focus = (value?: T | Option<T>[]) => {
        focused !== undefined && currentOptions[focused]?.dom.toggleAttribute('data-focused', false);
        if(value !== undefined)
        {
            const i = currentOptions.findIndex(e => e.item?.value === value);
            if(i !== -1)
            {
                currentOptions[i]?.dom.toggleAttribute('data-focused', true);
                currentOptions[i]?.dom.scrollIntoView({ behavior: 'instant', block: 'nearest' });
                focused = i;
            }
            else
            {
                focused = undefined;
            }
        }
        else
        {
            focused = undefined;
        }
    }
    const show = () => {
        if(disabled || (context && context.container.parentElement))
            return;

        const box = container.getBoundingClientRect();
        focus();
        context = followermenu(container, currentOptions.length > 0 ? currentOptions.map(e => e.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden', settings?.class?.popup], style: { "min-width": settings?.fill === 'cover' && `${box.width}px`, "max-width": settings?.fill === 'contain' && `${box.width}px` }, blur: hide });
        if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
    };
    const hide = () => {
        if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red');
        tree = [];

        context && context.container.parentElement && context.close();
    };
    const progress = (option: StoredOption<T>) => {
        if(!context || !context.container.parentElement || option.container === undefined)
            return;

        context.container.replaceChildren(option.container);
        tree.push(option);
        currentOptions = option.children!;
        focus();
    };
    const back = () => {
        tree.pop();
        const last = tree.slice(-1)[0];
        currentOptions = last?.children ?? optionElements;

        last ? context.container.replaceChildren(last.container ?? last.dom) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom));
    };
    const render = (option: Option<T>): StoredOption<T> | undefined => {
        if(option === undefined)
            return;

        if(Array.isArray(option.value))
        {
            const children = option.value.map(render).filter(e => !!e);

            const stored = { item: option, dom: dom('div', { listeners: { click: () => progress(stored), mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer flex justify-between items-center', settings?.class?.option] }, [ text(option.text), icon('radix-icons:caret-right', { width: 20, height: 20 }) ]), container: div('flex flex-1 flex-col', [div('flex flex-row justify-between items-center text-light-100 dark:text-dark-100 py-1 px-2 text-sm select-none sticky top-0 bg-light-20 dark:bg-dark-20 font-semibold', [button(icon('radix-icons:caret-left', { width: 16, height: 16 }), back, 'p-px'), text(option.text), div()]), div('flex flex-col flex-1', children.map(e => e?.dom))]), children };
            return stored;
        }
        else
        {
            return { item: option, dom: dom('div', { listeners: { click: (_e) => {
                select.value = option.text;
                settings?.change && settings?.change(option.value as T);
                selected = true;
                !_e.ctrlKey && hide();
            }, mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ option?.render ? option?.render() : text(option.text) ]) };
        }
    }
    const filter = (value: string, option?: StoredOption<T>): StoredOption<T>[] => {
        if(option && option.children !== undefined)
        {
            return option.children.flatMap(e => filter(value, e));
        }
        else if(option && option.item)
        {
            return option.item.text.toLowerCase().normalize().includes(value) ? [ option ] : [];
        }
        else
        {
            return [];
        }
    }

    let disabled = settings?.disabled ?? false;
    const optionElements = currentOptions = options.map(render).filter(e => !!e);
    const select = dom('input', { listeners: { focus: show, input: () => {
        focus();
        currentOptions = context && select.value ? optionElements.flatMap(e => filter(select.value.toLowerCase().trim().normalize(), e)) : optionElements.filter(e => !!e);
        context && context.container.replaceChildren(...currentOptions.map(e => e.dom));
        selected = false;
        if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
    }, keydown: (e) => {
        switch(e.key.toLocaleLowerCase())
        {
            case 'arrowdown':
                focus(currentOptions[clamp((focused ?? -1) + 1, 0, currentOptions.length - 1)]?.item?.value);
                return;
            case 'arrowup':
                focus(currentOptions[clamp((focused ?? 1) - 1, 0, currentOptions.length - 1)]?.item?.value);
                return;
            case 'pageup':
                focus(currentOptions[0]?.item?.value);
                return;
            case 'pagedown':
                focus(currentOptions[currentOptions.length - 1]?.item?.value);
                return;
            case 'enter':
                focused !== undefined ? currentOptions[focused]?.dom.click() : currentOptions[0]?.dom.click();
                return;
            case 'escape':
                context?.close();
                return;
            default: return;
        }
    } }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
    settings?.defaultValue && Tree.each(options, 'value', (item) => { if(item.value === settings?.defaultValue) select.value = item.text });

    const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]);

    Object.defineProperty(container, 'disabled', {
        get: () => disabled,
        set: (v) => {
            disabled = !!v;
            container.toggleAttribute('data-disabled', disabled);
            select.toggleAttribute('disabled', disabled);
        },
    })
    return container;
}
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean, placeholder?: string }): HTMLInputElement
{
    const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder }, class: [`mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
    bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
    border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, settings?.class], listeners: {
        input: () => settings?.input && settings.input(input.value),
        change: () => settings?.change && settings.change(input.value),
        focus: () => settings?.focus,
        blur: () => settings?.blur,
    }})
    if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;

    return input;
}
export function numberpicker(settings?: { defaultValue?: number, change?: (value: number) => void, input?: (value: number) => void, focus?: () => void, blur?: () => void, class?: Class, min?: number, max?: number, disabled?: boolean }): HTMLInputElement
{
    let storedValue = settings?.defaultValue ?? 0;
    const validateAndChange = (value: number) => {
        if(isNaN(value))
            field.value = '';
        else
        {
            value = clamp(value, settings?.min ?? -Infinity, settings?.max ?? Infinity);
            field.value = value.toString(10);
            if(storedValue !== value)
            {
                storedValue = value;
                return true;
            }
        }
        return false;
    }
    const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 disabled:shadow-none disabled:bg-light-20 dark:disabled:bg-dark-20 disabled:border-dashed disabled:border-light-30 dark:disabled:border-dark-30`, settings?.class], listeners: {
        input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
        keydown: (e: KeyboardEvent) => {
            if(field.disabled)
                return;

            switch(e.key)
            {
                case "ArrowUp":
                    validateAndChange(storedValue + (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
                    break;
                case "ArrowDown":
                    validateAndChange(storedValue - (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
                    break;
                case "PageUp":
                    settings?.max && validateAndChange(settings.max) && settings?.input && settings.input(storedValue);
                    break;
                case "PageDown":
                    settings?.min && validateAndChange(settings.min) && settings?.input && settings.input(storedValue);
                    break;
                default:
                    return;
            }
        },
        change: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.change && settings.change(storedValue),
        focus: () => settings?.focus && settings.focus(),
        blur: () => settings?.blur && settings.blur(),
    }});
    if(settings?.defaultValue !== undefined) field.value = storedValue.toString(10);

    return field;
}
// Open by default
export function foldable(content: NodeChildren | (() => NodeChildren), title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
{
    let _content: NodeChildren;
    const display = (state: boolean) => {
        if(state && !_content)
        {
            _content = typeof content === 'function' ? content() : content;
            _content && contentContainer.replaceChildren(..._content.filter(e => !!e));
        }
    }
    const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
    const fold = div(['group flex w-full flex-col', settings?.class?.container], [
        div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center', noobserver: true }) ]), div(['flex-1', settings?.class?.title], title) ]),
        contentContainer
    ]);
    display(settings?.open ?? true);
    fold.toggleAttribute('data-active', settings?.open ?? true);
    return fold;
}
type TableRow = Record<string, (() => HTMLElement) | HTMLElement | string>;
export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class, cell?: Class } })
{
    const render = (item: (() => HTMLElement) | HTMLElement | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item;
    return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: ['', properties?.class?.cell] }, [ render(e[f]!) ]) : undefined)))) ]);
}
export function toggle(settings?: { defaultValue?: boolean, change?: (value: boolean) => void, disabled?: boolean, class?: { container?: Class } })
{
    let state = settings?.defaultValue ?? false;
    const element = 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]`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
                click: function(e: Event) {
                    if(this.hasAttribute('data-disabled'))
                        return;
                    
                    state = !state;
                    element.setAttribute('data-state', state ? "checked" : "unchecked");
                    settings?.change && settings.change(state);
                }
            }
        }, [ 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') ]);
    return element;
}
export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HTMLElement, value: boolean) => void, disabled?: boolean, class?: { container?: Class, icon?: Class } })
{
    let state = settings?.defaultValue ?? false;
    const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20 
        cursor-pointer hover:bg-light-30 dark:hover:bg-dark-30 hover:border-light-60 dark:hover:border-dark-60
        data-[disabled]:cursor-default data-[disabled]:border-dashed data-[disabled]:border-light-40 dark:data-[disabled]:border-dark-40 data-[disabled]:bg-0 dark:data-[disabled]:bg-0`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
                click: function(e: Event) {
                    if(this.hasAttribute('data-disabled'))
                        return;

                    state = !state;
                    element.setAttribute('data-state', state ? "checked" : "unchecked");
                    settings?.change && settings.change.bind(this)(state);
                }
            }
        }, [ icon('radix-icons:check', { width: 14, height: 14, class: ['hidden group-data-[state="checked"]:block data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50', settings?.class?.icon] }), ]);
    return element;
}
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class } }): HTMLDivElement & { refresh: () => void }
{
    let focus = settings?.focused ?? tabs[0]?.id;
    const titles = tabs.map((e, i) => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() {
        if(this.hasAttribute('data-focus'))
            return;

        titles.forEach(e => e.toggleAttribute('data-focus', false));
        this.toggleAttribute('data-focus', true);
        focus = e.id;
        const _content = typeof e.content === 'function' ? e.content() : e.content;
        _content && content.replaceChildren(..._content?.filter(e => !!e));
    }}}, e.title));
    const _content = tabs.find(e => e.id === focus)?.content;
    const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content);

    const container = div(['flex flex-col', settings?.class?.container], [
        div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
        content
    ]);
    Object.defineProperty(container, 'refresh', {
        writable: false,
        configurable: false,
        enumerable: false,
        value: () => {
            let _content = tabs.find(e => e.id === focus)?.content;
            _content = (typeof _content === 'function' ? _content() : _content);
            _content && content.replaceChildren(..._content.filter(e => !!e));
        }
    })
    return container as HTMLDivElement & { refresh: () => void };
}
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
{
    let viewport = document.getElementById('mainContainer') ?? undefined;
    let diffX, diffY;
    let minimizeBox: DOMRect, minimized = false;

    const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({
        show: ['mouseenter', 'mousemove', 'focus'],
        hide: ['mouseleave', 'blur'],
    }, settings?.events ?? {});

    if(settings?.pinned)
    {
        events.onshow = (state) => { 
            if(!settings?.events?.onshow || settings?.events?.onshow(state))
            {
                floating.show();
                pin();
            }
            return false;
        };
    }

    const dragstart = (e: MouseEvent) => {
        e.preventDefault();

        if(minimized)
            return;

        window.addEventListener('mousemove', dragmove);
        window.addEventListener('mouseup', dragend);

        const box = floating.content.getBoundingClientRect();
        diffX = e.clientX - box.x;
        diffY = e.clientY - box.y;
    };
    const resizestart = (e: MouseEvent) => {
        e.preventDefault();
        window.addEventListener('mousemove', resizemove);
        window.addEventListener('mouseup', resizeend);
    };

    const dragmove = (e: MouseEvent) => {
        const box = floating.content.getBoundingClientRect();
        const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
        box.x = clamp(e.clientX - diffX!, viewbox?.left ?? 0, viewbox.right - box.width);
        box.y = clamp(e.clientY - diffY!, viewbox?.top ?? 0, viewbox.bottom - box.height);

        Object.assign(floating.content.style, {
            left: `${box.x}px`,
            top: `${box.y}px`,
        });
    };
    const resizemove = (e: MouseEvent) => {
        const box = floating.content.getBoundingClientRect();
        const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
        box.width = clamp(e.clientX - box.x, 200, Math.min(750, viewbox.right - box.x));
        box.height = clamp(e.clientY - box.y, 150, Math.min(750, viewbox.bottom - box.y));

        Object.assign(floating.content.style, {
            width: `${box.width}px`,
            height: `${box.height}px`,
        });
    };

    const dragend = (e: MouseEvent) => {
        e.preventDefault();
        window.removeEventListener('mousemove', dragmove);
        window.removeEventListener('mouseup', dragend);
    };
    const resizeend = (e: MouseEvent) => {
        e.preventDefault();
        window.removeEventListener('mousemove', resizemove);
        window.removeEventListener('mouseup', resizeend);
    };

    const pin = () => {
        if(floating.content.hasAttribute('data-pinned'))
            return;

        const box = floating.content.children.item(0)!.getBoundingClientRect();
        const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
        Object.assign(floating.content.style, {
            left: `${clamp(box.left, viewbox.left, viewbox.right)}px`,
            top: `${clamp(box.top, viewbox.top, viewbox.bottom)}px`,
            width: `${box.width + 21}px`,
            height: `${box.height + 21}px`,
        });
        floating.content.attributeStyleMap.delete('bottom');
        floating.content.attributeStyleMap.delete('right');
        floating.stop();

        floating.content.addEventListener('mousedown', function() {
            if(!floating.content.hasAttribute('data-pinned'))
                return;

            [...this.parentElement?.children ?? []].forEach(e => (e as any as HTMLElement).attributeStyleMap.set('z-index', -1));
            this.attributeStyleMap.set('z-index', 0);
        }, { passive: true });
    }
    const minimize = () => {
        minimized = !minimized;
        floating.content.toggleAttribute('data-minimized', minimized);
        if(minimized)
        {
            minimizeBox = floating.content.getBoundingClientRect();
            Object.assign(floating.content.style, {
                left: `0px`,
                top: `initial`,
                bottom: `0px`,
                width: `150px`,
                height: `21px`,
            });
        }
        else
        {
            Object.assign(floating.content.style, {
                left: `${minimizeBox.left}px`,
                top: `${minimizeBox.top}px`,
                width: `${minimizeBox.width}px`,
                height: `${minimizeBox.height}px`,
            });
        }
    };
    
    const floating = popper(container, {
        arrow: true,
        delay: settings?.pinned ? 0 : 150,
        offset: 12,
        cover: settings?.cover,
        placement: settings?.position,
        class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
        content: () => [
            settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [ 
                dom('span', { class: 'flex-1 w-full h-full cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }), 
                settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined, 
                settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { ((e.ctrlKey || e.button === 1) ? window.open : useRouter().push)(useRouter().resolve(settings.href!).href); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined, 
                tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { 
                    e.stopImmediatePropagation();
                    floating.hide();
                    
                    floating.content.toggleAttribute('data-minimized', false);
                    minimized && Object.assign(floating.content.style, {
                        left: `${minimizeBox.left}px`,
                        top: `${minimizeBox.top}px`,
                        width: `${minimizeBox.width}px`,
                        height: `${minimizeBox.height}px`,
                    });
                    minimized = false;
                } } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
            div('group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
        ],
        viewport,
        events: events
    });

    if(settings?.pinned === false)
        floating.content.addEventListener('dblclick', pin);

    return container;
}

export interface ToastConfig 
{
    closeable?: boolean
    duration: number
    title?: string
    content?: string
    timer?: boolean
    type?: ToastType
}
type ToastDom = ToastConfig & { dom: HTMLElement };
export type ToastType = 'info' | 'success' | 'error';
export class Toaster
{
    private static _MAX_DRAG = 150;
    private static _list: Array<ToastDom> = [];
    private static _container: HTMLDivElement;

    static init()
    {
        Toaster._container = dom('div', { attributes: { id: 'toaster' }, class: 'fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72' });
        document.body.appendChild(Toaster._container);
    }
    static add(_config: ToastConfig)
    {
        let start: number;
        const dragstart = (e: MouseEvent) => {
            window.addEventListener('mousemove', dragmove);
            window.addEventListener('mouseup', dragend);

            start = e.clientX;
        };
        const dragmove = (e: MouseEvent) => {
            const drag = e.clientX - start;
            if(drag > Toaster._MAX_DRAG)
            {
                dragend();
                config.dom.animate([{ transform: `translateX(${drag}px)` }, { transform: `translateX(150%)` }], { duration: 100, easing: 'ease-out' });
                Toaster.close(config);
            }
            else if(drag > 0)
            {
                config.dom.style.transform = `translateX(${drag}px)`;
            }
        };
        const dragend = () => {
            window.removeEventListener('mousemove', dragmove);
            window.removeEventListener('mouseup', dragend);
            
            config.dom.style.transform = `translateX(0px)`;
        };
        const config = _config as ToastDom;
        const loader = config.timer ? div('bg-light-50 dark:bg-dark-50 h-full w-full transition-[width] ease-linear') : undefined;
        loader?.animate([{ width: '0' }, { width: '100%' }], { duration: config.duration, easing: 'linear' });
        config.dom = dom('div', { class: 'ToastRoot bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 group select-none', attributes: { 'data-type': config.type, 'data-state': 'open' } }, [
            div('grid grid-cols-8 px-3 pt-2 pb-2', [ 
                config.title ? dom('h4', { class: 'font-semibold text-xl col-span-7 text-light-100 dark:text-dark-100', text: config.title }) : undefined, 
                config.closeable ? dom('span', { class: 'translate-x-4 text-light-100 dark:text-dark-100', listeners: { click: () => Toaster.close(config), } }, [ icon('radix-icons:cross-1', { width: 12, height: 12, noobserver: true, class: 'cursor-pointer' }) ]) : undefined, 
                config.content ? dom('span', { class: 'text-sm col-span-8 text-light-100 dark:text-dark-100', text: config.content }) : undefined, 
            ]),
            config.timer ? dom('div', { class: 'relative overflow-hidden bg-light-25 dark:bg-dark-25 h-1 mb-0 mt-0 w-full group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green group-data-[type=error]:bg-light-red dark:group-data-[type=error]:bg-dark-red group-data-[type=success]:bg-light-green dark:group-data-[type=success]:bg-dark-green !bg-opacity-50' }, [ loader ]) : undefined
        ]);
        config.dom.addEventListener('mousedown', dragstart);
        config.dom.animate([{ transform: 'translateX(100%)' }, { transform: 'translateX(0)' }], { duration: 150, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' });
        Toaster._container?.appendChild(config.dom);
        Toaster._list.push(config);

        setTimeout(() => Toaster.close(config), config.duration);
    }
    static clear(type?: ToastType)
    {
        Toaster._list.filter(e => e.type !== type || (Toaster.close(e), false));
    }
    private static close(config: ToastDom)
    {
        config.dom.animate([
            { opacity: 1 }, { opacity: 0 },
        ], { easing: 'ease-in', duration: 100 }).onfinish = () => config.dom.remove();
        Toaster._list = Toaster._list.filter(e => e !== config);
    }
}