<script setup lang="ts">
import("~/assets/canvas.css")

import type { Canvas, CanvasNode } from '~/types/canvas';

interface Props
{
    canvas: Canvas;
}
const props = defineProps<Props>();

let dragging = false, posX = 0, posY = 0, dispX = ref(0), dispY = ref(0), minZoom = ref(0.3), zoom = ref(1);
let centerX = ref(0), centerY = ref(0), canvas = ref<HTMLDivElement>();
let minX = ref(+Infinity), minY = ref(+Infinity), maxX = ref(-Infinity), maxY = ref(-Infinity);
let bbox = ref<DOMRect>();

let _minX = +Infinity, _minY = +Infinity, _maxX = -Infinity, _maxY = -Infinity;

onMounted(async () => {
    props.canvas.body.nodes.forEach((e) => {
        _minX = Math.min(_minX, e.x);
        _minY = Math.min(_minY, e.y);
        _maxX = Math.max(_maxX, e.x + e.width);
        _maxY = Math.max(_maxY, e.y + e.height);
    });

    await nextTick();

    window.addEventListener('resize', onResize);
    onResize();

    dispX.value = -(_maxX + _minX) / 2;
    dispY.value = -(_maxY + _minY) / 2;

    zoom.value = minZoom.value;
});

onUnmounted(() => {
    window.removeEventListener('resize', onResize);
})

const onResize = (event?: Event) => {
    minX.value = _minX = _minX - 32;
    minY.value = _minY = _minY - 32;
    maxX.value = _maxX = _maxX + 32;
    maxY.value = _maxY = _maxY + 32;

    minZoom.value = Math.min((canvas.value?.clientWidth ?? 0) / (_maxX - _minX), (canvas.value?.clientHeight ?? 0) / (_maxY - _minY)) * 0.9;
    zoom.value = clamp(zoom.value, minZoom.value, 3);

    bbox.value = canvas.value?.getBoundingClientRect();

    centerX.value = (bbox.value?.width ?? 0) / 2;
    centerY.value = (bbox.value?.height ?? 0) / 2;
}

const onPointerDown = (event: PointerEvent) => {
    if (event.isPrimary === false) return;
    dragging = true;

    posX = event.clientX;
    posY = event.clientY;

    document.addEventListener('pointermove', onPointerMove);
    document.addEventListener('pointerup', onPointerUp);
}

const onPointerMove = (event: PointerEvent) => {
    if (event.isPrimary === false) return;
    dispX.value -= (posX - event.clientX) / zoom.value;
    dispY.value -= (posY - event.clientY) / zoom.value;

    posX = event.clientX;
    posY = event.clientY;
}

const onPointerUp = (event: PointerEvent) => {
    if (event.isPrimary === false) return;
    dragging = false;
    document.removeEventListener('pointermove', onPointerMove);
    document.removeEventListener('pointerup', onPointerUp);
    document.removeEventListener('pointercancel', onPointerUp);
}

const onWheel = (event: WheelEvent) => {
    zoom.value  = clamp(zoom.value * 1 + (event.deltaY * -0.001), minZoom.value, 3);
}

