import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
import { clamp, lerp } from "#shared/general.util";
import { dom, icon, svg, text } from "./dom.util";
import render from "./markdown.util";
import { popper } from "#shared/floating.util";
import { Content } from "./content.util";
import type { History } from "./history.util";
import { EventEmitter } from "nodemailer/lib/xoauth2";

export type Direction = 'bottom' | 'top' | 'left' | 'right';
export type Position = { x: number, y: number };
export type Box = Position & { w: number, h: number };
export type Path = {
    path: string;
    from: Position;
    to: Position;
    side: Direction;
}

export const rotation: Record<Direction, string> = {
    top: "180",
    bottom: "0",
    left: "90",
    right: "270"
};
export const opposite: Record<Direction, Direction> = {
    top: "bottom",
    bottom: "top",
    left: "right",
    right: "left"
}

export function edgePos(side: Direction, pos: Position, offset: number): Position {
    switch (side) {
        case "left":
            return {
                x: pos.x - offset,
                y: pos.y
            };
        case "right":
            return {
                x: pos.x + offset,
                y: pos.y
            };
        case "top":
            return {
                x: pos.x,
                y: pos.y - offset
            };
        case "bottom":
            return {
                x: pos.x,
                y: pos.y + offset
            }
    }
}
export function getNode(nodes: CanvasNode[], id: string): CanvasNode | undefined
{
    return nodes.find(e => e.id === id);
}
export function posFromDir(e: { minX: number, minY: number, maxX: number, maxY: number }, t: Direction): Position {
    switch (t) {
        case "top":
            return { x: (e.minX + e.maxX) / 2, y: e.minY };
        case "right":
            return { x: e.maxX, y: (e.minY + e.maxY) / 2 };
        case "bottom":
            return { x: (e.minX + e.maxX) / 2, y: e.maxY };
        case "left":
            return { x: e.minX, y: (e.minY + e.maxY) / 2 };
    }
}
export function getBbox(node: CanvasNode): { minX: number, minY: number, maxX: number, maxY: number } {
    return { minX: node.x, minY: node.y, maxX: node.x + node.width, maxY: node.y + node.height };
}
export function getPath(from: CanvasNode, fromSide: Direction, to: CanvasNode, toSide: Direction): Path | undefined {
    if(from === undefined || to === undefined)
        return;

    const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
    return bezier(start, fromSide, end, toSide);
}
export function bezier(from: Position, fromSide: Direction, to: Position, toSide: Direction): Path {
    const r = Math.hypot(from.x - to.x, from.y - to.y), o = clamp(r / 2, 70, 150), a = edgePos(fromSide, from, o), s = edgePos(toSide, to, o);
    return {
        path: `M${from.x},${from.y} C${a.x},${a.y} ${s.x},${s.y} ${to.x},${to.y}`,
        from: from,
        to: to,
        side: toSide,
    };
}
export function labelCenter(from: CanvasNode, fromSide: Direction, to: CanvasNode, toSide: Direction): string {
    const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
    const len = Math.hypot(start.x - end.x, start.y - end.y), offset = clamp(len / 2, 70, 150), b = edgePos(fromSide, start, offset), s = edgePos(toSide, end, offset);
    const center = getCenter(start, end, b, s, 0.5);
    return `translate(${center.x}px, ${center.y}px)`;
}
export function getCenter(n: Position, i: Position, r: Position, o: Position, e: number): Position {
    const a = 1 - e, s = a * a * a, l = 3 * e * a * a, c = 3 * e * e * a, u = e * e * e;
    return {
        x: s * n.x + l * r.x + c * o.x + u * i.x,
        y: s * n.y + l * r.y + c * o.y + u * i.y
    };
}

export function gridSnap(value: number, grid: number): number
{
    return Math.round(value / grid) * grid;
}

const cancelEvent = (e: Event) => e.preventDefault();
function center(touches: TouchList): Position
{
    const pos = { x: 0, y: 0 };

    for(const touch of touches)
    {
        pos.x += touch.clientX;
        pos.y += touch.clientY;
    }

    pos.x /= touches.length;
    pos.y /= touches.length;
    return pos;
}
function distance(touches: TouchList): number
{
    const [A, B] = touches;
    return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
}

