import { useEffect, useRef, useState } from 'react';

/**
 * Produce a data URL for the cropped sub-region of an image element,
 * suitable for use as the `src` of a thumbnail <img> in the layers
 * panel (or anywhere else we render a small representative bitmap of
 * a cropped photo).
 *
 * Why this exists
 * ───────────────
 * Image elements with a crop applied store the crop separately from
 * the source pixels:
 *
 *   element.src   = URL of the ORIGINAL (uncropped) image
 *   element.crop  = { sx, sy, sWidth, sHeight }  // sub-region to render
 *
 * Konva.Image consumes both via its native `crop` attr — it renders
 * only the sub-region. But anywhere else we display the image as a
 * plain HTML <img> (the LayersPanel thumbnail, the recent-uploads
 * thumbnails), `<img src={element.src} />` shows the WHOLE original,
 * not the crop. The user sees their cropped photo on canvas but the
 * uncropped version in the layers panel, which is confusing — the
 * layer-panel thumbnail is meant to read as "this is the layer I'm
 * looking at," and if the cropped face becomes a tiny corner of an
 * un-cropped landscape in the thumbnail, the user can't pick out the
 * layer at a glance.
 *
 * This hook closes the gap by rendering the cropped sub-region into
 * an offscreen <canvas> and returning its toDataURL(). The data URL
 * is a drop-in replacement for `element.src` in any <img> we render.
 *
 * Fast path
 * ─────────
 * Elements WITHOUT a crop bypass canvas work entirely — the hook
 * returns `element.src` as-is. There's no perf cost for the common
 * case of uncropped photos. Canvas work only runs when:
 *
 *   1. The element has a non-null `element.crop`, AND
 *   2. The (src, crop) signature differs from the last successful
 *      render. The signature lives on the ref alongside the data URL
 *      so re-renders triggered by unrelated state changes don't redo
 *      the rasterization.
 *
 * Returns
 * ───────
 * The string passed back is always usable as an <img src>. While the
 * canvas render is in flight we return the previous successful URL
 * (or `element.src` if there isn't one yet) so the thumbnail doesn't
 * flicker to an empty placeholder mid-update. On error (image fails
 * to load, taint from cross-origin, etc.) we fall back to
 * `element.src` and let the browser show whatever it would have
 * shown before this hook existed.
 *
 * Implementation notes
 * ────────────────────
 * • The image is loaded with `crossOrigin = 'anonymous'` so the
 *   canvas remains untainted and `toDataURL()` succeeds for both
 *   local blob: URLs and remote https URLs (when the server sends
 *   permissive CORS headers, which our upload backend does). If a
 *   future URL source is added that doesn't support CORS, the catch
 *   handler quietly falls back to the original src — no crash, just
 *   no crop in the thumbnail for that one image.
 *
 * • Output dimensions are clamped via the `maxSize` parameter (default
 *   96px) so the data URL stays small even for high-resolution source
 *   images. The thumbnail viewport is 24px square, so 96px is 4x the
 *   display size — enough resolution for HiDPI screens without
 *   bloating the data URL.
 *
 * • The aspect ratio of the output matches the crop region's aspect
 *   ratio (sWidth / sHeight). The <img> in the layers panel uses
 *   `object-fit: cover`, so a non-square crop will be center-cropped
 *   inside the square 24px viewport. That matches how Konva renders
 *   the cropped photo on canvas (the canvas shows the entire crop
 *   region within the element's bounds), so the thumbnail and the
 *   on-canvas photo share visual content even if neither's aspect
 *   matches the row's icon slot exactly.
 *
 * • An AbortController-style cancellation flag handles unmount during
 *   in-flight loads: setState after unmount is a memory leak warning
 *   in dev and an undefined-behavior risk in production. The
 *   cancelled-flag pattern is the lightest-weight way to short-circuit
 *   the load-then-setState chain without requiring AbortController
 *   support on the underlying `new Image()` API (which doesn't
 *   accept one).
 */
