<template>
  <div class="crop-editor" ref="containerRef">
    <canvas
      ref="canvasRef"
      class="crop-editor__canvas"
      @pointerdown="onPointerDown"
      @pointermove="onPointerMove"
      @pointerup="onPointerUp"
      @pointercancel="onPointerUp"
    />

    <div class="crop-editor__top">
      <div v-if="deviceName" class="crop-editor__label">{{ deviceName }}</div>

      <!-- Orientation toggle: switches the crop frame's aspect ratio -->
      <div class="crop-editor__orient" role="radiogroup" aria-label="Crop orientation">
        <button
          v-for="opt in ORIENT_OPTS"
          :key="opt.value"
          type="button"
          role="radio"
          :aria-checked="cropOrientation === opt.value"
          :aria-label="opt.label"
          :class="['crop-editor__orient-btn', { 'crop-editor__orient-btn--active': cropOrientation === opt.value }]"
          @click="setOrientation(opt.value)"
        >
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
            <rect v-if="opt.value === 'landscape'" x="2" y="6" width="20" height="12" rx="1.5"/>
            <rect v-else x="6" y="2" width="12" height="20" rx="1.5"/>
          </svg>
        </button>
      </div>

      <!-- Mismatch chip: only shown when crop orientation differs from frame's -->
      <div
        v-if="mismatch"
        class="crop-editor__mismatch"
        role="status"
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
          <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
          <line x1="12" y1="9" x2="12" y2="13"/>
          <line x1="12" y1="17" x2="12.01" y2="17"/>
        </svg>
        <span>Frame is set to {{ orientation }}. Use the tool above to recrop for the current frame orientation, or switch the frame in Settings to display this crop.</span>
      </div>
    </div>

    <div class="crop-editor__actions">
      <BaseButton variant="primary" class="crop-editor__use-btn" @click="useCrop">
        Use this crop
      </BaseButton>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import { panelDims, type CropParams, type DeviceModel } from '@/types'

type Orientation = 'landscape' | 'portrait'

const props = defineProps<{
  src: string
  /** Frame's current orientation — used as the toggle's initial value and for the mismatch chip. */
  orientation: Orientation
  /** Frame's hardware model — drives crop output dimensions. V1 (7.3", 5:3) and
   *  V2 (13.3", 4:3) have different aspect ratios. Defaults to 'v1' so existing
   *  callers that don't pass a model keep the original behaviour. */
  model?: DeviceModel
  deviceName?: string
  initialParams?: CropParams | null
  /** When editing an existing image, defaults the toggle to the saved choice instead of `orientation`. */
  initialOrientation?: Orientation | null
}>()

const emit = defineEmits<{
  (e: 'crop', result: { blob: Blob; params: CropParams; orientation: Orientation }): void
}>()

const ORIENT_OPTS: Array<{ value: Orientation; label: string }> = [
  { value: 'landscape', label: 'Landscape crop' },
  { value: 'portrait',  label: 'Portrait crop'  },
]

// User's chosen crop orientation. Initial value: the saved choice if editing,
// otherwise the frame's current orientation. Reactive — toggling re-shapes
// the crop frame and the eventual output blob.
const cropOrientation = ref<Orientation>(props.initialOrientation ?? props.orientation)

const outputDims = computed(() => {
  const { width, height } = panelDims(props.model ?? 'v1', cropOrientation.value)
  return { w: width, h: height }
})
const aspect = computed(() => outputDims.value.w / outputDims.value.h)

// Visible only when the user's choice doesn't match the frame's setting.
const mismatch = computed(() => cropOrientation.value !== props.orientation)

const containerRef = ref<HTMLDivElement>()
const canvasRef    = ref<HTMLCanvasElement>()
let ctx: CanvasRenderingContext2D | null = null
let img: HTMLImageElement | null = null
let rafId = 0

// State: pan (canvas px from centered) + zoom multiplier
const panX  = ref(0)
const panY  = ref(0)
const zoom  = ref(1)

// Crop rect on canvas (set when canvas is sized)
let cropRect = { x: 0, y: 0, w: 0, h: 0 }
let minScale = 1  // natural px → canvas px at zoom=1 (cover)

function setOrientation(o: Orientation) {
  if (cropOrientation.value === o) return
  cropOrientation.value = o
  // Re-shape the crop frame to the new aspect; reset pan/zoom because the
  // saved coordinates no longer make sense against the new frame.
  panX.value = 0
  panY.value = 0
  zoom.value = 1
  sizeCanvas()
}

