import { useMemo, useRef, useState } from 'react';
import type { RefObject } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import type { TTSObject } from '@tts/shared';
import { useBounds } from '@react-three/drei';
import Scene from './Scene';
import { CardObjectMesh } from './CardMesh';

/** How many cards to show to each side of the active card. */
const HALF_WINDOW = 3;
/** Angular spacing between adjacent cards in the arc, in radians. */
const ARC_STEP = 0.32;
/** Minimum radius of the arc, in world units. */
const ARC_RADIUS = 3.2;
/** Extra clearance between the active card's edge and its neighbors, in world units. */
const ARC_PADDING = 0.2;
/**
 * A deck carousel: the deck's contained cards are fanned in a 3D arc with the
 * active card front and center. Prev/next controls step through the deck, each
 * card animating to its new slot. Side cards are turned 90° in y (album flow)
 * so only the active card's face is framed; all neighbors edge-on around it.
 *
 * The camera is fitted to just the active card (not the whole carousel): the
 * shared scene's auto-fit is disabled and `useBounds` refits whenever the
 * selection changes.
 *
 * Only a window of cards around the active one is rendered (the rest stay
 * hidden), so large decks stay lean. Falls back to a single card (the deck
 * object itself) when there are no contained cards.
 */
export default function DeckViewer({ object }: { object: TTSObject }) {
  const cards = (object.ContainedObjects ?? []).filter(
    (o) => o.CardID != null || o.CustomImage != null,
  );
  const count = cards.length;
  const [active, setActive] = useState(0);
  const hasCards = count > 0;
  const centerRef = useRef<THREE.Group>(null);
  // The active card's world-space width, measured once it's laid out. Used to
  // widen the arc so neighbors clear the card's edges (a fixed radius only fits
  // square cards; wider cards clip their neighbors).
  const [cardWidth, setCardWidth] = useState<number | null>(null);

  const step = (dir: number) => setActive((a) => (a + dir + count) % count);
  const radius = arcRadius(cardWidth);

  const visible = useMemo(
    () =>
      cards
        .map((card, i) => ({ card, i, k: i - active }))
        .filter((v) => Math.abs(v.k) <= HALF_WINDOW),
    [cards, active],
  );

  // The active card always settles to the arc center, so the camera only needs
  // to frame it once on mount.
  const didFit = useRef(false);

  return (
    <Scene
      fit={false}
      autoRotate={false}
      overlay={
        hasCards ? (
          <CarouselControls active={active} count={count} onStep={step} />
        ) : undefined
      }
    >
      {hasCards ? (
        <>
          {visible.map(({ card, i, k }) => (
            // Key by the card's index (stable across renders) so the element
            // persists and tweens as its slot changes; the index-path key is
            // unique even though cards in a deck share the same GUID.
            <CarouselCard
              key={i}
              card={card}
              k={k}
              radius={radius}
              groupRef={k === 0 ? centerRef : undefined}
            />
          ))}
          <FitActive
            targetRef={centerRef}
            didFit={didFit}
            onMeasure={setCardWidth}
          />
        </>
      ) : (
        <CardObjectMesh object={object} />
      )}
    </Scene>
  );
}

/**
 * Fits the camera once, on the first frame, to frame the active card from the
 * front. Runs in a frame callback (not an effect) so the active card has been
 * moved to its arc slot by its own `useFrame` first — otherwise the group is
 * still at the origin and the camera would frame the carousel center.
 *
 * The card's front face points toward +Z, so the camera is placed directly in
 * front of it and looks straight at it, rather than keeping its initial side
 * angle (which drei's `fit()` would do). The active card always settles in the
 * same slot, so no refit is needed while navigating — that would restart the
 * camera tween on every step and feel laggy.
 */
