<template>
  <div class="sticker-canvas" ref="containerRef">
    <v-stage
      ref="stageRef"
      :config="stageConfig"
      @click="onStageClick"
      @tap="onStageClick"
    >
      <v-layer>
        <v-image :config="imageConfig" />
      </v-layer>
      <v-layer ref="stickerLayerRef">
        <template v-for="s in stickers" :key="s.id">
          <v-image
            v-if="s.imageAsset"
            :config="imageStickerConfig(s)"
            @click="selectSticker(s.id, $event)"
            @tap="selectSticker(s.id, $event)"
            @dragend="onDragEnd(s.id, $event)"
            @transformend="onTransformEnd(s.id, $event)"
          />
          <v-text
            v-else
            :config="emojiStickerConfig(s)"
            @click="selectSticker(s.id, $event)"
            @tap="selectSticker(s.id, $event)"
            @dragend="onDragEnd(s.id, $event)"
            @transformend="onTransformEnd(s.id, $event)"
          />
        </template>
        <v-transformer ref="transformerRef" :config="transformerConfig" />
      </v-layer>
    </v-stage>

    <!-- Floating delete handle — follows the selected sticker so the user
         doesn't have to hunt for an off-canvas X. Trash icon (not X) reads
         as "remove" not "close." -->
    <button
      v-if="handlePos"
      class="sticker-canvas__delete-handle"
      type="button"
      aria-label="Remove sticker"
      :style="{ left: handlePos.x + 'px', top: handlePos.y + 'px' }"
      @pointerdown.stop
      @click.stop="removeSelected"
    >
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <polyline points="3 6 5 6 21 6"/>
        <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
        <path d="M10 11v6"/>
        <path d="M14 11v6"/>
        <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
      </svg>
    </button>

    <!-- Add sticker / Next -->
    <div class="sticker-canvas__bar">
      <button class="sticker-canvas__add-btn" type="button" @click="trayOpen = true">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
          <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>
        </svg>
        Add sticker
      </button>
      <BaseButton variant="primary" class="sticker-canvas__next-btn" @click="done">Next</BaseButton>
    </div>

    <!-- Sticker tray -->
    <StickerTray v-model="trayOpen" @pick="addStickerFromTray" />
  </div>
</template>

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

const props = defineProps<{
  croppedUrl: string
  orientation: 'landscape' | 'portrait'
  /** Frame's hardware model — drives stage aspect ratio + final output dims.
   *  Defaults to 'v1' so existing callers keep their behaviour. */
  model?: DeviceModel
  stickers: StickerLayer[]
}>()

const emit = defineEmits<{
  (e: 'add-sticker', s: StickerLayer): void
  (e: 'update-sticker', id: string, patch: Partial<StickerLayer>): void
  (e: 'remove-sticker', id: string): void
  (e: 'done', blob: Blob): void
}>()

const containerRef   = ref<HTMLDivElement>()
const stageRef       = ref()
const transformerRef = ref()
const stickerLayerRef = ref()

const trayOpen   = ref(false)
const selectedId = ref<string | null>(null)

// Stage dimensions fitted to container
const stageW = ref(375)
const stageH = ref(225)

const { width: OUT_W, height: OUT_H } = panelDims(props.model ?? 'v1', props.orientation)
const ASPECT = OUT_W / OUT_H

function sizeStage() {
  if (!containerRef.value) return
  const { width, height } = containerRef.value.getBoundingClientRect()
  const availH = height - 72  // bottom bar
  if (width / availH > ASPECT) {
    stageH.value = availH
    stageW.value = availH * ASPECT
  } else {
    stageW.value = width
    stageH.value = width / ASPECT
  }
  loadImage()
}

const ro = new ResizeObserver(sizeStage)
onMounted(() => {
  if (containerRef.value) ro.observe(containerRef.value)
  sizeStage()
  attachPinchListeners()
})
onBeforeUnmount(() => {
  ro.disconnect()
  detachPinchListeners()
  detachHandleListeners?.()
})

// Pinch-resize and the parent's stickers prop both bypass Konva's
// drag/transform events, so refresh the handle when the props change.
watch(() => props.stickers, () => {
  if (!selectedId.value) return
  const layer = stickerLayerRef.value?.getNode()
  const node = layer?.findOne(`#${selectedId.value}`)
  if (node) refreshHandle(node)
}, { deep: true })

// ── Image ─────────────────────────────────────────────────────────────────────

const bgImage = ref<HTMLImageElement | null>(null)
function loadImage() {
  const i = new Image()
  i.onload = () => { bgImage.value = i }
  i.src = props.croppedUrl
}
watch(() => props.croppedUrl, () => loadImage(), { immediate: true })

