<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from "vue"
import { useAppStore } from "@/stores/app"
import { getDatumColor } from "@/lib/datums"
import type { Datum, Point } from "@/types"

const store = useAppStore()

const containerRef = ref<HTMLDivElement | null>(null)
const stageWidth = ref(800)
const stageHeight = ref(600)

const scale = ref(1)
const offsetX = ref(0)
const offsetY = ref(0)

// Touch state for pinch-to-zoom and pan
let lastPinchDist = 0
let isPanning = false
let isPinching = false
let panStart = { x: 0, y: 0 }
// Track whether a Konva shape is being dragged (touch)
let isDraggingShape = false

const imageConfig = computed(() => {
    const img = store.loadedImage
    if (!img) return null
    return {
        image: img,
        x: 0,
        y: 0,
        width: img.naturalWidth,
        height: img.naturalHeight,
    }
})

const stageDraggable = ref(true)

const stageConfig = computed(() => ({
    width: stageWidth.value,
    height: stageHeight.value,
    scaleX: scale.value,
    scaleY: scale.value,
    x: offsetX.value,
    y: offsetY.value,
    draggable: stageDraggable.value,
}))

function datumIndex(datum: Datum): number {
    return store.datums.findIndex((d) => d.id === datum.id)
}

function datumPoints(datum: Datum): Point[] {
    if (datum.type === "rectangle") return datum.corners as unknown as Point[]
    if (datum.type === "line") return datum.endpoints as unknown as Point[]
    // Index 0 is the center (translate all); 1..N are the on-curve points
    // the user drags to reshape the fitted ellipse.
    return [datum.center, ...datum.points]
}

function getPointConfigs(datum: Datum, dIdx: number) {
    const color = getDatumColor(dIdx)
    const isSelected = store.selectedDatumId === datum.id
    const points = datumPoints(datum)
    const baseRadius = isSelected ? 6 : 4
    const visualRadius = Math.max(
        baseRadius / scale.value,
        baseRadius * 0.5,
    )

    return points.map((pt, pIdx) => ({
        // Ellipse center (index 0) is visually bigger + hollow to distinguish it
        x: pt.x,
        y: pt.y,
        radius:
            datum.type === "ellipse" && pIdx === 0
                ? visualRadius * 1.4
                : visualRadius,
        fill: datum.type === "ellipse" && pIdx === 0 ? "transparent" : color,
        stroke: isSelected ? "#fff" : color,
        strokeWidth: (datum.type === "ellipse" && pIdx === 0 ? 2.5 : 1.5) /
            scale.value,
        draggable: true,
        _datumId: datum.id,
        _pointIndex: pIdx,
        hitStrokeWidth: 12 / scale.value,
    }))
}

function ellipseCurvePoints(datum: Datum & { type: "ellipse" }): number[] {
    const vAx = datum.axisEndA.x - datum.center.x
    const vAy = datum.axisEndA.y - datum.center.y
    const vBx = datum.axisEndB.x - datum.center.x
    const vBy = datum.axisEndB.y - datum.center.y
    const N = 72
    const pts: number[] = []
    for (let i = 0; i <= N; i++) {
        const t = (2 * Math.PI * i) / N
        const cs = Math.cos(t)
        const sn = Math.sin(t)
        pts.push(
            datum.center.x + vAx * cs + vBx * sn,
            datum.center.y + vAy * cs + vBy * sn,
        )
    }
    return pts
}

function getLineConfigs(datum: Datum, dIdx: number) {
    const color = getDatumColor(dIdx)
    const isSelected = store.selectedDatumId === datum.id
    const dash = isSelected ? [] : [8 / scale.value, 4 / scale.value]
    const strokeWidth = (isSelected ? 3 : 2) / scale.value

    if (datum.type === "line") {
        return [
            {
                points: [
                    datum.endpoints[0].x,
                    datum.endpoints[0].y,
                    datum.endpoints[1].x,
                    datum.endpoints[1].y,
                ],
                stroke: color,
                strokeWidth,
                dash,
            },
        ]
    }

    if (datum.type === "rectangle") {
        const c = datum.corners
        const pts = [c[0], c[1], c[2], c[3], c[0]].flatMap((p) => [p.x, p.y])
        return [
            {
                points: pts,
                stroke: color,
                strokeWidth,
                closed: true,
                dash,
            },
        ]
    }

    // Ellipse: sampled curve + two thin axis lines for visual reference
    return [
        {
            points: ellipseCurvePoints(datum),
            stroke: color,
            strokeWidth,
            dash,
        },
        {
            points: [
                datum.center.x,
                datum.center.y,
                datum.axisEndA.x,
                datum.axisEndA.y,
            ],
            stroke: color,
            strokeWidth: 1 / scale.value,
            opacity: 0.5,
        },
        {
            points: [
                datum.center.x,
                datum.center.y,
                datum.axisEndB.x,
                datum.axisEndB.y,
            ],
            stroke: color,
            strokeWidth: 1 / scale.value,
            opacity: 0.5,
        },
    ]
}