const reset = (_: MouseEvent) => {
    zoom.value = minZoom.value; 
    dispX.value = -(maxX.value + minX.value) / 2; 
    dispY.value = -(maxY.value + minY.value) / 2;
}
function clamp(x: number, min: number, max: number): number {
    if (x > max)
        return max;
    if (x < min)
        return min;
    return x;
}
function edgePos(side: 'bottom' | 'top' | 'left' | 'right', pos: { x: number, y: number }, n: number): { x: number, y: number } {
    switch (side) {
        case "left":
            return {
                x: pos.x - n,
                y: pos.y
            };
        case "right":
            return {
                x: pos.x + n,
                y: pos.y
            };
        case "top":
            return {
                x: pos.x,
                y: pos.y - n
            };
        case "bottom":
            return {
                x: pos.x,
                y: pos.y + n
            }
    }
}
function getNode(id: string): CanvasNode | undefined
{
    return props.canvas.body.nodes.find(e => e.id === id);
}
function mK(e: { minX: number, minY: number, maxX: number, maxY: number }, t: 'bottom' | 'top' | 'left' | 'right'): { x: number, y: number } {
    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 };
    }
}
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 };
}
function path(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
    if(from === undefined || to === undefined)
    {
        return {
            path: '',
            from: {},
            to: {},
            toSide: '',
        }
    }
    const a = mK(getBbox(from), fromSide), 
    l = mK(getBbox(to), toSide);
    return bezier(a, fromSide, l, toSide);
}
function bezier(from: { x: number, y: number }, fromSide: 'bottom' | 'top' | 'left' | 'right', to: { x: number, y: number }, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
    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,
    };
}
function labelCenter(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): string {
    const a = mK(getBbox(from), fromSide), l = mK(getBbox(to), toSide);
    const r = Math.hypot(a.x - l.x, a.y - l.y), o = clamp(r / 2, 70, 150), b = edgePos(fromSide, a, o), s = edgePos(toSide, l, o);
    const center = getCenter(a, l, b, s, 0.5);
    return `translate(${center.x}px, ${center.y}px)`;
}
function getCenter(n: { x: number, y: number }, i: { x: number, y: number }, r: { x: number, y: number }, o: { x: number, y: number }, e: number): { x: number, y: number } {
    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
    };
}
</script>

<template>
    <div ref="canvas" @pointerdown="onPointerDown" @wheel.passive="onWheel" @touchstart.prevent="" @dragstart.prevent=""
        class="canvas-wrapper node-insert-event mod-zoomed-out"
        :style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
        <div class="canvas-controls" style="z-index: 999;">
            <div class="canvas-control-group">
                <div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="canvas-control-item" aria-label="Zoom in"
                    data-tooltip-position="left">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
                        stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
                        class="svg-icon lucide-plus">
                        <path d="M5 12h14"></path>
                        <path d="M12 5v14"></path>
                    </svg>
                </div>
                <div @click="zoom = 1" class="canvas-control-item" aria-label="Reset zoom" data-tooltip-position="left">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
                        stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
                        class="svg-icon lucide-rotate-cw">
                        <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
                        <path d="M21 3v5h-5"></path>
                    </svg>
                </div>
                <div @click="reset" class="canvas-control-item" aria-label="Zoom to fit" data-tooltip-position="left">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
                        stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
                        class="svg-icon lucide-maximize">
                        <path d="M8 3H5a2 2 0 0 0-2 2v3"></path>
                        <path d="M21 8V5a2 2 0 0 0-2-2h-3"></path>
                        <path d="M3 16v3a2 2 0 0 0 2 2h3"></path>
                        <path d="M16 21h3a2 2 0 0 0 2-2v-3"></path>
                    </svg>
                </div>
                <div @click="zoom  = clamp(zoom * 0.9, minZoom, 3)" class="canvas-control-item" aria-label="Zoom out"
                    data-tooltip-position="left">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
                        stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
                        class="svg-icon lucide-minus">
                        <path d="M5 12h14"></path>
                    </svg>
                </div>
            </div>
        </div>
        <div class="canvas"
            :style="{transform: `translate(${centerX}px, ${centerY}px) scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
            <svg class="canvas-edges">
                <CanvasEdge v-for="edge of props.canvas.body.edges" :key="edge.id"
                    :path="path(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide)"
                    :color="edge.color" :label="edge.label" />
            </svg>
            <CanvasNode v-for="node of props.canvas.body.nodes" :key="node.id" :node="node" :zoom="zoom" />
            <template v-for="edge of props.canvas.body.edges">
                <div :key="edge.id" v-if="edge.label" class="canvas-path-label-wrapper"
                    :style="{ transform: labelCenter(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide) }">
                    <div class="canvas-path-label">{{ edge.label }}</div>
                </div>
            </template>
        </div>
    </div>
</template>