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

import {resolveCompatibilityTemplateWorldType} from '../../data/customWorldRuntime';
import {MONSTERS_BY_WORLD, PLAYER_BASE_X_METERS} from '../../data/hostileNpcs';
import {buildEncounterFromSceneNpc} from '../../data/scenePresets';
import {
  resolveActiveSceneActBackgroundImage,
  resolveActiveSceneActEncounterNpcIds,
} from '../../services/customWorldSceneActRuntime';
import {AnimationState, type Encounter, type SceneNpc, WorldType} from '../../types';
import {GameCanvasEffectLayer} from './GameCanvasEffectLayer';
import {GameCanvasEntityLayer} from './GameCanvasEntityLayer';
import {GameCanvasOverlayLayer} from './GameCanvasOverlayLayer';
import {GameCanvasSceneLayer} from './GameCanvasSceneLayer';
import {
  type GameCanvasProps,
  getCharacterBottomOffsetPx,
  getMirroredStageEntityLeft,
  getMonsterWorldLeft,
  getPlayerWorldLeft,
  SCENE_TRANSITION_LOWER_COMPANION_DELAY_S,
  SCENE_TRANSITION_SPEED_PX_PER_S,
  SCENE_TRANSITION_SPRITE_CLEARANCE_PX,
} from './GameCanvasShared';

