<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from "vue"
import { useAppStore } from "@/stores/app"
import { Button } from "@/components/ui/button"
import {
    Card,
    CardContent,
    CardHeader,
    CardTitle,
} from "@/components/ui/card"
import { saveCropRotate, loadCropRotate } from "@/lib/crop-cache"
import { rotatedBboxSize } from "@/lib/crop-transform"
import type { Point } from "@/types"

// Crop & Rotate step.
//   * The user picks an arbitrary rotation in degrees (-180..180).
//   * Rotation is applied first, around the deskewed image's centre.
//     The rotated bitmap's axis-aligned bounding box is the canvas the
//     crop rectangle lives in.
//   * The crop rectangle is then dragged via 8 handles (4 corners + 4
//     edge midpoints). It's stored as fractions of the rotated bbox so
//     the same crop survives a re-deskew at a different output px/mm.
//
// Persistence: identical pattern to measurement-cache.ts, keyed by
// `store.fileHash`. We write to localStorage on every change.

const store = useAppStore()

const containerRef = ref<HTMLDivElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)
const overlayRef = ref<HTMLCanvasElement | null>(null)

const img = ref<HTMLImageElement | null>(null)
const imgUrl = ref<string | null>(null)

// Rotation in degrees. Crop fractions of the rotated bbox.
const rotationDeg = ref(0)
const cropLeft = ref(0)
const cropTop = ref(0)
const cropRight = ref(1)
const cropBottom = ref(1)

// Live-fit transform from rotated-bbox space → screen canvas pixels.
// Recomputed on resize / rotation change so the user always sees the
// whole bbox + a bit of breathing room.
const fitScale = ref(1)
const fitOffsetX = ref(0)
const fitOffsetY = ref(0)

const HANDLE_HIT_PX = 14

const rotBbox = computed(() => {
    const i = img.value
    if (!i) return { rotW: 1, rotH: 1 }
    return rotatedBboxSize(i.naturalWidth, i.naturalHeight, rotationDeg.value)
})

function persist() {
    if (!store.fileHash) return
    const state = {
        rotationDeg: rotationDeg.value,
        crop: {
            left: cropLeft.value,
            top: cropTop.value,
            right: cropRight.value,
            bottom: cropBottom.value,
        },
    }
    store.setCropRotate(state)
    saveCropRotate(store.fileHash, state)
}

function loadImage(url: string) {
    const el = new Image()
    el.onload = () => {
        img.value = el
        fitToContainer()
        redraw()
    }
    el.src = url
}

function fitToContainer() {
    const c = containerRef.value
    const i = img.value
    if (!c || !i) return
    const cw = c.clientWidth
    const ch = c.clientHeight
    if (canvasRef.value) {
        canvasRef.value.width = cw
        canvasRef.value.height = ch
    }
    if (overlayRef.value) {
        overlayRef.value.width = cw
        overlayRef.value.height = ch
    }
    const { rotW, rotH } = rotBbox.value
    const fit = Math.min(cw / rotW, ch / rotH) * 0.9
    fitScale.value = fit
    fitOffsetX.value = (cw - rotW * fit) / 2
    fitOffsetY.value = (ch - rotH * fit) / 2
}

function rotatedToScreen(p: Point): Point {
    return {
        x: p.x * fitScale.value + fitOffsetX.value,
        y: p.y * fitScale.value + fitOffsetY.value,
    }
}

function screenToRotated(sx: number, sy: number): Point {
    return {
        x: (sx - fitOffsetX.value) / fitScale.value,
        y: (sy - fitOffsetY.value) / fitScale.value,
    }
}

function redraw() {
    drawImage()
    drawOverlay()
}

function drawImage() {
    const canvas = canvasRef.value
    const i = img.value
    if (!canvas || !i) return
    const ctx = canvas.getContext("2d")
    if (!ctx) return
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    const { rotW, rotH } = rotBbox.value

    ctx.save()
    // Place rotated-bbox origin at fit offset, then scale.
    ctx.translate(fitOffsetX.value, fitOffsetY.value)
    ctx.scale(fitScale.value, fitScale.value)
    // Rotation around the bbox centre — cancels back to deskewed-image
    // axis-aligned drawing offset by (rotW-srcW)/2 etc.
    ctx.translate(rotW / 2, rotH / 2)
    ctx.rotate((rotationDeg.value * Math.PI) / 180)
    ctx.drawImage(i, -i.naturalWidth / 2, -i.naturalHeight / 2)
    ctx.restore()
}

