import { useTexture } from '@react-three/drei';
import { useMemo } from 'react';
import * as THREE from 'three';
import type { TTSObject } from '@tts/shared';
import {
  extrudeShapeParts,
  roundedRectShape,
  type ExtrudedGeometry,
} from '@tts/mesh';
import Scene from './Scene';
import { assetUrl } from '@tts/http';
import { cardAspect, resolveCardConfig, spriteUv } from './cardResolution';
import { flipTexture } from './flipTexture';
import { getSharedGeometry, objectTint, tintedColor } from './sharedResources';

/** Longer card dimension, in world units. */
const CARD_LENGTH = 2;
/** Corner radius as a fraction of the shorter card edge. */
const CORNER_RADIUS = 0.05;
/** Thickness of the card. */
const CARD_THICKNESS = 0.06;

// A 1x1 transparent placeholder so `useTexture` always receives a valid URL.
// Without it, the face/back hooks would be called conditionally, which breaks
// React's rules of hooks when switching between objects with different URL
// combinations (e.g. a deck with a back vs. a `CardCustom` with only an image).
const FALLBACK_URL =
  'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

/**
 * A playing card: a thin rounded rect with the face texture on the front and
 * the back texture on the rear. Covers `Card`/`Deck`/`DeckCustom`/`Custom_Deck`
 * (via `CustomDeck` face/back URLs) and `CardCustom` (via `CustomImage`).
 *
 * A deck image is a sheet divided into a `NumWidth` x `NumHeight` grid of
 * sprites. The card footprint is sized to a single sprite's aspect ratio, and
 * the face material uses UV offset/scaling to show the sprite selected by
 * `CardID`. The corners stay circular because the rounded rect is built from
 * the final width/height rather than scaling a square.
 *
 * `CardID` encodes the deck index in the hundreds place and the 1-based card
 * number in the last two digits (e.g. 354 -> deck 3, card 54). The deck config
 * (grid, face/back URLs) is resolved from the containing deck object's
 * `CustomDeck[deckIndex]`, since a card's own `CustomDeck` may be keyed
 * differently or absent.
 *
 * The back is treated like a tile (a single full image) unless the deck has
 * `UniqueBack`, in which case it is a sheet too and gets the same sprite cell.
 * It is flipped left/right so it isn't mirrored when viewed from the back of
 * the card.
 */
export default function CardViewer({ object }: { object: TTSObject }) {
  return (
    <Scene>
      <CardObjectMesh object={object} />
    </Scene>
  );
}

/**
 * The card mesh for an object, exported so the full-setup view can compose it
 * into a shared scene.
 */
export function CardObjectMesh({ object }: { object: TTSObject }) {
  const { cardId, faceUrl, backUrl, numWidth, numHeight, uniqueBack } =
    resolveCardConfig(object);

  return (
    <CardMesh
      faceUrl={faceUrl}
      backUrl={backUrl}
      numWidth={numWidth}
      numHeight={numHeight}
      uniqueBack={uniqueBack}
      cardId={cardId}
      tint={objectTint(object)}
    />
  );
}

