import {
  useEffect,
  useRef,
  useState,
  type CSSProperties,
  type ReactNode,
} from 'react';
import { SPRINGS, prefersReducedMotion, type Spring } from './utils.js';

export interface RevealProps {
  children: ReactNode;
  /** Entry direction. Default: 'up' (translateY 8px). */
  from?: 'up' | 'down' | 'left' | 'right' | 'fade' | 'scale';
  /** Distance in pixels for translate variants. Default 8. */
  distance?: number;
  /** Animation duration (ms). Default 320. */
  duration?: number;
  /** Trigger delay (ms). Default 0. */
  delay?: number;
  /** Spring curve preset. Default 'snappy'. */
  spring?: Spring;
  /** IntersectionObserver threshold. Default 0.1. */
  threshold?: number;
  /** Trigger only once (default) vs every time it enters the viewport. */
  once?: boolean;
  /** Bypass animation (e.g. for SSR-stable visual-regression tests). */
  disableMotion?: boolean;
  /** Tailwind escape hatch. */
  className?: string;
  /** Inline style escape hatch — merged AFTER motion vars. */
  style?: CSSProperties;
}

/**
 * `<Reveal>` — fades + slides a child into view on viewport entry.
 *
 * Uses IntersectionObserver + CSS transitions; zero runtime physics.
 * Automatically disables animation under `prefers-reduced-motion`.
 *
 * @example
 * ```tsx
 * <Reveal from="up" delay={120}>
 *   <h1>Hero headline</h1>
 * </Reveal>
 * ```
 */
export function Reveal({
  children,
  from = 'up',
  distance = 8,
  duration = 320,
  delay = 0,
  spring = 'snappy',
  threshold = 0.1,
  once = true,
  disableMotion,
  className,
  style,
}: RevealProps) {
  const ref = useRef<HTMLDivElement>(null);
  const [visible, setVisible] = useState(false);
  const reduced = disableMotion ?? prefersReducedMotion();

  useEffect(() => {
    if (reduced) {
      setVisible(true);
      return;
    }
    const el = ref.current;
    if (!el || typeof IntersectionObserver === 'undefined') {
      setVisible(true);
      return;
    }
    const io = new IntersectionObserver(
      entries => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            setVisible(true);
            if (once) io.disconnect();
          } else if (!once) {
            setVisible(false);
          }
        }
      },
      { threshold },
    );
    io.observe(el);
    return () => io.disconnect();
  }, [threshold, once, reduced]);

  const transform = visible || reduced ? 'none' : initialTransform(from, distance);
  const opacity = visible || reduced ? 1 : from === 'scale' ? 0.92 : 0;

  return (
    <div
      ref={ref}
      data-testid="bl-reveal"
      data-visible={visible || reduced}
      className={className}
      style={{
        transform,
        opacity,
        transition: reduced
          ? 'none'
          : `transform ${duration}ms ${SPRINGS[spring]} ${delay}ms, opacity ${duration}ms ${SPRINGS[spring]} ${delay}ms`,
        willChange: 'transform, opacity',
        ...style,
      }}
    >
      {children}
    </div>
  );
}

function initialTransform(from: RevealProps['from'], d: number): string {
  switch (from) {
    case 'up':
      return `translate3d(0, ${d}px, 0)`;
    case 'down':
      return `translate3d(0, ${-d}px, 0)`;
    case 'left':
      return `translate3d(${d}px, 0, 0)`;
    case 'right':
      return `translate3d(${-d}px, 0, 0)`;
    case 'scale':
      return 'scale(0.96)';
    case 'fade':
    default:
      return 'none';
  }
}
