<script lang="ts">
import { bezier, getBbox, opposite, posFromDir, rotation, type Box, type Direction, type Path, type Position } from '#shared/canvas.util';
import type CanvasNodeEditor from './canvas/CanvasNodeEditor.vue';
import type CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue';
import { SnapFinder, type SnapHint } from '#shared/physics.util';
import type { CanvasPreferences } from '~/types/general';
export type Element = { type: 'node' | 'edge', id: string };

interface ActionMap {
    remove: CanvasNode | CanvasEdge | undefined;
    create: CanvasNode | CanvasEdge | undefined;
    property: CanvasNode | CanvasEdge;
}
type Action = keyof ActionMap;
interface HistoryEvent<T extends Action = Action>
{
    event: T;
    actions: HistoryAction<T>[];
}
interface HistoryAction<T extends Action>
{
    element: Element;
    from: ActionMap[T];
    to: ActionMap[T];
}

type NodeEditor = InstanceType<typeof CanvasNodeEditor>;
type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;

const cancelEvent = (e: Event) => e.preventDefault();
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
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);
}
</script>

<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import { clamp, getID, ID_SIZE } from '#shared/general.util';
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';

const canvas = defineModel<CanvasContent>({ required: true });
const props = defineProps<{
    path: string,
}>();

const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5), spacing = ref<number | undefined>(32);
const focusing = ref<Element>(), editing = ref<Element>();
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'), patternRef = useTemplateRef('patternRef'), toolbarRef = useTemplateRef('toolbarRef'), viewportSize = useElementBounding(canvasRef);
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
const canvasSettings = useCookie<CanvasPreferences>('canvasPreference', { default: () => ({ gridSnap: true, neighborSnap: true, spacing: 32 }) });
const hints = ref<SnapHint[]>([]);
const viewport = computed<Box>(() => {
    const width = viewportSize.width.value / zoom.value, height = viewportSize.height.value / zoom.value;
    const movementX = viewportSize.width.value - width, movementY = viewportSize.height.value - height;
    return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, width, height };
});
const updateScaleVar = useDebounceFn(() => {
    if(transformRef.value)
    {
        transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
    }
    if(canvasRef.value)
    {
        canvasRef.value.style.setProperty('--zoom-multiplier', (1 / Math.pow(zoom.value, 0.7)).toFixed(3));
    }
}, 100);

type DragOrigin = { type: 'edge', id: string, destination: 'from' | 'to', node: string } | { type: 'node', id: string };
const fakeEdge = ref<{ from?: Position, fromSide?: Direction, to?: Position, toSide?: Direction, path?: Path, style?: { stroke: string, fill: string }, hex?: string, drag?: DragOrigin, snapped?: { node: string, side: Direction } }>({});

const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined);
let snapFinder: SnapFinder;

const history = ref<HistoryEvent[]>([]);
const historyPos = ref(-1);
const historyCursor = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);

watch(props, () => {
    snapFinder = new SnapFinder(hints, viewport, { gridSize: 512, preferences: canvasSettings.value, threshold: 16, cellSize: 64 });

    canvas.value.nodes?.forEach((e) => {
        snapFinder.update(e);
    });

    focusing.value = undefined;
    editing.value = undefined;

    dispX.value = 0;
    dispY.value = 0;
    zoom.value = 0.5;

    history.value = [];
    historyPos.value = -1;
    fakeEdge.value = {};
}, { immediate: true });

watch(canvas, () => {
    updateToolbarTransform();
}, { immediate: true, deep: true, });

const reset = (_: MouseEvent) => {
    zoom.value = minZoom.value;

    dispX.value = 0;
    dispY.value = 0;

    updateTransform();
}