// Rendered inside the Canvas so `useTexture` can access the R3F store.
// Exported so the full-setup view can compose it into a shared scene.
export function CardMesh({
  faceUrl,
  backUrl,
  numWidth,
  numHeight,
  uniqueBack,
  cardId,
  tint,
}: {
  faceUrl?: string;
  backUrl?: string;
  numWidth?: number;
  numHeight?: number;
  uniqueBack: boolean;
  cardId?: number;
  tint: THREE.Color;
}) {
  // Always call both hooks so the hook count is stable across renders. The
  // placeholder is used only when a URL is absent; presence is checked via the
  // URL strings below, not the texture objects.
  const face = useTexture(faceUrl ? assetUrl(faceUrl) : FALLBACK_URL);
  const back = useTexture(backUrl ? assetUrl(backUrl) : FALLBACK_URL);

  // Front texture: the sprite cell from the sheet (or the full image when there
  // is no grid). Cloned so the sprite offset/repeat don't leak into other cards
  // that share the same sheet URL (drei caches textures globally by URL).
  const faceMap = useMemo(() => {
    if (!faceUrl) return null;
    const tex = face.clone();
    const { repeatX, repeatY, offsetX, offsetY } = spriteUv(cardId, numWidth, numHeight);
    tex.repeat.set(repeatX, repeatY);
    tex.offset.set(offsetX, offsetY);
    return tex;
  }, [faceUrl, face, cardId, numWidth, numHeight]);

  // Back texture: a single full image (tile) unless the deck has unique backs,
  // in which case it's a sheet too. Flipped left/right so it reads correctly
  // instead of being mirrored on the back face.
  const backMap = useMemo(() => {
    if (!backUrl) return null;
    const tex = back.clone();
    const { repeatX, repeatY, offsetX, offsetY } = uniqueBack
      ? spriteUv(cardId, numWidth, numHeight)
      : { repeatX: 1, repeatY: 1, offsetX: 0, offsetY: 0 };
    tex.repeat.set(repeatX, repeatY);
    tex.offset.set(offsetX, offsetY);
    return flipTexture(tex);
  }, [backUrl, back, uniqueBack, cardId, numWidth, numHeight]);

  // Build the rounded-rect geometry from the card sprite's aspect ratio. The
  // front and back faces each get their own material; the walls are a solid
  // white, matching TTS card tinting. Geometry is shared across cards of the
  // same size so the full-setup view reuses it; the face/back materials stay
  // per-card because each card clones its texture for sprite UVs.
  const { frontGeo, backGeo, wallsGeo } = useMemo(() => {
    const img = (faceUrl ? face.image : backUrl ? back.image : undefined) as
      | HTMLImageElement
      | undefined;
    const aspect = cardAspect(img, numWidth, numHeight);
    const width = CARD_LENGTH * aspect;
    const height = CARD_LENGTH;
    // Radius scales with the shorter edge so corners look proportional and
    // stay circular (no scaling distortion).
    const shape = roundedRectShape(width, height, CORNER_RADIUS * Math.min(width, height));
    const parts = extrudeShapeParts(shape, { height: CARD_THICKNESS });
    const key = `card:${width}:${height}:${CARD_THICKNESS}`;
    return {
      frontGeo: getSharedGeometry(key + ':front', () => toGeometry(parts.front)),
      backGeo: getSharedGeometry(key + ':back', () => toGeometry(parts.back)),
      wallsGeo: getSharedGeometry(key + ':walls', () => toGeometry(parts.walls)),
    };
  }, [faceUrl, face, backUrl, back, numWidth, numHeight]);

  return (
    <group>
      <mesh geometry={frontGeo}>
        <meshStandardMaterial
          color={tintedColor(faceMap ? new THREE.Color('#ffffff') : new THREE.Color('#52525b'), tint)}
          map={faceMap ?? undefined}
          roughness={0.6}
        />
      </mesh>
      <mesh geometry={backGeo}>
        <meshStandardMaterial
          color={tintedColor(backMap ? new THREE.Color('#ffffff') : new THREE.Color('#52525b'), tint)}
          map={backMap ?? undefined}
          roughness={0.6}
        />
      </mesh>
      <mesh geometry={wallsGeo}>
        <meshStandardMaterial color={tintedColor(new THREE.Color('#ffffff'), tint)} roughness={0.6} />
      </mesh>
    </group>
  );
}

/** Convert raw extruded arrays into a three.js `BufferGeometry`. */
function toGeometry(extruded: ExtrudedGeometry) {
  const { positions, normals, uvs, indices } = extruded;
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geo.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
  geo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
  geo.setIndex(new THREE.BufferAttribute(indices, 1));
  return geo;
}