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

import type {Character, CombatVisualEffect, SceneHostileNpc} from '../../types';
import {getEntityEffectBottom} from './GameCanvasShared';

interface GameCanvasEffectLayerProps {
  activeCombatEffects: CombatVisualEffect[];
  getPlayerEffectLeft: (effectX: number, offsetPx?: number) => string;
  getHostileNpcEffectLeft: (effectX: number, hostileNpcId?: string, offsetPx?: number) => string;
  sceneCombatants: SceneHostileNpc[];
  playerCharacter: Character | null;
  groundBottom: string;
  stageLiftPx: number;
  playerOffsetY: number;
  stageRef: React.RefObject<HTMLDivElement | null>;
}

function useCombatEffectFrames(effect: CombatVisualEffect) {
  const [frameIndex, setFrameIndex] = useState(0);

  useEffect(() => {
    setFrameIndex(0);

    if (effect.frames.length <= 1) return;

    const interval = window.setInterval(() => {
      setFrameIndex(prev => Math.min(prev + 1, effect.frames.length - 1));
    }, Math.max(50, Math.round(1000 / effect.fps)));

    return () => window.clearInterval(interval);
  }, [effect.fps, effect.frames, effect.id]);

  return Math.min(frameIndex, Math.max(0, effect.frames.length - 1));
}

function TravelingSpriteCombatEffect({
  effect,
  startLeft,
  endLeft,
  startBottom,
  endBottom,
  stageRef,
}: {
  effect: CombatVisualEffect;
  startLeft: string;
  endLeft: string;
  startBottom: string;
  endBottom: string;
  stageRef: React.RefObject<HTMLDivElement | null>;
}) {
  const frameIndex = useCombatEffectFrames(effect);
  const startMarkerRef = useRef<HTMLDivElement>(null);
  const endMarkerRef = useRef<HTMLDivElement>(null);
  const [vector, setVector] = useState({x: 0, y: 0});
  const [measured, setMeasured] = useState(false);

  useLayoutEffect(() => {
    setMeasured(false);
    let cancelled = false;

    const measure = () => {
      const stage = stageRef.current;
      const startEl = startMarkerRef.current;
      const endEl = endMarkerRef.current;
      if (cancelled) return;
      if (!stage || !startEl || !endEl) {
        setVector({x: 0, y: 0});
        setMeasured(true);
        return;
      }

      const stageRect = stage.getBoundingClientRect();
      const startRect = startEl.getBoundingClientRect();
      const endRect = endEl.getBoundingClientRect();
      const startX = startRect.left + startRect.width / 2 - stageRect.left;
      const startY = startRect.top + startRect.height / 2 - stageRect.top;
      const endX = endRect.left + endRect.width / 2 - stageRect.left;
      const endY = endRect.top + endRect.height / 2 - stageRect.top;
      setVector({x: endX - startX, y: endY - startY});
      setMeasured(true);
    };

    const frameId = requestAnimationFrame(() => {
      requestAnimationFrame(measure);
    });

    return () => {
      cancelled = true;
      cancelAnimationFrame(frameId);
    };
  }, [effect.id, endBottom, endLeft, stageRef, startBottom, startLeft]);

  const half = effect.sizePx / 2;
  const markerBox: React.CSSProperties = {
    position: 'absolute',
    width: effect.sizePx,
    height: effect.sizePx,
    marginLeft: -half,
    pointerEvents: 'none',
    visibility: 'hidden',
    zIndex: 0,
  };

  return (
    <>
      <div ref={startMarkerRef} aria-hidden style={{...markerBox, left: startLeft, bottom: startBottom}} />
      <div ref={endMarkerRef} aria-hidden style={{...markerBox, left: endLeft, bottom: endBottom}} />
      {measured && (
        <motion.div
          initial={{x: 0, y: 0, opacity: 0.98}}
          animate={{x: vector.x, y: vector.y, opacity: [1, 1, 0.94]}}
          transition={{duration: effect.durationMs / 1000, ease: 'linear'}}
          className="pointer-events-none absolute"
          style={{
            left: startLeft,
            bottom: startBottom,
            width: `${effect.sizePx}px`,
            height: `${effect.sizePx}px`,
            zIndex: effect.zIndex ?? 24,
            marginLeft: `-${half}px`,
          }}
        >
          <img
            src={effect.frames[frameIndex]}
            alt=""
            className="h-full w-full object-contain"
            style={{
              imageRendering: 'pixelated',
              transform: effect.facing === 'left'
                ? `scaleX(-1) scale(${effect.scale ?? 1})`
                : `scale(${effect.scale ?? 1})`,
            }}
          />
        </motion.div>
      )}
    </>
  );
}

