import { memo, useState } from 'react';
import { useCroppedThumbnail } from '../../hooks/useCroppedThumbnail';
import '../../styles/LayersPanel.css';

/**
 * Layers panel.
 *
 * Lists all elements on the canvas in render order (last in array = topmost).
 * Click selects, shift-click toggles a row's membership in a multi-selection
 * set (S3) — when more than one is selected, the panel shows a "Delete N
 * selected" affordance and bulk-deletes on click.
 *
 * Per-row controls (Change 4 in [[Refinements_2026-05-20_Part2]]):
 *   • To the LEFT of each row's main button: nothing (the previous ledger-style
 *     close-X delete affordance moved to the right side and became a proper
 *     trash-can icon).
 *   • To the RIGHT of each row's main button, as siblings (NOT nested inside
 *     the main button), in order: duplicate, lock/unlock, trash-can delete,
 *     drag grip. These three actions used to live in the floating
 *     ElementToolbar; co-locating them with the layer they target makes
 *     them discoverable from the same place the user reasons about layers,
 *     and makes them reachable without having to click the layer first.
 *
 * Drag handles on each row let the user reorder items via HTML5 DnD (S3).
 * Reordering produces a single history entry (the parent's reorder callback
 * uses replaceElements internally).
 *
 * Accessibility (S24):
 *   • Each row is a flex container holding multiple <button>s (main row
 *     selector + per-row actions), so keyboard users can Tab through each
 *     action independently.
 *   • Each action button has an explicit aria-label so screen readers
 *     announce "Duplicate Photo" rather than just "button".
 *   • Selection is indicated by aria-current="true" PLUS a visible
 *     check-glyph and a pink ring — not background color alone.
 *     Colorblind users can tell selected rows apart from unselected.
 *   • The drag handle is keyboard-skippable (tabindex=-1) since DnD is a
 *     pointer-only affordance.
 */