// ── Custom-sprite asset preloader ─────────────────────────────────────────────
// Image-based stickers need an HTMLImageElement loaded before Konva can
// paint them; preload all known custom assets up front so the first
// pick renders without a flicker.
const imageAssetCache = ref<Record<string, HTMLImageElement>>({})
onMounted(() => {
  for (const a of CUSTOM_STICKER_ASSETS) {
    const i = new Image()
    i.onload = () => {
      imageAssetCache.value = { ...imageAssetCache.value, [a.id]: i }
    }
    i.src = a.url
  }
})

const stageConfig = computed(() => ({ width: stageW.value, height: stageH.value }))
const imageConfig = computed(() => ({
  image: bgImage.value,
  x: 0, y: 0,
  width:  stageW.value,
  height: stageH.value,
}))

const transformerConfig = {
  enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
  rotateEnabled: true,
  borderStroke: 'rgba(255,255,255,0.8)',
  anchorFill: '#fff',
  anchorSize: 18,   // larger for touch
  keepRatio: true,
  boundBoxFunc: (_: any, newBox: any) => newBox,
}

// ── Stickers ──────────────────────────────────────────────────────────────────

const EMOJI_FONT_SIZE = 52
const IMAGE_STICKER_BASE_SIZE = 96

function emojiStickerConfig(s: StickerLayer) {
  return {
    id: s.id,
    text: emojiFor(s),
    fontSize: EMOJI_FONT_SIZE,
    fontFamily: '"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif',
    x: s.x,
    y: s.y,
    scaleX: s.scale,
    scaleY: s.scale,
    rotation: s.rotation,
    draggable: true,
    offsetX: EMOJI_FONT_SIZE / 2,
    offsetY: EMOJI_FONT_SIZE / 2,
  }
}

function imageStickerConfig(s: StickerLayer) {
  return {
    id: s.id,
    image: s.imageAsset ? imageAssetCache.value[s.imageAsset] : null,
    x: s.x,
    y: s.y,
    width: IMAGE_STICKER_BASE_SIZE,
    height: IMAGE_STICKER_BASE_SIZE,
    scaleX: s.scale,
    scaleY: s.scale,
    rotation: s.rotation,
    draggable: true,
    offsetX: IMAGE_STICKER_BASE_SIZE / 2,
    offsetY: IMAGE_STICKER_BASE_SIZE / 2,
  }
}

import { STICKERS } from '@/assets/stickers/index'
import { CUSTOM_STICKER_ASSETS } from '@/assets/stickers/customAssets'

function emojiFor(s: StickerLayer): string {
  // New stickers carry the glyph directly; legacy stickers fall back to
  // the curated table by id.
  if (s.emoji) return s.emoji
  return STICKERS.find(x => x.id === s.type)?.emoji ?? '⭐'
}

// Screen-space position of the delete handle. Tracks the selected
// sticker's transformed bbox so the handle stays glued to its top-right
// corner through drag/resize/rotate.
const handlePos = ref<{ x: number; y: number } | null>(null)
let detachHandleListeners: (() => void) | null = null

function refreshHandle(node: any) {
  const box = node.getClientRect()
  // Sit just outside the top-right of the sticker; nudge inward a touch
  // so it never disappears beyond the stage edge for a corner sticker.
  const margin = 4
  const half = 14
  let x = box.x + box.width + margin - half
  let y = box.y - margin - half
  // Clamp inside stage so the button is always tappable.
  x = Math.max(half, Math.min(stageW.value - half, x))
  y = Math.max(half, Math.min(stageH.value - half, y))
  handlePos.value = { x, y }
}

function attachHandleTracking(id: string) {
  detachHandleListeners?.()
  detachHandleListeners = null
  const layer = stickerLayerRef.value?.getNode()
  const node  = layer?.findOne(`#${id}`)
  if (!node) { handlePos.value = null; return }
  refreshHandle(node)
  const update = () => refreshHandle(node)
  node.on('dragmove.handle transform.handle transformend.handle dragend.handle', update)
  detachHandleListeners = () => node.off('.handle')
}

function selectSticker(id: string, e: any) {
  e.cancelBubble = true
  selectedId.value = id
  nextTick(() => {
    const layer = stickerLayerRef.value?.getNode()
    const node  = layer?.findOne(`#${id}`)
    const tr    = transformerRef.value?.getNode()
    if (node && tr) tr.nodes([node])
    attachHandleTracking(id)
  })
}

