/**
 * CameraController - handles camera movement and focus
 */
import { useEffect, useRef, type RefObject } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { Vector3 } from 'three';
import type { CelestialBody } from '../types';
import { calculateRenderPosition, findParentPlanet } from '../utils/renderPosition';
import { getCelestialSize } from '../config/celestialSizes';

interface OrbitControlsLike {
  target: Vector3;
  update: () => void;
}

type CelestialTypeConfigs = Record<string, { ratio?: number; default?: number }> | null;

interface CameraControllerProps {
  focusTarget: CelestialBody | null;
  allBodies: CelestialBody[];
  onAnimationComplete?: () => void;
  resetTrigger?: number;
  defaultPosition?: [number, number, number];
  typeConfigs?: CelestialTypeConfigs;
  controlsRef: RefObject<OrbitControlsLike | null>;
}

export function CameraController({ 
  focusTarget, 
  allBodies, 
  onAnimationComplete, 
  resetTrigger = 0,
  defaultPosition = [10, 8, 10],
  typeConfigs,
  controlsRef,
}: CameraControllerProps) {
  const { camera } = useThree();
  const targetPosition = useRef(new Vector3());
  const targetLookPosition = useRef(new Vector3());
  const isAnimating = useRef(false);
  const isResetting = useRef(false);
  const animationProgress = useRef(0);
  const startPosition = useRef(new Vector3());
  const startLookPosition = useRef(new Vector3());
  const trackedLookPosition = useRef(new Vector3());
  const lastResetTrigger = useRef(0);

  // Keep the latest bodies list without making it a trigger for the focus effect.
  // `allBodies` gets a new array reference on every timeline/historical frame, and we
  // don't want that to re-fire the fly-to animation while the user is watching playback.
  const allBodiesRef = useRef(allBodies);
  allBodiesRef.current = allBodies;

  // Handle manual reset trigger
  useEffect(() => {
    if (resetTrigger !== lastResetTrigger.current) {
      lastResetTrigger.current = resetTrigger;
      // Force reset
      targetPosition.current.set(...defaultPosition);
      targetLookPosition.current.set(0, 0, 0);
      startPosition.current.copy(camera.position);
      startLookPosition.current.copy(controlsRef.current?.target ?? new Vector3());
      isAnimating.current = true;
      isResetting.current = true;
      animationProgress.current = 0;
    }
  }, [resetTrigger, camera, defaultPosition, controlsRef]); // Only run when resetTrigger changes

  // Handle focus target changes
  // Trigger only on the SELECTED body changing (by id), not on every new bodies array.
  useEffect(() => {
    const bodies = allBodiesRef.current;
    if (focusTarget) {
      isResetting.current = false; // Cancel reset if target selected

      // Focus on target - use smart rendered position
      const latestBody = bodies.find((body) => body.id === focusTarget.id) ?? focusTarget;
      const renderPos = calculateRenderPosition(latestBody, bodies, typeConfigs);
      const currentTargetPos = new Vector3(renderPos.x, renderPos.z, renderPos.y);
      const bodySize = getCelestialSize(latestBody, undefined, typeConfigs);
      const parentInfo = findParentPlanet(latestBody, bodies);
      const focusDistance = latestBody.type === 'star'
        ? Math.max(5, bodySize * 9)
        : latestBody.type === 'planet'
          ? Math.max(4, bodySize * 7)
          : latestBody.type === 'dwarf_planet'
            ? Math.max(2.4, bodySize * 8)
            : parentInfo
              ? Math.max(1.4, bodySize * 8)
              : Math.max(1.6, bodySize * 9);
      const previousLook = controlsRef.current?.target ?? new Vector3();
      const viewDirection = camera.position.clone().sub(previousLook);
      if (viewDirection.lengthSq() < 0.0001) viewDirection.set(1, 0.65, 1);
      viewDirection.normalize();

      targetLookPosition.current.copy(currentTargetPos);
      targetPosition.current.copy(currentTargetPos)
        .addScaledVector(viewDirection, focusDistance)
        .add(new Vector3(0, focusDistance * 0.12, 0));

      // Start animation to target
      startPosition.current.copy(camera.position);
      startLookPosition.current.copy(previousLook);
      trackedLookPosition.current.copy(currentTargetPos);
      isAnimating.current = true;
      animationProgress.current = 0;

    } else {
      // Target became null (e.g. info window closed)
      // Only stop animation if we are NOT in the middle of a reset
      if (!isResetting.current && isAnimating.current) {
        isAnimating.current = false;
        animationProgress.current = 0;
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [focusTarget?.id, camera, controlsRef, typeConfigs]);

  useFrame((_, delta) => {
    const orbitControls = controlsRef.current;
    if (isAnimating.current) {
      // Smooth animation using easing
      animationProgress.current += delta * 0.8; // Animation speed

      if (animationProgress.current >= 1) {
        animationProgress.current = 1;
        isAnimating.current = false;
        isResetting.current = false; // Animation done
        if (onAnimationComplete) onAnimationComplete();
      }

      // Easing function (ease-in-out)
      const t = animationProgress.current;
      const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;

      // Interpolate camera position
      camera.position.lerpVectors(startPosition.current, targetPosition.current, eased);

      if (orbitControls) {
        orbitControls.target.lerpVectors(startLookPosition.current, targetLookPosition.current, eased);
        orbitControls.update();
      }
      camera.lookAt(targetLookPosition.current);
    } else if (focusTarget && orbitControls) {
      const bodies = allBodiesRef.current;
      const latestBody = bodies.find((body) => body.id === focusTarget.id) ?? focusTarget;
      const renderPos = calculateRenderPosition(latestBody, bodies, typeConfigs);
      const nextLook = new Vector3(renderPos.x, renderPos.z, renderPos.y);
      const movement = nextLook.sub(trackedLookPosition.current);
      if (movement.lengthSq() > 0.00000001) {
        camera.position.add(movement);
        orbitControls.target.add(movement);
        trackedLookPosition.current.add(movement);
        orbitControls.update();
      }
    }
  });

  return null;
}
