<template>
    <panZoom
        v-if="isVisible(visibility)"
        :style="[
            {
                width,
                height
            },
            style
        ]"
        :class="classes"
        selector=".g1"
        :options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10, zoomDoubleClickSpeed: 1 }"
        ref="stage"
        @init="onInit"
        @mousemove="drag"
        @touchmove="drag"
        @mousedown="(e: MouseEvent) => mouseDown(e)"
        @touchstart="(e: TouchEvent) => mouseDown(e)"
        @mouseup="() => endDragging(unref(draggingNode))"
        @touchend.passive="() => endDragging(unref(draggingNode))"
        @mouseleave="() => endDragging(unref(draggingNode), true)"
    >
        <svg class="stage" width="100%" height="100%">
            <g class="g1">
                <transition-group name="link" appear>
                    <g
                        v-for="link in unref(links) || []"
                        :key="`${link.startNode.id}-${link.endNode.id}`"
                    >
                        <BoardLinkVue :link="link" />
                    </g>
                </transition-group>
                <transition-group name="grow" :duration="500" appear>
                    <g v-for="node in sortedNodes" :key="node.id" style="transition-duration: 0s">
                        <BoardNodeVue
                            :node="node"
                            :nodeType="types[node.type]"
                            :dragging="unref(draggingNode)"
                            :dragged="unref(draggingNode) === node ? dragged : undefined"
                            :hasDragged="hasDragged"
                            :receivingNode="unref(receivingNode)?.id === node.id"
                            :selectedNode="unref(selectedNode)"
                            :selectedAction="unref(selectedAction)"
                            @mouseDown="mouseDown"
                            @endDragging="endDragging"
                            @clickAction="(actionId: string) => clickAction(node, actionId)"
                        />
                    </g>
                </transition-group>
            </g>
        </svg>
    </panZoom>
</template>

<script setup lang="ts">
import type {
    BoardData,
    BoardNode,
    BoardNodeLink,
    GenericBoardNodeAction,
    GenericNodeType
} from "features/boards/board";
import { getNodeProperty } from "features/boards/board";
import type { StyleValue } from "features/feature";
import { Visibility, isVisible } from "features/feature";
import type { ProcessedComputable } from "util/computed";
import { Ref, computed, ref, toRefs, unref, watchEffect } from "vue";
import BoardLinkVue from "./BoardLink.vue";
import BoardNodeVue from "./BoardNode.vue";

const _props = defineProps<{
    nodes: Ref<BoardNode[]>;
    types: Record<string, GenericNodeType>;
    state: Ref<BoardData>;
    visibility: ProcessedComputable<Visibility | boolean>;
    width?: ProcessedComputable<string>;
    height?: ProcessedComputable<string>;
    style?: ProcessedComputable<StyleValue>;
    classes?: ProcessedComputable<Record<string, boolean>>;
    links: Ref<BoardNodeLink[] | null>;
    selectedAction: Ref<GenericBoardNodeAction | null>;
    selectedNode: Ref<BoardNode | null>;
    draggingNode: Ref<BoardNode | null>;
    receivingNode: Ref<BoardNode | null>;
    mousePosition: Ref<{ x: number; y: number } | null>;
    setReceivingNode: (node: BoardNode | null) => void;
    setDraggingNode: (node: BoardNode | null) => void;
}>();
const props = toRefs(_props);

const lastMousePosition = ref({ x: 0, y: 0 });
const dragged = ref({ x: 0, y: 0 });
const hasDragged = ref(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stage = ref<any>(null);

const sortedNodes = computed(() => {
    const nodes = props.nodes.value.slice();
    if (props.draggingNode.value) {
        const node = nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0];
        nodes.push(node);
    }
    return nodes;
});