export function GameCanvasRuntime({
  scrollWorld,
  animationState,
  playerCharacter,
  encounter,
  currentScenePreset,
  worldType,
  customWorldProfile = null,
  storyEngineMemory = null,
  sceneHostileNpcs,
  playerX,
  playerOffsetY,
  playerFacing,
  playerActionMode = 'idle',
  inBattle,
  playerHp,
  playerMaxHp,
  activeCombatEffects = [],
  companions = [],
  dialogueIndicator = null,
  npcAffinityEffect = null,
  onEntitySelect = null,
  onSceneNameClick = null,
  sceneTransitionPhase = 'idle',
  sceneTransitionToken = 0,
  onSceneTransitionDurationsChange = null,
}: GameCanvasProps) {
  const stageRef = useRef<HTMLDivElement>(null);
  const [stageOuterWidth, setStageOuterWidth] = useState(0);
  const [backgroundLoadFailed, setBackgroundLoadFailed] = useState(false);
  const [sceneTitleSpinToken, setSceneTitleSpinToken] = useState(0);
  const previousSceneTitleRef = useRef<string | null>(currentScenePreset?.name ?? null);
  const resolvedWorldType = worldType
    ? resolveCompatibilityTemplateWorldType(worldType) ?? WorldType.WUXIA
    : null;
  const activeSceneActBackground =
    currentScenePreset?.id
      ? resolveActiveSceneActBackgroundImage({
          profile: customWorldProfile,
          sceneId: currentScenePreset.id,
          storyEngineMemory,
        })
      : null;
  const backgroundSrc = activeSceneActBackground
    || currentScenePreset?.imageSrc
    || (resolvedWorldType === WorldType.WUXIA ? '/scene_bg/45_PixelSky.png' : '/scene_bg/47_PixelSky.png');
  const activeSceneActEncounterNpcIds =
    currentScenePreset?.id
      ? resolveActiveSceneActEncounterNpcIds({
          profile: customWorldProfile,
          sceneId: currentScenePreset.id,
          storyEngineMemory,
        })
      : [];
  const activeSceneActNpcIdSet = new Set(activeSceneActEncounterNpcIds);
  const sceneActAmbientEncounters = (currentScenePreset?.npcs ?? [])
    .filter((npc: SceneNpc) => {
      if (activeSceneActNpcIdSet.size === 0) {
        return false;
      }

      const candidateIds = [npc.id, npc.characterId].filter(
        (value): value is string => Boolean(value),
      );
      const encounterIds = [encounter?.id, encounter?.characterId].filter(
        (value): value is string => Boolean(value),
      );

      return (
        candidateIds.some((id) => activeSceneActNpcIdSet.has(id)) &&
        !candidateIds.some((id) => encounterIds.includes(id))
      );
    })
    .slice(0, 2)
    .map((npc: SceneNpc, index): Encounter => {
      const npcEncounter = buildEncounterFromSceneNpc(npc);
      return {
        ...npcEncounter,
        xMeters: 3.2 + 1.08,
        id: npcEncounter.id ?? `${npc.id}:ambient-${index}`,
      };
    });
  const monsters = resolvedWorldType ? MONSTERS_BY_WORLD[resolvedWorldType] : [];
  const groundBottom = '18%';
  const stageLiftPx = 68;
  const playerGroundOffset = playerCharacter?.groundOffsetY ?? 22;
  const cameraAnchorX = scrollWorld ? playerX : PLAYER_BASE_X_METERS;
  const closestHostileNpcDistance = sceneHostileNpcs.length > 0
    ? Math.min(...sceneHostileNpcs.map(hostileNpc => Math.abs(hostileNpc.xMeters - playerX)))
    : Infinity;
  const escapeLead = scrollWorld ? Math.max(0, Math.min(1, (closestHostileNpcDistance - 1.2) / 3.4)) : 0;
  const sideAnchor = '15%';
  const playerMeleeLeft = `calc(100% - ${sideAnchor} - 13rem)`;
  const monsterMeleeLeft = `calc(100% - ${sideAnchor} - 20rem)`;
  const playerStageLeft = getMirroredStageEntityLeft(sideAnchor, 'player');
  const playerWorldLeft = getPlayerWorldLeft(sideAnchor, playerX, cameraAnchorX);
  const companionAnchorX = inBattle && !scrollWorld ? PLAYER_BASE_X_METERS : playerX;
  const companionAnchorLeft = getPlayerWorldLeft(sideAnchor, companionAnchorX, cameraAnchorX);
  const companionAnchorBottom = `calc(${groundBottom} + ${stageLiftPx}px - ${playerGroundOffset}px + ${playerOffsetY}px)`;
  const playerBottomOffsetPx = getCharacterBottomOffsetPx(stageLiftPx, playerCharacter, playerOffsetY);
  const playerLeft = playerActionMode === 'melee' && !scrollWorld
    ? playerMeleeLeft
    : scrollWorld
      ? playerWorldLeft
      : playerStageLeft;
  const monsterAnchorMeters = 3.2;
  const getHostileNpcOuterLeft = (hostileNpc: (typeof sceneHostileNpcs)[number]) => {
    if (hostileNpc.animation === 'attack' && hostileNpc.combatMode !== 'ranged' && !scrollWorld) {
      return monsterMeleeLeft;
    }

    return getMonsterWorldLeft(
      sideAnchor,
      hostileNpc.xMeters,
      cameraAnchorX,
      monsterAnchorMeters,
    );
  };
  const getPlayerEffectLeft = (effectX: number, offsetPx = 0) => {
    const base = playerActionMode === 'melee' && !scrollWorld
      ? playerMeleeLeft
      : getPlayerWorldLeft(sideAnchor, effectX, cameraAnchorX);
    return `calc(${base} + 3.5rem + ${offsetPx}px)`;
  };
  const getHostileNpcEffectLeft = (effectX: number, hostileNpcId?: string, offsetPx = 0) => {
    const effectHostileNpc = hostileNpcId ? sceneHostileNpcs.find(hostileNpc => hostileNpc.id === hostileNpcId) : null;
    const base = effectHostileNpc
      ? getHostileNpcOuterLeft(effectHostileNpc)
      : getMonsterWorldLeft(sideAnchor, effectX, cameraAnchorX, monsterAnchorMeters);
    return `calc(${base} + 3.5rem + ${offsetPx}px)`;
  };
  const isSceneTransitionExiting = sceneTransitionPhase === 'exiting';
  const isSceneTransitionEntering = sceneTransitionPhase === 'entering';
  const effectivePlayerAnimationState = sceneTransitionPhase === 'idle' ? animationState : AnimationState.RUN;
  const effectivePlayerFacing = sceneTransitionPhase === 'idle' ? playerFacing : 'right';
  const shouldShowPlayerDialogueIcon =
    Boolean(dialogueIndicator?.showPlayer)
    && sceneTransitionPhase === 'idle'
    && effectivePlayerAnimationState !== AnimationState.RUN;
  const transitionSweepPx = Math.max(stageOuterWidth + SCENE_TRANSITION_SPRITE_CLEARANCE_PX, 320);
  const sceneTransitionTravelDurationS = transitionSweepPx / SCENE_TRANSITION_SPEED_PX_PER_S;
  const sceneTransitionExitDurationS = sceneTransitionTravelDurationS;
  const sceneTransitionEntryDurationS = sceneTransitionTravelDurationS;
  const sceneTransitionEntryTotalDurationS =
    sceneTransitionEntryDurationS + SCENE_TRANSITION_LOWER_COMPANION_DELAY_S;

  useLayoutEffect(() => {
    const stage = stageRef.current;
    if (!stage) return;

    const measure = () => setStageOuterWidth(stage.clientWidth);

    measure();
    const observer = new ResizeObserver(() => measure());
    observer.observe(stage);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    setBackgroundLoadFailed(false);
  }, [backgroundSrc]);

  useEffect(() => {
    onSceneTransitionDurationsChange?.({
      exitMs: Math.round(sceneTransitionExitDurationS * 1000),
      entryMs: Math.round(sceneTransitionEntryTotalDurationS * 1000),
    });
  }, [
    onSceneTransitionDurationsChange,
    sceneTransitionEntryTotalDurationS,
    sceneTransitionExitDurationS,
  ]);

  useEffect(() => {
    const nextSceneTitle = currentScenePreset?.name ?? null;
    const previousSceneTitle = previousSceneTitleRef.current;

    if (nextSceneTitle && previousSceneTitle && previousSceneTitle !== nextSceneTitle) {
      setSceneTitleSpinToken(current => current + 1);
    }

    previousSceneTitleRef.current = nextSceneTitle;
  }, [currentScenePreset?.name]);

  return (
    <div ref={stageRef} className="relative h-full w-full overflow-hidden bg-black">
      <GameCanvasSceneLayer
        backgroundLoadFailed={backgroundLoadFailed}
        backgroundSrc={backgroundSrc}
        currentScenePreset={currentScenePreset}
        resolvedWorldType={resolvedWorldType}
        sceneTitleSpinToken={sceneTitleSpinToken}
        onSceneNameClick={onSceneNameClick}
        onBackgroundLoadError={() => setBackgroundLoadFailed(true)}
      />
      <GameCanvasEntityLayer
        companions={companions}
        sceneActAmbientEncounters={sceneActAmbientEncounters}
        currentScenePreset={currentScenePreset}
        sceneTransitionToken={sceneTransitionToken}
        isSceneTransitionEntering={isSceneTransitionEntering}
        isSceneTransitionExiting={isSceneTransitionExiting}
        transitionSweepPx={transitionSweepPx}
        sceneTransitionExitDurationS={sceneTransitionExitDurationS}
        sceneTransitionEntryDurationS={sceneTransitionEntryDurationS}
        companionAnchorLeft={companionAnchorLeft}
        companionAnchorBottom={companionAnchorBottom}
        playerBottomOffsetPx={playerBottomOffsetPx}
        sceneTransitionPhase={sceneTransitionPhase}
        inBattle={inBattle}
        onEntitySelect={onEntitySelect}
        playerLeft={playerLeft}
        playerCharacter={playerCharacter}
        playerHp={playerHp}
        playerMaxHp={playerMaxHp}
        effectivePlayerFacing={effectivePlayerFacing}
        effectivePlayerAnimationState={effectivePlayerAnimationState}
        shouldShowPlayerDialogueIcon={shouldShowPlayerDialogueIcon}
        dialogueIndicator={dialogueIndicator}
        npcAffinityEffect={npcAffinityEffect}
        sceneCombatants={sceneHostileNpcs}
        monsters={monsters}
        getHostileNpcOuterLeft={getHostileNpcOuterLeft}
        groundBottom={groundBottom}
        stageLiftPx={stageLiftPx}
        encounter={encounter}
        sideAnchor={sideAnchor}
        cameraAnchorX={cameraAnchorX}
        monsterAnchorMeters={monsterAnchorMeters}
        playerX={playerX}
      />
      <GameCanvasEffectLayer
        activeCombatEffects={activeCombatEffects}
        getPlayerEffectLeft={getPlayerEffectLeft}
        getHostileNpcEffectLeft={getHostileNpcEffectLeft}
        sceneCombatants={sceneHostileNpcs}
        playerCharacter={playerCharacter}
        groundBottom={groundBottom}
        stageLiftPx={stageLiftPx}
        playerOffsetY={playerOffsetY}
        stageRef={stageRef}
      />
      <GameCanvasOverlayLayer escapeLead={escapeLead} />
    </div>
  );
}
