// ============================================================
// Crop UI — IconCrop + CropModal for the per-image crop popup
//
// CropModal renders into a portal at document.body so it escapes
// the header's backdrop-filter containing block (same trick as
// AuthModal / SignupPromptModal).
//
// Coordinates inside this file:
//   * rect (state)   — pixel coords in the DISPLAYED image space
//                      (i.e., the image as currently sized in the modal)
//   * onApply emits  — normalized {x,y,w,h} in 0..1 of the SOURCE image
//
// Storage on the item:
//   item.crop = { x, y, w, h }  — all 0..1
//
// Both presets and Custom share the same drag-and-resize machinery.
// Presets lock the aspect ratio while resizing; Custom is free.
// ============================================================

const ASPECT_RATIOS = [
  { id: '1:1',  label: '1:1',  ratio: 1 },
  { id: '4:3',  label: '4:3',  ratio: 4/3 },
  { id: '3:4',  label: '3:4',  ratio: 3/4 },
  { id: '3:2',  label: '3:2',  ratio: 3/2 },
  { id: '2:3',  label: '2:3',  ratio: 2/3 },
  { id: '5:4',  label: '5:4',  ratio: 5/4 },
  { id: '4:5',  label: '4:5',  ratio: 4/5 },
  { id: '16:9', label: '16:9', ratio: 16/9 },
  { id: '9:16', label: '9:16', ratio: 9/16 },
];
const ALL_RATIOS = [...ASPECT_RATIOS, { id: 'custom', label: 'Custom', ratio: null }];
window.ASPECT_RATIOS = ASPECT_RATIOS;

function ratioFromId(id) {
  const r = ASPECT_RATIOS.find(r => r.id === id);
  return r ? r.ratio : null;
}
window.ratioFromId = ratioFromId;

// Centered crop for a target ratio — used by bulk crop and as the
// initial preset rectangle in the modal.
function centeredCropPx(W, H, ratio) {
  const sourceRatio = W / H;
  if (sourceRatio > ratio) {
    const newW = H * ratio;
    return { x: (W - newW) / 2, y: 0, w: newW, h: H };
  }
  if (sourceRatio < ratio) {
    const newH = W / ratio;
    return { x: 0, y: (H - newH) / 2, w: W, h: newH };
  }
  return { x: 0, y: 0, w: W, h: H };
}
window.centeredCropPx = centeredCropPx;

// ────────────────────────────────────────────────────────────
// Icon used by the queue row
// ────────────────────────────────────────────────────────────
const IconCrop = (props) => (
  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
       strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <path d="M6 2v14a2 2 0 0 0 2 2h14" />
    <path d="M2 6h14a2 2 0 0 1 2 2v14" />
  </svg>
);
window.IconCrop = IconCrop;

