import React, { useEffect, useRef, useState } from 'react';

import type { AppToken, SceneToken, SceneTokensSessionState } from '../../../shared/types';

import { useTokenImageUrl } from './useTokenImageUrl';
import styles from './SceneTokensOverlay.module.css';

type Viewport = { x: number; y: number; w: number; h: number };

type Props = {
  placements: readonly SceneToken[];
  library: readonly AppToken[];
  session: SceneTokensSessionState | null;
  viewport: Viewport | null;
  editable?: boolean;
  onMove?: (placementId: string, nx: number, ny: number) => void;
};

function TokenSprite({
  placement,
  nx,
  ny,
  viewport,
  editable,
  onMove,
}: {
  placement: SceneToken;
  nx: number;
  ny: number;
  viewport: Viewport;
  editable: boolean;
  onMove?: (placementId: string, nx: number, ny: number) => void;
}) {
  const url = useTokenImageUrl(placement.tokenId);
  const dragRef = useRef<{
    startNx: number;
    startNy: number;
    pointerNx: number;
    pointerNy: number;
    pointerId: number;
    lastNx: number;
    lastNy: number;
  } | null>(null);
  const [localPos, setLocalPos] = useState<{ nx: number; ny: number } | null>(null);
  const frameRef = useRef(0);

  useEffect(() => {
    if (dragRef.current) return;
    setLocalPos(null);
  }, [nx, ny]);

  useEffect(() => {
    return () => {
      if (frameRef.current) cancelAnimationFrame(frameRef.current);
    };
  }, []);

  const posNx = localPos?.nx ?? nx;
  const posNy = localPos?.ny ?? ny;

  const minDim = Math.min(viewport.w, viewport.h);
  const sizePx = Math.max(16, placement.sizeN * minDim);
  const left = viewport.x + posNx * viewport.w;
  const top = viewport.y + posNy * viewport.h;

  const hostToNorm = (clientX: number, clientY: number, host: HTMLElement) => {
    const r = host.getBoundingClientRect();
    const w = Math.max(1e-6, viewport.w);
    const h = Math.max(1e-6, viewport.h);
    return {
      x: Math.max(0, Math.min(1, (clientX - (r.left + viewport.x)) / w)),
      y: Math.max(0, Math.min(1, (clientY - (r.top + viewport.y)) / h)),
    };
  };

  const endDrag = (el: HTMLDivElement, pointerId: number) => {
    const d = dragRef.current;
    if (!d || d.pointerId !== pointerId) return;
    dragRef.current = null;
    if (frameRef.current) {
      cancelAnimationFrame(frameRef.current);
      frameRef.current = 0;
    }
    // Финальный commit в session store — один раз на отпускание.
    onMove?.(String(placement.id), d.lastNx, d.lastNy);
    setLocalPos({ nx: d.lastNx, ny: d.lastNy });
    try {
      if (el.hasPointerCapture(pointerId)) el.releasePointerCapture(pointerId);
    } catch {
      /* ignore */
    }
  };

  return (
    <div
      className={[styles.token, editable ? styles.tokenEditable : ''].filter(Boolean).join(' ')}
      style={{
        left,
        top,
        width: sizePx,
        height: sizePx,
        transform: `translate(-50%, -50%) rotate(${String(placement.rotationDeg)}deg)`,
      }}
      onPointerDown={
        editable && onMove
          ? (e) => {
              if (e.button !== 0) return;
              e.stopPropagation();
              e.preventDefault();
              const host = (e.currentTarget.parentElement as HTMLElement | null) ?? e.currentTarget;
              const p = hostToNorm(e.clientX, e.clientY, host);
              dragRef.current = {
                startNx: posNx,
                startNy: posNy,
                pointerNx: p.x,
                pointerNy: p.y,
                pointerId: e.pointerId,
                lastNx: posNx,
                lastNy: posNy,
              };
              setLocalPos({ nx: posNx, ny: posNy });
              (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
            }
          : undefined
      }
      onPointerMove={
        editable && onMove
          ? (e) => {
              const d = dragRef.current;
              if (!d || d.pointerId !== e.pointerId) return;
              const host = (e.currentTarget.parentElement as HTMLElement | null) ?? e.currentTarget;
              const p = hostToNorm(e.clientX, e.clientY, host);
              const nextNx = Math.max(0, Math.min(1, d.startNx + (p.x - d.pointerNx)));
              const nextNy = Math.max(0, Math.min(1, d.startNy + (p.y - d.pointerNy)));
              d.lastNx = nextNx;
              d.lastNy = nextNy;
              if (frameRef.current) return;
              frameRef.current = requestAnimationFrame(() => {
                frameRef.current = 0;
                const cur = dragRef.current;
                if (!cur) return;
                setLocalPos({ nx: cur.lastNx, ny: cur.lastNy });
              });
            }
          : undefined
      }
      onPointerUp={(e) => {
        endDrag(e.currentTarget as HTMLDivElement, e.pointerId);
      }}
      onPointerCancel={(e) => {
        endDrag(e.currentTarget as HTMLDivElement, e.pointerId);
      }}
    >
      {url ? <img className={styles.tokenImg} src={url} alt="" draggable={false} /> : null}
    </div>
  );
}

export function SceneTokensOverlay({
  placements,
  library,
  session,
  viewport,
  editable = false,
  onMove,
}: Props) {
  if (!viewport || placements.length === 0) return null;
  const known = new Set(library.map((t) => t.id));

  return (
    <div className={styles.layer}>
      {placements
        .filter((p) => known.has(p.tokenId))
        .map((placement) => {
          const key = String(placement.id);
          const override = session?.byPlacementId[key] ?? session?.byPlacementId[placement.id];
          const nx = override?.nx ?? placement.nx;
          const ny = override?.ny ?? placement.ny;
          return (
            <TokenSprite
              key={key}
              placement={placement}
              nx={nx}
              ny={ny}
              viewport={viewport}
              editable={editable}
              onMove={onMove}
            />
          );
        })}
    </div>
  );
}