export function useCroppedThumbnail(element, maxSize = 96) {
  const [thumb, setThumb] = useState(() => {
    // Initial value: if there's no crop, the source IS the thumbnail.
    // If there is a crop, return null so the consumer can show a
    // placeholder or fall back until the canvas render completes.
    if (!element) return null;
    if (!element.crop || !element.src) return element.src ?? null;
    return element.src ?? null;
  });

  // Signature cache. Avoids redoing canvas work when the consumer
  // re-renders for reasons unrelated to the cropped image — e.g. the
  // selection state changed, or a sibling layer was added. Keyed on
  // (src, crop) because the same source can be cropped differently
  // across duplicated layers, and the same crop value can be applied
  // to a freshly-uploaded source.
  const lastRenderRef = useRef(null);

  useEffect(() => {
    if (!element) return undefined;
    const { src, crop } = element;

    // No source → nothing to render. Clear state so a stale thumb
    // from a previous element doesn't leak across.
    if (!src) {
      setThumb(null);
      lastRenderRef.current = null;
      return undefined;
    }

    // No crop → the source IS the thumbnail. Bypass canvas entirely.
    if (!crop) {
      setThumb(src);
      lastRenderRef.current = null; // clear so a future crop re-renders
      return undefined;
    }

    // Build the signature. Object equality on crop would force a
    // re-render every time the parent re-renders (new object each
    // time), but the actual values rarely change — stringifying
    // gives us value equality without committing to a deep-equal
    // helper.
    const signature = `${src}::${crop.sx}::${crop.sy}::${crop.sWidth}::${crop.sHeight}`;
    if (lastRenderRef.current?.signature === signature) {
      // Already rendered this exact (src, crop) combination. The
      // cached data URL is in state already; nothing to do.
      return undefined;
    }

    let cancelled = false;
    const img = new Image();
    img.crossOrigin = 'anonymous';

    img.onload = () => {
      if (cancelled) return;
      try {
        // Compute output dimensions: keep the crop's aspect ratio,
        // clamp the longest side to maxSize. Going smaller than the
        // source crop is fine — the canvas downsamples cleanly. Going
        // LARGER would blow up file pixels with no visual benefit.
        const sW = Math.max(1, crop.sWidth);
        const sH = Math.max(1, crop.sHeight);
        const scale = Math.min(1, maxSize / Math.max(sW, sH));
        const outW = Math.max(1, Math.round(sW * scale));
        const outH = Math.max(1, Math.round(sH * scale));

        const canvas = document.createElement('canvas');
        canvas.width = outW;
        canvas.height = outH;
        const ctx = canvas.getContext('2d');
        if (!ctx) {
          // No 2D context (vanishingly rare — Safari private mode in
          // some old versions, headless browsers without canvas). Fall
          // back to the original src so the thumbnail still shows
          // SOMETHING, just uncropped.
          setThumb(src);
          return;
        }

        // drawImage(sx, sy, sW, sH, dx, dy, dW, dH) — the 9-arg form
        // maps a source sub-rect onto a destination rect. This is
        // exactly the same math Konva does internally via the `crop`
        // attribute on Image nodes, so the thumbnail content matches
        // the on-canvas photo for any crop value.
        ctx.drawImage(img, crop.sx, crop.sy, sW, sH, 0, 0, outW, outH);

        const dataUrl = canvas.toDataURL('image/png');
        lastRenderRef.current = { signature, dataUrl };
        setThumb(dataUrl);
      } catch {
        // Canvas was tainted (cross-origin without CORS), toDataURL
        // failed, or some other unexpected failure. Fall back to the
        // original source so the thumbnail still renders SOMETHING —
        // just without the crop applied. Better degradation than a
        // missing image.
        if (!cancelled) setThumb(src);
      }
    };

    img.onerror = () => {
      // Image failed to load entirely. Fall back to the raw src; the
      // <img> tag in the consumer will surface the same load error
      // there. The hook's job is to provide a string; whether it
      // actually loads is the renderer's problem.
      if (!cancelled) setThumb(src);
    };

    img.src = src;

    return () => {
      cancelled = true;
    };
    // We deliberately depend on the granular crop fields rather than
    // the crop object itself — the App-level state replaces the crop
    // object on every update, so depending on `element.crop`
    // (reference) would re-run the effect every render. Field-level
    // deps re-run only when the actual values change.
  }, [
    element?.src,
    element?.crop?.sx,
    element?.crop?.sy,
    element?.crop?.sWidth,
    element?.crop?.sHeight,
    maxSize,
  ]);

  return thumb;
}