function FitActive({
  targetRef,
  didFit,
  onMeasure,
}: {
  targetRef: RefObject<THREE.Group | null>;
  didFit: RefObject<boolean>;
  onMeasure: (width: number) => void;
}) {
  const bounds = useBounds();

  useFrame(() => {
    const node = targetRef.current;
    if (!node) return;
    bounds.refresh(node);
    const { size, center, distance } = bounds.getSize();
    // Report the active card's width so the arc radius can widen for wide
    // cards (see `arcRadius`). Runs every frame until the radius settles.
    onMeasure(size.x);
    if (didFit.current) return;
    didFit.current = true;
    // The card's front face points toward +Z, so put the camera in front of it
    // and look straight at it.
    bounds
      .moveTo([center.x, center.y, center.z + distance])
      .lookAt({ target: center });
  });

  return null;
}

/** A single card that tweens into its arc slot each frame. */
function CarouselCard({
  card,
  k,
  radius,
  groupRef,
}: {
  card: TTSObject;
  k: number;
  radius: number;
  groupRef?: RefObject<THREE.Group | null>;
}) {
  const localRef = useRef<THREE.Group>(null);
  const group = groupRef ?? localRef;
  // Start at the target so the first render doesn't tween into place.
  const state = useRef(slotTransform(k, radius));
  const target = useMemo(() => slotTransform(k, radius), [k, radius]);

  useFrame((_, dt) => {
    const g = group.current;
    if (!g) return;
    // Smooth per-frame damping independent of frame rate.
    const f = 1 - Math.pow(0.0001, dt);
    const t = target;
    const s = state.current;
    s.x += (t.x - s.x) * f;
    s.z += (t.z - s.z) * f;
    s.rot += (t.rot - s.rot) * f;
    s.scale += (t.scale - s.scale) * f;
    g.position.set(s.x, 0, s.z);
    g.rotation.y = s.rot;
    g.scale.setScalar(s.scale);
  });

  return (
    <group ref={group}>
      <CardObjectMesh object={card} />
    </group>
  );
}

interface Slots {
  x: number;
  z: number;
  rot: number;
  scale: number;
}

/** World transform for a card at arc offset `k` (0 = front and center). */
function slotTransform(k: number, radius: number): Slots {
  const ang = k * ARC_STEP;
  // Side cards turn edge-on (album flow); the active card stays forward.
  const turn = k === 0 ? 0 : Math.sign(k) * (Math.PI / 2);
  return {
    x: Math.sin(ang) * radius,
    z: Math.cos(ang) * radius,
    rot: turn,
    scale: 1.15 - 0.15 * Math.abs(k),
  };
}

/**
 * Arc radius that keeps the nearest neighbors clear of the active card's
 * edges. A neighbor at `k = 1` sits at `x = sin(ARC_STEP) * radius`, so the
 * radius must exceed `cardWidth / 2 / sin(ARC_STEP)` for the neighbor to clear
 * the card's half-width (plus padding). Falls back to the minimum when the
 * card width isn't known yet.
 */
function arcRadius(cardWidth: number | null): number {
  if (cardWidth == null) return ARC_RADIUS;
  return Math.max(ARC_RADIUS, (cardWidth / 2 + ARC_PADDING) / Math.sin(ARC_STEP));
}

/** Prev/next controls and a counter, rendered as the scene overlay. */
function CarouselControls({
  active,
  count,
  onStep,
}: {
  active: number;
  count: number;
  onStep: (dir: number) => void;
}) {
  const prev = () => onStep(-1);
  const next = () => onStep(1);
  return (
    <div className="absolute inset-x-0 bottom-2 z-10 flex items-center justify-center gap-3">
      <button
        onClick={prev}
        aria-label="Previous card"
        className="flex h-8 w-8 items-center justify-center rounded-md border border-zinc-700 bg-zinc-900/80 text-zinc-300 backdrop-blur transition hover:bg-zinc-800 hover:text-zinc-100"
      >
        ‹
      </button>
      <span className="rounded-md bg-zinc-900/80 px-3 py-1 font-mono text-xs text-zinc-300 backdrop-blur">
        {active + 1} / {count}
      </span>
      <button
        onClick={next}
        aria-label="Next card"
        className="flex h-8 w-8 items-center justify-center rounded-md border border-zinc-700 bg-zinc-900/80 text-zinc-300 backdrop-blur transition hover:bg-zinc-800 hover:text-zinc-100"
      >
        ›
      </button>
    </div>
  );
}