function getLabelConfig(datum: Datum, dIdx: number) {
    const color = getDatumColor(dIdx)
    let pos: Point

    if (datum.type === "rectangle") {
        pos = {
            x: (datum.corners[0].x + datum.corners[2].x) / 2,
            y: (datum.corners[0].y + datum.corners[2].y) / 2,
        }
    } else if (datum.type === "line") {
        pos = {
            x: (datum.endpoints[0].x + datum.endpoints[1].x) / 2,
            y:
                (datum.endpoints[0].y + datum.endpoints[1].y) / 2 -
                20 / scale.value,
        }
    } else {
        pos = {
            x: datum.center.x,
            y: datum.center.y - 20 / scale.value,
        }
    }

    return {
        x: pos.x,
        y: pos.y,
        text: datum.label,
        fontSize: 14 / scale.value,
        fill: color,
        fontStyle: "bold",
        align: "center" as const,
        offsetX: (datum.label.length * 7) / 2 / scale.value,
    }
}

function onPointDragMove(e: {
    target: {
        x: () => number
        y: () => number
        attrs: { _datumId: string; _pointIndex: number }
    }
}) {
    const { _datumId, _pointIndex } = e.target.attrs
    const datum = store.datums.find((d) => d.id === _datumId)
    if (!datum) return

    const newPos: Point = { x: e.target.x(), y: e.target.y() }

    if (datum.type === "rectangle") {
        const newCorners = [...datum.corners] as [Point, Point, Point, Point]
        newCorners[_pointIndex] = newPos
        store.updateDatum(_datumId, { corners: newCorners })
    } else if (datum.type === "line") {
        const newEndpoints = [...datum.endpoints] as [Point, Point]
        newEndpoints[_pointIndex] = newPos
        store.updateDatum(_datumId, { endpoints: newEndpoints })
    } else if (_pointIndex === 0) {
        // Ellipse center — translate all on-curve points by the same delta
        // and let the store refit.
        const dx = newPos.x - datum.center.x
        const dy = newPos.y - datum.center.y
        const translated = datum.points.map((p) => ({
            x: p.x + dx,
            y: p.y + dy,
        }))
        store.updateEllipsePoints(_datumId, translated)
    } else {
        const newPoints = datum.points.map((p, i) =>
            i === _pointIndex - 1 ? newPos : p,
        )
        store.updateEllipsePoints(_datumId, newPoints)
    }
}

function onPointClick(datumId: string) {
    store.selectedDatumId = datumId
}

// Zoom with mouse wheel
function onWheel(e: WheelEvent) {
    e.preventDefault()
    const scaleBy = 1.08
    const oldScale = scale.value
    const newScale = e.deltaY < 0 ? oldScale * scaleBy : oldScale / scaleBy

    const clampedScale = Math.max(0.05, Math.min(10, newScale))

    const rect = containerRef.value?.getBoundingClientRect()
    if (!rect) return

    const pointerX = e.clientX - rect.left
    const pointerY = e.clientY - rect.top

    const mousePointTo = {
        x: (pointerX - offsetX.value) / oldScale,
        y: (pointerY - offsetY.value) / oldScale,
    }

    scale.value = clampedScale
    offsetX.value = pointerX - mousePointTo.x * clampedScale
    offsetY.value = pointerY - mousePointTo.y * clampedScale
}

// Touch handlers for pinch-to-zoom and pan
function getTouchDistance(t1: Touch, t2: Touch): number {
    return Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
}

function getTouchCenter(t1: Touch, t2: Touch): { x: number; y: number } {
    return {
        x: (t1.clientX + t2.clientX) / 2,
        y: (t1.clientY + t2.clientY) / 2,
    }
}

let pendingPanTouch: { x: number; y: number } | null = null

function onTouchStart(e: TouchEvent) {
    if (e.touches.length === 2) {
        e.preventDefault()
        isPanning = false
        isDraggingShape = false
        isPinching = true
        pendingPanTouch = null
        // Disable stage drag so Konva doesn't fight with pinch-zoom
        stageDraggable.value = false
        const t0 = e.touches[0]
        const t1 = e.touches[1]
        if (t0 && t1) {
            lastPinchDist = getTouchDistance(t0, t1)
        }
    } else if (e.touches.length === 1) {
        // Record the touch but don't start panning yet —
        // give Konva a chance to claim it as a shape drag.
        const t0 = e.touches[0]
        if (!t0) return
        pendingPanTouch = { x: t0.clientX, y: t0.clientY }
        isPanning = false
    }
}

