// phys8-drag.js — универсальный drag-and-drop движок для Физики 8.
// Поддерживает:
//   - drag SVG-объектов (по data-p8-drag атрибуту или ref)
//   - drag DOM-элементов (HTML chips, palette items)
//   - drag «логических» объектов в canvas-симуляциях (через hit-callback)
//   - constraints: lockX, lockY, bounds, snap, snapToGrid
//   - touch + mouse + pointer events
// Экспорт в window.P8Drag = { attach, attachCanvas, snapToGrid, hitTest }

(function () {
  'use strict';

  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));

  // === Конвертация события в координаты внутри контейнера ===
  function pointerInContainer(ev, container) {
    const r = container.getBoundingClientRect();
    // Учитываем масштаб SVG (viewBox vs реальный размер)
    let scaleX = 1, scaleY = 1;
    if (container.tagName && container.tagName.toLowerCase() === 'svg') {
      const vb = container.viewBox && container.viewBox.baseVal;
      if (vb && vb.width) {
        scaleX = vb.width / r.width;
        scaleY = vb.height / r.height;
      }
    }
    return {
      x: (ev.clientX - r.left) * scaleX,
      y: (ev.clientY - r.top) * scaleY,
      clientX: ev.clientX,
      clientY: ev.clientY
    };
  }

  // === Snap-to-grid ===
  function snapToGrid(x, y, gridSize = 20) {
    return {
      x: Math.round(x / gridSize) * gridSize,
      y: Math.round(y / gridSize) * gridSize
    };
  }

  // === Hit-test для логических объектов на canvas ===
  // objects: [{ x, y, r, ...meta }]
  // returns: object or null
  function hitTest(objects, px, py) {
    for (let i = objects.length - 1; i >= 0; i--) {
      const o = objects[i];
      const dx = px - o.x, dy = py - o.y;
      const r = o.r || 20;
      if (dx * dx + dy * dy <= r * r) return o;
    }
    return null;
  }

  // === attach: универсальный drag для DOM/SVG элемента ===
  // Использование:
  //   const handle = P8Drag.attach(svgCircle, {
  //     onStart: (ev, pos) => {...},
  //     onMove:  (ev, pos, delta) => {...},
  //     onEnd:   (ev, pos) => {...},
  //     lockX: false, lockY: false,
  //     bounds: { minX, maxX, minY, maxY },
  //     snap: 0 // grid size, 0 = no snap
  //   });
  //   handle.destroy()
  function attach(el, opts = {}) {
    if (!el) return { destroy: () => {} };
    const container = opts.container || el.ownerSVGElement || el.parentElement || el;
    const onStart = opts.onStart || (() => {});
    const onMove  = opts.onMove  || (() => {});
    const onEnd   = opts.onEnd   || (() => {});
    const lockX = !!opts.lockX;
    const lockY = !!opts.lockY;
    const snap = opts.snap || 0;
    const bounds = opts.bounds || null;

    let dragging = false;
    let startPos = null;
    let pointerId = null;

    el.style.touchAction = 'none';
    el.classList.add('p8-draggable');

    function applyConstraints(pos) {
      let x = pos.x, y = pos.y;
      if (snap > 0) {
        const s = snapToGrid(x, y, snap);
        x = s.x; y = s.y;
      }
      if (bounds) {
        if (bounds.minX !== undefined) x = clamp(x, bounds.minX, bounds.maxX || x);
        if (bounds.minY !== undefined) y = clamp(y, bounds.minY, bounds.maxY || y);
      }
      if (lockX) x = startPos.x;
      if (lockY) y = startPos.y;
      return { x, y, clientX: pos.clientX, clientY: pos.clientY };
    }

    function down(ev) {
      if (ev.button !== undefined && ev.button !== 0) return;
      ev.preventDefault();
      dragging = true;
      pointerId = ev.pointerId;
      try { el.setPointerCapture(pointerId); } catch (e) {}
      el.classList.add('is-dragging');
      const pos = pointerInContainer(ev, container);
      startPos = pos;
      onStart(ev, pos);
    }

    function move(ev) {
      if (!dragging) return;
      const pos = applyConstraints(pointerInContainer(ev, container));
      const delta = { dx: pos.x - startPos.x, dy: pos.y - startPos.y };
      onMove(ev, pos, delta);
    }

    function up(ev) {
      if (!dragging) return;
      dragging = false;
      el.classList.remove('is-dragging');
      try { el.releasePointerCapture(pointerId); } catch (e) {}
      const pos = applyConstraints(pointerInContainer(ev, container));
      onEnd(ev, pos);
      startPos = null;
      pointerId = null;
    }

    el.addEventListener('pointerdown', down);
    el.addEventListener('pointermove', move);
    el.addEventListener('pointerup', up);
    el.addEventListener('pointercancel', up);

    return {
      destroy() {
        el.removeEventListener('pointerdown', down);
        el.removeEventListener('pointermove', move);
        el.removeEventListener('pointerup', up);
        el.removeEventListener('pointercancel', up);
        el.classList.remove('p8-draggable', 'is-dragging');
      },
      get isDragging() { return dragging; }
    };
  }

  // === attachCanvas: drag «логических» объектов на canvas ===
  // Использование:
  //   const handle = P8Drag.attachCanvas(canvas, {
  //     objects: [...],         // массив объектов с { x, y, r }
  //     onPickup: (obj, pos) => {...},
  //     onDrag:   (obj, pos, delta) => {...},
  //     onDrop:   (obj, pos) => {...},
  //     onClick:  (pos, hitObj) => {...},   // when no drag (mousedown without move)
  //   });
  function attachCanvas(canvas, opts = {}) {
    if (!canvas) return { destroy: () => {} };
    const objects = opts.objects || [];
    const onPickup = opts.onPickup || (() => {});
    const onDrag   = opts.onDrag   || (() => {});
    const onDrop   = opts.onDrop   || (() => {});
    const onClick  = opts.onClick  || (() => {});

    let active = null;
    let startPos = null;
    let lastPos = null;
    let pointerId = null;
    let moved = false;
    const dragThreshold = 4;

    canvas.style.touchAction = 'none';

    function getPos(ev) {
      const r = canvas.getBoundingClientRect();
      return {
        x: (ev.clientX - r.left) * canvas.width / r.width,
        y: (ev.clientY - r.top) * canvas.height / r.height,
        clientX: ev.clientX, clientY: ev.clientY
      };
    }

    function down(ev) {
      if (ev.button !== undefined && ev.button !== 0) return;
      ev.preventDefault();
      const pos = getPos(ev);
      const hit = hitTest(objects, pos.x, pos.y);
      if (hit) {
        active = hit;
        startPos = pos;
        lastPos = pos;
        moved = false;
        pointerId = ev.pointerId;
        try { canvas.setPointerCapture(pointerId); } catch (e) {}
        canvas.style.cursor = 'grabbing';
        onPickup(active, pos);
      } else {
        // start an empty drag — used for "click anywhere" handlers
        startPos = pos;
        lastPos = pos;
        moved = false;
        pointerId = ev.pointerId;
        try { canvas.setPointerCapture(pointerId); } catch (e) {}
      }
    }

    function move(ev) {
      if (!startPos) return;
      const pos = getPos(ev);
      const dx = pos.x - startPos.x, dy = pos.y - startPos.y;
      if (!moved && (dx * dx + dy * dy) > dragThreshold * dragThreshold) {
        moved = true;
      }
      if (active && moved) {
        active.x = pos.x;
        active.y = pos.y;
        onDrag(active, pos, { dx: pos.x - lastPos.x, dy: pos.y - lastPos.y });
        lastPos = pos;
      }
    }

    function up(ev) {
      if (!startPos) return;
      const pos = getPos(ev);
      if (active) {
        onDrop(active, pos);
      } else if (!moved) {
        onClick(pos, null);
      }
      try { canvas.releasePointerCapture(pointerId); } catch (e) {}
      canvas.style.cursor = '';
      active = null;
      startPos = null;
      lastPos = null;
      pointerId = null;
    }

    // Hover cursor change
    function hover(ev) {
      if (active) return;
      const pos = getPos(ev);
      const hit = hitTest(objects, pos.x, pos.y);
      canvas.style.cursor = hit ? 'grab' : '';
    }

    canvas.addEventListener('pointerdown', down);
    canvas.addEventListener('pointermove', move);
    canvas.addEventListener('pointerup', up);
    canvas.addEventListener('pointercancel', up);
    canvas.addEventListener('mousemove', hover);

    return {
      destroy() {
        canvas.removeEventListener('pointerdown', down);
        canvas.removeEventListener('pointermove', move);
        canvas.removeEventListener('pointerup', up);
        canvas.removeEventListener('pointercancel', up);
        canvas.removeEventListener('mousemove', hover);
        canvas.style.cursor = '';
      },
      get active() { return active; },
      updateObjects(newObjects) { objects.length = 0; objects.push(...newObjects); }
    };
  }

  // === Palette: drag items from palette to canvas/svg ===
  // items: NodeList of .p8-palette-item with data-component attr
  // dropTarget: HTML element with .p8-droptarget class
  // onDrop(componentType, pos): callback when dropped on target
  function attachPalette(paletteItems, dropTarget, opts = {}) {
    const onDrop = opts.onDrop || (() => {});
    const onDragStart = opts.onDragStart || (() => {});
    const onDragEnd = opts.onDragEnd || (() => {});

    const handlers = [];

    [...paletteItems].forEach(item => {
      let ghost = null;
      let pointerId = null;

      function down(ev) {
        if (ev.button !== undefined && ev.button !== 0) return;
        ev.preventDefault();
        pointerId = ev.pointerId;
        try { item.setPointerCapture(pointerId); } catch (e) {}

        const r = item.getBoundingClientRect();
        ghost = item.cloneNode(true);
        ghost.style.cssText = `position:fixed;z-index:9999;pointer-events:none;opacity:.85;
          width:${r.width}px;left:${r.left}px;top:${r.top}px;
          box-shadow:0 12px 28px rgba(0,0,0,.20);transform:rotate(-2deg);`;
        document.body.appendChild(ghost);
        item.style.opacity = '.35';
        const componentType = item.dataset.component || item.textContent.trim();
        onDragStart(componentType);
      }

      function move(ev) {
        if (!ghost) return;
        ghost.style.left = (ev.clientX - 30) + 'px';
        ghost.style.top  = (ev.clientY - 20) + 'px';
        if (dropTarget) {
          const tr = dropTarget.getBoundingClientRect();
          const inside = ev.clientX >= tr.left && ev.clientX <= tr.right &&
                         ev.clientY >= tr.top && ev.clientY <= tr.bottom;
          dropTarget.classList.toggle('p8-drop-over', inside);
        }
      }

      function up(ev) {
        if (!ghost) return;
        const componentType = item.dataset.component || item.textContent.trim();
        if (dropTarget) {
          const tr = dropTarget.getBoundingClientRect();
          const inside = ev.clientX >= tr.left && ev.clientX <= tr.right &&
                         ev.clientY >= tr.top && ev.clientY <= tr.bottom;
          dropTarget.classList.remove('p8-drop-over');
          if (inside) {
            const pos = {
              x: ev.clientX - tr.left,
              y: ev.clientY - tr.top,
              clientX: ev.clientX, clientY: ev.clientY
            };
            onDrop(componentType, pos);
          }
        }
        ghost.remove();
        ghost = null;
        item.style.opacity = '';
        try { item.releasePointerCapture(pointerId); } catch (e) {}
        pointerId = null;
        onDragEnd(componentType);
      }

      item.style.touchAction = 'none';
      item.addEventListener('pointerdown', down);
      item.addEventListener('pointermove', move);
      item.addEventListener('pointerup', up);
      item.addEventListener('pointercancel', up);
      handlers.push({ item, down, move, up });
    });

    return {
      destroy() {
        handlers.forEach(({ item, down, move, up }) => {
          item.removeEventListener('pointerdown', down);
          item.removeEventListener('pointermove', move);
          item.removeEventListener('pointerup', up);
          item.removeEventListener('pointercancel', up);
        });
      }
    };
  }

  // === Export ===
  window.P8Drag = {
    attach,
    attachCanvas,
    attachPalette,
    snapToGrid,
    hitTest,
    pointerInContainer
  };
})();
