import { useTexture } from '@react-three/drei';
import { useMemo } from 'react';
import * as THREE from 'three';
import type { TTSObject } from '@tts/shared';
import {
  circleShape,
  extrudeShapeParts,
  hexShape,
  rectShape,
  roundedRectShape,
  scaleShape,
  type ExtrudedGeometry,
} from '@tts/mesh';
import { assetUrl } from '@tts/http';
import { flipTexture } from './flipTexture';
import {
  getSharedGeometry,
  getSharedMaterial,
  objectTint,
  tintKey,
  tintedColor,
} from './sharedResources';

/** `CustomTile.Type` enum from Tabletop Simulator. */
const TileType = {
  Box: 0,
  Hex: 1,
  Circle: 2,
  Rounded: 3,
} as const;

const TILE_SIZE = 2;

/**
 * The tile mesh for an object, exported so the full-setup view can compose it
 * into a shared scene.
 */
export function TileObjectMesh({ object }: { object: TTSObject }) {
  const url = object.CustomImage?.ImageURL ?? object.CustomImage?.ImageSecondaryURL;
  const thickness = object.CustomImage?.CustomTile?.Thickness ?? 0.2;
  const type = object.CustomImage?.CustomTile?.Type ?? TileType.Box;
  const stretch = object.CustomImage?.CustomTile?.Stretch ?? true;

  return (
    <TileMesh
      url={url}
      thickness={thickness}
      type={type}
      stretch={stretch}
      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 TileMesh({
  url,
  thickness,
  type,
  stretch,
  tint,
}: {
  url?: string;
  thickness: number;
  type: number;
  stretch: boolean;
  tint: THREE.Color;
}) {
  const texture: THREE.Texture | null = url ? useTexture(assetUrl(url)) : null;

  // Tile art is sRGB-encoded; `TextureLoader` leaves `colorSpace` as
  // `NoColorSpace`, which double-decodes it in the shader and washes out
  // contrast. Mark it sRGB so the GPU decodes it once, correctly.
  if (texture) texture.colorSpace = THREE.SRGBColorSpace;

  // Build the extruded geometry from the tile shape. When `stretch` is false
  // and a texture is available, scale the shape to the image's aspect ratio so
  // the tile matches the source proportions instead of being square. Shared
  // across tiles with the same shape so the full-setup view reuses geometry.
  const { front, back, walls } = useMemo(() => {
    const img = texture?.image as HTMLImageElement;
    const aspect = stretch ? img.width / img.height : 1;
    const shape = tileShape(type, aspect);
    const parts = extrudeShapeParts(shape, { height: thickness });
    const key = `tile:${type}:${aspect}:${thickness}`;
    return {
      front: getSharedGeometry(key + ':front', () => toGeometry(parts.front)),
      back: getSharedGeometry(key + ':back', () => toGeometry(parts.back)),
      walls: getSharedGeometry(key + ':walls', () => toGeometry(parts.walls)),
    };
  }, [type, thickness, stretch, texture]);

  // The back face maps with the same planar UVs as the front, so flip it
  // left/right to avoid a mirrored texture when viewed from behind.
  const backMap = useMemo(() => (texture ? flipTexture(texture) : null), [texture]);

  // Shared materials: the front/back carry the tile texture (or a neutral
  // color when absent); the walls are a solid white, matching TTS tinting.
  // The tint is baked into the color and the cache key so tinted variants
  // don't collide.
  const tintK = tintKey(tint);
  const faceKey = `tile-face:${url ?? 'none'}:${tintK}`;
  const faceMat = getSharedMaterial(faceKey, {
    color: tintedColor(texture ? new THREE.Color('#ffffff') : new THREE.Color('#52525b'), tint),
    map: texture ?? undefined,
    roughness: 0.8,
  });
  const backMat = getSharedMaterial(faceKey + ':back', {
    color: tintedColor(texture ? new THREE.Color('#ffffff') : new THREE.Color('#52525b'), tint),
    map: backMap ?? undefined,
    roughness: 0.8,
  });
  const wallMat = getSharedMaterial(`tile-wall:${tintK}`, {
    color: tintedColor(new THREE.Color('#ffffff'), tint),
    roughness: 0.8,
  });

  return (
    <group>
      {/* Front face carries the tile texture. */}
      <mesh geometry={front} material={faceMat} />
      {/* Back face, flipped so it isn't mirrored. */}
      <mesh geometry={back} material={backMat} />
      {/* Sides are a solid white, matching TTS tile tinting. */}
      <mesh geometry={walls} material={wallMat} />
    </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;
}

/** Build the 2D footprint for a tile type, scaled to a target aspect ratio. */
function tileShape(type: number, aspect: number) {
  // Base shape is square (1x1); scale x to the aspect ratio so the tile is
  // `aspect` wide and 1 tall (or keep 1x1 when the aspect is 1).
  const sx = aspect;
  const sy = 1;
  switch (type) {
    case TileType.Hex:
      return scaleShape(hexShape(TILE_SIZE / 2), sx, sy);
    case TileType.Circle:
      return scaleShape(circleShape(TILE_SIZE / 2), sx, sy);
    case TileType.Rounded:
      return scaleShape(roundedRectShape(TILE_SIZE, TILE_SIZE, 0.08), sx, sy);
    case TileType.Box:
    default:
      return scaleShape(rectShape(TILE_SIZE, TILE_SIZE), sx, sy);
  }
}