export const LayersPanel = memo(function LayersPanel({
  elements,
  selectedId,
  selectedIds,         // optional: Set<string> for multi-select (S3)
  onSelect,
  onToggleInSelection, // optional: shift-click toggle for S3
  onDelete,
  onDeleteMany,        // optional: bulk delete for S3
  onReorder,           // optional: (sourceId, targetId, position: 'before' | 'after') for S3
  // Per-row action callbacks (Change 4). Optional so existing call
  // sites that haven't been updated continue to work — the controls
  // just don't render when the callback is missing.
  onDuplicate,
  onUpdate,            // (id, attrs) — used by the lock toggle to flip element.locked
}) {
  // Icon slot — returns JSX rendered inside `.layers-item-icon`. We use
  // real thumbnails for image/sticker layers (mini preview of the actual
  // element bytes) so users can identify their content at a glance without
  // having to read a generic glyph; text layers get a stylized 'T' icon
  // matching the Text tab's nav icon.
  //
  // For images with a CROP applied, the thumbnail mirrors the cropped
  // sub-region rather than the full original source — see <LayerThumb>
  // below for the rationale and implementation. Without that, cropping
  // to a tight region (e.g. a face) would leave the layers panel
  // showing the full uncropped photo, making the cropped layer
  // visually identical to the pre-crop layer and defeating the
  // thumbnail's purpose of identifying which layer is which.
  //
  // For images with FILTERS or BG-REMOVAL applied: those are applied
  // by Konva at render time over `element.src`. The thumbnail shows
  // the cropped (if applicable) but unfiltered version. Mirroring the
  // full filter pipeline on a 24px thumbnail would either lag the
  // panel or require a separate render path. The canvas itself is
  // the source of truth for what the user sees; the thumbnail is a
  // "close enough" identifier.
  const renderIcon = (el) => {
    if (el.type === 'image' || el.type === 'sticker') {
      // `el.src` should always be defined for these types — sticker assets
      // and image uploads both set it on add. Guard anyway so a malformed
      // saved state doesn't crash the panel.
      if (!el.src) return <span className="layers-item-icon-fallback" aria-hidden="true">·</span>;
      return <LayerThumb element={el} />;
    }
    if (el.type === 'text') {
      // Match the Text-tab nav icon: capital T with serifs. Inlined SVG so
      // it picks up `currentColor` from the row text color and inverts
      // properly when the row is selected.
      return (
        <svg className="layers-item-icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M4 7V5h16v2" />
          <path d="M9 5v14" />
          <path d="M15 5v14" />
          <path d="M7 19h4" />
          <path d="M13 19h4" />
        </svg>
      );
    }
    return <span className="layers-item-icon-fallback" aria-hidden="true">·</span>;
  };
  const getName = (el) => {
    if (el.type === 'text') return el.text || 'Text';
    // Stickers all read as 'Sticker' regardless of whether they're an
    // image sticker or a rasterized emoji glyph. The renderIcon path
    // already shows the actual sticker thumbnail on the left of the
    // row, so including the emoji character in the name (e.g.
    // 'Sticker 😀') duplicated the same glyph on both sides of the row.
    if (el.type === 'sticker') return 'Sticker';
    // Photo layers all read as "Photo" regardless of bg-removal state.
    // The bg-removed status is visible on the canvas and via the
    // ElementToolbar; surfacing it in the layer name (e.g. "Image (BG ✓)")
    // added noise to the row without helping the user pick out the layer.
    if (el.type === 'image') return 'Photo';
    return 'Element';
  };

  // Resolve the active selection set. If the parent passes `selectedIds`
  // we use it (multi-select aware); otherwise fall back to a single-item
  // set built from `selectedId` so the rendering path is uniform.
  const activeSelectionSet = selectedIds instanceof Set
    ? selectedIds
    : new Set(selectedId ? [selectedId] : []);
  const selectedCount = activeSelectionSet.size;

  // DnD state. The dragged row's id is in dragId; dragOverId tracks the
  // current drop target so we can render an insertion line above or below
  // it. We compute insertion position based on whether the cursor is in
  // the upper or lower half of the row's bounding rect.
  const [dragId, setDragId] = useState(null);
  const [dropTarget, setDropTarget] = useState(null); // { id, position }

  const handleDragStart = (e, id) => {
    if (!onReorder) return;
    setDragId(id);
    e.dataTransfer.effectAllowed = 'move';
    // Setting some data is required for Firefox to actually fire drag events.
    try { e.dataTransfer.setData('text/plain', id); } catch { /* ignore */ }
  };

  const handleDragOver = (e, id) => {
    if (!onReorder || !dragId || dragId === id) return;
    e.preventDefault();
    const rect = e.currentTarget.getBoundingClientRect();
    const position = (e.clientY - rect.top) < rect.height / 2 ? 'before' : 'after';
    setDropTarget((prev) =>
      prev?.id === id && prev?.position === position ? prev : { id, position }
    );
  };

  const handleDragLeave = (e, id) => {
    // Only clear if we're actually leaving the row (not just entering a
    // child element). currentTarget is the row; relatedTarget is where
    // the pointer's heading. If relatedTarget is still inside the row,
    // ignore the leave.
    if (e.currentTarget.contains(e.relatedTarget)) return;
    // `id` is passed in from the JSX rather than read out of the row's
    // DOM via e.currentTarget.dataset — React's synthetic events aren't
    // safe to read asynchronously, and by the time the setDropTarget
    // updater below runs, e.currentTarget has been nulled out. Reading
    // e.currentTarget.dataset.layerId there would throw "Cannot read
    // properties of null (reading 'dataset')".
    setDropTarget((prev) => (prev?.id === id ? null : prev));
  };

  const handleDrop = (e, targetId) => {
    if (!onReorder || !dragId || dragId === targetId) {
      setDragId(null);
      setDropTarget(null);
      return;
    }
    e.preventDefault();
    // `dropTarget.position` is a PANEL position computed from the cursor's
    // Y coordinate in handleDragOver:
    //   'before' = cursor in upper half of row → visually above the row
    //   'after'  = cursor in lower half of row → visually below the row
    //
    // But `onReorder` (→ reorderElement in useDesignEditor) takes ARRAY
    // positions, and the panel renders the elements array reversed
    // (top-of-panel = last-in-array). So "above in panel" is "after in
    // array" and vice-versa — we invert the position here, at the boundary
    // between the two coordinate systems. Without this translation,
    // dropping A above B in the panel asks reorderElement to put A
    // immediately before B in the array, which keeps A below B in the
    // panel (the opposite of what the user did) — or no-ops entirely
    // when A was already before B, which is the common case and reads
    // as "drag does nothing".
    const panelPosition = dropTarget?.id === targetId ? dropTarget.position : 'after';
    const arrayPosition = panelPosition === 'before' ? 'after' : 'before';
    onReorder(dragId, targetId, arrayPosition);
    setDragId(null);
    setDropTarget(null);
  };

  const handleDragEnd = () => {
    setDragId(null);
    setDropTarget(null);
  };

  const handleRowClick = (e, id) => {
    if (e.shiftKey && onToggleInSelection) {
      onToggleInSelection(id);
    } else {
      onSelect(id);
    }
  };

  if (elements.length === 0) {
    return <div className="layers-empty">No elements yet. Add photos, text, or stickers to your design.</div>;
  }

  return (
    <div>
      <div className="layers-titlebar">
        <h3 className="layers-title">Layers ({elements.length})</h3>
        {selectedCount > 1 && onDeleteMany && (
          <button
            type="button"
            className="layers-bulk-delete"
            onClick={() => onDeleteMany([...activeSelectionSet])}
          >
            Delete {selectedCount} selected
          </button>
        )}
      </div>

      <ul className="layers-list" role="list">
        {/* Render top-down (most recently added on top) — Konva renders
            elements later in the array on top, but in a layers panel
            users expect the topmost item at the top of the list. */}
        {[...elements].reverse().map((element) => {
          const isSelected = activeSelectionSet.has(element.id);
          const isDragging = dragId === element.id;
          const isDropBefore = dropTarget?.id === element.id && dropTarget.position === 'before';
          const isDropAfter = dropTarget?.id === element.id && dropTarget.position === 'after';

          return (
            <li key={element.id} className="layers-row-wrap">
              {isDropBefore && <div className="layers-drop-indicator" aria-hidden="true" />}
              <div
                className={`layers-item${isSelected ? ' selected' : ''}${isDragging ? ' is-dragging' : ''}`}
                draggable={!!onReorder}
                onDragStart={(e) => handleDragStart(e, element.id)}
                onDragOver={(e) => handleDragOver(e, element.id)}
                onDragLeave={(e) => handleDragLeave(e, element.id)}
                onDrop={(e) => handleDrop(e, element.id)}
                onDragEnd={handleDragEnd}
              >
                <button
                  type="button"
                  className="layers-item-main"
                  onClick={(e) => handleRowClick(e, element.id)}
                  aria-current={isSelected ? 'true' : undefined}
                  aria-label={`${isSelected ? 'Selected: ' : ''}${getName(element)}, ${element.type}${onToggleInSelection ? '. Shift-click to add to selection' : ''}`}
                >
                  {/* Visible non-color selection indicator (S24): a small
                      check chip on the left of selected rows. Colorblind
                      users can identify selection without relying on the
                      pink background. */}
                  <span
                    className={`layers-item-check${isSelected ? ' is-on' : ''}`}
                    aria-hidden="true"
                  >
                    {isSelected ? '✓' : ''}
                  </span>
                  <span className="layers-item-icon" aria-hidden="true">{renderIcon(element)}</span>
                  <span className="layers-item-name">{getName(element)}</span>
                </button>

                {/* Per-row actions (Change 4). Siblings of the main row
                    button, NOT nested inside it — nesting <button>s is
                    invalid HTML and breaks keyboard / screen-reader
                    navigation. Each is its own tab stop with its own
                    aria-label so the user can keyboard through them. */}
                {onDuplicate && (
                  <button
                    type="button"
                    onClick={(e) => { e.stopPropagation(); onDuplicate(element.id); }}
                    className="layers-item-action"
                    aria-label={`Duplicate ${getName(element)}`}
                    title="Duplicate"
                  >
                    <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                      {/* Two overlapping rounded squares — the standard
                          duplicate / copy glyph. Matches the icon used in
                          the previous ElementToolbar so the muscle memory
                          transfers. */}
                      <rect x="9" y="9" width="11" height="11" rx="2" />
                      <path d="M5 15V6a2 2 0 0 1 2-2h9" />
                    </svg>
                  </button>
                )}

                {onUpdate && (
                  <button
                    type="button"
                    onClick={(e) => { e.stopPropagation(); onUpdate(element.id, { locked: !element.locked }); }}
                    className={`layers-item-action${element.locked ? ' is-active' : ''}`}
                    aria-label={element.locked ? `Unlock ${getName(element)}` : `Lock ${getName(element)}`}
                    aria-pressed={!!element.locked}
                    title={element.locked ? 'Unlock' : 'Lock'}
                  >
                    {element.locked ? (
                      <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                        {/* Closed padlock — base rectangle + bowed shackle
                            entering both sides of the body. */}
                        <rect x="4" y="11" width="16" height="10" rx="2" />
                        <path d="M8 11V7a4 4 0 0 1 8 0v4" />
                      </svg>
                    ) : (
                      <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                        {/* Open padlock — shackle disconnected from the
                            right side so the user reads it as "unlocked,
                            click to lock". */}
                        <rect x="4" y="11" width="16" height="10" rx="2" />
                        <path d="M8 11V7a4 4 0 0 1 7-3" />
                      </svg>
                    )}
                  </button>
                )}

                {/* Delete — now a proper trash-can glyph (Change 4).
                    Previously this was a bare ✕ close icon which read as
                    "close this row" rather than "delete this layer". The
                    classic trash-can with lid + body + three vertical
                    streaks reads unambiguously as destructive deletion. */}
                <button
                  type="button"
                  onClick={(e) => { e.stopPropagation(); onDelete(element.id); }}
                  className="layers-item-action layers-item-action--delete"
                  aria-label={`Delete ${getName(element)}`}
                  title="Delete"
                >
                  <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    {/* Trash-can outline: top lid, side handles, body
                        edges, and three vertical streaks for the trash
                        "slats". Same path used by the floating ElementToolbar
                        delete button before Change 4 collapsed that button
                        into this single location. */}
                    <path d="M3 6h18" />
                    <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
                    <path d="M6 6l1 14a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-14" />
                    <path d="M10 11v6" />
                    <path d="M14 11v6" />
                  </svg>
                </button>

                {onReorder && (
                  <span
                    className="layers-item-grip"
                    aria-hidden="true"
                    title="Drag to reorder"
                    tabIndex={-1}
                  >
                    ⋮⋮
                  </span>
                )}
              </div>
              {isDropAfter && <div className="layers-drop-indicator" aria-hidden="true" />}
            </li>
          );
        })}
      </ul>
    </div>
  );
});

