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

import type { AppToken, SceneToken, SceneTokensSessionState } from '../../../shared/types';
import { clampSceneTokenSizeN } from '../../../shared/types/appTokens';
import { useLiveResizeBroadcast } from '../playerToken/useLiveDragBroadcast';

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

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

export type TokenPoseOverride = {
  nx: number;
  ny: number;
  rotationDeg?: 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;
  onResize?: (placementId: string, sizeN: number) => void;
  resizeTitle?: string;
  /** Snap во время drag (пульт, привязка к сетке). */
  snapNorm?: (nx: number, ny: number) => { nx: number; ny: number };
  onContextMenu?: (e: React.MouseEvent, placement: SceneToken) => void;
  /** Live path playback / other pose overrides by placement id. */
  poseOverrides?: Record<string, TokenPoseOverride> | null;
  /** Per-placement drag lock (e.g. while path is animating). Default: all editable. */
  dragEnabledById?: Record<string, boolean> | null;
};

function layerFromEvent(e: React.PointerEvent<HTMLElement>): HTMLElement | null {
  return e.currentTarget.closest('[data-token-layer="1"]');
}

function TokenSprite({
  placement,
  nx,
  ny,
  sizeN,
  rotationDeg,
  viewport,
  editable,
  dragEnabled,
  onMove,
  onResize,
  resizeTitle,
  snapNorm,
  onContextMenu,
}: {
  placement: SceneToken;
  nx: number;
  ny: number;
  sizeN: number;
  rotationDeg: number;
  viewport: Viewport;
  editable: boolean;
  dragEnabled: boolean;
  onMove?: (placementId: string, nx: number, ny: number) => void;
  onResize?: (placementId: string, sizeN: number) => void;
  resizeTitle?: string;
  snapNorm?: (nx: number, ny: number) => { nx: number; ny: number };
  onContextMenu?: (e: React.MouseEvent, placement: SceneToken) => 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 resizeRef = useRef<{
    pointerId: number;
    startSize: number;
    startDist: number;
    lastSize: number;
  } | null>(null);
  const [localPos, setLocalPos] = useState<{ nx: number; ny: number } | null>(null);
  const [localSize, setLocalSize] = useState<number | null>(null);
  const frameRef = useRef(0);
  const { schedule: scheduleResize, flush: flushResize } = useLiveResizeBroadcast(
    onResize,
    String(placement.id),
  );

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

  useEffect(() => {
    if (resizeRef.current) return;
    setLocalSize(null);
  }, [sizeN]);

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

  const posNx = localPos?.nx ?? nx;
  const posNy = localPos?.ny ?? ny;
  const shownSizeN = localSize ?? sizeN;

  const minDim = Math.min(viewport.w, viewport.h);
  const sizePx = Math.max(16, shownSizeN * minDim);
  const left = viewport.x + posNx * viewport.w;
  const top = viewport.y + posNy * viewport.h;
  const canDrag = editable && dragEnabled && Boolean(onMove);
  const canResize = editable && dragEnabled && Boolean(onResize);
  const interactive = canDrag || canResize || Boolean(onContextMenu);

  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;
    }
    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 */
    }
  };

  const endResize = (el: HTMLDivElement, pointerId: number) => {
    const d = resizeRef.current;
    if (!d || d.pointerId !== pointerId) return;
    resizeRef.current = null;
    flushResize(d.lastSize);
    setLocalSize(null);
    try {
      if (el.hasPointerCapture(pointerId)) el.releasePointerCapture(pointerId);
    } catch {
      /* ignore */
    }
  };

  return (
    <div
      className={[
        styles.token,
        canDrag ? styles.tokenEditable : '',
        interactive && !canDrag ? styles.tokenInteractive : '',
      ]
        .filter(Boolean)
        .join(' ')}
      style={{
        left,
        top,
        width: sizePx,
        height: sizePx,
        transform: `translate(-50%, -50%) rotate(${String(rotationDeg)}deg)`,
      }}
      onContextMenu={
        onContextMenu
          ? (e) => {
              e.preventDefault();
              e.stopPropagation();
              onContextMenu(e, placement);
            }
          : undefined
      }
      onPointerDown={
        canDrag
          ? (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={
        canDrag
          ? (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)));
              const snapped = snapNorm ? snapNorm(nextNx, nextNy) : { nx: nextNx, ny: nextNy };
              d.lastNx = snapped.nx;
              d.lastNy = snapped.ny;
              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 ? (
        <div className={styles.tokenFace}>
          <img className={styles.tokenImg} src={url} alt="" draggable={false} />
        </div>
      ) : null}
      {canResize ? (
        <div
          className={styles.resizeHandle}
          title={resizeTitle}
          onPointerDown={(e) => {
            if (e.button !== 0) return;
            e.stopPropagation();
            e.preventDefault();
            const host = layerFromEvent(e);
            if (!host) return;
            const p = hostToNorm(e.clientX, e.clientY, host);
            const dist = Math.max(1e-4, Math.hypot(p.x - posNx, p.y - posNy));
            resizeRef.current = {
              pointerId: e.pointerId,
              startSize: shownSizeN,
              startDist: dist,
              lastSize: shownSizeN,
            };
            setLocalSize(shownSizeN);
            e.currentTarget.setPointerCapture(e.pointerId);
          }}
          onPointerMove={(e) => {
            const d = resizeRef.current;
            if (!d || d.pointerId !== e.pointerId) return;
            const host = layerFromEvent(e);
            if (!host) return;
            const p = hostToNorm(e.clientX, e.clientY, host);
            const dist = Math.max(1e-4, Math.hypot(p.x - posNx, p.y - posNy));
            const next = clampSceneTokenSizeN(d.startSize * (dist / d.startDist));
            d.lastSize = next;
            setLocalSize(next);
            scheduleResize(next);
          }}
          onPointerUp={(e) => {
            endResize(e.currentTarget, e.pointerId);
          }}
          onPointerCancel={(e) => {
            endResize(e.currentTarget, e.pointerId);
          }}
        />
      ) : null}
    </div>
  );
}

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

  return (
    <div className={styles.layer} data-token-layer="1">
      {placements
        .filter((p) => known.has(p.tokenId))
        .map((placement) => {
          const key = String(placement.id);
          const pose = poseOverrides?.[key];
          const override = session?.byPlacementId[key] ?? session?.byPlacementId[placement.id];
          const nx = pose?.nx ?? override?.nx ?? placement.nx;
          const ny = pose?.ny ?? override?.ny ?? placement.ny;
          const sizeN = override?.sizeN ?? placement.sizeN;
          const rotationDeg = pose?.rotationDeg ?? placement.rotationDeg;
          const dragEnabled = dragEnabledById ? Boolean(dragEnabledById[key]) : true;
          return (
            <TokenSprite
              key={key}
              placement={placement}
              nx={nx}
              ny={ny}
              sizeN={sizeN}
              rotationDeg={rotationDeg}
              viewport={viewport}
              editable={editable}
              dragEnabled={dragEnabled}
              {...(onMove ? { onMove } : {})}
              {...(onResize ? { onResize } : {})}
              {...(resizeTitle ? { resizeTitle } : {})}
              {...(snapNorm ? { snapNorm } : {})}
              {...(onContextMenu ? { onContextMenu } : {})}
            />
          );
        })}
    </div>
  );
}