function cropRectScreen(): { x: number; y: number; w: number; h: number } {
    const { rotW, rotH } = rotBbox.value
    const tl = rotatedToScreen({ x: cropLeft.value * rotW, y: cropTop.value * rotH })
    const br = rotatedToScreen({ x: cropRight.value * rotW, y: cropBottom.value * rotH })
    return { x: tl.x, y: tl.y, w: br.x - tl.x, h: br.y - tl.y }
}

function drawOverlay() {
    const canvas = overlayRef.value
    const i = img.value
    if (!canvas || !i) return
    const ctx = canvas.getContext("2d")
    if (!ctx) return
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    const r = cropRectScreen()

    // Dim everything outside the crop rect with a 50% black overlay.
    // Draw a full-canvas dark fill with a punched-out hole — fillRect +
    // even-odd fill is overkill; clipping with two rects is simpler.
    ctx.save()
    ctx.fillStyle = "rgba(0,0,0,0.55)"
    ctx.fillRect(0, 0, canvas.width, r.y)
    ctx.fillRect(0, r.y + r.h, canvas.width, canvas.height - (r.y + r.h))
    ctx.fillRect(0, r.y, r.x, r.h)
    ctx.fillRect(r.x + r.w, r.y, canvas.width - (r.x + r.w), r.h)
    ctx.restore()

    // Crop rectangle outline.
    ctx.save()
    ctx.strokeStyle = "#fff"
    ctx.lineWidth = 1.5
    ctx.setLineDash([6, 4])
    ctx.strokeRect(r.x, r.y, r.w, r.h)
    ctx.restore()

    // Rule-of-thirds guides — light grey, no dash.
    ctx.save()
    ctx.strokeStyle = "rgba(255,255,255,0.25)"
    ctx.lineWidth = 1
    for (let k = 1; k < 3; k++) {
        const x = r.x + (r.w * k) / 3
        ctx.beginPath()
        ctx.moveTo(x, r.y)
        ctx.lineTo(x, r.y + r.h)
        ctx.stroke()
        const y = r.y + (r.h * k) / 3
        ctx.beginPath()
        ctx.moveTo(r.x, y)
        ctx.lineTo(r.x + r.w, y)
        ctx.stroke()
    }
    ctx.restore()

    // Handle dots: 4 corners + 4 edge midpoints.
    const handles = handlePositionsScreen()
    ctx.save()
    ctx.fillStyle = "#fff"
    ctx.strokeStyle = "rgba(0,0,0,0.5)"
    ctx.lineWidth = 1
    for (const h of handles) {
        ctx.beginPath()
        ctx.arc(h.pt.x, h.pt.y, 5, 0, Math.PI * 2)
        ctx.fill()
        ctx.stroke()
    }
    ctx.restore()
}

type HandleKey =
    | "tl" | "tr" | "br" | "bl"
    | "t" | "r" | "b" | "l"

function handlePositionsScreen(): { key: HandleKey; pt: Point }[] {
    const r = cropRectScreen()
    const cx = r.x + r.w / 2
    const cy = r.y + r.h / 2
    return [
        { key: "tl", pt: { x: r.x, y: r.y } },
        { key: "tr", pt: { x: r.x + r.w, y: r.y } },
        { key: "br", pt: { x: r.x + r.w, y: r.y + r.h } },
        { key: "bl", pt: { x: r.x, y: r.y + r.h } },
        { key: "t", pt: { x: cx, y: r.y } },
        { key: "r", pt: { x: r.x + r.w, y: cy } },
        { key: "b", pt: { x: cx, y: r.y + r.h } },
        { key: "l", pt: { x: r.x, y: cy } },
    ]
}

