/* ============================================================
   Touch-friendly drag-and-drop
   ------------------------------------------------------------
   Replaces the HTML5 drag-and-drop API (which is mouse-only on
   most touch devices) with a tiny pointer-events shim. One code
   path covers mouse and touch — drag a chip on a phone the same
   way you do on a desktop.

   Surface area:
     useDraggable(data, opts?)  → React ref to put on the draggable element
     useDropZone(onDrop, opts?) → React ref to put on a drop-zone element

   The hook returns a ref. Spread the ref onto the target DOM node
   (NOT a React component) and the shim wires up pointer events
   imperatively. A ghost copy of the dragged element follows the
   pointer; drop zones get a `.dnd-hover` class while hovered.
   ============================================================ */

(function () {
  // ---- Module-level state for the in-flight drag ----
  let activeDrag = null;
  // shape: { data, sourceEl, startX, startY, dragging, ghostEl,
  //          threshold, captureId, onEnd }

  const zones = new Map();   // element → { onDrop, accepts? }

  function findZoneFor(x, y) {
    // Hide the ghost briefly so elementFromPoint actually sees the
    // zone underneath the finger instead of the ghost itself.
    if (activeDrag && activeDrag.ghostEl) activeDrag.ghostEl.style.display = 'none';
    let el = document.elementFromPoint(x, y);
    if (activeDrag && activeDrag.ghostEl) activeDrag.ghostEl.style.display = '';
    while (el) {
      if (zones.has(el)) return { el, opts: zones.get(el) };
      el = el.parentElement;
    }
    return null;
  }

  function clearHovers() {
    zones.forEach((_, el) => el.classList.remove('dnd-hover'));
  }

  function makeGhost(sourceEl) {
    const rect = sourceEl.getBoundingClientRect();
    const ghost = sourceEl.cloneNode(true);
    ghost.style.position = 'fixed';
    ghost.style.left = rect.left + 'px';
    ghost.style.top  = rect.top + 'px';
    ghost.style.width = rect.width + 'px';
    ghost.style.height = rect.height + 'px';
    ghost.style.pointerEvents = 'none';
    ghost.style.zIndex = 9999;
    ghost.style.opacity = '0.85';
    ghost.style.transform = 'rotate(-2deg) scale(1.02)';
    ghost.style.boxShadow = '0 12px 30px rgba(0,0,0,0.25)';
    ghost.style.transition = 'transform 0.06s ease';
    ghost.classList.add('dnd-ghost');
    document.body.appendChild(ghost);
    return ghost;
  }

  function onPointerMove(e) {
    if (!activeDrag) return;
    const dx = e.clientX - activeDrag.startX;
    const dy = e.clientY - activeDrag.startY;
    if (!activeDrag.dragging) {
      if (Math.hypot(dx, dy) < activeDrag.threshold) return;
      // Lift-off — promote to a real drag.
      activeDrag.dragging = true;
      activeDrag.sourceEl.classList.add('dnd-source-dragging');
      activeDrag.ghostEl = makeGhost(activeDrag.sourceEl);
      try { activeDrag.sourceEl.setPointerCapture(activeDrag.captureId); } catch (_) {}
    }
    activeDrag.ghostEl.style.transform =
      `translate(${dx}px,${dy}px) rotate(-2deg) scale(1.02)`;
    clearHovers();
    const hit = findZoneFor(e.clientX, e.clientY);
    if (hit) hit.el.classList.add('dnd-hover');
  }

  function onPointerUp(e) {
    const drag = activeDrag;
    // Detach listeners FIRST so a follow-up event doesn't double-fire.
    window.removeEventListener('pointermove', onPointerMove);
    window.removeEventListener('pointerup', onPointerUp);
    window.removeEventListener('pointercancel', onPointerUp);
    activeDrag = null;
    if (!drag) return;
    if (drag.dragging) {
      const hit = findZoneFor(e.clientX, e.clientY);
      clearHovers();
      if (drag.ghostEl) drag.ghostEl.remove();
      drag.sourceEl.classList.remove('dnd-source-dragging');
      try { drag.sourceEl.releasePointerCapture(drag.captureId); } catch (_) {}
      if (hit && hit.opts && typeof hit.opts.onDrop === 'function') {
        try { hit.opts.onDrop(drag.data, { sourceEl: drag.sourceEl, zoneEl: hit.el }); }
        catch (err) { console.error('DnD onDrop threw', err); }
      }
    }
    if (drag.onEnd) drag.onEnd();
  }

  // Bind an element as a draggable source. Returns an unbind fn.
  function bindDraggable(el, getData, opts = {}) {
    if (!el) return () => {};
    const onPointerDown = (e) => {
      if (e.button && e.button !== 0) return;
      // Don't start a drag from interactive content inside the
      // draggable (buttons, inputs, links).
      if (e.target.closest('button,input,select,textarea,a')) return;
      activeDrag = {
        data: typeof getData === 'function' ? getData() : getData,
        sourceEl: el,
        startX: e.clientX, startY: e.clientY,
        threshold: opts.threshold || 6,
        dragging: false,
        ghostEl: null,
        captureId: e.pointerId,
        onEnd: opts.onEnd,
      };
      window.addEventListener('pointermove', onPointerMove);
      window.addEventListener('pointerup', onPointerUp);
      window.addEventListener('pointercancel', onPointerUp);
    };
    el.addEventListener('pointerdown', onPointerDown);
    // `touch-action: none` prevents the browser scrolling while the
    // user holds the element down. Without it, vertical drags on a
    // phone scroll the page instead of moving the chip.
    el.style.touchAction = 'none';
    el.classList.add('dnd-draggable');
    return () => {
      el.removeEventListener('pointerdown', onPointerDown);
      el.style.touchAction = '';
      el.classList.remove('dnd-draggable');
    };
  }

  function bindDropZone(el, opts) {
    if (!el) return () => {};
    zones.set(el, opts || {});
    el.classList.add('dnd-zone');
    return () => {
      zones.delete(el);
      el.classList.remove('dnd-zone', 'dnd-hover');
    };
  }

  // ---- React hooks ----
  window.useDraggable = function (getData, opts) {
    const ref = React.useRef(null);
    React.useEffect(() => {
      if (!ref.current) return undefined;
      return bindDraggable(ref.current, getData, opts);
      // Re-bind whenever getData reference changes so stale closures
      // don't trap old data. Cheap — just re-attaches one listener.
    }, [getData]);
    return ref;
  };
  window.useDropZone = function (onDrop, opts) {
    const ref = React.useRef(null);
    React.useEffect(() => {
      if (!ref.current) return undefined;
      return bindDropZone(ref.current, { ...(opts || {}), onDrop });
    }, [onDrop]);
    return ref;
  };

  // Expose the imperative API too (rare, but useful for tables).
  window.TouchDnD = { bindDraggable, bindDropZone };
})();