function onStageClick(e: any) {
  if (e.target === e.target.getStage()) {
    selectedId.value = null
    transformerRef.value?.getNode()?.nodes([])
    detachHandleListeners?.()
    detachHandleListeners = null
    handlePos.value = null
  }
}

function removeSelected() {
  if (!selectedId.value) return
  emit('remove-sticker', selectedId.value)
  selectedId.value = null
  transformerRef.value?.getNode()?.nodes([])
  detachHandleListeners?.()
  detachHandleListeners = null
  handlePos.value = null
}

function onDragEnd(id: string, e: any) {
  emit('update-sticker', id, { x: e.target.x(), y: e.target.y() })
}

function onTransformEnd(id: string, e: any) {
  emit('update-sticker', id, {
    x:        e.target.x(),
    y:        e.target.y(),
    scale:    e.target.scaleX(),
    rotation: e.target.rotation(),
  })
}

function addStickerFromTray(payload: { emoji?: string; imageAsset?: string }) {
  const seed = payload.imageAsset ?? payload.emoji ?? 'sticker'
  const s: StickerLayer = {
    id:         `${seed}-${Date.now()}`,
    // `type` doubles as a coarse kind label so legacy serialization
    // keeps a non-empty value (the field is required server-side).
    type:       payload.imageAsset ? 'image' : 'emoji',
    emoji:      payload.emoji,
    imageAsset: payload.imageAsset,
    x:          stageW.value / 2,
    y:          stageH.value / 2,
    scale:      1,
    rotation:   0,
  }
  emit('add-sticker', s)
  trayOpen.value = false
  // Auto-select the new sticker
  nextTick(() => selectSticker(s.id, { cancelBubble: false }))
}

// ── Pinch-to-resize ───────────────────────────────────────────────────────────

let pinchStartDist  = 0
let pinchStartScale = 1

function touchDist(touches: TouchList) {
  const dx = touches[0].clientX - touches[1].clientX
  const dy = touches[0].clientY - touches[1].clientY
  return Math.hypot(dx, dy)
}

function onPinchStart(e: TouchEvent) {
  if (e.touches.length !== 2 || !selectedId.value) return
  pinchStartDist = touchDist(e.touches)
  const s = props.stickers.find(x => x.id === selectedId.value)
  pinchStartScale = s?.scale ?? 1
}

function onPinchMove(e: TouchEvent) {
  if (e.touches.length !== 2 || !selectedId.value || pinchStartDist === 0) return
  e.preventDefault()
  const newScale = Math.max(0.2, Math.min(6, pinchStartScale * (touchDist(e.touches) / pinchStartDist)))
  emit('update-sticker', selectedId.value, { scale: newScale })
}

function onPinchEnd() {
  pinchStartDist  = 0
  pinchStartScale = 1
}

function attachPinchListeners() {
  const el = containerRef.value
  if (!el) return
  el.addEventListener('touchstart', onPinchStart, { passive: true })
  el.addEventListener('touchmove',  onPinchMove,  { passive: false })
  el.addEventListener('touchend',   onPinchEnd,   { passive: true })
}

function detachPinchListeners() {
  const el = containerRef.value
  if (!el) return
  el.removeEventListener('touchstart', onPinchStart)
  el.removeEventListener('touchmove',  onPinchMove)
  el.removeEventListener('touchend',   onPinchEnd)
}

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

async function done() {
  // Deselect to hide transformer handles before capture
  selectedId.value = null
  transformerRef.value?.getNode()?.nodes([])
  await nextTick()

  const stage: Konva.Stage = stageRef.value?.getNode()
  if (!stage) return

  const pixelRatio = OUT_W / stageW.value

  // Use explicit mimeType so the blob is a real JPEG, not the default PNG
  const blob = await stage.toBlob({ pixelRatio, mimeType: 'image/jpeg', quality: 0.92 }) as Blob | null
  if (blob) emit('done', blob)
}
</script>

<style scoped lang="scss">
.sticker-canvas {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  background: #111;
  overflow: hidden;

  :deep(.konvajs-content) {
    flex-shrink: 0;
  }

  &__delete-handle {
    position: absolute;
    width: 28px;
    height: 28px;
    margin: 0;
    padding: 0;
    border-radius: 50%;
    background: rgba(200, 30, 30, 0.95);
    border: 2px solid #fff;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    z-index: 10;
    transform: translate(-50%, -50%);
    touch-action: none;
  }

  &__bar {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 72px;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 0 var(--space-4);
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
  }

  &__add-btn {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-md);
    border: 1.5px solid var(--color-border);
    background: transparent;
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
  }

  &__next-btn {
    margin-left: auto;
    min-width: 96px;
  }
}
</style>