interface DragState {
    handle: HandleKey | "body"
    /** Crop in fractions at drag start, used as the base for delta updates. */
    startLeft: number
    startTop: number
    startRight: number
    startBottom: number
    /** Pointer position in rotated-bbox space at drag start. */
    startRot: Point
}
let drag: DragState | null = null

function pickHandle(sx: number, sy: number): HandleKey | null {
    for (const h of handlePositionsScreen()) {
        if (Math.hypot(sx - h.pt.x, sy - h.pt.y) <= HANDLE_HIT_PX) return h.key
    }
    return null
}

function onPointerDown(ev: PointerEvent) {
    const canvas = overlayRef.value
    if (!canvas) return
    const rect = canvas.getBoundingClientRect()
    const sx = ev.clientX - rect.left
    const sy = ev.clientY - rect.top
    const handle = pickHandle(sx, sy)
    const r = cropRectScreen()
    const insideBody =
        handle === null &&
        sx >= r.x && sx <= r.x + r.w &&
        sy >= r.y && sy <= r.y + r.h
    if (!handle && !insideBody) return
    canvas.setPointerCapture(ev.pointerId)
    drag = {
        handle: handle ?? "body",
        startLeft: cropLeft.value,
        startTop: cropTop.value,
        startRight: cropRight.value,
        startBottom: cropBottom.value,
        startRot: screenToRotated(sx, sy),
    }
}

function onPointerMove(ev: PointerEvent) {
    if (!drag) return
    const canvas = overlayRef.value
    if (!canvas) return
    const rect = canvas.getBoundingClientRect()
    const sx = ev.clientX - rect.left
    const sy = ev.clientY - rect.top
    const cur = screenToRotated(sx, sy)
    const { rotW, rotH } = rotBbox.value
    const dx = (cur.x - drag.startRot.x) / rotW
    const dy = (cur.y - drag.startRot.y) / rotH

    const minSize = 0.05 // 5% minimum width/height to keep the crop usable.
    let l = drag.startLeft
    let t = drag.startTop
    let r2 = drag.startRight
    let b = drag.startBottom

    const k = drag.handle
    if (k === "body") {
        const w = drag.startRight - drag.startLeft
        const h = drag.startBottom - drag.startTop
        let nl = drag.startLeft + dx
        let nt = drag.startTop + dy
        nl = Math.min(Math.max(nl, 0), 1 - w)
        nt = Math.min(Math.max(nt, 0), 1 - h)
        l = nl
        t = nt
        r2 = nl + w
        b = nt + h
    } else {
        if (k === "tl" || k === "l" || k === "bl") {
            l = Math.min(Math.max(drag.startLeft + dx, 0), drag.startRight - minSize)
        }
        if (k === "tr" || k === "r" || k === "br") {
            r2 = Math.min(Math.max(drag.startRight + dx, drag.startLeft + minSize), 1)
        }
        if (k === "tl" || k === "t" || k === "tr") {
            t = Math.min(Math.max(drag.startTop + dy, 0), drag.startBottom - minSize)
        }
        if (k === "bl" || k === "b" || k === "br") {
            b = Math.min(Math.max(drag.startBottom + dy, drag.startTop + minSize), 1)
        }
    }

    cropLeft.value = l
    cropTop.value = t
    cropRight.value = r2
    cropBottom.value = b
    drawOverlay()
}

function onPointerUp(ev: PointerEvent) {
    const canvas = overlayRef.value
    if (canvas?.hasPointerCapture(ev.pointerId)) {
        canvas.releasePointerCapture(ev.pointerId)
    }
    if (drag) {
        drag = null
        persist()
    }
}

function resetCrop() {
    cropLeft.value = 0
    cropTop.value = 0
    cropRight.value = 1
    cropBottom.value = 1
    rotationDeg.value = 0
    persist()
    fitToContainer()
    redraw()
}

function onRotationInput(v: number) {
    if (!Number.isFinite(v)) return
    // Clamp to [-180, 180] and normalise so the slider/spinbox can't escape.
    const clamped = Math.min(Math.max(v, -180), 180)
    rotationDeg.value = clamped
    fitToContainer()
    redraw()
    persist()
}

function rotateBy(delta: number) {
    onRotationInput(rotationDeg.value + delta)
}

let resizeObs: ResizeObserver | null = null

