import * as THREE from 'three';
import type { FloorType, WallFinish, FurnitureTexture } from '@house-plan-maker/shared';

/**
 * Loads and caches PBR texture sets (albedo + normal + roughness) for floor
 * and wall materials. Textures are CC0 photo-based maps from ambientCG,
 * downsampled to 512x512 JPEG. They live in `apps/client/public/textures/`
 * and are fetched lazily on first use; subsequent calls return the cached
 * `MeshStandardMaterial` instance so the renderer can reuse it across rooms
 * without re-uploading to the GPU.
 *
 * Each material is keyed by enum value, so swapping floorType/wallFinish in
 * the editor only triggers a single material swap on the mesh — no texture
 * re-decode.
 *
 * The PAINT wall finish is intentionally not in this cache: it has no
 * texture data, only the room's `wallColor` tint, and lives in WallMesh.
 */

const TEXTURE_LOADER = new THREE.TextureLoader();

// Real-world tiling for each material, in meters. e.g. a value of 1 means
// "the texture covers 1m x 1m of the surface". Larger numbers => smaller
// pattern repeats; smaller numbers => texture stretches over more area.
const FLOOR_TILE_METERS: Record<FloorType, number> = {
  CONCRETE: 2.0,
  WOOD_LIGHT: 1.4,
  WOOD_MEDIUM: 1.4,
  WOOD_DARK: 1.4,
  WOOD_HERRINGBONE: 1.0,
  OAK_NATURAL: 1.4,
  TILE_WHITE: 1.0,
  TILE_GRAY: 1.0,
  LAMINATE: 1.4,
};

const WALL_TILE_METERS: Record<Exclude<WallFinish, 'PAINT'>, number> = {
  PLASTER: 1.5,
  BRICK: 1.0,
  CONCRETE: 1.5,
  WOOD_PANEL: 1.0,
  WALLPAPER: 0.6,
};

interface PbrSet {
  readonly material: THREE.MeshStandardMaterial;
  /** Real-world meters covered by one full UV repeat. */
  readonly tileMeters: number;
}

const floorCache = new Map<FloorType, PbrSet>();
const wallCache = new Map<Exclude<WallFinish, 'PAINT'>, PbrSet>();

function loadMap(url: string, isColor: boolean): THREE.Texture {
  const tex = TEXTURE_LOADER.load(url);
  tex.wrapS = THREE.RepeatWrapping;
  tex.wrapT = THREE.RepeatWrapping;
  // Color maps are sRGB-encoded JPEGs; normal/roughness must stay linear.
  if (isColor) {
    tex.colorSpace = THREE.SRGBColorSpace;
  } else {
    tex.colorSpace = THREE.NoColorSpace;
  }
  // Anisotropic filtering eliminates the blurry-at-grazing-angle look that
  // long floors have under default settings. 8x is a good ceiling on most
  // GPUs and is a no-op if unsupported.
  tex.anisotropy = 8;
  return tex;
}

function buildPbrSet(basePath: string, tileMeters: number, roughnessMul = 1): PbrSet {
  const colorMap = loadMap(`${basePath}/color.jpg`, true);
  const normalMap = loadMap(`${basePath}/normal.jpg`, false);
  const roughnessMap = loadMap(`${basePath}/roughness.jpg`, false);

  const material = new THREE.MeshStandardMaterial({
    map: colorMap,
    normalMap,
    roughnessMap,
    roughness: roughnessMul,
    metalness: 0,
    side: THREE.DoubleSide,
  });
  return { material, tileMeters };
}

export function getFloorPbr(floorType: FloorType): PbrSet {
  let entry = floorCache.get(floorType);
  if (!entry) {
    entry = buildPbrSet(
      `/textures/floors/${floorType.toLowerCase()}`,
      FLOOR_TILE_METERS[floorType],
    );
    floorCache.set(floorType, entry);
  }
  return entry;
}

export function getWallPbr(finish: Exclude<WallFinish, 'PAINT'>): PbrSet {
  let entry = wallCache.get(finish);
  if (!entry) {
    entry = buildPbrSet(
      `/textures/walls/${finish.toLowerCase()}`,
      WALL_TILE_METERS[finish],
    );
    wallCache.set(finish, entry);
  }
  return entry;
}

// ── Furniture surface textures ──
//
// Reuses the floor texture assets. The tiling is tighter (0.4m default) so
// the wood grain looks appropriately scaled on desk/shelf/table surfaces
// rather than stretched.

const FURNITURE_TEXTURE_TILE: Record<Exclude<FurnitureTexture, 'NONE'>, number> = {
  WOOD_LIGHT: 0.4,
  WOOD_MEDIUM: 0.4,
  WOOD_DARK: 0.4,
  WOOD_HERRINGBONE: 0.3,
  OAK_NATURAL: 0.4,
  LAMINATE: 0.4,
  CONCRETE: 0.6,
};

const furnitureCache = new Map<Exclude<FurnitureTexture, 'NONE'>, PbrSet>();

/**
 * Get a PBR material for furniture surface textures. Reuses floor texture
 * images but with tighter tiling appropriate for furniture-scale surfaces.
 * Returns `null` for 'NONE' — the caller should fall back to the solid color.
 */
export function getFurnitureSurfacePbr(texture: FurnitureTexture): PbrSet | null {
  if (texture === 'NONE') return null;
  let entry = furnitureCache.get(texture);
  if (!entry) {
    entry = buildPbrSet(
      `/textures/floors/${texture.toLowerCase()}`,
      FURNITURE_TEXTURE_TILE[texture],
    );
    furnitureCache.set(texture, entry);
  }
  return entry;
}

/**
 * Compute UV repeat counts for a given physical surface so the texture tiles
 * once per `tileMeters`. Apply this on the geometry side rather than the
 * material side, otherwise every mesh sharing the same material would force
 * the same repeat count.
 */
export function computeTextureRepeat(
  widthMeters: number,
  heightMeters: number,
  tileMeters: number,
): { readonly u: number; readonly v: number } {
  const u = Math.max(1, widthMeters / tileMeters);
  const v = Math.max(1, heightMeters / tileMeters);
  return { u, v };
}
