/* global React */
const { useMemo, useRef, useState, useEffect, useCallback } = React;

// FloorMap — renders the official OshiUpLink PNG as an SVG <image> backdrop
// with transparent hit zones overlaid at the exact pixel coords of each booth.
// Pan via pointer events; pinch-zoom on touch; +/-/0 keyboard shortcuts.
// When filters are active, the backdrop desaturates (grayscale) so matching
// booths stand out. Hover (mouse only) shows a small tooltip with artist name.

function dist(a, b) {
  const dx = a.x - b.x, dy = a.y - b.y;
  return Math.sqrt(dx * dx + dy * dy);
}

function FloorMap({
  tables,
  structure,
  selectedId,
  matchIds,
  filtersActive,
  savedIds,
  visitedIds,
  notes,
  onSelect,
}) {
  const { width, height, bgImage } = structure;
  const svgRef = useRef(null);
  const [viewBox, setViewBox] = useState([0, 0, width, height]);
  const [hover, setHover] = useState(null); // { id, clientX, clientY }

  const pointers = useRef(new Map());
  const drag = useRef(null);
  const pinch = useRef(null);
  const didDrag = useRef(false);

  const reset = () => setViewBox([0, 0, width, height]);
  useEffect(() => { reset(); /* eslint-disable-next-line */ }, [width, height]);

  const clientToSvg = (clientX, clientY) => {
    const el = svgRef.current;
    if (!el) return { x: 0, y: 0 };
    const rect = el.getBoundingClientRect();
    const [vx, vy, vw, vh] = viewBox;
    return {
      x: vx + ((clientX - rect.left) / rect.width) * vw,
      y: vy + ((clientY - rect.top) / rect.height) * vh,
    };
  };

  const zoomAt = (factor, anchorSvg) => {
    const [vx, vy, vw, vh] = viewBox;
    const minW = width * 0.15;
    const maxW = width * 1.5;
    const nw = Math.max(minW, Math.min(maxW, vw * factor));
    const nh = nw * (height / width);
    const ax = (anchorSvg.x - vx) / vw;
    const ay = (anchorSvg.y - vy) / vh;
    setViewBox([anchorSvg.x - ax * nw, anchorSvg.y - ay * nh, nw, nh]);
  };

  const onPointerDown = (e) => {
    pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
    didDrag.current = false;
    setHover(null); // dismiss hover when user starts interacting
    if (pointers.current.size === 1) {
      drag.current = { startVb: viewBox, startX: e.clientX, startY: e.clientY };
      pinch.current = null;
    } else if (pointers.current.size === 2) {
      const [a, b] = [...pointers.current.values()];
      const center = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
      pinch.current = {
        startDist: dist(a, b),
        startVb: viewBox,
        centerSvg: clientToSvg(center.x, center.y),
      };
      drag.current = null;
      didDrag.current = true;
    }
  };

  const onPointerMove = (e) => {
    if (!pointers.current.has(e.pointerId)) return;
    pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });

    if (pinch.current && pointers.current.size >= 2) {
      const [a, b] = [...pointers.current.values()];
      const cur = dist(a, b);
      const factor = pinch.current.startDist / Math.max(1, cur);
      const [svx, svy, svw] = pinch.current.startVb;
      const minW = width * 0.15, maxW = width * 1.5;
      const nw = Math.max(minW, Math.min(maxW, svw * factor));
      const nh = nw * (height / width);
      const anchor = pinch.current.centerSvg;
      const ax = (anchor.x - svx) / svw;
      const ay = (anchor.y - svy) / pinch.current.startVb[3];
      setViewBox([anchor.x - ax * nw, anchor.y - ay * nh, nw, nh]);
      return;
    }

    if (drag.current && pointers.current.size === 1) {
      const svgEl = svgRef.current;
      if (!svgEl) return;
      const rect = svgEl.getBoundingClientRect();
      const dx = e.clientX - drag.current.startX;
      const dy = e.clientY - drag.current.startY;
      if (Math.abs(dx) + Math.abs(dy) > 6) didDrag.current = true;
      const [svx, svy, svw, svh] = drag.current.startVb;
      setViewBox([
        svx - dx * (svw / rect.width),
        svy - dy * (svh / rect.height),
        svw, svh,
      ]);
    }
  };

  const endPointer = (e) => {
    pointers.current.delete(e.pointerId);
    if (pointers.current.size < 2) pinch.current = null;
    if (pointers.current.size === 0) drag.current = null;
  };

  useEffect(() => {
    const onGlobalUp = (e) => {
      if (!pointers.current.has(e.pointerId)) return;
      pointers.current.delete(e.pointerId);
      if (pointers.current.size < 2) pinch.current = null;
      if (pointers.current.size === 0) drag.current = null;
    };
    window.addEventListener('pointerup', onGlobalUp);
    window.addEventListener('pointercancel', onGlobalUp);
    return () => {
      window.removeEventListener('pointerup', onGlobalUp);
      window.removeEventListener('pointercancel', onGlobalUp);
    };
  }, []);

  useEffect(() => {
    const onCmd = (e) => {
      const cx = viewBox[0] + viewBox[2] / 2;
      const cy = viewBox[1] + viewBox[3] / 2;
      if (e.detail === 'in')    zoomAt(0.82, { x: cx, y: cy });
      if (e.detail === 'out')   zoomAt(1.22, { x: cx, y: cy });
      if (e.detail === 'reset') reset();
    };
    window.addEventListener('as:zoom', onCmd);
    return () => window.removeEventListener('as:zoom', onCmd);
    // eslint-disable-next-line
  }, [viewBox]);

  const onBoothClick = (id) => (e) => {
    if (didDrag.current) { e.preventDefault(); return; }
    onSelect && onSelect(id);
  };

  // Hover tooltip — mouse only. Touch devices skip this.
  const onBoothEnter = (id) => (e) => {
    if (e.pointerType && e.pointerType !== 'mouse') return;
    setHover({ id, clientX: e.clientX, clientY: e.clientY });
  };
  const onBoothHoverMove = (id) => (e) => {
    if (e.pointerType && e.pointerType !== 'mouse') return;
    if (!hover || hover.id !== id) return;
    setHover({ id, clientX: e.clientX, clientY: e.clientY });
  };
  const onBoothLeave = () => setHover(null);

  const hasNote = (id) => notes && !!(notes[id] && notes[id].trim());

  // Hit-zone inflation — larger on touch devices for finger accuracy.
  const isTouch = typeof window !== 'undefined' &&
    window.matchMedia('(hover: none), (pointer: coarse)').matches;
  const HIT_PAD = isTouch ? 10 : 3;

  // Tooltip content resolution
  const hoveredTable = hover && tables.find(t => t.id === hover.id);

  // Render the SVG image with desaturation when filters are active so
  // non-matching booths fade to grayscale.
  return (
    <div className="map-viewport">
      <svg
        ref={svgRef}
        viewBox={viewBox.join(' ')}
        preserveAspectRatio="xMidYMid meet"
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={endPointer}
        onPointerCancel={endPointer}
      >
        {/* Backdrop image — grayscale when filters narrow the visible set */}
        <image
          href={bgImage}
          x="0" y="0"
          width={width} height={height}
          className={filtersActive ? 'map-bg is-dim' : 'map-bg'}
          style={{ pointerEvents: 'none', userSelect: 'none' }}
        />

        {/* Booth overlays + hit zones */}
        {tables.map(t => {
          const isMatch    = matchIds && matchIds.has(t.id);
          const isDimmed   = filtersActive && matchIds && !matchIds.has(t.id);
          const isSelected = t.id === selectedId;
          const isSaved    = savedIds && savedIds.has(t.id);
          const isVisited  = visitedIds && visitedIds.has(t.id);
          const noted      = hasNote(t.id);

          const matchVariant = t.type === 'exhibitor' ? 'ov-match-exhibit' : 'ov-match-alley';
          return (
            <g key={t.id}
               className={cx(
                 'booth-hit',
                 isDimmed && 'is-dimmed',
                 isMatch && 'is-match',
                 isSelected && 'is-selected',
               )}
               onClick={onBoothClick(t.id)}
               onPointerEnter={onBoothEnter(t.id)}
               onPointerMove={onBoothHoverMove(t.id)}
               onPointerLeave={onBoothLeave}
               data-table-id={t.id}>
              {/* Match: opaque colored fill restores booth color above the
                  grayscaled backdrop, plus a bold gold outline for emphasis. */}
              {isMatch && (
                <>
                  <rect className={cx('ov', 'ov-match-fill', matchVariant)}
                    x={t.x} y={t.y} width={t.w} height={t.h}
                    rx="3" ry="3" />
                  <rect className="ov ov-match-ring"
                    x={t.x - 3} y={t.y - 3}
                    width={t.w + 6} height={t.h + 6}
                    rx="5" ry="5" />
                </>
              )}

              {/* Visited scrim */}
              {isVisited && !isSaved && (
                <rect className="ov ov-visited"
                  x={t.x} y={t.y} width={t.w} height={t.h}
                  rx="3" ry="3" />
              )}

              {/* Saved scrim */}
              {isSaved && (
                <rect className="ov ov-saved"
                  x={t.x} y={t.y} width={t.w} height={t.h}
                  rx="3" ry="3" />
              )}

              {/* Selection ring */}
              {isSelected && (
                <rect className="ov ov-selected"
                  x={t.x - 3} y={t.y - 3}
                  width={t.w + 6} height={t.h + 6}
                  rx="5" ry="5" />
              )}

              {/* Re-render the booth ID on top whenever an overlay is present.
                  The match/saved/visited fills obscure the PNG's baked-in
                  labels, especially on exhibitor booths whose labels sit
                  inside their shape. paint-order: stroke gives the text a
                  contrasting halo so it's readable against any overlay. */}
              {(isMatch || isSaved || isVisited || isSelected) && (
                <text
                  className={cx(
                    'booth-label',
                    t.type === 'exhibitor' && 'booth-label-exhibit',
                    (t.w >= 120 || t.h >= 80) && 'booth-label-lg',
                  )}
                  x={t.x + t.w / 2}
                  y={t.y + t.h / 2}
                  textAnchor="middle"
                  dominantBaseline="central"
                >
                  {t.id}
                </text>
              )}

              {/* Hit target — inflated on touch devices */}
              <rect className="hit"
                x={t.x - HIT_PAD} y={t.y - HIT_PAD}
                width={t.w + HIT_PAD * 2} height={t.h + HIT_PAD * 2}
                fill="transparent" />

              {/* State badges */}
              {isVisited && (
                <>
                  <circle className="state-badge state-badge-visited"
                    cx={t.x + 8} cy={t.y + 8} r="7" />
                  <text className="state-glyph state-glyph-visited"
                    x={t.x + 8} y={t.y + 8}
                    textAnchor="middle" dominantBaseline="central">✓</text>
                </>
              )}
              {isSaved && (
                <>
                  <circle className="state-badge state-badge-saved"
                    cx={t.x + t.w - 8} cy={t.y + 8} r="7" />
                  <text className="state-glyph state-glyph-saved"
                    x={t.x + t.w - 8} y={t.y + 8}
                    textAnchor="middle" dominantBaseline="central">♥</text>
                </>
              )}
              {noted && (
                <>
                  <circle className="state-badge state-badge-note"
                    cx={t.x + t.w - 8} cy={t.y + t.h - 8} r="7" />
                  <text className="state-glyph state-glyph-note"
                    x={t.x + t.w - 8} y={t.y + t.h - 8}
                    textAnchor="middle" dominantBaseline="central">✎</text>
                </>
              )}
            </g>
          );
        })}
      </svg>

      {/* Hover tooltip — HTML overlay, mouse only */}
      {hoveredTable && (() => {
        const svgEl = svgRef.current;
        if (!svgEl) return null;
        const rect = svgEl.getBoundingClientRect();
        const a0 = hoveredTable.artists[0];
        // Flip to the cursor's left side when we'd otherwise clip the right
        // edge of the map or collide with the top-right zoom controls.
        // Rough width estimate is enough to decide which side to anchor to;
        // the actual sizing is handled by CSS transform so it's always
        // pixel-accurate.
        const estW = 80 + Math.min(200, (a0.name.length + 4) * 7);
        const gap = 14;
        const reservedRight = 72;  // zoom-ctl footprint
        const cursorX = hover.clientX - rect.left;
        const cursorY = hover.clientY - rect.top;
        const flipLeft = cursorX + gap + estW > rect.width - reservedRight;
        const left = flipLeft ? cursorX - gap : cursorX + gap;
        const top = Math.max(6, Math.min(rect.height - 28, cursorY - 8));
        const maxW = Math.max(140, rect.width - reservedRight - 24);
        return (
          <div
            className={cx('map-tooltip', flipLeft && 'map-tooltip-left')}
            style={{ left, top, maxWidth: maxW }}
          >
            <span className="tt-id">{hoveredTable.id}</span>
            <span className="tt-name">{a0.name}</span>
          </div>
        );
      })()}

      <div className="map-overlay zoom-ctl">
        <button onClick={() => zoomAt(0.82, { x: viewBox[0] + viewBox[2] / 2, y: viewBox[1] + viewBox[3] / 2 })}
          aria-label="zoom in" title="zoom in (+)">+</button>
        <button onClick={() => zoomAt(1.22, { x: viewBox[0] + viewBox[2] / 2, y: viewBox[1] + viewBox[3] / 2 })}
          aria-label="zoom out" title="zoom out (−)">−</button>
        <button onClick={reset} className="zoom-reset"
          aria-label="reset view" title="reset view (0)">⤢</button>
      </div>
    </div>
  );
}

// Small classNames helper matching components.jsx.
function cx(...xs) { return xs.filter(Boolean).join(' '); }

window.FloorMap = FloorMap;