onMounted(() => {
    if (!store.deskewResult) {
        store.goToStep(4)
        return
    }
    // Pre-fill from the store / cache so the user's previous crop &
    // rotation come back when they re-enter the step.
    const cached = store.fileHash ? loadCropRotate(store.fileHash) : null
    const seed = cached ?? store.cropRotate
    rotationDeg.value = seed.rotationDeg
    cropLeft.value = seed.crop.left
    cropTop.value = seed.crop.top
    cropRight.value = seed.crop.right
    cropBottom.value = seed.crop.bottom
    if (cached) store.setCropRotate(cached)

    imgUrl.value = URL.createObjectURL(store.deskewResult.correctedImageBlob)
    loadImage(imgUrl.value)

    if (containerRef.value) {
        resizeObs = new ResizeObserver(() => {
            fitToContainer()
            redraw()
        })
        resizeObs.observe(containerRef.value)
    }
})

onUnmounted(() => {
    resizeObs?.disconnect()
    if (imgUrl.value) URL.revokeObjectURL(imgUrl.value)
})

watch(rotationDeg, () => {
    fitToContainer()
    redraw()
})

watch([cropLeft, cropTop, cropRight, cropBottom], () => {
    drawOverlay()
})

function next() {
    persist()
    store.goToStep(6)
}
</script>

<template>
    <div class="space-y-4">
        <div class="flex flex-wrap items-center justify-between gap-2">
            <div>
                <h2 class="text-xl font-semibold">Crop &amp; Rotate</h2>
                <p class="text-sm text-muted-foreground">
                    Optionally rotate and crop the deskewed image before
                    measuring.
                </p>
            </div>
            <div class="flex shrink-0 gap-2">
                <Button variant="outline" @click="store.goToStep(4)">Back</Button>
                <Button variant="outline" @click="resetCrop">Reset</Button>
                <Button @click="next">Next: Measure</Button>
            </div>
        </div>

        <Card>
            <CardHeader class="space-y-2">
                <CardTitle class="text-base">Rotation</CardTitle>
                <div class="flex flex-wrap items-center gap-3">
                    <input
                        type="range"
                        min="-180"
                        max="180"
                        step="0.1"
                        :value="rotationDeg"
                        class="h-2 w-full max-w-md flex-1 accent-primary"
                        @input="(e) => onRotationInput(Number((e.target as HTMLInputElement).value))"
                    />
                    <div class="flex items-center gap-1">
                        <Button
                            variant="outline"
                            size="sm"
                            class="h-8 px-2"
                            @click="rotateBy(-90)"
                            >-90°</Button
                        >
                        <Button
                            variant="outline"
                            size="sm"
                            class="h-8 px-2"
                            @click="rotateBy(-1)"
                            >-1°</Button
                        >
                        <input
                            type="number"
                            min="-180"
                            max="180"
                            step="0.1"
                            :value="rotationDeg.toFixed(1)"
                            class="h-8 w-20 rounded-md border border-input bg-background px-2 text-sm"
                            @input="(e) => onRotationInput(Number((e.target as HTMLInputElement).value))"
                        />
                        <span class="text-sm text-muted-foreground">°</span>
                        <Button
                            variant="outline"
                            size="sm"
                            class="h-8 px-2"
                            @click="rotateBy(1)"
                            >+1°</Button
                        >
                        <Button
                            variant="outline"
                            size="sm"
                            class="h-8 px-2"
                            @click="rotateBy(90)"
                            >+90°</Button
                        >
                    </div>
                </div>
            </CardHeader>
            <CardContent>
                <div
                    ref="containerRef"
                    class="relative h-[calc(100vh-22rem)] min-h-[320px] w-full overflow-hidden rounded-md bg-muted"
                >
                    <canvas
                        ref="canvasRef"
                        class="absolute inset-0"
                    />
                    <canvas
                        ref="overlayRef"
                        class="absolute inset-0 touch-none"
                        @pointerdown="onPointerDown"
                        @pointermove="onPointerMove"
                        @pointerup="onPointerUp"
                        @pointercancel="onPointerUp"
                    />
                </div>
            </CardContent>
        </Card>
    </div>
</template>