export class Node extends EventEmitter
{
    properties: CanvasNode;

    nodeDom!: HTMLDivElement;

    constructor(properties: CanvasNode)
    {
        super();

        this.properties = properties;

        this.getDOM()
    }

    protected getDOM()
    {
        const style = this.style;

        this.nodeDom = dom('div', { class: ['absolute', {'-z-10': this.properties.type === 'group', 'z-10': this.properties.type !== 'group'}], style: { transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, width: `${this.properties.width}px`, height: `${this.properties.height}px`, '--canvas-color': this.properties.color?.hex } }, [
            dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4', style.border] }, [
                dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg] }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text)]) : undefined])
            ])
        ]);

        if(this.properties.type === 'group')
        {
            if(this.properties.label !== undefined)
            {
                this.nodeDom.appendChild(dom('div', { class: ['origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin', style.border], style: 'max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))', text: this.properties.label }));
            }
        }
    }

    get style()
    {
        return this.properties.color ? this.properties.color?.class ? 
            { bg: `bg-light-${this.properties.color?.class} dark:bg-dark-${this.properties.color?.class}`, border: `border-light-${this.properties.color?.class} dark:border-dark-${this.properties.color?.class}` } : 
            { bg: `bg-colored`, border: `border-[color:var(--canvas-color)]` } : 
            { border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` }
    }
}
export class NodeEditable extends Node {
    
    constructor(properties: CanvasNode)
    {
        super(properties);
    }
    protected override getDOM()
    {
        super.getDOM();
    }
}
export class Edge extends EventEmitter
{
    properties: CanvasEdge;

    edgeDom!: HTMLDivElement;
    protected from: CanvasNode;
    protected to: CanvasNode;
    protected path: Path;
    protected labelPos: string;

    constructor(properties: CanvasEdge, nodes: CanvasNode[])
    {
        super();
        this.properties = properties;

        this.from = nodes.find(f => f.id === properties.fromNode)!;
        this.to = nodes.find(f => f.id === properties.toNode)!;
        this.path = getPath(this.from, properties.fromSide, this.to, properties.toSide)!;
        this.labelPos = labelCenter(this.from, properties.fromSide, this.to, properties.toSide);
    }

    protected getDOM()
    {
        const style = this.style;
        this.edgeDom = dom('div', { class: 'absolute overflow-visible' }, [
            this.properties.label ? dom('div', { style: { transform: `${this.labelPos} translate(-50%, -50%)` }, class: 'relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20', text: this.properties.label }) : undefined,
            svg('svg', { class: 'absolute top-0 overflow-visible h-px w-px' }, [
                svg('g', { style: {'--canvas-color': this.properties.color?.hex}, class: 'z-0'  }, [
                    svg('g', { style: `transform: translate(${this.path!.to.x}px, ${this.path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[this.path!.side]}deg);` }, [
                        svg('polygon', { class: style.fill, attributes: { points: '0,0 6.5,10.4 -6.5,10.4' } }),
                    ]),
                    svg('path', { style: `stroke-width: calc(3px * var(--zoom-multiplier)); stroke-linecap: butt;`, class: [style.stroke, 'fill-none stroke-[4px]'], attributes: { d: this.path!.path } }),
                ]),
            ]),
        ]);
    }

    get style()
    {
        return this.properties.color ? this.properties.color?.class ? 
            { fill: `fill-light-${this.properties.color?.class} dark:fill-dark-${this.properties.color?.class}`, stroke: `stroke-light-${this.properties.color?.class} dark:stroke-dark-${this.properties.color?.class}` } : 
            { fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]` } : 
            { stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` }
    }
}
export class EdgeEditable extends Edge
{
    private static input: HTMLInputElement = dom('input', { class: 'relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20 -translate-x-1/2 -translate-y-1/2', listeners: { click: e => e.stopImmediatePropagation() } });
    private focusing: boolean = false;
    private editing: boolean = false;

    private pathDom!: SVGPathElement;
    private inputDom!: HTMLDivElement;
    constructor(properties: CanvasEdge, nodes: CanvasNode[])
    {
        super(properties, nodes);
    }
    protected override getDOM()
    {
        const style = this.style;
        this.pathDom = svg('path', { style: { 'stroke-width': `calc(3px * var(--zoom-multiplier))`, 'stroke-linecap': 'butt' }, class: ['transition-[stroke-width] fill-none stroke-[4px]', style.stroke], listeners: { click: e => this.emit('select', e), dblclick: e => this.emit('edit', e) } });
        this.inputDom = dom('div', { class: ['relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20 -translate-x-1/2 -translate-y-1/2', { 'hidden': this.properties.label === undefined }], style: { transform: this.labelPos }, text: this.properties.label });
        this.edgeDom = dom('div', { class: 'absolute overflow-visible group' }, [
            this.inputDom,
            svg('svg', { class: 'absolute top-0 overflow-visible h-px w-px' }, [
                svg('g', { style: { '--canvas-color': this.properties.color?.hex } }, [
                    svg('g', { style: { transform: `translate(${this.path.to.x}px, ${this.path.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[this.path.side]}deg)` } }, [
                        svg('polygon', { class: style.fill, attributes: { 'points': '0,0 6.5,10.4 -6.5,10.4' } }),
                    ]),
                    this.pathDom,
                    svg('path', { style: { 'stroke-width': `calc(22px * var(--zoom-multiplier))` }, class: ['fill-none transition-opacity z-30 opacity-0 hover:opacity-25', style.stroke], attributes: { d: this.path.path } }),
                ]),
            ]),
        ]);
    }
}

export class Canvas
{
    static minZoom: number = 0.3;
    static maxZoom: number = 3;

    protected content: Required<CanvasContent>;
    protected zoom: number = 0.5;
    protected x: number = 0;
    protected y: number = 0;

    private centerX: number = 0;
    private centerY: number = 0;
    private containZoom: number = 0.5;

    protected visualZoom: number = this.zoom;
    protected visualX: number = this.x;
    protected visualY: number = this.y;

    protected tweener: Tweener = new Tweener();
    private debouncedTimeout: Timer = setTimeout(() => {}, 0);

    protected transform!: HTMLDivElement;
    container!: HTMLDivElement;

    constructor(content?: CanvasContent)
    {
        if(!content)
            content = { nodes: [], edges: [], groups: [] };

        if(!content.nodes)
            content.nodes = [];

        if(!content.edges)
            content.edges = [];

        if(!content.groups)
            content.groups = [];

        this.content = content as Required<CanvasContent>;

        this.createDOM();

        this.computeLimits();
        this.reset();

        this.mount();
    }

    protected createDOM()
    {
        this.transform = dom('div', { class: 'origin-center h-full' }, [
            dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
                ...this.content.nodes.map(e => new Node(e).nodeDom), ...this.content.edges.map(e => new Edge(e, this.content.nodes).edgeDom),
            ])
        ]);
        //TODO: --zoom-multiplier dynamic
        this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
            dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
                dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom * 1.1, Canvas.minZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom avant')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:reload')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Reset')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.zoomTo(this.x, this.y, Canvas.minZoom); } } }, [icon('radix-icons:corners')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Tout contenir')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom / 1.1, Canvas.minZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom arrière')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                ]), //dom('a') Edition link
            ]), this.transform,
        ]);
        
        /*
                <NuxtLink v-if="overview && isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])" :to="{ name: 'explore-edit', hash: `#${encodeURIComponent(overview!.path)}` }" class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
                    <Tooltip message="Modifier" side="right">
                        <Icon icon="radix-icons:pencil-1" class="w-8 h-8 p-2" />
                    </Tooltip>
                </NuxtLink>
        */
    }

    private computeLimits()
    {
        const box = this.container.getBoundingClientRect();
        let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
        
        this.content.nodes.forEach(e => {
            minX = Math.min(minX, e.x);
            minY = Math.min(minY, e.y);
            maxX = Math.max(maxX, e.x);
            maxY = Math.max(maxY, e.y);
        });

        this.centerX = minX + (maxX - minX) / 2;
        this.centerY = minY + (maxY - minY) / 2;
        this.containZoom = Math.min((maxX - minX) / box.width, (maxY - minY) / box.height);
    }

    mount()
    {
        let lastX = 0, lastY = 0, lastDistance = 0;
        const dragMove = (e: MouseEvent) => {
            this.x = this.visualX = this.x - (lastX - e.layerX) / this.zoom;
            this.y = this.visualY = this.y - (lastY - e.layerY) / this.zoom;
    
            lastX = e.layerX;
            lastY = e.layerY;
    
            this.updateTransform();
        };
        const dragEnd = (e: MouseEvent) => {
            window.removeEventListener('mouseup', dragEnd);
            window.removeEventListener('mousemove', dragMove);
        };
        this.container.addEventListener('mouseenter', () => { 
            window.addEventListener('wheel', cancelEvent, { passive: false });
            document.addEventListener('gesturestart', cancelEvent);
            document.addEventListener('gesturechange', cancelEvent);
    
            this.container.addEventListener('mouseleave', () => {
                window.removeEventListener('wheel', cancelEvent);
                document.removeEventListener('gesturestart', cancelEvent);
                document.removeEventListener('gesturechange', cancelEvent);
            });
        })
        this.container.addEventListener('mousedown', (e) => {
            lastX = e.layerX;
            lastY = e.layerY;
    
            window.addEventListener('mouseup', dragEnd, { passive: true });
            window.addEventListener('mousemove', dragMove, { passive: true });
        }, { passive: true });
        this.container.addEventListener('wheel', (e) => {
            if((this.zoom >= Canvas.maxZoom && e.deltaY < 0) || (this.zoom <= Canvas.minZoom && e.deltaY > 0))
                return;

            let box = this.container.getBoundingClientRect()!;

            const diff = Math.exp(e.deltaY * -0.001);
            const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
            const mousex = centerX - e.clientX, mousey = centerY - e.clientY;

            this.zoomTo(this.x - (mousex / (diff * this.zoom) - mousex / this.zoom), this.y - (mousey / (diff * this.zoom) - mousey / this.zoom), clamp(this.zoom * diff, Canvas.minZoom, Canvas.maxZoom));
        }, { passive: true });
        this.container.addEventListener('touchstart', (e) => {
            ({ x: lastX, y: lastY } = center(e.touches));
    
            if(e.touches.length > 1)
            {
                lastDistance = distance(e.touches);
            }
    
            this.container.addEventListener('touchend', touchend, { passive: true });
            this.container.addEventListener('touchcancel', touchcancel, { passive: true });
            this.container.addEventListener('touchmove', touchmove, { passive: true });
        }, { passive: true });
        const touchend = (e: TouchEvent) => {
            if(e.touches.length > 1)
            {
                ({ x: lastX, y: lastY } = center(e.touches));
            }
    
            this.container.removeEventListener('touchend', touchend);
            this.container.removeEventListener('touchcancel', touchcancel);
            this.container.removeEventListener('touchmove', touchmove);
        };
        const touchcancel = (e: TouchEvent) => {
            if(e.touches.length > 1)
            {
                ({ x: lastX, y: lastY } = center(e.touches));
            }
    
            this.container.removeEventListener('touchend', touchend);
            this.container.removeEventListener('touchcancel', touchcancel);
            this.container.removeEventListener('touchmove', touchmove);
        };
        const touchmove = (e: TouchEvent) => {
            const pos = center(e.touches);
            this.x = this.visualX = this.x - (lastX - pos.x) / this.zoom;
            this.y = this.visualY = this.y - (lastY - pos.y) / this.zoom;
            lastX = pos.x;
            lastY = pos.y;
    
            if(e.touches.length === 2)
            {
                const dist = distance(e.touches);
                const diff = dist / lastDistance;
                
                this.zoom = clamp(this.zoom * diff, Canvas.minZoom, Canvas.maxZoom);
            }
    
            this.updateTransform();
        };
    
        this.updateTransform();
    }

    private updateTransform()
    {
        this.transform.style.transform = `scale3d(${this.visualZoom}, ${this.visualZoom}, 1) translate3d(${this.visualX}px, ${this.visualY}px, 0)`;

        clearTimeout(this.debouncedTimeout);
        this.debouncedTimeout = setTimeout(this.updateScale.bind(this), 150);
    }

    private updateScale()
    {
        this.transform.style.setProperty('--tw-scale', this.visualZoom.toString());
        this.container.style.setProperty('--zoom-multiplier', (1 / Math.pow(this.visualZoom, 0.7)).toFixed(3));
    }

    protected zoomTo(x: number, y: number, zoom: number)
    {
        const oldX = this.x, oldY = this.y, oldZoom = this.zoom;
        this.x = x;
        this.y = y;
        this.zoom = zoom;

        this.tweener.update((e) => {
            this.visualX = lerp(e, oldX, x);
            this.visualY = lerp(e, oldY, y);
            this.visualZoom = lerp(e, oldZoom, zoom);

            this.updateTransform();
        }, 50);
    }

    protected reset()
    {
        this.zoomTo(this.centerX, this.centerY, this.containZoom);
    }
}
export class CanvasEditor extends Canvas
{
    private history: History;
    private selection: Array<NodeEditable | EdgeEditable> = [];
    constructor(history: History, content?: CanvasContent)
    {
        super(content);
        this.history = history;
    }
    protected override createDOM()
    {
        this.transform = dom('div', { class: 'origin-center h-full' }, [
            dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
                ...this.content.nodes.map(e => new NodeEditable(e).nodeDom), ...this.content.edges.map(e => new EdgeEditable(e, this.content.nodes).edgeDom),
            ])
        ]);
        //TODO: --zoom-multiplier dynamic
        this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
            dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
                dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom * 1.1, Canvas.minZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom avant')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:reload')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Reset')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.zoomTo(this.x, this.y, Canvas.minZoom); } } }, [icon('radix-icons:corners')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Tout contenir')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom / 1.1, Canvas.minZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom arrière')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                ]),
                dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('ph:arrow-bend-up-left')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Annuler (Ctrl+Z)')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('ph:arrow-bend-up-right')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Rétablir (Ctrl+Y)')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                ]),
                dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('radix-icons:gear')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Préférences')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                    popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('radix-icons:question-mark-circled')]), {
                        delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Aide')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
                    }),
                ]),
            ]), this.transform,
        ]);
    }
}

class Tweener
{
    static linear = (progress: number) => progress;

    private progress: number;
    private duration: number;
    private last: number;

    private animationFrame: number = 0;

    private animation: (progress: number) => number;
    private tick?: (progress: number) => void;

    constructor(animation: (progress: number) => number = Tweener.linear)
    {
        this.progress = 0, this.duration = 0, this.last = 0;
        this.animation = animation;
    }
    private loop(t: DOMHighResTimeStamp)
    {
        const elapsed = t - this.last;
        this.progress = clamp(this.progress + elapsed, 0, this.duration);
        this.last = t;

        const step = this.animation(clamp(this.progress / this.duration, 0, 1));
        this.tick!(step);

        if(this.progress < this.duration)
            this.animationFrame = requestAnimationFrame(this.loop.bind(this));
    }
    update(tick: (progress: number) => void, duration: number)
    {
        this.duration = duration + this.duration - this.progress;
        this.progress = 0;
        this.last = performance.now();
        this.tick = tick;

        cancelAnimationFrame(this.animationFrame);
        this.animationFrame = requestAnimationFrame(this.loop.bind(this));
    }
    stop()
    {
        cancelAnimationFrame(this.animationFrame);

        this.duration = 0;
        this.progress = 0;
    }
}