function onTouchMove(e: TouchEvent) {
    if (e.touches.length === 2) {
        e.preventDefault()
        const t0 = e.touches[0]
        const t1 = e.touches[1]
        if (!t0 || !t1) return
        const dist = getTouchDistance(t0, t1)
        const center = getTouchCenter(t0, t1)

        const rect = containerRef.value?.getBoundingClientRect()
        if (!rect) return

        const scaleFactor = dist / lastPinchDist
        const oldScale = scale.value
        const newScale = Math.max(0.05, Math.min(10, oldScale * scaleFactor))

        const cx = center.x - rect.left
        const cy = center.y - rect.top

        const mousePointTo = {
            x: (cx - offsetX.value) / oldScale,
            y: (cy - offsetY.value) / oldScale,
        }

        scale.value = newScale
        offsetX.value = cx - mousePointTo.x * newScale
        offsetY.value = cy - mousePointTo.y * newScale

        lastPinchDist = dist
    } else if (e.touches.length === 1 && !isDraggingShape) {
        const t0 = e.touches[0]
        if (!t0) return

        // If we haven't started panning yet, promote the pending touch
        if (!isPanning && pendingPanTouch) {
            isPanning = true
            panStart = {
                x: pendingPanTouch.x - offsetX.value,
                y: pendingPanTouch.y - offsetY.value,
            }
            pendingPanTouch = null
        }

        if (isPanning) {
            offsetX.value = t0.clientX - panStart.x
            offsetY.value = t0.clientY - panStart.y
        }
    }
}

function onTouchEnd() {
    lastPinchDist = 0
    isPanning = false
    isDraggingShape = false
    pendingPanTouch = null
    if (isPinching) {
        isPinching = false
        // Re-enable stage drag after pinch ends
        stageDraggable.value = true
    }
}

function onPointDragStart() {
    isDraggingShape = true
    isPanning = false
    pendingPanTouch = null
    stageDraggable.value = false
}

function onPointDragEnd() {
    isDraggingShape = false
    stageDraggable.value = true
}

function onStageDragEnd(e: { target: { x: () => number; y: () => number; nodeType: string } }) {
    // Only sync offset when the stage itself was dragged, not a child shape
    if (e.target.nodeType !== "Stage") return
    offsetX.value = e.target.x()
    offsetY.value = e.target.y()
}

// Fit image to canvas on mount
function fitToCanvas() {
    const img = store.loadedImage
    const container = containerRef.value
    if (!img || !container) return

    const cw = container.clientWidth
    const ch = container.clientHeight
    stageWidth.value = cw
    stageHeight.value = ch

    const fitScale =
        Math.min(cw / img.naturalWidth, ch / img.naturalHeight) * 0.9
    scale.value = fitScale
    offsetX.value = (cw - img.naturalWidth * fitScale) / 2
    offsetY.value = (ch - img.naturalHeight * fitScale) / 2
}

let resizeObserver: ResizeObserver | null = null

onMounted(() => {
    fitToCanvas()
    if (containerRef.value) {
        resizeObserver = new ResizeObserver(() => {
            if (!containerRef.value) return
            const cw = containerRef.value.clientWidth
            const ch = containerRef.value.clientHeight
            // Skip if the container is hidden (0-sized)
            if (cw === 0 || ch === 0) return
            // Re-fit whenever the container size actually changes
            fitToCanvas()
        })
        resizeObserver.observe(containerRef.value)
    }
})

onUnmounted(() => {
    resizeObserver?.disconnect()
})

watch(() => store.loadedImage, fitToCanvas)
</script>

<template>
    <div
        ref="containerRef"
        class="h-full w-full cursor-grab overflow-hidden rounded-lg border border-border bg-muted active:cursor-grabbing"
        style="touch-action: none"
        @wheel.prevent="onWheel"
        @touchstart="onTouchStart"
        @touchmove="onTouchMove"
        @touchend="onTouchEnd"
    >
        <v-stage :config="stageConfig" @dragend="onStageDragEnd">
            <v-layer>
                <!-- Background image -->
                <v-image v-if="imageConfig" :config="imageConfig" />

                <!-- Datum shapes -->
                <template v-for="datum in store.datums" :key="datum.id">
                    <!-- Lines/edges -->
                    <v-line
                        v-for="(lineCfg, li) in getLineConfigs(
                            datum,
                            datumIndex(datum),
                        )"
                        :key="`${datum.id}-line-${li}`"
                        :config="lineCfg"
                    />

                    <!-- Center label -->
                    <v-text
                        :config="getLabelConfig(datum, datumIndex(datum))"
                    />

                    <!-- Draggable points -->
                    <v-circle
                        v-for="ptCfg in getPointConfigs(
                            datum,
                            datumIndex(datum),
                        )"
                        :key="`${datum.id}-pt-${ptCfg._pointIndex}`"
                        :config="ptCfg"
                        @dragstart="onPointDragStart"
                        @dragmove="onPointDragMove"
                        @dragend="onPointDragEnd"
                        @click="onPointClick(datum.id)"
                        @tap="onPointClick(datum.id)"
                    />
                </template>
            </v-layer>
        </v-stage>
    </div>
</template>
