'use strict';

(() => {

/**
 * AnnotationCanvas: renders a step's working image plus its normalized
 * annotation scene graph, and provides editing interactions (create, select,
 * move, resize, nudge, crop). Geometry rules mirror core/raster.js so the
 * editor shows what exports produce.
 */

const DRAW_ORDER = { blur: 0, highlight: 1, magnify: 2, rect: 3, oval: 3, line: 3, arrow: 3, cursor: 4, number: 5, text: 6, tooltip: 7 };
const POINT_TOOLS = new Set(['line', 'arrow']);
const HANDLE_SIZE = 8;

class AnnotationCanvas {
  constructor(canvasEl, callbacks = {}) {
    this.canvas = canvasEl;
    this.ctx = canvasEl.getContext('2d');
    this.cb = callbacks; // { onChange, onSelect, onCrop, onRequestText }
    this.image = null;   // HTMLImageElement
    this.imgW = 0;
    this.imgH = 0;
    this.annotations = [];
    this.tool = 'select';
    this.zoomMode = 'fit';
    this.scale = 1;
    this.selectedId = null;
    this.drag = null;
    this.cropRect = null;

    canvasEl.addEventListener('pointerdown', (e) => this.onDown(e));
    canvasEl.addEventListener('pointermove', (e) => this.onMove(e));
    canvasEl.addEventListener('pointerup', (e) => this.onUp(e));
    canvasEl.addEventListener('dblclick', (e) => this.onDblClick(e));
  }

  setImage(image, w, h) {
    this.image = image;
    this.imgW = w || 0;
    this.imgH = h || 0;
    this.cropRect = null;
    if (!image || !this.imgW || !this.imgH) {
      this.canvas.width = 1;
      this.canvas.height = 1;
      this.render();
      return;
    }
    this.applyZoom();
  }

  setAnnotations(annotations) {
    this.annotations = annotations || [];
    if (!this.annotations.some((a) => a.id === this.selectedId)) this.selectedId = null;
    this.render();
  }

  setTool(tool) {
    this.tool = tool;
    this.cropRect = null;
    if (tool !== 'select') this.select(null);
    this.render();
  }

  setZoom(mode) {
    this.zoomMode = mode;
    this.applyZoom();
  }

  applyZoom() {
    if (!this.image) return;
    const wrap = this.canvas.parentElement;
    if (this.zoomMode === 'fit') {
      const availW = Math.max(100, wrap.clientWidth - 40);
      const availH = Math.max(100, wrap.clientHeight - 40);
      this.scale = Math.min(availW / this.imgW, availH / this.imgH, 1);
    } else {
      this.scale = Number(this.zoomMode) || 1;
    }
    this.canvas.width = Math.round(this.imgW * this.scale);
    this.canvas.height = Math.round(this.imgH * this.scale);
    this.render();
  }

  select(id) {
    this.selectedId = id;
    if (this.cb.onSelect) this.cb.onSelect(this.annotations.find((a) => a.id === id) || null);
    this.render();
  }

  selected() {
    return this.annotations.find((a) => a.id === this.selectedId) || null;
  }

  changed() {
    if (this.cb.onChange) this.cb.onChange(this.annotations);
    this.render();
  }

  // ---- coordinate helpers ----
  toNorm(e) {
    const rect = this.canvas.getBoundingClientRect();
    return {
      x: (e.clientX - rect.left) / rect.width,
      y: (e.clientY - rect.top) / rect.height,
    };
  }

  px(ann) {
    return {
      x: ann.x * this.canvas.width,
      y: ann.y * this.canvas.height,
      w: ann.w * this.canvas.width,
      h: ann.h * this.canvas.height,
    };
  }

  // ---- rendering ----
  render() {
    const { ctx, canvas } = this;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    if (!this.image) return;
    ctx.imageSmoothingEnabled = true;
    ctx.drawImage(this.image, 0, 0, canvas.width, canvas.height);

    const ordered = [...this.annotations].sort((a, b) => (DRAW_ORDER[a.type] ?? 3) - (DRAW_ORDER[b.type] ?? 3));
    for (const ann of ordered) this.drawAnnotation(ann);

    const sel = this.selected();
    if (sel) this.drawSelection(sel);
    if (this.cropRect) this.drawCropOverlay();
  }

  strokePx(ann) {
    return Math.max(1, ((ann.style && ann.style.strokeWidth) || 3) * this.canvas.width / 1000);
  }

  fontPx(ann) {
    return Math.max(9, ((ann.style && ann.style.fontSize) || 0.022) * this.canvas.height);
  }

  drawAnnotation(ann) {
    const { ctx } = this;
    const { x, y, w, h } = this.px(ann);
    const style = ann.style || {};
    const stroke = style.stroke || '#E5484D';
    const fill = style.fill && style.fill !== 'transparent' ? style.fill : null;
    ctx.save();
    ctx.lineWidth = this.strokePx(ann);
    ctx.strokeStyle = stroke;

    switch (ann.type) {
      case 'rect':
        if (fill) { ctx.fillStyle = fill; ctx.fillRect(x, y, w, h); }
        ctx.strokeRect(x, y, w, h);
        break;
      case 'oval':
        ctx.beginPath();
        ctx.ellipse(x + w / 2, y + h / 2, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
        if (fill) { ctx.fillStyle = fill; ctx.fill(); }
        ctx.stroke();
        break;
      case 'line':
        ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + w, y + h); ctx.stroke();
        break;
      case 'arrow': {
        const len = Math.hypot(w, h) || 1;
        const head = Math.min(len * 0.4, Math.max(10, ctx.lineWidth * 4));
        const ux = w / len, uy = h / len;
        const bx = x + w - ux * head, by = y + h - uy * head;
        ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(bx, by); ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(x + w, y + h);
        ctx.lineTo(bx - uy * head * 0.5, by + ux * head * 0.5);
        ctx.lineTo(bx + uy * head * 0.5, by - ux * head * 0.5);
        ctx.closePath();
        ctx.fillStyle = stroke; ctx.fill();
        break;
      }
      case 'blur': {
        // preview: pixelate the region by down/up-scaling
        const f = Math.max(6, (ann.radius || 8));
        try {
          ctx.imageSmoothingEnabled = true;
          const tw = Math.max(1, Math.round(w / f)), th = Math.max(1, Math.round(h / f));
          const off = document.createElement('canvas');
          off.width = tw; off.height = th;
          off.getContext('2d').drawImage(this.canvas, x, y, w, h, 0, 0, tw, th);
          ctx.imageSmoothingEnabled = true;
          ctx.drawImage(off, 0, 0, tw, th, x, y, w, h);
        } catch { /* region may be degenerate while dragging */ }
        break;
      }
      case 'highlight':
        ctx.fillStyle = 'rgba(255, 235, 59, 0.41)';
        ctx.fillRect(x, y, w, h);
        break;
      case 'magnify': {
        const zoom = ann.zoom || 2;
        ctx.save();
        ctx.beginPath();
        ctx.ellipse(x + w / 2, y + h / 2, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
        ctx.clip();
        const sw = w / zoom, sh = h / zoom;
        ctx.drawImage(
          this.image,
          (x + w / 2 - sw / 2) / this.scale, (y + h / 2 - sh / 2) / this.scale,
          sw / this.scale, sh / this.scale,
          x, y, w, h
        );
        ctx.restore();
        ctx.beginPath();
        ctx.ellipse(x + w / 2, y + h / 2, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
        ctx.stroke();
        break;
      }
      case 'text': {
        ctx.font = `600 ${this.fontPx(ann)}px system-ui, sans-serif`;
        ctx.fillStyle = stroke;
        ctx.textBaseline = 'top';
        let ty = y;
        for (const line of String(ann.text || 'Text').split('\n')) {
          ctx.fillText(line, x, ty);
          ty += this.fontPx(ann) * 1.25;
        }
        break;
      }
      case 'tooltip': {
        const bg = fill || '#1F2937';
        const ts = Math.max(6, Math.min(Math.abs(w), Math.abs(h)) * 0.25);
        ctx.fillStyle = bg;
        ctx.beginPath();
        const r = 6;
        ctx.roundRect(x, y, w, h, r);
        ctx.fill();
        const tail = style.tail || 'bottom';
        ctx.beginPath();
        if (tail === 'bottom') { ctx.moveTo(x + w / 2 - ts, y + h); ctx.lineTo(x + w / 2 + ts, y + h); ctx.lineTo(x + w / 2, y + h + ts * 1.4); }
        if (tail === 'top') { ctx.moveTo(x + w / 2 - ts, y); ctx.lineTo(x + w / 2 + ts, y); ctx.lineTo(x + w / 2, y - ts * 1.4); }
        if (tail === 'left') { ctx.moveTo(x, y + h / 2 - ts); ctx.lineTo(x, y + h / 2 + ts); ctx.lineTo(x - ts * 1.4, y + h / 2); }
        if (tail === 'right') { ctx.moveTo(x + w, y + h / 2 - ts); ctx.lineTo(x + w, y + h / 2 + ts); ctx.lineTo(x + w + ts * 1.4, y + h / 2); }
        ctx.closePath(); ctx.fill();
        ctx.fillStyle = style.textColor || '#fff';
        ctx.font = `600 ${this.fontPx(ann)}px system-ui, sans-serif`;
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        ctx.fillText(String(ann.text || '…'), x + w / 2, y + h / 2, Math.abs(w) - 8);
        break;
      }
      case 'number': {
        const rr = Math.max(8, Math.min(Math.abs(w), Math.abs(h)) / 2);
        ctx.fillStyle = stroke;
        ctx.beginPath();
        ctx.arc(x + w / 2, y + h / 2, rr, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = style.textColor || '#fff';
        ctx.font = `700 ${rr}px system-ui, sans-serif`;
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        ctx.fillText(String(ann.value ?? '?'), x + w / 2, y + h / 2 + 1);
        break;
      }
      case 'cursor': {
        const s = Math.max(12, Math.min(Math.abs(w), Math.abs(h)));
        ctx.fillStyle = '#fff';
        ctx.strokeStyle = '#111827';
        ctx.lineWidth = Math.max(1, s / 12);
        ctx.beginPath();
        ctx.moveTo(x, y); ctx.lineTo(x, y + s); ctx.lineTo(x + s * 0.28, y + s * 0.75);
        ctx.lineTo(x + s * 0.45, y + s * 1.05); ctx.lineTo(x + s * 0.58, y + s * 0.98);
        ctx.lineTo(x + s * 0.42, y + s * 0.68); ctx.lineTo(x + s * 0.72, y + s * 0.68);
        ctx.closePath();
        ctx.fill(); ctx.stroke();
        break;
      }
      default: break;
    }
    ctx.restore();
  }

  drawSelection(ann) {
    const { ctx } = this;
    const { x, y, w, h } = this.px(ann);
    ctx.save();
    ctx.strokeStyle = '#2563eb';
    ctx.setLineDash([5, 4]);
    ctx.lineWidth = 1.2;
    ctx.strokeRect(Math.min(x, x + w) - 3, Math.min(y, y + h) - 3, Math.abs(w) + 6, Math.abs(h) + 6);
    ctx.setLineDash([]);
    ctx.fillStyle = '#2563eb';
    for (const hd of this.handles(ann)) {
      ctx.fillRect(hd.px - HANDLE_SIZE / 2, hd.py - HANDLE_SIZE / 2, HANDLE_SIZE, HANDLE_SIZE);
    }
    ctx.restore();
  }

  handles(ann) {
    const { x, y, w, h } = this.px(ann);
    if (POINT_TOOLS.has(ann.type)) {
      return [
        { id: 'p1', px: x, py: y },
        { id: 'p2', px: x + w, py: y + h },
      ];
    }
    return [
      { id: 'nw', px: x, py: y }, { id: 'n', px: x + w / 2, py: y }, { id: 'ne', px: x + w, py: y },
      { id: 'w', px: x, py: y + h / 2 }, { id: 'e', px: x + w, py: y + h / 2 },
      { id: 'sw', px: x, py: y + h }, { id: 's', px: x + w / 2, py: y + h }, { id: 'se', px: x + w, py: y + h },
    ];
  }

  drawCropOverlay() {
    const { ctx, canvas } = this;
    const r = this.cropRect;
    const x = Math.min(r.x0, r.x1) * canvas.width;
    const y = Math.min(r.y0, r.y1) * canvas.height;
    const w = Math.abs(r.x1 - r.x0) * canvas.width;
    const h = Math.abs(r.y1 - r.y0) * canvas.height;
    ctx.save();
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.beginPath();
    ctx.rect(0, 0, canvas.width, canvas.height);
    ctx.rect(x, y, w, h);
    ctx.fill('evenodd');
    ctx.strokeStyle = '#fff';
    ctx.setLineDash([6, 4]);
    ctx.strokeRect(x, y, w, h);
    ctx.restore();
  }

  // ---- interactions ----
  hitTest(pt) {
    // topmost first (reverse draw order)
    const ordered = [...this.annotations].sort((a, b) => (DRAW_ORDER[b.type] ?? 3) - (DRAW_ORDER[a.type] ?? 3));
    for (const ann of ordered) {
      const x0 = Math.min(ann.x, ann.x + ann.w) - 0.008;
      const y0 = Math.min(ann.y, ann.y + ann.h) - 0.008;
      const x1 = Math.max(ann.x, ann.x + ann.w) + 0.008;
      const y1 = Math.max(ann.y, ann.y + ann.h) + 0.008;
      if (pt.x >= x0 && pt.x <= x1 && pt.y >= y0 && pt.y <= y1) return ann;
    }
    return null;
  }

  handleAt(e) {
    const sel = this.selected();
    if (!sel) return null;
    const rect = this.canvas.getBoundingClientRect();
    const px = e.clientX - rect.left;
    const py = e.clientY - rect.top;
    for (const hd of this.handles(sel)) {
      if (Math.abs(px - hd.px) <= HANDLE_SIZE && Math.abs(py - hd.py) <= HANDLE_SIZE) return hd.id;
    }
    return null;
  }

  onDown(e) {
    if (!this.image) return;
    this.canvas.setPointerCapture(e.pointerId);
    const pt = this.toNorm(e);

    if (this.tool === 'crop') {
      this.cropRect = { x0: pt.x, y0: pt.y, x1: pt.x, y1: pt.y };
      this.drag = { kind: 'crop' };
      return;
    }
    if (this.tool === 'select') {
      const handle = this.handleAt(e);
      if (handle) {
        this.drag = { kind: 'resize', handle, start: pt, orig: { ...this.selected() } };
        return;
      }
      const hit = this.hitTest(pt);
      this.select(hit ? hit.id : null);
      if (hit) this.drag = { kind: 'move', start: pt, orig: { ...hit } };
      return;
    }

    // creation tools
    const ann = {
      id: `ann-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e6).toString(36)}`,
      type: this.tool,
      x: pt.x, y: pt.y, w: 0, h: 0,
      text: this.tool === 'tooltip' ? 'Tooltip' : this.tool === 'text' ? 'Text' : '',
      style: this.cb.defaultStyle ? this.cb.defaultStyle(this.tool) : {},
    };
    if (this.tool === 'number') ann.value = this.cb.nextNumber ? this.cb.nextNumber() : 1;
    if (this.tool === 'magnify') ann.zoom = 2;
    if (this.tool === 'blur') ann.radius = 8;
    this.annotations.push(ann);
    this.selectedId = ann.id;
    this.drag = { kind: 'create', start: pt, ann };
  }

  onMove(e) {
    if (!this.drag) return;
    const pt = this.toNorm(e);
    const d = this.drag;

    if (d.kind === 'crop') {
      this.cropRect.x1 = pt.x;
      this.cropRect.y1 = pt.y;
      this.render();
      return;
    }
    if (d.kind === 'create') {
      d.ann.w = pt.x - d.start.x;
      d.ann.h = pt.y - d.start.y;
      this.render();
      return;
    }
    const sel = this.selected();
    if (!sel) return;
    if (d.kind === 'move') {
      sel.x = d.orig.x + (pt.x - d.start.x);
      sel.y = d.orig.y + (pt.y - d.start.y);
      this.render();
    } else if (d.kind === 'resize') {
      this.resizeBy(sel, d, pt);
      this.render();
    }
  }

  resizeBy(ann, d, pt) {
    const dx = pt.x - d.start.x;
    const dy = pt.y - d.start.y;
    const o = d.orig;
    const h = d.handle;
    if (h === 'p1') { ann.x = o.x + dx; ann.y = o.y + dy; ann.w = o.w - dx; ann.h = o.h - dy; return; }
    if (h === 'p2') { ann.w = o.w + dx; ann.h = o.h + dy; return; }
    if (h.includes('w')) { ann.x = o.x + dx; ann.w = o.w - dx; }
    if (h.includes('e')) { ann.w = o.w + dx; }
    if (h.includes('n')) { ann.y = o.y + dy; ann.h = o.h - dy; }
    if (h.includes('s')) { ann.h = o.h + dy; }
  }

  onUp(e) {
    const d = this.drag;
    this.drag = null;
    if (!d) return;
    if (d.kind === 'crop') {
      const r = this.cropRect;
      this.cropRect = null;
      const rect = {
        x: Math.min(r.x0, r.x1), y: Math.min(r.y0, r.y1),
        w: Math.abs(r.x1 - r.x0), h: Math.abs(r.y1 - r.y0),
      };
      this.render();
      if (rect.w > 0.02 && rect.h > 0.02 && this.cb.onCrop) this.cb.onCrop(rect);
      return;
    }
    if (d.kind === 'create') {
      // degenerate drags get a sensible default size
      if (Math.abs(d.ann.w) < 0.01 && Math.abs(d.ann.h) < 0.01) {
        const defaults = { number: [0.05, 0.08], text: [0.2, 0.05], tooltip: [0.18, 0.07], cursor: [0.04, 0.06] };
        const [dw, dh] = defaults[d.ann.type] || [0.15, 0.1];
        d.ann.w = dw; d.ann.h = dh;
      }
      this.normalizeRect(d.ann);
      this.changed();
      this.select(d.ann.id);
      if ((d.ann.type === 'text' || d.ann.type === 'tooltip') && this.cb.onRequestText) {
        this.cb.onRequestText(d.ann);
      }
      return;
    }
    if (d.kind === 'move' || d.kind === 'resize') {
      const sel = this.selected();
      if (sel) this.normalizeRect(sel);
      this.changed();
    }
  }

  normalizeRect(ann) {
    if (POINT_TOOLS.has(ann.type)) return; // lines keep direction
    if (ann.w < 0) { ann.x += ann.w; ann.w = -ann.w; }
    if (ann.h < 0) { ann.y += ann.h; ann.h = -ann.h; }
  }

  onDblClick(e) {
    const hit = this.hitTest(this.toNorm(e));
    if (hit && (hit.type === 'text' || hit.type === 'tooltip') && this.cb.onRequestText) {
      this.select(hit.id);
      this.cb.onRequestText(hit);
    }
  }

  nudgeSelected(dx, dy) {
    const sel = this.selected();
    if (!sel) return false;
    sel.x += dx / this.canvas.width;
    sel.y += dy / this.canvas.height;
    this.changed();
    return true;
  }

  deleteSelected() {
    if (!this.selectedId) return false;
    this.annotations = this.annotations.filter((a) => a.id !== this.selectedId);
    this.select(null);
    this.changed();
    return true;
  }
}

window.AnnotationCanvas = AnnotationCanvas;
})();
