import React, {type CSSProperties, type ReactNode} from "react";
import {spring, useCurrentFrame, useVideoConfig} from "remotion";
import {finite, inOut, lerp, nonNegative, phase, positive, reveal, staggerAt, type MotionTiming} from "./core.js";

export type StaggeredWordsProps = MotionTiming & {text: string; stagger?: number; distance?: number; style?: CSSProperties};
export function StaggeredWords({text, at = 0, duration = 0.55, stagger = 0.07, distance = 24, style}: StaggeredWordsProps) {
  const frame = useCurrentFrame(), {fps} = useVideoConfig();
  nonNegative(distance, "distance"); nonNegative(stagger, "stagger"); positive(duration, "duration"); finite(at, "at");
  return <div aria-label={text} style={{display: "flex", flexWrap: "wrap", columnGap: "0.28em", rowGap: 0, ...style}}>{text.trim().split(/\s+/).filter(Boolean).map((word, i) => {
    const p = reveal(frame, fps, staggerAt(at, i, stagger), duration);
    return <span aria-hidden key={i} style={{display: "inline-block", opacity: p, transform: `translateY(${distance * (1 - p)}px)`}}>{word}</span>;
  })}</div>;
}

export type KineticCaptionProps = MotionTiming & {text: string; recedeAt?: number; recedeDuration?: number; captionScale?: number; captionY?: number; style?: CSSProperties};
/** A headline establishes the idea, then makes room for a product shot. */
export function KineticCaption({text, at = 0, duration = 0.55, recedeAt = 1.7, recedeDuration = 0.6, captionScale = 0.42, captionY, style}: KineticCaptionProps) {
  const frame = useCurrentFrame(), {fps, height} = useVideoConfig();
  positive(captionScale, "captionScale"); finite(captionY ?? height * 0.88, "captionY");
  const p = inOut(phase(frame, fps, recedeAt, recedeDuration));
  return <div style={{position: "absolute", left: 0, right: 0, textAlign: "center", fontSize: 88, fontWeight: 700, letterSpacing: "-0.05em", ...style, top: lerp(height / 2, captionY ?? height * 0.88, p), transform: `translateY(-50%) scale(${lerp(1, captionScale, p)})`, transformOrigin: "50% 50%"}}>
    <StaggeredWords text={text} at={at} duration={duration} style={{justifyContent: "center"}} />
  </div>;
}

export type SlamWordProps = MotionTiming & {text: string; size?: number; color?: string; style?: CSSProperties};
export function SlamWord({text, at = 0, duration = 0.6, size = 150, color = "currentColor", style}: SlamWordProps) {
  const frame = useCurrentFrame(), {fps} = useVideoConfig();
  positive(size, "size"); positive(duration, "duration"); finite(at, "at");
  const local = frame - at * fps;
  const p = spring({frame: Math.max(0, local), fps, durationInFrames: Math.max(1, Math.round(duration * fps)), config: {damping: 13, stiffness: 320, mass: 0.6}});
  const scale = 1.55 - 0.55 * p;
  return <div style={{fontSize: size, fontWeight: 850, lineHeight: 0.95, letterSpacing: "-0.055em", color, ...style, opacity: local < 0 ? 0 : 1, transform: `scale(${scale})`}}>{text}</div>;
}

export type HypeTickerProps = MotionTiming & {value: number; from?: number; prefix?: string; suffix?: string; decimals?: number; style?: CSSProperties};
export function HypeTicker({value, from = 0, prefix = "", suffix = "", decimals = 0, at = 0, duration = 1.7, style}: HypeTickerProps) {
  const frame = useCurrentFrame(), {fps} = useVideoConfig();
  finite(value, "value"); finite(from, "from");
  if (!Number.isInteger(decimals) || decimals < 0 || decimals > 6) throw new RangeError("decimals must be an integer from zero to six.");
  const p = reveal(frame, fps, at, duration);
  const formatted = new Intl.NumberFormat("en-US", {minimumFractionDigits: decimals, maximumFractionDigits: decimals}).format(lerp(from, value, p));
  return <span style={{fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", ...style, opacity: frame / fps < at ? 0 : 1}}>{prefix}{formatted}{suffix}</span>;
}

export type RuleDrawProps = MotionTiming & {width?: number; thickness?: number; color?: string; style?: CSSProperties};
export function RuleDraw({at = 0, duration = 0.65, width = 420, thickness = 6, color = "currentColor", style}: RuleDrawProps) {
  const p = reveal(useCurrentFrame(), useVideoConfig().fps, at, duration);
  positive(width, "width"); positive(thickness, "thickness");
  return <div style={{width, height: thickness, borderRadius: thickness / 2, background: color, ...style, opacity: p === 0 ? 0 : 1, transform: `scaleX(${p})`, transformOrigin: "left center"}} />;
}

export type HighlightSweepProps = MotionTiming & {children: ReactNode; color?: string; opacity?: number; style?: CSSProperties};
export function HighlightSweep({children, at = 0, duration = 0.65, color = "#b5f36d", opacity = 1, style}: HighlightSweepProps) {
  const p = reveal(useCurrentFrame(), useVideoConfig().fps, at, duration);
  if (finite(opacity, "opacity") < 0 || opacity > 1) throw new RangeError("opacity must be between zero and one.");
  return <span style={{position: "relative", display: "inline-block", isolation: "isolate", ...style}}>
    <span aria-hidden style={{position: "absolute", inset: "0.05em -0.1em 0", background: color, opacity, zIndex: -1, transform: `scaleX(${p})`, transformOrigin: "left center"}} />
    {children}
  </span>;
}