function SpriteCombatEffect({
  effect,
  startLeft,
  endLeft,
  startBottom,
  endBottom,
}: {
  effect: CombatVisualEffect;
  startLeft: string;
  endLeft?: string;
  startBottom: string;
  endBottom?: string;
}) {
  const frameIndex = useCombatEffectFrames(effect);

  return (
    <motion.div
      initial={{left: startLeft, bottom: startBottom, opacity: 0.98}}
      animate={{
        left: endLeft ?? startLeft,
        bottom: endBottom ?? startBottom,
        opacity: [1, 1, 0.94],
      }}
      transition={{duration: effect.durationMs / 1000, ease: 'linear'}}
      className="pointer-events-none absolute"
      style={{
        width: `${effect.sizePx}px`,
        height: `${effect.sizePx}px`,
        zIndex: effect.zIndex ?? 24,
        marginLeft: `-${effect.sizePx / 2}px`,
      }}
    >
      <img
        src={effect.frames[frameIndex]}
        alt=""
        className="h-full w-full object-contain"
        style={{
          imageRendering: 'pixelated',
          transform: effect.facing === 'left'
            ? `scaleX(-1) scale(${effect.scale ?? 1})`
            : `scale(${effect.scale ?? 1})`,
        }}
      />
    </motion.div>
  );
}

export function GameCanvasEffectLayer({
  activeCombatEffects,
  getPlayerEffectLeft,
  getHostileNpcEffectLeft,
  sceneCombatants,
  playerCharacter,
  groundBottom,
  stageLiftPx,
  playerOffsetY,
  stageRef,
}: GameCanvasEffectLayerProps) {
  return (
    <>
      {activeCombatEffects.map(effect => {
        const startLeft = effect.startOrigin === 'player'
          ? getPlayerEffectLeft(effect.startX, effect.startOffsetX ?? 0)
          : getHostileNpcEffectLeft(effect.startX, effect.startHostileNpcId ?? effect.startMonsterId, effect.startOffsetX ?? 0);
        const endLeft = effect.endOrigin === 'player'
          ? getPlayerEffectLeft(effect.endX ?? effect.startX, effect.endOffsetX ?? effect.startOffsetX ?? 0)
          : effect.endOrigin === 'hostile_npc' || effect.endOrigin === 'monster'
            ? getHostileNpcEffectLeft(effect.endX ?? effect.startX, effect.endHostileNpcId ?? effect.endMonsterId, effect.endOffsetX ?? effect.startOffsetX ?? 0)
            : undefined;
        const startBottom = `calc(${getEntityEffectBottom({
          origin: effect.startOrigin,
          hostileNpcId: effect.startHostileNpcId ?? effect.startMonsterId,
          sceneCombatants,
          playerCharacter,
          groundBottom,
          stageLiftPx,
          playerOffsetY,
          anchorOffsetY: effect.startAnchorOffsetY ?? 0,
        })} + ${effect.startYOffset}px)`;
        const endBottom = `calc(${getEntityEffectBottom({
          origin: effect.endOrigin ?? effect.startOrigin,
          hostileNpcId: effect.endHostileNpcId ?? effect.endMonsterId ?? effect.startHostileNpcId ?? effect.startMonsterId,
          sceneCombatants,
          playerCharacter,
          groundBottom,
          stageLiftPx,
          playerOffsetY,
          anchorOffsetY: effect.endAnchorOffsetY ?? effect.startAnchorOffsetY ?? 0,
        })} + ${(effect.endYOffset ?? effect.startYOffset)}px)`;

        const useTravelingPath = Boolean(
          effect.traveling
          && endLeft
          && endBottom
          && (startLeft !== endLeft || startBottom !== endBottom),
        );

        if (useTravelingPath && endLeft && endBottom) {
          return (
            <TravelingSpriteCombatEffect
              key={effect.id}
              effect={effect}
              startLeft={startLeft}
              endLeft={endLeft}
              startBottom={startBottom}
              endBottom={endBottom}
              stageRef={stageRef}
            />
          );
        }

        return (
          <SpriteCombatEffect
            key={effect.id}
            effect={effect}
            startLeft={startLeft}
            endLeft={endLeft}
            startBottom={startBottom}
            endBottom={endBottom}
          />
        );
      })}
    </>
  );
}
