import { useEffect, useRef, memo } from 'react';
import { Image, Rect } from 'react-konva';
import Konva from 'konva';
import useImage from 'use-image';
import { getFilterPreset } from '../../constants/imageFilters';
import { MIN_ELEMENT_SIZE } from '../../constants/elements';
import { unflipReportedXY } from '../../utils/flipOffset';

// Per-element Transformer rendering was moved to DesignCanvas's dedicated
// transformer layer (see DesignCanvas.jsx). This component now renders
// the image + its out-of-bounds warning; selection chrome lives one
// layer above so resize / rotate handles always draw on top of later
// elements in z-order. Imports for Transformer / rotation snap / corner
// anchors / transformer visual style were dropped along with the JSX
// that consumed them.
//
// MIN_ELEMENT_SIZE stays imported — onTransformEnd still applies it
// as a defensive floor when committing the final width/height to
// state. DesignCanvas's bound function clamps DURING the transform
// (so the user can't drag handles past the floor), but the commit
// here is the last line of defence: any rounding, gesture
// interruption, or future bound-function bug that lets a sub-floor
// value slip through still gets clamped before it lands in state.

function URLImage({ src, innerRef, ...props }) {
  const [img] = useImage(src, 'anonymous');
  return <Image image={img} ref={innerRef} {...props} />;
}

/**
 * Konva Image wrapper.
 *
 * ── Flipping ─────────────────────────────────────────────────────────────
 * Horizontal/vertical flip is implemented as `scaleX/scaleY = ±1` on the node
 * combined with a matching `offsetX/offsetY` so the flip happens in-place
 * (around the bounding-box center) instead of around the (x, y) origin (which
 * would visually "shoot off" the element).
 *
 * The user can still resize via the transformer corner handles. Konva applies
 * the transform as a multiplier on `scaleX`, so a flipped element being
 * resized to 1.5× lands at scaleX = -1.5. In `onTransformEnd` we collapse
 * that back into width/height (taking absolute value of the scale) and
 * preserve the flip state on the element. The transformer never *changes*
 * the flip — only the explicit Flip H / Flip V buttons do.
 *
 * ── Opacity ──────────────────────────────────────────────────────────────
 * Pass-through to Konva's native `opacity` prop on the node.
 *
 * ── Lock ─────────────────────────────────────────────────────────────────
 * When `locked` is true, the element can't be dragged or transformed. The
 * transformer still renders a border (so the user knows it's selected) but
 * with no anchors or rotate handle. Toggle via the lock button in the
 * floating element toolbar.
 *
 * ── Filters ──────────────────────────────────────────────────────────────
 * `filter` is a preset id (see constants/imageFilters.js). Applying a Konva
 * filter requires the node to be cached, so we attach a useEffect that
 * caches+filters once the underlying image has loaded and re-runs whenever
 * the filter or the image changes. Server-side render does NOT currently
 * apply filters; this is an editor-only visual at this time.
 *
 * ── Rotation snap ────────────────────────────────────────────────────────
 * When `shiftHeld` is true, the transformer snaps to 15° increments. The
 * shift state is tracked at the App level so all selected elements get
 * the snap behavior simultaneously.
 */