function addAction<T extends Action = Action>(event: T, actions: HistoryAction<T>[])
{
    historyPos.value++;
    history.value.splice(historyPos.value, history.value.length - historyPos.value);
    history.value[historyPos.value] = { event, actions };
}
onMounted(() => {
    let lastX = 0, lastY = 0, lastDistance = 0;
    const dragMove = (e: MouseEvent) => {
        dispX.value = dispX.value - (lastX - e.clientX) / zoom.value;
        dispY.value = dispY.value - (lastY - e.clientY) / zoom.value;

        lastX = e.clientX;
        lastY = e.clientY;

        updateTransform();
    };
    const dragEnd = (e: MouseEvent) => {
        window.removeEventListener('mouseup', dragEnd);
        window.removeEventListener('mousemove', dragMove);
    };
    canvasRef.value?.addEventListener('mouseenter', () => { 
        window.addEventListener('wheel', cancelEvent, { passive: false });
        document.addEventListener('gesturestart', cancelEvent);
        document.addEventListener('gesturechange', cancelEvent);

        canvasRef.value?.addEventListener('mouseleave', () => {
            window.removeEventListener('wheel', cancelEvent);
            document.removeEventListener('gesturestart', cancelEvent);
            document.removeEventListener('gesturechange', cancelEvent);
        });
    })
    canvasRef.value?.addEventListener('mousedown', (e) => {
        if(e.button === 1)
        {
            lastX = e.clientX;
            lastY = e.clientY;

            window.addEventListener('mouseup', dragEnd, { passive: true });
            window.addEventListener('mousemove', dragMove, { passive: true });
        }
    }, { passive: true });
    canvasRef.value?.addEventListener('wheel', (e) => {
        if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
            return;

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

        dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
        dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);

        zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
        spacing.value = canvasSettings.value.gridSnap ? canvasSettings.value.spacing ?? 32 : undefined;

        updateTransform();
    }, { passive: true });
    canvasRef.value?.addEventListener('touchstart', (e) => {
        ({ x: lastX, y: lastY } = center(e.touches));

        if(e.touches.length > 1)
        {
            lastDistance = distance(e.touches);
        }

        canvasRef.value?.addEventListener('touchend', touchend, { passive: true });
        canvasRef.value?.addEventListener('touchcancel', touchcancel, { passive: true });
        canvasRef.value?.addEventListener('touchmove', touchmove, { passive: true });
    }, { passive: true });
    const touchend = (e: TouchEvent) => {
        if(e.touches.length > 1)
        {
            ({ x: lastX, y: lastY } = center(e.touches));
        }

        canvasRef.value?.removeEventListener('touchend', touchend);
        canvasRef.value?.removeEventListener('touchcancel', touchcancel);
        canvasRef.value?.removeEventListener('touchmove', touchmove);
    };
    const touchcancel = (e: TouchEvent) => {
        if(e.touches.length > 1)
        {
            ({ x: lastX, y: lastY } = center(e.touches));
        }

        canvasRef.value?.removeEventListener('touchend', touchend);
        canvasRef.value?.removeEventListener('touchcancel', touchcancel);
        canvasRef.value?.removeEventListener('touchmove', touchmove);
    };
    const touchmove = (e: TouchEvent) => {
        const pos = center(e.touches);
        dispX.value = dispX.value - (lastX - pos.x) / zoom.value;
        dispY.value = dispY.value - (lastY - pos.y) / zoom.value;
        lastX = pos.x;
        lastY = pos.y;

        if(e.touches.length === 2)
        {
            const dist = distance(e.touches);
            const diff = dist / lastDistance;
            
            zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
        }

        updateTransform();
    };

    updateTransform();
});