/**
 * Per-row thumbnail for image and sticker layers.
 *
 * Exists as its own component so it can call `useCroppedThumbnail`
 * (a React hook, can't live inside the .map() loop in the parent's
 * render body). The hook returns a data URL of the cropped sub-region
 * when the element has a crop applied, or the raw `element.src`
 * otherwise — see the hook's docblock for the rasterization details.
 *
 * Memoized so a re-render of the parent panel (e.g. when an unrelated
 * layer's selection state changes) doesn't redo the thumbnail's load
 * and crop computation. The hook itself also caches by (src, crop)
 * signature, so even without memo this would be fast — but skipping
 * the render path entirely for unchanged elements is faster still.
 *
 * The visual styling (size, object-fit, etc.) lives on the existing
 * `.layers-item-icon-img` CSS class, unchanged from when this was an
 * inline <img> in renderIcon. The only thing this wrapper changes
 * about the rendered output is which URL ends up in the `src` attr.
 */
const LayerThumb = memo(function LayerThumb({ element }) {
  const src = useCroppedThumbnail(element);
  // The hook returns null in the rare initial-render-with-no-src case;
  // fall back to the fallback dot to avoid emitting an <img> with an
  // empty src (which would trigger a broken-image icon in some browsers).
  if (!src) {
    return <span className="layers-item-icon-fallback" aria-hidden="true">·</span>;
  }
  return (
    <img
      src={src}
      alt=""
      className="layers-item-icon-img"
      loading="lazy"
      decoding="async"
      draggable={false}
    />
  );
});