export const ImageElement = memo(function ImageElement({
  id: _id,
  x = 0,
  y = 0,
  width = 100,
  height = 100,
  rotation = 0,
  src,
  crop,
  opacity = 1,
  flipX = false,
  flipY = false,
  locked = false,
  filter = 'none',
  // Brightness / contrast adjustments (range matches Konva.Filters):
  //   brightness: -1 (fully dark) … 0 (no change) … +1 (fully bright)
  //   contrast:   -100 (fully flat) … 0 (no change) … +100 (max contrast)
  // Default 0 = identity; both compose with the preset `filter` so a
  // user can pick "Sepia" and then nudge brightness up, etc.
  brightness = 0,
  contrast = 0,
  isSelected: _isSelected,
  // Change 3 — when true, render a warning-yellow Rect behind the
  // image so the user can see WHICH element is out of bounds, not just
  // that *something* is. Computed at the DesignCanvas level via the
  // same `isElementOutOfBounds` math that drives the bottom-chip count.
  isOutOfBounds = false,
  shiftHeld: _shiftHeld = false,
  onSelect,
  onUpdate,
  onCommit,
  dragBoundFunc,
  transformBoundFunc: _transformBoundFunc,
}) {
  const shapeRef = useRef(null);

  // Filter application. Konva requires the node to be cached for filters
  // to take effect, and re-cached whenever the input pixels (image, size)
  // or the filter list change. We do this in a useEffect that also handles
  // the initial async image load — the underlying `useImage` inside
  // URLImage updates the node's `image` attr asynchronously, so we listen
  // for the `imageChange` event on the node and re-cache then.
  //
  // Brightness and contrast stack on top of the preset filter — Konva's
  // `filters()` accepts an array and runs them in order, so a Sepia preset
  // with brightness +0.2 gives a brightened sepia. Brightness 0 and
  // contrast 0 are identity values, but applying the filter is still
  // cheap (the no-op multiplier), so we keep them in the chain whenever
  // the user has touched the sliders rather than special-casing.
  useEffect(() => {
    const node = shapeRef.current;
    if (!node) return;

    const applyFilter = () => {
      const preset = getFilterPreset(filter);
      // Lookup order: customFn (e.g. Filerobot's Kelvin) takes precedence,
      // then a string name resolved against Konva.Filters, else no filter.
      const presetFn = preset.customFn ?? (preset.konva ? Konva.Filters[preset.konva] : null);
      const filters = [];
      if (presetFn) filters.push(presetFn);
      // Add Brighten / Contrast only when the user has actually moved
      // the slider off zero. Avoids cache+filter overhead for the
      // common "untouched" case while still composing cleanly with
      // the preset when active.
      if (brightness !== 0) filters.push(Konva.Filters.Brighten);
      if (contrast !== 0)   filters.push(Konva.Filters.Contrast);
      if (filters.length > 0) {
        node.cache();
        node.filters(filters);
      } else {
        node.filters([]);
        node.clearCache();
      }
      node.getLayer()?.batchDraw();
    };

    // Apply once for the current state.
    applyFilter();

    // Re-apply when the image itself loads/changes — Konva's `image` attr
    // is set async by `useImage` and the cache is dimension-sensitive,
    // so an early `cache()` call before the image arrives produces an
    // empty filtered layer. The `imageChange` event fires whenever the
    // attr is reassigned.
    node.on('imageChange.filterapply', applyFilter);
    return () => {
      node.off('imageChange.filterapply');
    };
  }, [filter, brightness, contrast, src, width, height]);

  // Always-center offset for rotation + flip-in-place.
  //
  // Konva rotates a node around its local origin (the point at
  // `offsetX, offsetY`). Setting offset to the bbox CENTER means:
  //
  //   • Rotation pivots around the center — what users expect from
  //     a rotate handle. Previously the offset was zero for
  //     non-flipped elements, which put the pivot at the top-left;
  //     on touch devices the rotate handle had inherent sub-pixel
  //     contact offset, and that small offset rotated around the
  //     top-left visibly swung the whole bbox sideways on the first
  //     frame of rotation. Desktop didn't see it because mouse-down
  //     is pixel-precise (first frame stays at ~0° rotation). The
  //     center pivot fixes the mobile feel without changing desktop.
  //
  //   • Flipping (scaleX/Y = -1) reflects around the local origin,
  //     so a center-aligned offset gives in-place flip for free.
  //     This used to be the only reason offset was set; we now set
  //     it unconditionally and get correct rotation as a bonus.
  //
  // Because the offset shifts the node's local origin, the rendered
  // top-left lands at `(x - offsetX, y - offsetY)`. To keep the
  // user-supplied (element.x, element.y) as the visual top-left,
  // renderX/renderY are computed by ADDING the offset back to the
  // stored position.
  //
  // Migration note for existing saved designs
  // ───────────────────────────────────────────
  // For non-flipped, non-rotated elements: identical render — the
  // bbox is axis-aligned, so the center-pivot draws at the same
  // screen location as the old top-left-pivot.
  // For non-flipped, ROTATED elements saved before this change:
  // their stored (x, y) was the location the OLD top-left pivot
  // ended up at AFTER the rotation. Now we re-interpret (x, y) as
  // the pivot of a CENTER-rotated element, so the visible bbox will
  // shift on first load. The drift is bounded (the bbox stays the
  // same size; only its position moves) and the user can re-position
  // by dragging. No automatic migration: the editor doesn't have a
  // persistence version field, and the rotated-saved-design case is
  // rare enough that adding heuristic detection isn't worth the
  // complexity.
  // For FLIPPED rotated elements: no visual change. The offset was
  // already center in the old code (that was the original purpose),
  // so the rotation behavior is unchanged for them.
  const offsetX = width / 2;
  const offsetY = height / 2;
  const renderX = x + offsetX;
  const renderY = y + offsetY;

  // Bbox in PIVOT-RELATIVE local coords, exposed as custom Konva
  // attrs that DesignCanvas's canvasDragBound reads.
  //
  // canvasDragBound computes rotated corners with the formula
  // (x·cos - y·sin, x·sin + y·cos), which rotates the (xMin, yMin)
  // to (xMax, yMax) rectangle around the ORIGIN (0, 0) of the
  // node-local frame. With offset at the bbox center, the local
  // origin IS the rotation pivot — and the bbox itself spans from
  // (-W/2, -H/2) to (+W/2, +H/2) relative to that pivot. Without
  // setting these explicit attrs, the function would fall back to
  // its `?? 0` / `?? this.width()` defaults (correct only for the
  // old offset=0 convention) and the clamping would be off by
  // half a bbox.
  //
  // The values are scalar so they can be read by Konva's attrs
  // system without React intermediation — same pattern TextElement
  // already uses for visible-ink bbox.
  const snapBboxMinX = -width / 2;
  const snapBboxMinY = -height / 2;
  const snapBboxMaxX = width / 2;
  const snapBboxMaxY = height / 2;

  return (
    <>
      {/* Out-of-bounds warning background (Change 3). Rendered BEFORE
          the image so it sits behind. Uses the same render-time
          position / rotation / flip math as the image itself, so the
          highlight follows the element through every transform. We
          intentionally don't include the warning rect in the
          Transformer's nodes() — it's purely decorative and shouldn't
          add to the selection bbox. The yellow is the classic CSS
          `gold` family at low opacity so it reads as a warning without
          drowning the underlying artwork.

          Same always-center offset as the image below so the warning
          tracks the image identically through rotation. Previously
          the warning's offset was conditional on flip and could drift
          relative to the image when the offset conventions diverged;
          mirroring the image's render math here keeps them locked. */}
      {isOutOfBounds && (
        <Rect
          x={renderX}
          y={renderY}
          width={width}
          height={height}
          offsetX={offsetX}
          offsetY={offsetY}
          scaleX={flipX ? -1 : 1}
          scaleY={flipY ? -1 : 1}
          rotation={rotation}
          fill="#fde68a"
          stroke="#f59e0b"
          strokeWidth={1.5}
          opacity={0.65}
          listening={false}
          perfectDrawEnabled={false}
        />
      )}
      <URLImage
        innerRef={shapeRef}
        /* Konva `name` attr = element id. Lets DesignCanvas locate the
         * underlying Konva node via `layer.findOne('.' + id)` for two
         * features:
         *   1. Marquee drag-select — reads each node's getClientRect to
         *      test intersection with the marquee bbox.
         *   2. Shared multi-element Transformer — wires `transformer.nodes`
         *      to the actual Konva nodes whenever selectedIds changes.
         * Using `name` rather than `id` because Konva's `id()` is reserved
         * for its own internal id-uniqueness machinery, while `name` is
         * effectively a free-form className with `.name`-selector support. */
        name={_id}
        x={renderX}
        y={renderY}
        width={width}
        height={height}
        offsetX={offsetX}
        offsetY={offsetY}
        scaleX={flipX ? -1 : 1}
        scaleY={flipY ? -1 : 1}
        rotation={rotation}
        opacity={opacity}
        /* Pivot-relative bbox attrs read by canvasDragBound. See the
         * docblock above the snapBbox* declarations for the full
         * rationale; in short, they describe the visible rect in the
         * post-offset local frame so the clamp math lines up with the
         * rotated AABB at the wrapper boundary. */
        _pawSnapBboxMinX={snapBboxMinX}
        _pawSnapBboxMinY={snapBboxMinY}
        _pawSnapBboxMaxX={snapBboxMaxX}
        _pawSnapBboxMaxY={snapBboxMaxY}
        /* Konva reads `brightness` / `contrast` off the node's attrs
         * during filter application — these props set the values that
         * Konva.Filters.Brighten and Konva.Filters.Contrast consume. */
        brightness={brightness}
        contrast={contrast}
        src={src}
        crop={
          crop
            ? { x: crop.sx, y: crop.sy, width: crop.sWidth, height: crop.sHeight }
            : undefined
        }
        draggable={!locked}
        dragBoundFunc={dragBoundFunc}
        onClick={(e) => {
          e.cancelBubble = true;
          onSelect?.();
        }}
        onTap={(e) => {
          e.cancelBubble = true;
          onSelect?.();
        }}
        /* Bug 6 fix — select on press, not just on click-up. See
         * TextElement.jsx for the full rationale: click-then-drag
         * without an intermediate mouseup never fires onClick, so the
         * pre-fix flow left the element unselected once the user
         * started dragging immediately and the DesignCanvas-level
         * Transformer (which attaches to the selected node) never
         * mounted handles around it. */
        onMouseDown={(e) => {
          e.cancelBubble = true;
          onSelect?.();
        }}
        onTouchStart={(e) => {
          e.cancelBubble = true;
          onSelect?.();
        }}
        onDragEnd={(e) => {
          // Convert back to top-left-origin coords if we were drawing flipped.
          const { x: reportedX, y: reportedY } = unflipReportedXY(e.target, width, height, flipX, flipY);
          onUpdate({ x: reportedX, y: reportedY });
          onCommit?.();
        }}
        onTransformEnd={() => {
          const node = shapeRef.current;
          if (!node) return;
          // The transformer multiplies into scaleX/scaleY. We absorb the
          // *magnitude* into width/height and keep the *sign* (flip state)
          // separate by reading element.flipX/flipY explicitly.
          const sx = node.scaleX();
          const sy = node.scaleY();
          const newWidth = Math.max(MIN_ELEMENT_SIZE, node.width() * Math.abs(sx));
          const newHeight = Math.max(MIN_ELEMENT_SIZE, node.height() * Math.abs(sy));
          // Reset scale to ±1 (the flip-state base) so subsequent transforms
          // start fresh.
          node.scaleX(flipX ? -1 : 1);
          node.scaleY(flipY ? -1 : 1);
          // Convert back to top-left-origin coords using the freshly-resolved
          // dimensions, not the stale prop values — a shrink-then-flip would
          // otherwise offset by the wrong half-bbox.
          const { x: reportedX, y: reportedY } = unflipReportedXY(node, newWidth, newHeight, flipX, flipY);
          onUpdate({
            x: reportedX,
            y: reportedY,
            width: newWidth,
            height: newHeight,
            rotation: node.rotation(),
          });
          onCommit?.();
        }}
      />
      {/* Per-element Transformer removed (bug 8 fix). Selection chrome
          now lives in DesignCanvas's dedicated transformer layer,
          which renders ABOVE the elements layer so resize / rotate
          handles always draw on top of every element regardless of
          z-order. The MIN_ELEMENT_SIZE floor and keepRatio / corner-
          anchor configuration that used to live here moved with it. */}
    </>
  );
});