watchEffect(() => {
    const node = props.draggingNode.value;
    if (node == null) {
        return null;
    }

    const position = {
        x: node.position.x + dragged.value.x,
        y: node.position.y + dragged.value.y
    };
    let smallestDistance = Number.MAX_VALUE;

    props.setReceivingNode.value(
        props.nodes.value.reduce((smallest: BoardNode | null, curr: BoardNode) => {
            if (curr.id === node.id) {
                return smallest;
            }
            const nodeType = props.types.value[curr.type];
            const canAccept = getNodeProperty(nodeType.canAccept, curr, node);
            if (!canAccept) {
                return smallest;
            }

            const distanceSquared =
                Math.pow(position.x - curr.position.x, 2) +
                Math.pow(position.y - curr.position.y, 2);
            let size = getNodeProperty(nodeType.size, curr);
            if (distanceSquared > smallestDistance || distanceSquared > size * size) {
                return smallest;
            }

            smallestDistance = distanceSquared;
            return curr;
        }, null)
    );
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function onInit(panzoomInstance: any) {
    panzoomInstance.setTransformOrigin(null);
    panzoomInstance.moveTo(stage.value.$el.clientWidth / 2, stage.value.$el.clientHeight / 2);
}

function mouseDown(e: MouseEvent | TouchEvent, node: BoardNode | null = null, draggable = false) {
    if (props.draggingNode.value == null) {
        e.preventDefault();
        e.stopPropagation();

        let clientX, clientY;
        if ("touches" in e) {
            if (e.touches.length === 1) {
                clientX = e.touches[0].clientX;
                clientY = e.touches[0].clientY;
            } else {
                return;
            }
        } else {
            clientX = e.clientX;
            clientY = e.clientY;
        }
        lastMousePosition.value = {
            x: clientX,
            y: clientY
        };
        dragged.value = { x: 0, y: 0 };
        hasDragged.value = false;

        if (draggable) {
            props.setDraggingNode.value(node);
        }
    }
    if (node != null) {
        props.state.value.selectedNode = null;
        props.state.value.selectedAction = null;
    }
}

function drag(e: MouseEvent | TouchEvent) {
    const { x, y, scale } = stage.value.panZoomInstance.getTransform();

    let clientX, clientY;
    if ("touches" in e) {
        if (e.touches.length === 1) {
            clientX = e.touches[0].clientX;
            clientY = e.touches[0].clientY;
        } else {
            endDragging(props.draggingNode.value);
            props.mousePosition.value = null;
            return;
        }
    } else {
        clientX = e.clientX;
        clientY = e.clientY;
    }

    props.mousePosition.value = {
        x: (clientX - x) / scale,
        y: (clientY - y) / scale
    };

    dragged.value = {
        x: dragged.value.x + (clientX - lastMousePosition.value.x) / scale,
        y: dragged.value.y + (clientY - lastMousePosition.value.y) / scale
    };
    lastMousePosition.value = {
        x: clientX,
        y: clientY
    };

    if (Math.abs(dragged.value.x) > 10 || Math.abs(dragged.value.y) > 10) {
        hasDragged.value = true;
    }

    if (props.draggingNode.value != null) {
        e.preventDefault();
        e.stopPropagation();
    }
}

function endDragging(node: BoardNode | null, mouseLeave = false) {
    if (props.draggingNode.value != null && props.draggingNode.value === node) {
        if (props.receivingNode.value == null) {
            props.draggingNode.value.position.x += Math.round(dragged.value.x / 25) * 25;
            props.draggingNode.value.position.y += Math.round(dragged.value.y / 25) * 25;
        }

        const nodes = props.nodes.value;
        nodes.push(nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0]);

        if (props.receivingNode.value) {
            props.types.value[props.receivingNode.value.type].onDrop?.(
                props.receivingNode.value,
                props.draggingNode.value
            );
        }

        props.setDraggingNode.value(null);
    } else if (!hasDragged.value && !mouseLeave) {
        props.state.value.selectedNode = null;
        props.state.value.selectedAction = null;
    }
}

function clickAction(node: BoardNode, actionId: string) {
    if (props.state.value.selectedAction === actionId) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        unref(props.selectedAction)!.onClick(unref(props.selectedNode)!);
    } else {
        props.state.value = { ...props.state.value, selectedAction: actionId };
    }
}
</script>

<style>
.vue-pan-zoom-scene {
    width: 100%;
    height: 100%;
    cursor: grab;
}

.vue-pan-zoom-scene:active {
    cursor: grabbing;
}

.g1 {
    transition-duration: 0s;
}

.link-enter-from,
.link-leave-to {
    opacity: 0;
}
</style>