// ────────────────────────────────────────────────────────────
// CropModal
// ────────────────────────────────────────────────────────────
function CropModal({ item, onApply, onClose }) {
  const { useState, useEffect, useRef, useMemo } = React;

  const [previewSrc, setPreviewSrc] = useState(null);   // URL for the <img>
  const [srcDims, setSrcDims]       = useState(null);   // { w, h } in source pixels
  const [stageDims, setStageDims]   = useState(null);   // { w, h } in displayed pixels (= img rendered size)
  const [ratioId, setRatioId]       = useState('custom');
  const [rect, setRect]             = useState(null);   // { x, y, w, h } in displayed pixels
  const stageRef = useRef(null);
  const imgRef   = useRef(null);

  // Load the image and figure out displayed dimensions.
  // For TIFFs we use item.preview (decoded PNG data URL); everything
  // else can use a blob URL from item.file.
  useEffect(() => {
    if (!item) return;
    let url = null, revoke = false;
    if (/\.tiff?$/i.test(item.name) || /tiff/i.test(item.file?.type || '')) {
      url = item.preview;
    } else {
      url = URL.createObjectURL(item.file);
      revoke = true;
    }
    setPreviewSrc(url);
    return () => { if (revoke && url) URL.revokeObjectURL(url); };
  }, [item]);

  // Measure the IMAGE's rendered size (not the stage's). The stage can
  // be a hair wider/taller than the image due to inline-block layout
  // quirks, which previously caused (a) the dim mask to bleed past the
  // image edge and (b) the crop rect to be unable to reach the actual
  // image edges. Measuring off the img and forcing the stage to match
  // exactly eliminates both.
  const measureImg = () => {
    const img = imgRef.current;
    if (!img || !img.naturalWidth) return;
    const r = img.getBoundingClientRect();
    if (r.width > 0 && r.height > 0) {
      setStageDims({ w: r.width, h: r.height });
    }
  };

  // When the <img> loads, capture source dims and trigger a measure on
  // the next frame so layout has settled.
  const onImgLoad = (e) => {
    setSrcDims({ w: e.target.naturalWidth, h: e.target.naturalHeight });
    requestAnimationFrame(measureImg);
  };

  // Re-measure on any size change (window resize, viewport rotation).
  useEffect(() => {
    if (!imgRef.current) return;
    const img = imgRef.current;
    const ro = new ResizeObserver(() => measureImg());
    ro.observe(img);
    return () => ro.disconnect();
  }, [previewSrc]);

  // Re-seed the rectangle whenever stage dims or source dims become
  // available. If item already has a crop, start from there. Otherwise
  // start with the entire image (which Custom-ratio).
  useEffect(() => {
    if (!stageDims || !srcDims) return;
    if (item?.crop) {
      // Convert normalized item.crop → displayed pixels
      setRect({
        x: item.crop.x * stageDims.w,
        y: item.crop.y * stageDims.h,
        w: item.crop.w * stageDims.w,
        h: item.crop.h * stageDims.h,
      });
    } else {
      setRect({ x: 0, y: 0, w: stageDims.w, h: stageDims.h });
    }
  }, [stageDims, srcDims]);

  // Apply a preset aspect ratio to the current rectangle, keeping
  // the centroid stable and fitting inside the image.
  function applyRatio(rid) {
    setRatioId(rid);
    if (rid === 'custom' || !stageDims) return;
    const ratio = ratioFromId(rid);
    if (!ratio) return;

    setRect(prev => {
      const r = prev || { x: 0, y: 0, w: stageDims.w, h: stageDims.h };
      const cx = r.x + r.w / 2;
      const cy = r.y + r.h / 2;
      // Largest rectangle that fits the image at this ratio
      let w, h;
      const stageRatio = stageDims.w / stageDims.h;
      if (stageRatio > ratio) { h = stageDims.h; w = h * ratio; }
      else                    { w = stageDims.w; h = w / ratio; }
      // Try to keep centroid in place, but clamp inside the image
      let x = cx - w / 2;
      let y = cy - h / 2;
      x = Math.max(0, Math.min(stageDims.w - w, x));
      y = Math.max(0, Math.min(stageDims.h - h, y));
      return { x, y, w, h };
    });
  }

  // ─────── Pointer / drag handling ───────
  // dragMode: null | 'move' | 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'
  const dragRef = useRef(null);

  function onPointerDown(mode) {
    return (e) => {
      e.preventDefault();
      e.stopPropagation();
      if (!stageDims || !rect) return;
      const stageRect = stageRef.current.getBoundingClientRect();
      dragRef.current = {
        mode,
        startX: e.clientX,
        startY: e.clientY,
        startRect: { ...rect },
        stageRect,
      };
      const target = e.currentTarget;
      try { target.setPointerCapture(e.pointerId); } catch (_) {}
    };
  }

  function onPointerMove(e) {
    if (!dragRef.current || !stageDims) return;
    const d = dragRef.current;
    const dx = e.clientX - d.startX;
    const dy = e.clientY - d.startY;
    const s = d.startRect;
    const minSize = Math.max(20, Math.min(stageDims.w, stageDims.h) * 0.05);
    const lockedRatio = ratioId === 'custom' ? null : ratioFromId(ratioId);

    let { x, y, w, h } = s;

    if (d.mode === 'move') {
      x = s.x + dx; y = s.y + dy;
      x = Math.max(0, Math.min(stageDims.w - w, x));
      y = Math.max(0, Math.min(stageDims.h - h, y));
    } else {
      // Resize from a handle
      let left = s.x, top = s.y, right = s.x + s.w, bottom = s.y + s.h;
      if (d.mode.includes('w')) left   = Math.min(s.x + s.w - minSize, s.x + dx);
      if (d.mode.includes('e')) right  = Math.max(s.x + minSize, s.x + s.w + dx);
      if (d.mode.includes('n')) top    = Math.min(s.y + s.h - minSize, s.y + dy);
      if (d.mode.includes('s')) bottom = Math.max(s.y + minSize, s.y + s.h + dy);
      // Clamp to image
      left   = Math.max(0, left);
      top    = Math.max(0, top);
      right  = Math.min(stageDims.w, right);
      bottom = Math.min(stageDims.h, bottom);

      w = right - left; h = bottom - top;

      // Locked aspect — anchor the OPPOSITE corner/edge of the drag
      if (lockedRatio) {
        // Decide which dimension drives the constraint based on which
        // edge(s) are being dragged. Use the larger delta as authority.
        const dragsX = d.mode.includes('w') || d.mode.includes('e');
        const dragsY = d.mode.includes('n') || d.mode.includes('s');
        if (dragsX && dragsY) {
          // Corner drag — pick the dim that grew more (proportionally)
          if (w / lockedRatio > h) h = w / lockedRatio;
          else                     w = h * lockedRatio;
        } else if (dragsX) {
          h = w / lockedRatio;
        } else {
          w = h * lockedRatio;
        }
        // Re-anchor based on drag direction
        if (d.mode.includes('w')) left = right - w;
        else if (!d.mode.includes('e')) {
          // n / s with no x drag — center horizontally on original centroid
          const cx = s.x + s.w / 2;
          left = cx - w / 2;
        }
        if (d.mode.includes('n')) top = bottom - h;
        else if (!d.mode.includes('s')) {
          const cy = s.y + s.h / 2;
          top = cy - h / 2;
        }
        // Clamp again after aspect re-anchor
        left = Math.max(0, Math.min(stageDims.w - w, left));
        top  = Math.max(0, Math.min(stageDims.h - h, top));
        // If clamping pushed the rect out of fit, recompute against bounds
        if (left + w > stageDims.w) w = stageDims.w - left;
        if (top + h > stageDims.h)  h = stageDims.h - top;
        // Re-fit aspect after potential clamp
        if (lockedRatio) {
          if (w / lockedRatio <= h) h = w / lockedRatio;
          else                      w = h * lockedRatio;
        }
      }

      x = left; y = top;
    }
    setRect({ x, y, w, h });
  }

  function onPointerUp(e) {
    if (!dragRef.current) return;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
    dragRef.current = null;
  }

  // ─────── Apply / cancel ───────
  function apply() {
    if (!stageDims || !rect) { onClose?.(); return; }
    // Convert displayed-pixel rect → normalized 0..1
    const nx = rect.x / stageDims.w;
    const ny = rect.y / stageDims.h;
    const nw = rect.w / stageDims.w;
    const nh = rect.h / stageDims.h;
    // If essentially the full image, treat as "no crop"
    const fullImage = nx < 0.001 && ny < 0.001 && nw > 0.999 && nh > 0.999;
    onApply?.(fullImage ? null : { x: nx, y: ny, w: nw, h: nh });
    onClose?.();
  }

  function reset() {
    if (!stageDims) return;
    setRatioId('custom');
    setRect({ x: 0, y: 0, w: stageDims.w, h: stageDims.h });
  }

  // ESC to close
  useEffect(() => {
    function onKey(e) {
      if (e.key === 'Escape') onClose?.();
      else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) apply();
    }
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [rect, stageDims]);

  if (!item) return null;
  return ReactDOM.createPortal(
    <div className="modal-backdrop" onClick={(e) => { if (e.target.classList.contains('modal-backdrop')) onClose?.(); }}>
      <div className="modal crop-modal" onClick={(e) => e.stopPropagation()}>
        <div className="crop-modal-head">
          <h2>Crop image</h2>
          <button className="icon-btn crop-close" onClick={onClose} title="Close">✕</button>
        </div>

        <div className="crop-ratios">
          {ALL_RATIOS.map(r => (
            <button
              key={r.id}
              type="button"
              className={`crop-chip ${ratioId === r.id ? 'active' : ''}`}
              onClick={() => applyRatio(r.id)}
            >
              {r.label}
            </button>
          ))}
        </div>

        <div className="crop-stage-wrap">
          <div
            className="crop-stage"
            ref={stageRef}
            style={stageDims ? { width: stageDims.w + 'px', height: stageDims.h + 'px' } : undefined}
            onPointerMove={onPointerMove}
            onPointerUp={onPointerUp}
            onPointerCancel={onPointerUp}
          >
            {previewSrc && (
              <img ref={imgRef} src={previewSrc} alt="" className="crop-img" onLoad={onImgLoad} draggable={false} />
            )}
            {rect && stageDims && (
              <>
                {/* Dark mask outside the rect (four bars around it) */}
                <div className="crop-mask crop-mask-top"
                     style={{ left: 0, top: 0, width: '100%', height: rect.y + 'px' }} />
                <div className="crop-mask crop-mask-bottom"
                     style={{ left: 0, top: (rect.y + rect.h) + 'px',
                              width: '100%', height: (stageDims.h - rect.y - rect.h) + 'px' }} />
                <div className="crop-mask crop-mask-left"
                     style={{ left: 0, top: rect.y + 'px',
                              width: rect.x + 'px', height: rect.h + 'px' }} />
                <div className="crop-mask crop-mask-right"
                     style={{ left: (rect.x + rect.w) + 'px', top: rect.y + 'px',
                              width: (stageDims.w - rect.x - rect.w) + 'px', height: rect.h + 'px' }} />

                {/* The crop rectangle itself */}
                <div
                  className="crop-rect"
                  style={{ left: rect.x + 'px', top: rect.y + 'px',
                           width: rect.w + 'px', height: rect.h + 'px' }}
                  onPointerDown={onPointerDown('move')}
                >
                  {/* Rule-of-thirds guides */}
                  <div className="crop-thirds" aria-hidden>
                    <div className="crop-third-v" style={{ left: '33.333%' }} />
                    <div className="crop-third-v" style={{ left: '66.666%' }} />
                    <div className="crop-third-h" style={{ top: '33.333%' }} />
                    <div className="crop-third-h" style={{ top: '66.666%' }} />
                  </div>
                  {/* Corner handles always */}
                  <span className="crop-handle nw" onPointerDown={onPointerDown('nw')} />
                  <span className="crop-handle ne" onPointerDown={onPointerDown('ne')} />
                  <span className="crop-handle sw" onPointerDown={onPointerDown('sw')} />
                  <span className="crop-handle se" onPointerDown={onPointerDown('se')} />
                  {/* Edge handles only for Custom (free aspect) */}
                  {ratioId === 'custom' && (
                    <>
                      <span className="crop-handle n" onPointerDown={onPointerDown('n')} />
                      <span className="crop-handle s" onPointerDown={onPointerDown('s')} />
                      <span className="crop-handle e" onPointerDown={onPointerDown('e')} />
                      <span className="crop-handle w" onPointerDown={onPointerDown('w')} />
                    </>
                  )}
                </div>
              </>
            )}
          </div>
        </div>

        <div className="crop-modal-actions">
          <button className="btn btn-ghost" onClick={reset}>Reset</button>
          <div style={{flex:1}} />
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" onClick={apply}>Apply crop</button>
        </div>
      </div>
    </div>,
    document.body
  );
}
window.CropModal = CropModal;