function sizeCanvas() {
  const canvas = canvasRef.value
  const container = containerRef.value
  if (!canvas || !container) return

  const available = container.getBoundingClientRect()
  // Leave space for bottom button bar
  const availH = available.height - 80
  const availW = available.width

  canvas.width  = availW
  canvas.height = availH

  ctx = canvas.getContext('2d')

  // Compute crop rect (inset 24px each side for comfort)
  const pad = 24
  const maxW = availW - pad * 2
  const maxH = availH - pad * 2

  let cropW: number, cropH: number
  if (maxW / maxH > aspect.value) {
    cropH = maxH
    cropW = cropH * aspect.value
  } else {
    cropW = maxW
    cropH = cropW / aspect.value
  }

  cropRect = {
    x: (availW - cropW) / 2,
    y: (availH - cropH) / 2,
    w: cropW,
    h: cropH,
  }

  if (img) {
    resetView()
  }
}

function resetView() {
  if (!img) return
  minScale = Math.max(cropRect.w / img.naturalWidth, cropRect.h / img.naturalHeight)
  if (props.initialParams) {
    restoreView(props.initialParams)
  } else {
    zoom.value = 1
    panX.value = 0
    panY.value = 0
    draw()
  }
}

function restoreView(p: CropParams) {
  if (!img) return
  // actualScale such that natW fills the crop frame width
  const actualScale = cropRect.w / p.natW
  zoom.value = actualScale / minScale
  // pan: offset from centered position so crop center = frame center
  panX.value = actualScale * (img.naturalWidth  / 2 - p.natX - p.natW / 2)
  panY.value = actualScale * (img.naturalHeight / 2 - p.natY - p.natH / 2)
  const [cx, cy] = clampPan(panX.value, panY.value)
  panX.value = cx
  panY.value = cy
  draw()
}

function clampPan(px: number, py: number): [number, number] {
  if (!img) return [px, py]
  const actualScale = minScale * zoom.value
  const imgW = img.naturalWidth  * actualScale
  const imgH = img.naturalHeight * actualScale
  const maxPx = (imgW - cropRect.w) / 2
  const maxPy = (imgH - cropRect.h) / 2
  return [
    Math.max(-maxPx, Math.min(maxPx, px)),
    Math.max(-maxPy, Math.min(maxPy, py)),
  ]
}

function draw() {
  if (!ctx || !img || !canvasRef.value) return
  const { width, height } = canvasRef.value
  const actualScale = minScale * zoom.value
  const imgW = img.naturalWidth  * actualScale
  const imgH = img.naturalHeight * actualScale
  const cx   = cropRect.x + cropRect.w / 2 + panX.value
  const cy   = cropRect.y + cropRect.h / 2 + panY.value
  const imgLeft = cx - imgW / 2
  const imgTop  = cy - imgH / 2

  ctx.clearRect(0, 0, width, height)
  ctx.drawImage(img, imgLeft, imgTop, imgW, imgH)

  // Dim *outside* the crop with 4 strips. The previous destination-out
  // trick using a semi-transparent fill made the crop area partially
  // transparent (revealing the black container bg) instead of leaving
  // it untouched, so the inside ended up *darker* than the outside —
  // the inverse of what a crop overlay is meant to do.
  const cx2 = cropRect.x + cropRect.w
  const cy2 = cropRect.y + cropRect.h
  ctx.fillStyle = 'rgba(0,0,0,0.6)'
  ctx.fillRect(0, 0, width, cropRect.y)                      // top
  ctx.fillRect(0, cy2, width, height - cy2)                  // bottom
  ctx.fillRect(0, cropRect.y, cropRect.x, cropRect.h)        // left
  ctx.fillRect(cx2, cropRect.y, width - cx2, cropRect.h)     // right

  // Crop border + corner marks
  ctx.strokeStyle = '#fff'
  ctx.lineWidth   = 2
  ctx.strokeRect(cropRect.x, cropRect.y, cropRect.w, cropRect.h)

  const cLen = 20
  ctx.lineWidth = 3
  ;[
    [cropRect.x, cropRect.y, cLen, 0, 0, cLen],
    [cropRect.x + cropRect.w, cropRect.y, -cLen, 0, 0, cLen],
    [cropRect.x, cropRect.y + cropRect.h, cLen, 0, 0, -cLen],
    [cropRect.x + cropRect.w, cropRect.y + cropRect.h, -cLen, 0, 0, -cLen],
  ].forEach(([x, y, dx1, dy1, dx2, dy2]) => {
    ctx!.beginPath()
    ctx!.moveTo(x + dx1, y + dy1)
    ctx!.lineTo(x, y)
    ctx!.lineTo(x + dx2, y + dy2)
    ctx!.stroke()
  })
}

// ── Touch / pointer handling ──────────────────────────────────────────────────

const activePointers = new Map<number, { x: number; y: number }>()
let lastPinchDist = 0