function updateTransform()
{
    if(transformRef.value)
    {
        transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
        updateScaleVar();
    }
    if(patternRef.value && canvasSettings.value.gridSnap)
    {
        patternRef.value.parentElement?.classList.remove('hidden');
        patternRef.value.setAttribute("x", (viewportSize.width.value / 2 + dispX.value % spacing.value! * zoom.value).toFixed(3));
        patternRef.value.setAttribute("y", (viewportSize.height.value / 2 + dispY.value % spacing.value! * zoom.value).toFixed(3));
        patternRef.value.setAttribute("width", (zoom.value * spacing.value!).toFixed(3));
        patternRef.value.setAttribute("height", (zoom.value * spacing.value!).toFixed(3));

        patternRef.value.children[0].setAttribute('cx', (zoom.value).toFixed(3));
        patternRef.value.children[0].setAttribute('cy', (zoom.value).toFixed(3));
        patternRef.value.children[0].setAttribute('r', (zoom.value).toFixed(3));
    }
    else if(patternRef.value && !canvasSettings.value.gridSnap)
    {
        patternRef.value.parentElement?.classList.remove('hidden');
    }
}
function updateToolbarTransform()
{
    const offsetY = -12;
    if(toolbarRef.value)
    {
        if(!focusing.value)
        {
            toolbarRef.value.style.transform = '';
        }
        else if(focusing.value.type === 'node')
        {
            const node = canvas.value.nodes!.find(e => e.id === focusing.value!.id)!;
            toolbarRef.value.style.transform = `translate(${node.x}px, ${node.y}px) translateY(-100%) translateY(${offsetY}px) translateX(-50%) translateX(${node.width / 2}px) scale(calc(1 / var(--tw-scale)))`;
        }
        else
        {
            const path = edges.value!.find(e => e.id === focusing.value!.id)!.path;
            const x = path.from.x + (path.to.x - path.from.x) / 2, y = path.from.y;
            toolbarRef.value.style.transform = `translate(${x}px, ${y}px) translateY(-100%) translateY(${offsetY}px) translateX(-50%) scale(calc(1 / var(--tw-scale)))`;
        }
    }
}
function moveNode(ids: string[], deltax: number, deltay: number)
{
    if(ids.length === 0)
        return;
    
    const actions: HistoryAction<'property'>[] = [];
    for(const id of ids)
    {
        const node = canvas.value.nodes!.find(e => e.id === id)!;
        snapFinder.update(node);

        actions.push({ element: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay }, to: { ...node } });
    }

    addAction('property', actions);
}
function resizeNode(ids: string[], deltax: number, deltay: number, deltaw: number, deltah: number)
{
    if(ids.length === 0)
        return;
    
    const actions: HistoryAction<'property'>[] = [];
    for(const id of ids)
    {
        const node = canvas.value.nodes!.find(e => e.id === id)!;
        snapFinder.update(node);

        actions.push({ element: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay, width: node.width - deltaw, height: node.height - deltah }, to: { ...node } });
    }

    addAction('property', actions);
}
function select(element: Element)
{
    if(focusing.value && (focusing.value.id !== element.id || focusing.value.type !== element.type))
    {
        unselect();
    }

    focusing.value = element;

    focused.value?.dom?.addEventListener('click', stopPropagation, { passive: true });
    canvasRef.value?.addEventListener('click', unselect, { once: true });
    updateToolbarTransform();
}
function edit(element: Element)
{
    editing.value = element;

    focused.value?.dom?.addEventListener('wheel', stopPropagation, { passive: true });
    focused.value?.dom?.addEventListener('dblclick', stopPropagation, { passive: true });
    canvasRef.value?.addEventListener('click', unselect, { once: true });
}
function createNode(e: MouseEvent)
{
    const width = 250, height = 100;
    const x = e.layerX / zoom.value - dispX.value - width  / 2;
    const y = e.layerY / zoom.value - dispY.value - height / 2;
    const node: CanvasNode = { id: getID(ID_SIZE), x, y, width, height, type: 'text' };

    if(!canvas.value.nodes)
        canvas.value.nodes = [node];
    else
        canvas.value.nodes.push(node);

    snapFinder.add(node);
    addAction('create', [{ element: { type: 'node', id: node.id }, from: undefined, to: node }]);
}
function remove(elements: Element[])
{
    if(elements.length === 0)
        return;

    const actions: HistoryAction<'remove'>[] = [];
    focusing.value = undefined;
    editing.value = undefined;

    const c = canvas.value;

    for(const element of elements)
    {
        if(element.type === 'node')
        {
            const edges = c.edges?.map((e, i) => ({ id: e.id, from: e.fromNode, to: e.toNode, index: i }))?.filter(e => e.from === element.id || e.to === element.id) ?? [];
            for(let i = edges.length - 1; i >= 0; i--)
            {
                actions.push({ element: { type: 'edge', id: edges[i].id }, from: c.edges!.splice(edges[i].index, 1)[0], to: undefined });
            }

            const index = c.nodes!.findIndex(e => e.id === element.id);
            const node = c.nodes!.splice(index, 1)[0];

            snapFinder.remove(node);
            actions.push({ element: { type: 'node', id: element.id }, from: node, to: undefined });
        }
        else if(element.type === 'edge' && !actions.find(e => e.element.type === 'edge' && e.element.id === element.id))
        {
            const index = c.edges!.findIndex(e => e.id === element.id);
            actions.push({ element: { type: 'edge', id: element.id }, from: c.edges!.splice(index, 1)[0], to: undefined });
        }
    }

    canvas.value = c;

    addAction('remove', actions);
}
function dragEdgeTo(e: MouseEvent): void
{
    (fakeEdge.value.to as Position).x += e.movementX / zoom.value;
    (fakeEdge.value.to as Position).y += e.movementY / zoom.value;

    const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.drag!.id, fakeEdge.value.to!.x, fakeEdge.value.to!.y);

    fakeEdge.value.snapped = result ? { node: result.node, side: result.direction } : undefined;
    fakeEdge.value.path = bezier((fakeEdge.value.from as Position), fakeEdge.value.fromSide!, result ?? (fakeEdge.value.to as Position), result?.direction ?? fakeEdge.value.toSide!);
}
function dragEndEdgeTo(e: MouseEvent): void
{
    window.removeEventListener('mousemove', dragEdgeTo);
    window.removeEventListener('mouseup', dragEndEdgeTo);

    if(fakeEdge.value.snapped)
    {
        const node = canvas.value.nodes!.find(e => e.id === fakeEdge.value.drag!.id)!;
        const edge: CanvasEdge = { fromNode: fakeEdge.value.drag!.id, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(ID_SIZE), color: node.color };
        canvas.value.edges?.push(edge);

        addAction('create', [{ from: undefined, to: edge, element: { id: edge.id, type: 'edge' } }]);
    }

    fakeEdge.value = {};
}
function dragStartEdgeTo(id: string, e: MouseEvent, direction: Direction): void
{
    const node = canvas.value.nodes!.find(e => e.id === id)!;
    fakeEdgeFromNode(node, direction);

    window.addEventListener('mousemove', dragEdgeTo, { passive: true });
    window.addEventListener('mouseup', dragEndEdgeTo, { passive: true });
}
function dragEdgeSide(e: MouseEvent): void
{
    if(fakeEdge.value.drag?.type === 'node')
        return;

    const destination = fakeEdge.value.drag!.destination;
    const pos = fakeEdge.value[destination]!;

    pos.x += e.movementX / zoom.value;
    pos.y += e.movementY / zoom.value;

    const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.drag!.node, pos.x, pos.y);

    fakeEdge.value.snapped = result ? { node: result.node, side: result.direction } : undefined;
    fakeEdge.value.path = bezier(destination === 'from' ? (result ?? pos) : fakeEdge.value.from!, destination === 'from' ? result?.direction ?? fakeEdge.value.fromSide! : fakeEdge.value.fromSide!, destination === 'to' ? (result ?? pos) : fakeEdge.value.to!, destination === 'to' ? result?.direction ?? fakeEdge.value.toSide! : fakeEdge.value.toSide!);
}
function dragEndEdgeSide(e: MouseEvent): void
{
    if(fakeEdge.value.drag?.type === 'node')
        return;

    window.removeEventListener('mousemove', dragEdgeSide);
    window.removeEventListener('mouseup', dragEndEdgeSide);

    if(fakeEdge.value.snapped)
    {
        const edge = canvas.value.edges!.find(e => e.id === fakeEdge.value.drag?.id)!
        const old = { ... edge };

        const destination = fakeEdge.value.drag!.destination;

        edge.fromNode = destination === 'to' ? fakeEdge.value.drag!.node : fakeEdge.value.snapped.node;
        edge.fromSide = destination === 'to' ? fakeEdge.value.fromSide! : fakeEdge.value.snapped.side;

        edge.toNode = destination === 'from' ? fakeEdge.value.drag!.node : fakeEdge.value.snapped.node;
        edge.toSide = destination === 'from' ? fakeEdge.value.toSide! : fakeEdge.value.snapped.side;

        addAction('property', [{ from: old, to: edge, element: { id: edge.id, type: 'edge' } }]);
    }

    fakeEdge.value = {};
}
function dragStartEdgeSide(id: string, e: MouseEvent, direction: 'from' | 'to'): void
{
    const edge = canvas.value.edges!.find(e => e.id === id)!;
    fakeEdgeFromEdge(edge, direction);

    window.addEventListener('mousemove', dragEdgeSide, { passive: true });
    window.addEventListener('mouseup', dragEndEdgeSide, { passive: true });
}
function fakeEdgeFromEdge(edge: CanvasEdge, direction: 'from' | 'to'): void
{
    fakeEdge.value.drag = { type: 'edge', id: edge.id, destination: direction, node: direction === 'to' ? edge.fromNode : edge.toNode };

    const destinationNode = direction === 'from' ? canvas.value.nodes!.find(e => e.id === edge.fromNode)! : canvas.value.nodes!.find(e => e.id === edge.toNode)!;
    const otherNode = direction === 'from' ? canvas.value.nodes!.find(e => e.id === edge.toNode)! : canvas.value.nodes!.find(e => e.id === edge.fromNode)!;
    const destinationPos = posFromDir(getBbox(destinationNode), direction === 'from' ? edge.fromSide : edge.toSide);
    const otherPos = posFromDir(getBbox(otherNode), direction === 'from' ? edge.toSide : edge.fromSide);

    fakeEdge.value.from = direction === 'from' ? destinationPos : otherPos;
    fakeEdge.value.fromSide = edge.fromSide;

    fakeEdge.value.to = direction === 'to' ? destinationPos : otherPos;
    fakeEdge.value.toSide = edge.toSide;

    fakeEdge.value.path = bezier(destinationPos, edge.fromSide, otherPos, edge.toSide);
    fakeEdge.value.hex = edge.color?.hex;
    
    fakeEdge.value.style = edge?.color ? edge.color?.class ? 
        { fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.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` };
}
function fakeEdgeFromNode(node: CanvasNode, direction: Direction): void
{
    const pos = posFromDir(getBbox(node), direction);

    fakeEdge.value.drag = { type: 'node', id: node.id };

    fakeEdge.value.from = { ... pos };
    fakeEdge.value.fromSide = direction;

    fakeEdge.value.to = { ... pos };
    fakeEdge.value.toSide = opposite[direction];

    fakeEdge.value.path = bezier(pos, fakeEdge.value.fromSide!, pos, fakeEdge.value.toSide!);
    fakeEdge.value.hex = node.color?.hex;
    
    fakeEdge.value.style = node?.color ? node.color?.class ? 
        { fill: `fill-light-${node.color?.class} dark:fill-dark-${node.color?.class}`, stroke: `stroke-light-${node.color?.class} dark:stroke-dark-${node.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` };
}
function editNodeProperty<T extends keyof CanvasNode>(ids: string[], property: T, value: CanvasNode[T])
{
    if(ids.length === 0)
        return;
    
    const actions: HistoryAction<'property'>[] = [];

    for(const id of ids)
    {
        const copy = JSON.parse(JSON.stringify(canvas.value.nodes!.find(e => e.id === id)!)) as CanvasNode;
        canvas.value.nodes!.find(e => e.id === id)![property] = value;
        actions.push({ element: { type: 'node', id }, from: copy, to: canvas.value.nodes!.find(e => e.id === id)! });
    }

    addAction('property', actions);
}
function editEdgeProperty<T extends keyof CanvasEdge>(ids: string[], property: T, value: CanvasEdge[T])
{
    if(ids.length === 0)
        return;
    
    const actions: HistoryAction<'property'>[] = [];

    for(const id of ids)
    {
        const copy = JSON.parse(JSON.stringify(canvas.value.edges!.find(e => e.id === id)!)) as CanvasEdge;
        canvas.value.edges!.find(e => e.id === id)![property] = value;
        actions.push({ element: { type: 'edge', id }, from: copy, to: canvas.value.edges!.find(e => e.id === id)! });
    }

    addAction('property', actions);
}

const unselect = () => {
    if(focusing.value !== undefined)
    {
        focused.value?.dom?.removeEventListener('click', stopPropagation);
        focused.value?.unselect();
        updateToolbarTransform();
    }
    focusing.value = undefined;

    if(editing.value !== undefined)
    {
        edited.value?.dom?.removeEventListener('wheel', stopPropagation);
        edited.value?.dom?.removeEventListener('dblclick', stopPropagation);
        edited.value?.dom?.removeEventListener('click', stopPropagation);
        edited.value?.unselect();
    }
    editing.value = undefined;
};
const undo = () => {
    if(!historyCursor.value)
        return;

    for(const action of historyCursor.value.actions)
    {
        if(action.element.type === 'node')
        {
            switch(historyCursor.value.event)
            {
                case 'create':
                {
                    const a = action as HistoryAction<'create'>;
                    const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
                    snapFinder.remove(canvas.value.nodes!.splice(index, 1)[0]);
                    break;
                }
                case 'remove':
                {
                    const a = action as HistoryAction<'remove'>;
                    canvas.value.nodes!.push(a.from as CanvasNode);
                    snapFinder.add(a.from as CanvasNode);
                    break;
                }
                case 'property':
                {
                    const a = action as HistoryAction<'property'>;
                    const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
                    canvas.value.nodes![index] = a.from as CanvasNode;
                    snapFinder.update(a.from as CanvasNode);
                    break;
                }
            }
        }
        else if(action.element.type === 'edge')
        {
            switch(historyCursor.value.event)
            {
                case 'create':
                {
                    const a = action as HistoryAction<'create'>;
                    const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
                    canvas.value.edges!.splice(index, 1);
                    break;
                }
                case 'remove':
                {
                    const a = action as HistoryAction<'remove'>;
                    canvas.value.edges!.push(a.from!  as CanvasEdge);
                    break;
                }
                case 'property':
                {
                    const a = action as HistoryAction<'property'>;
                    const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
                    canvas.value.edges![index] = a.from as CanvasEdge;
                    break;
                }
            }
        }
    }

    historyPos.value--;
};
const redo = () => {
    if(!history.value || history.value.length - 1 <= historyPos.value)
        return;

    historyPos.value++;
    
    if(!historyCursor.value)
    {
        historyPos.value--;
        return;
    }

    for(const action of historyCursor.value.actions)
    {        
        if(action.element.type === 'node')
        {
            switch(historyCursor.value.event)
            {
                case 'create':
                {
                    const a = action as HistoryAction<'create'>;
                    canvas.value.nodes!.push(a.to as CanvasNode);
                    snapFinder.add(a.to as CanvasNode);
                    break;
                }
                case 'remove':
                {
                    const a = action as HistoryAction<'remove'>;
                    const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
                    snapFinder.remove(canvas.value.nodes!.splice(index, 1)[0]);
                    break;
                }
                case 'property':
                {
                    const a = action as HistoryAction<'property'>;
                    const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
                    canvas.value.nodes![index] = a.to as CanvasNode;
                    snapFinder.update(a.to as CanvasNode);
                    break;
                }
            }
        }
        else if(action.element.type === 'edge')
        {
            switch(historyCursor.value.event)
            {
                case 'create':
                {
                    const a = action as HistoryAction<'create'>;
                    canvas.value.edges!.push(a.to as CanvasEdge);
                    break;
                }
                case 'remove':
                {
                    const a = action as HistoryAction<'remove'>;
                    const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
                    canvas.value.edges!.splice(index, 1);
                    break;
                }
                case 'property':
                {
                    const a = action as HistoryAction<'property'>;
                    const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
                    canvas.value.edges![index] = a.to as CanvasEdge;
                    break;
                }
            }
        }
    }
};

useShortcuts({
    meta_z: undo,
    meta_y: redo,
    Delete: () => { if(focusing.value !== undefined) { remove([focusing.value]) } }
});
</script>

<template>
    <div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" @dblclick.left="createNode">
        <div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4" @click="stopPropagation" @dblclick="stopPropagation">
            <div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
                <Tooltip message="Zoom avant" side="right">
                    <div @click="zoom = clamp(zoom * 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                        <Icon icon="radix-icons:plus" />
                    </div>
                </Tooltip>
                <Tooltip message="Reset" side="right">
                    <div @click="zoom = 1; updateTransform();" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                        <Icon icon="radix-icons:reload" />
                    </div>
                </Tooltip>
                <Tooltip message="Tout contenir" side="right">
                    <div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                        <Icon icon="radix-icons:corners" />
                    </div>
                </Tooltip>
                <Tooltip message="Zoom arrière" side="right">
                    <div @click="zoom = clamp(zoom / 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                        <Icon icon="radix-icons:minus" />
                    </div>
                </Tooltip>
            </div>
            <div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
                <Tooltip message="Annuler (Ctrl+Z)" side="right">
                    <div @click="undo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === -1 }">
                        <Icon icon="ph:arrow-bend-up-left" />
                    </div>
                </Tooltip>
                <Tooltip message="Retablir (Ctrl+Y)" side="right">
                    <div @click="redo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === history.length - 1 }">
                        <Icon icon="ph:arrow-bend-up-right" />
                    </div>
                </Tooltip>
            </div>
            <div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
                <Tooltip message="Préférences" side="right">
                    <Dialog title="Préférences" iconClose>
                        <template #trigger>
                            <div class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                                <Icon icon="radix-icons:gear" />
                            </div>
                        </template>
                        <template #default>
                            <Switch v-model="canvasSettings.neighborSnap" label="S'accrocher aux voisins" @update:model-value="snapFinder.config.preferences = canvasSettings" />
                            <Switch v-model="canvasSettings.gridSnap" label="S'accrocher à la grille" @update:model-value="(v) => { canvasSettings.spacing = v ? 32 : undefined; snapFinder.config.preferences = canvasSettings }" />
                            <NumberPicker v-model="canvasSettings.spacing" label="Taille de la grille" :disabled="!canvasSettings.gridSnap" @update:model-value="(v) => { spacing = v; updateTransform(); snapFinder.config.preferences = canvasSettings}" />
                        </template>
                    </Dialog>
                </Tooltip>
                <Tooltip message="Aide" side="right">
                    <Dialog title="Aide" iconClose>
                        <template #trigger>
                            <div class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                                <Icon icon="radix-icons:question-mark-circled" />
                            </div>
                        </template>
                        <template #default>
                            <div class="flex flex-row justify-between px-4">
                                <div class="flex flex-col gap-2">
                                    <ProseH4>Ordinateur</ProseH4>
                                    <div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Selectionner</div>
                                    <div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Modifier</div>
                                    <div class="flex items-center"><Icon icon="ph:mouse-middle-click-fill" class="w-6 h-6"/>: Déplacer</div>
                                    <div class="flex items-center"><Icon icon="ph:mouse-right-click-fill" class="w-6 h-6"/>: Menu</div>
                                </div>
                                <div class="flex flex-col gap-2">
                                    <ProseH4>Mobile</ProseH4>
                                    <div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Selectionner</div>
                                    <div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Modifier</div>
                                    <div class="flex items-center"><Icon icon="mdi:gesture-pinch" class="w-6 h-6"/>: Zoomer</div>
                                    <div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/> maintenu: Menu</div>
                                </div>
                            </div>
                        </template>
                    </Dialog>
                </Tooltip>
            </div>
        </div>
        <svg class="absolute top-0 left-0 w-full h-full pointer-events-none">
            <pattern ref="patternRef" id="canvasPattern" patternUnits="userSpaceOnUse">
                <circle cx="0.75" cy="0.75" r="0.75" class="fill-light-35 dark:fill-dark-35"></circle>
            </pattern>
            <rect x="0" y="0" width="100%" height="100%" fill="url(#canvasPattern)"></rect>
        </svg>
        <div ref="transformRef" :style="{
            'transform-origin': 'center center',
        }" class="h-full">
            <div class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
                <div class="absolute z-20 destination-bottom" ref="toolbarRef">
                    <template v-if="focusing">
                        <div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row" v-if="focusing.type === 'node'">
                            <PopoverRoot>
                                <PopoverTrigger asChild>
                                    <div @click="stopPropagation">
                                        <Tooltip message="Couleur" side="top">
                                            <div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                                                <Icon icon="ph:palette" class="w-6 h-6" />
                                            </div>
                                        </Tooltip>
                                    </div>
                                </PopoverTrigger>
                                <PopoverPortal disabled>
                                    <PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
                                        <div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
                                            <div @click="editNodeProperty([focusing.id], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editNodeProperty([focusing.id], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editNodeProperty([focusing.id], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editNodeProperty([focusing.id], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editNodeProperty([focusing.id], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editNodeProperty([focusing.id], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editNodeProperty([focusing.id], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
                                            </div>
                                            <label>
                                                <div @click="stopPropagation" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                    <span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editNodeProperty([focusing!.id], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
                                                </div>
                                            </label>
                                        </div>
                                    </PopoverContent>
                                </PopoverPortal>
                            </PopoverRoot>
                            <Tooltip message="Supprimer" side="top">
                                <div @click="remove([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                                    <Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
                                </div>
                            </Tooltip>
                        </div>
                        <div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row" v-else>
                            <PopoverRoot>
                                <PopoverTrigger asChild>
                                    <div @click="stopPropagation">
                                        <Tooltip message="Couleur" side="top">
                                            <div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                                                <Icon icon="ph:palette" class="w-6 h-6" />
                                            </div>
                                        </Tooltip>
                                    </div>
                                </PopoverTrigger>
                                <PopoverPortal disabled>
                                    <PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
                                        <div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
                                            <div @click="editEdgeProperty([focusing.id], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editEdgeProperty([focusing.id], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editEdgeProperty([focusing.id], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editEdgeProperty([focusing.id], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editEdgeProperty([focusing.id], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editEdgeProperty([focusing.id], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
                                            </div>
                                            <div @click="editEdgeProperty([focusing.id], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                <span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
                                            </div>
                                            <label>
                                                <div @click="stopPropagation" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
                                                    <span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editEdgeProperty([focusing!.id], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
                                                </div>
                                            </label>
                                        </div>
                                    </PopoverContent>
                                </PopoverPortal>
                            </PopoverRoot>
                            <Tooltip message="Supprimer" side="top">
                                <div @click="remove([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
                                    <Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
                                </div>
                            </Tooltip>
                        </div>
                    </template>
                </div>
                <div>
                    <CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom"
                    @select="select" @edit="edit" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" @input="(id, text) => editNodeProperty([id], node.type === 'group' ? 'label' : 'text', text)" :snap="snapFinder.findNodeSnapPosition.bind(snapFinder)" @edge="dragStartEdgeTo" />
                </div>
                <div>
                    <CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" @input="(id, text) => editEdgeProperty([id], 'label', text)" @drag="dragStartEdgeSide" />
                    <div v-if="fakeEdge.path" class="absolute overflow-visible">
                        <svg class="absolute top-0 overflow-visible h-px w-px">
                            <g :style="{'--canvas-color': fakeEdge.hex}" class="z-0">
                                <g :style="`transform: translate(${fakeEdge.path!.to.x}px, ${fakeEdge.path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[fakeEdge.path!.side]}deg);`">
                                    <polygon :class="fakeEdge.style?.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
                                </g>
                                <path :style="`stroke-width: calc(3px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="fakeEdge.style?.stroke" class="fill-none stroke-[4px]" :d="fakeEdge.path.path"></path>
                            </g>
                        </svg>
                    </div>
                </div>
                <svg class="absolute overflow-visible top-0 h-px w-px fill-accent-purple stroke-accent-purple stroke-1 z-50">
                    <g v-for="hint of hints">
                        <circle :cx="hint.start.x" :cy="hint.start.y" r="3"  />
                        <circle v-if="hint.end" :cx="hint.end.x" :cy="hint.end.y" r="3" />
                        <line v-if="hint.end" :x1="hint.start.x" :x2="hint.end.x" :y1="hint.start.y" :y2="hint.end.y"/>
                    </g>
                </svg>
            </div>
        </div>
    </div>
</template>