function onPointerDown(e: PointerEvent) {
  canvasRef.value?.setPointerCapture(e.pointerId)
  activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
  if (activePointers.size === 2) {
    const pts = [...activePointers.values()]
    lastPinchDist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y)
  }
}

function onPointerMove(e: PointerEvent) {
  if (!activePointers.has(e.pointerId)) return
  const prev = activePointers.get(e.pointerId)!
  activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY })

  if (activePointers.size === 1) {
    const dx = e.clientX - prev.x
    const dy = e.clientY - prev.y
    const [cx, cy] = clampPan(panX.value + dx, panY.value + dy)
    panX.value = cx
    panY.value = cy
    scheduleDraw()
    return
  }

  if (activePointers.size === 2) {
    const pts = [...activePointers.values()]
    const dist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y)
    if (lastPinchDist > 0) {
      const ratio = dist / lastPinchDist
      const newZoom = Math.max(1, zoom.value * ratio)
      zoom.value = newZoom
      const [cx, cy] = clampPan(panX.value, panY.value)
      panX.value = cx
      panY.value = cy
      scheduleDraw()
    }
    lastPinchDist = dist
  }
}

function onPointerUp(e: PointerEvent) {
  activePointers.delete(e.pointerId)
  lastPinchDist = 0
}

function scheduleDraw() {
  cancelAnimationFrame(rafId)
  rafId = requestAnimationFrame(draw)
}

// ── Output ────────────────────────────────────────────────────────────────────

async function useCrop() {
  if (!img) return
  const actualScale = minScale * zoom.value
  const cx   = cropRect.x + cropRect.w / 2 + panX.value
  const cy   = cropRect.y + cropRect.h / 2 + panY.value
  const imgLeft = cx - img.naturalWidth  * actualScale / 2
  const imgTop  = cy - img.naturalHeight * actualScale / 2

  const natCropX = (cropRect.x - imgLeft) / actualScale
  const natCropY = (cropRect.y - imgTop)  / actualScale
  const natCropW = cropRect.w / actualScale
  const natCropH = cropRect.h / actualScale

  const { w: outW, h: outH } = outputDims.value
  const out = new OffscreenCanvas(outW, outH)
  const outCtx = out.getContext('2d')!
  outCtx.drawImage(img, natCropX, natCropY, natCropW, natCropH, 0, 0, outW, outH)
  const blob = await out.convertToBlob({ type: 'image/jpeg', quality: 0.92 })
  emit('crop', {
    blob,
    params: { natX: natCropX, natY: natCropY, natW: natCropW, natH: natCropH },
    orientation: cropOrientation.value,
  })
}

// ── Lifecycle ─────────────────────────────────────────────────────────────────

const ro = new ResizeObserver(sizeCanvas)

onMounted(() => {
  if (containerRef.value) ro.observe(containerRef.value)
  sizeCanvas()

  img = new Image()
  img.onload = () => {
    sizeCanvas()
    resetView()
  }
  img.src = props.src
})

watch(() => props.src, src => {
  if (!img) return
  img.onload = () => resetView()
  img.src = src
})

onBeforeUnmount(() => {
  ro.disconnect()
  cancelAnimationFrame(rafId)
})
</script>

<style scoped lang="scss">
.crop-editor {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: #000;
  touch-action: none;

  &__canvas {
    flex: 1;
    min-height: 0;
    display: block;
    touch-action: none;
    cursor: grab;
    &:active { cursor: grabbing; }
  }

  &__top {
    position: absolute;
    top: 12px;
    left: 12px;
    right: 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    pointer-events: none;
  }

  &__label {
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: var(--text-xs);
    font-weight: 700;
    padding: 4px 12px;
    border-radius: 999px;
    letter-spacing: 0.04em;
  }

  &__orient {
    pointer-events: auto;
    display: flex;
    gap: 4px;
    background: rgba(0, 0, 0, 0.6);
    border-radius: 999px;
    padding: 4px;
  }

  &__orient-btn {
    width: 36px;
    height: 32px;
    border: none;
    background: transparent;
    color: rgba(255, 255, 255, 0.65);
    border-radius: 999px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background var(--duration-fast), color var(--duration-fast);

    &--active {
      background: #fff;
      color: #000;
    }
  }

  &__mismatch {
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 6px;
    background: var(--color-warning, #f59e0b);
    color: #fff;
    font-size: var(--text-xs);
    font-weight: 600;
    padding: 6px 12px;
    border-radius: 999px;
    max-width: 100%;
    line-height: 1.3;

    svg { flex-shrink: 0; }
  }

  &__actions {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: var(--space-4);
    display: flex;
    justify-content: center;
  }

  &__use-btn {
    width: 100%;
    max-width: 320px;
  }
}
</style>
