import { useEffect, useMemo, useRef } from 'react';
import * as THREE from 'three';
import type { FurnitureItem, FurnitureType } from '@house-plan-maker/shared';
import { rotatedAnchorOffsetToCenter, TEXTURABLE_FURNITURE } from '@house-plan-maker/shared';
import { getCurtainLeftOpen, getCurtainRightOpen, getCurtainFabricColor } from '../utils/curtainMetadata';
import { getFurnitureTexture } from '../utils/furnitureTextureMetadata';
import { getFurnitureSurfacePbr, computeTextureRepeat } from './utils/pbrTextures';

/**
 * Below this opacity threshold, furniture meshes stop casting shadows. Cast
 * shadows from near-invisible objects look like floating dark blobs because
 * the shadow map ignores material opacity, so the visual is jarring. 1% gives
 * users a clean "ghost" mode when they slide opacity to zero.
 */
const SHADOW_OPACITY_THRESHOLD = 0.01;

interface FurnitureMeshProps {
  readonly item: FurnitureItem;
  readonly isSelected: boolean;
  readonly onSelect?: (id: string) => void;
  /** Global furniture opacity multiplier from the toolbar slider (0..1). */
  readonly globalOpacity?: number;
}

const FURNITURE_COLORS: Record<FurnitureType, string> = {
  BED: '#8fa5b2',
  CRIB: '#f0e4d2',
  DESK: '#b08968',
  WARDROBE: '#7a6652',
  SOFA: '#9b8e7e',
  TABLE: '#c4a882',
  CHAIR: '#a0937d',
  OFFICE_CHAIR: '#2a2a30',
  SHELF: '#b09e8a',
  NIGHTSTAND: '#9b8b7a',
  DRESSER: '#8a7a6a',
  DRESSING_TABLE: '#c4a882',
  BOOKCASE: '#7a6a5a',
  TV: '#2a2a3a',
  PC_TOWER: '#2a2a33',
  AC_UNIT: '#e8e8e8',
  RADIATOR: '#e0e0dc',
  WALL_COLLAGE: '#3a2f1e',
  CURTAIN: '#e8dfc8',
  PLANT: '#4a7a3a',
  MIRROR: '#8a7a5c',
  DIGITAL_PIANO: '#1a1a22',
  SPEAKER: '#20201e',
  OTHER: '#a0a0a0',
};

const SELECTED_COLOR = '#6fa8dc';
const LEG_COLOR = '#4a3728';
const LEG_RADIUS = 0.02;
const LEG_SEGMENTS = 6;

// ── Shared materials (module-level singletons) ──
//
// These are the *template* materials picked up by the inner mesh
// components (BedMesh, DeskMesh, …). Every FurnitureMesh CLONES them at
// mount time so each item can have its own opacity without affecting
// others. Without the clone step the shared `legMaterial` would be
// referenced by every bed frame in the scene — setting its `opacity` to
// 0.3 on one bed would ghost every bed.

const legMaterial = new THREE.MeshStandardMaterial({ color: LEG_COLOR, roughness: 0.6 });
const legMaterialSmooth = new THREE.MeshStandardMaterial({ color: LEG_COLOR, roughness: 0.5 });

const furnitureMaterials: Record<string, THREE.MeshStandardMaterial> = {};
function getFurnitureMaterial(color: string, roughness: number): THREE.MeshStandardMaterial {
  const key = `${color}_${roughness}`;
  const existing = furnitureMaterials[key];
  if (existing) return existing;
  const mat = new THREE.MeshStandardMaterial({ color, roughness });
  furnitureMaterials[key] = mat;
  return mat;
}

/**
 * Legacy no-op kept for backward compatibility with any external callers.
 * The previous implementation mutated the shared material singletons above
 * to express a scene-wide furniture opacity, which broke per-item opacity
 * because items share materials. Per-item cloning in FurnitureMesh now
 * handles both the per-item and global factors via a single effective
 * opacity product, so this function no longer needs to do anything.
 */
export function setFurnitureGlobalOpacity(_opacity: number): void {
  // intentionally empty — per-item material clones own their opacity now
}

/**
 * Returns a material for furniture surfaces: either a PBR textured material
 * (when the item has a surfaceTexture set) or the solid-color fallback.
 * The PBR material's UV repeat is scaled for the given surface dimensions.
 */
function useSurfaceMaterial(
  item: FurnitureItem,
  solidColor: string,
  solidRoughness: number,
  surfaceWidth: number,
  surfaceDepth: number,
): THREE.MeshStandardMaterial {
  return useMemo(() => {
    if (!TEXTURABLE_FURNITURE.includes(item.type)) {
      return getFurnitureMaterial(solidColor, solidRoughness);
    }
    const texture = getFurnitureTexture(item.metadata);
    const pbr = getFurnitureSurfacePbr(texture);
    if (!pbr) {
      return getFurnitureMaterial(solidColor, solidRoughness);
    }
    // Clone the PBR material so UV repeat is per-item, not global.
    const mat = pbr.material.clone();
    const repeat = computeTextureRepeat(surfaceWidth, surfaceDepth, pbr.tileMeters);
    if (mat.map) { mat.map = mat.map.clone(); mat.map.repeat.set(repeat.u, repeat.v); }
    if (mat.normalMap) { mat.normalMap = mat.normalMap.clone(); mat.normalMap.repeat.set(repeat.u, repeat.v); }
    if (mat.roughnessMap) { mat.roughnessMap = mat.roughnessMap.clone(); mat.roughnessMap.repeat.set(repeat.u, repeat.v); }
    return mat;
  }, [item.type, item.metadata, solidColor, solidRoughness, surfaceWidth, surfaceDepth]);
}

// ── Shared geometries for common shapes ──

const legGeometry = new THREE.CylinderGeometry(LEG_RADIUS, LEG_RADIUS, 1, LEG_SEGMENTS);
const dividerLineGeometry = new THREE.BoxGeometry(0.005, 1, 0.002);

function degToRad(degrees: number): number {
  return (degrees * Math.PI) / 180;
}

/** Simple bed: mattress box + headboard */
function BedMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const mattressHeight = item.height * 0.4;
  const frameHeight = item.height * 0.3;
  const headboardHeight = item.height;
  const mattressMaterial = useMemo(() => getFurnitureMaterial(color, 0.9), [color]);
  const frameMaterial = useSurfaceMaterial(item, LEG_COLOR, 0.6, item.width, item.depth);
  const headboardMaterial = useSurfaceMaterial(item, LEG_COLOR, 0.5, item.width, headboardHeight);

  return (
    <group>
      {/* Frame */}
      <mesh position={[0, frameHeight / 2, 0]} castShadow material={frameMaterial}>
        <boxGeometry args={[item.width, frameHeight, item.depth]} />
      </mesh>
      {/* Mattress */}
      <mesh position={[0, frameHeight + mattressHeight / 2, 0]} castShadow material={mattressMaterial}>
        <boxGeometry args={[item.width * 0.95, mattressHeight, item.depth * 0.95]} />
      </mesh>
      {/* Headboard */}
      <mesh position={[0, headboardHeight / 2, -item.depth / 2 + 0.02]} castShadow material={headboardMaterial}>
        <boxGeometry args={[item.width, headboardHeight, 0.04]} />
      </mesh>
    </group>
  );
}

/** Desk: top slab + 4 legs */
function DeskMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const topThickness = 0.04;
  const legHeight = item.height - topThickness;
  const inset = 0.05;
  const topMaterial = useSurfaceMaterial(item, color, 0.5, item.width, item.depth);

  return (
    <group>
      {/* Top */}
      <mesh position={[0, item.height - topThickness / 2, 0]} castShadow material={topMaterial}>
        <boxGeometry args={[item.width, topThickness, item.depth]} />
      </mesh>
      {/* Legs */}
      {[
        [-item.width / 2 + inset, -item.depth / 2 + inset],
        [item.width / 2 - inset, -item.depth / 2 + inset],
        [-item.width / 2 + inset, item.depth / 2 - inset],
        [item.width / 2 - inset, item.depth / 2 - inset],
      ].map(([x, z], i) => (
        <mesh key={i} position={[x, legHeight / 2, z]} castShadow material={legMaterial} scale={[1, legHeight, 1]}>
          <primitive object={legGeometry} attach="geometry" />
        </mesh>
      ))}
    </group>
  );
}

/** Wardrobe: tall box with slight door line */
function WardrobeMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const bodyMaterial = useSurfaceMaterial(item, color, 0.6, item.width, item.height);

  return (
    <group>
      <mesh position={[0, item.height / 2, 0]} castShadow material={bodyMaterial}>
        <boxGeometry args={[item.width, item.height, item.depth]} />
      </mesh>
      {/* Door divider line */}
      <mesh
        position={[0, item.height / 2, item.depth / 2 + 0.001]}
        castShadow
        material={legMaterialSmooth}
        scale={[1, item.height * 0.9, 1]}
      >
        <primitive object={dividerLineGeometry} attach="geometry" />
      </mesh>
    </group>
  );
}

/** Sofa: seat + back (L-shape profile) */
function SofaMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const seatHeight = item.height * 0.45;
  const backHeight = item.height;
  const backDepth = item.depth * 0.25;
  const sofaMaterial = useMemo(() => getFurnitureMaterial(color, 0.9), [color]);

  return (
    <group>
      {/* Seat */}
      <mesh position={[0, seatHeight / 2, backDepth / 2]} castShadow material={sofaMaterial}>
        <boxGeometry args={[item.width, seatHeight, item.depth - backDepth]} />
      </mesh>
      {/* Backrest */}
      <mesh position={[0, backHeight / 2, -item.depth / 2 + backDepth / 2]} castShadow material={sofaMaterial}>
        <boxGeometry args={[item.width, backHeight, backDepth]} />
      </mesh>
      {/* Armrests */}
      {[-1, 1].map((side) => (
        <mesh
          key={side}
          position={[side * (item.width / 2 - 0.04), seatHeight + 0.1, 0]}
          castShadow
          material={sofaMaterial}
        >
          <boxGeometry args={[0.08, 0.2, item.depth * 0.8]} />
        </mesh>
      ))}
    </group>
  );
}

/** Table: top slab + 4 legs */
function TableMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const topThickness = 0.03;
  const legHeight = item.height - topThickness;
  const inset = 0.05;
  const topMaterial = useSurfaceMaterial(item, color, 0.5, item.width, item.depth);

  return (
    <group>
      <mesh position={[0, item.height - topThickness / 2, 0]} castShadow material={topMaterial}>
        <boxGeometry args={[item.width, topThickness, item.depth]} />
      </mesh>
      {[
        [-item.width / 2 + inset, -item.depth / 2 + inset],
        [item.width / 2 - inset, -item.depth / 2 + inset],
        [-item.width / 2 + inset, item.depth / 2 - inset],
        [item.width / 2 - inset, item.depth / 2 - inset],
      ].map(([x, z], i) => (
        <mesh key={i} position={[x, legHeight / 2, z]} castShadow material={legMaterial} scale={[1, legHeight, 1]}>
          <primitive object={legGeometry} attach="geometry" />
        </mesh>
      ))}
    </group>
  );
}

/** Chair: seat + back + 4 legs */
function ChairMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const seatHeight = item.height * 0.5;
  const seatThickness = 0.03;
  const legHeight = seatHeight - seatThickness;
  const inset = 0.03;
  const chairMaterial = useSurfaceMaterial(item, color, 0.6, item.width, item.depth);

  return (
    <group>
      {/* Seat */}
      <mesh position={[0, seatHeight - seatThickness / 2, 0]} castShadow material={chairMaterial}>
        <boxGeometry args={[item.width, seatThickness, item.depth]} />
      </mesh>
      {/* Backrest */}
      <mesh position={[0, (seatHeight + item.height) / 2, -item.depth / 2 + 0.015]} castShadow material={chairMaterial}>
        <boxGeometry args={[item.width * 0.9, item.height - seatHeight, 0.03]} />
      </mesh>
      {/* Legs */}
      {[
        [-item.width / 2 + inset, -item.depth / 2 + inset],
        [item.width / 2 - inset, -item.depth / 2 + inset],
        [-item.width / 2 + inset, item.depth / 2 - inset],
        [item.width / 2 - inset, item.depth / 2 - inset],
      ].map(([x, z], i) => (
        <mesh key={i} position={[x, legHeight / 2, z]} castShadow material={legMaterial} scale={[1, legHeight, 1]}>
          <primitive object={legGeometry} attach="geometry" />
        </mesh>
      ))}
    </group>
  );
}

/** Shelf / Bookcase / Nightstand / Dresser / Other: simple box */
function SimpleBoxMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const material = useSurfaceMaterial(item, color, 0.6, item.width, item.depth);

  return (
    <mesh position={[0, item.height / 2, 0]} castShadow material={material}>
      <boxGeometry args={[item.width, item.height, item.depth]} />
    </mesh>
  );
}

/**
 * Bookcase: open shelves with a back panel and two sides.
 *
 * The number of shelf rows is read from `metadata.shelfRows` when
 * present. `shelfRows` counts the STORAGE COMPARTMENTS — so 3 rows
 * means 3 usable spaces which the mesh draws as 4 horizontal shelf
 * boards (top + bottom + two intermediate dividers). If the metadata
 * value is missing or invalid, the shelf count is derived from the
 * item's height (~one compartment every 35cm) so legacy bookcases
 * render unchanged.
 *
 * Variants via `metadata.hasBackPanel`: when explicitly `false`, the
 * back panel is omitted — used by the "Open Bookshelf" preset which
 * reads as a room divider / floating shelf unit rather than a cabinet
 * with a back.
 */
function BookcaseMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const metadataRows = item.metadata?.['shelfRows'];
  const rawRows =
    typeof metadataRows === 'number' && Number.isFinite(metadataRows) && metadataRows >= 1
      ? Math.round(metadataRows)
      : Math.max(2, Math.round(item.height / 0.35));
  // Clamp — 12 is plenty even for tall library units.
  const shelfRows = Math.max(1, Math.min(12, rawRows));

  const hasBackPanelRaw = item.metadata?.['hasBackPanel'];
  const hasBackPanel = typeof hasBackPanelRaw === 'boolean' ? hasBackPanelRaw : true;

  const panelThickness = 0.02;
  const material = useSurfaceMaterial(item, color, 0.6, item.width, item.height);

  return (
    <group>
      {/* Back panel (optional). When omitted the bookshelf reads as an
          open unit that can be seen through from both sides. */}
      {hasBackPanel && (
        <mesh position={[0, item.height / 2, -item.depth / 2 + panelThickness / 2]} castShadow material={material}>
          <boxGeometry args={[item.width, item.height, panelThickness]} />
        </mesh>
      )}
      {/* Side panels */}
      {[-1, 1].map((side) => (
        <mesh
          key={side}
          position={[side * (item.width / 2 - panelThickness / 2), item.height / 2, 0]}
          castShadow
          material={material}
        >
          <boxGeometry args={[panelThickness, item.height, item.depth]} />
        </mesh>
      ))}
      {/* Horizontal shelf boards. `shelfRows` compartments produce
          `shelfRows + 1` boards (top + bottom + internal dividers),
          spaced evenly along the full height. */}
      {Array.from({ length: shelfRows + 1 }).map((_, i) => {
        const y = (i / shelfRows) * item.height;
        return (
          <mesh key={i} position={[0, y, 0]} castShadow material={material}>
            <boxGeometry args={[item.width - panelThickness * 2, panelThickness, item.depth]} />
          </mesh>
        );
      })}
    </group>
  );
}

/** TV: thin screen panel, optionally on a stand */
function TvMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const screenMaterial = useMemo(() => getFurnitureMaterial('#1a1a2e', 0.1), []);
  const frameMaterial = useMemo(() => getFurnitureMaterial(color, 0.4), [color]);
  const screenThickness = 0.03;
  const hasStand = !item.label?.includes('[no-stand]');
  const standHeight = hasStand ? item.height * 0.15 : 0;
  const screenHeight = item.height - standHeight;

  return (
    <group>
      {/* Screen */}
      <mesh position={[0, standHeight + screenHeight / 2, 0]} castShadow material={screenMaterial}>
        <boxGeometry args={[item.width, screenHeight, screenThickness]} />
      </mesh>
      {/* Frame border */}
      <mesh position={[0, standHeight + screenHeight / 2, screenThickness / 2 + 0.001]} material={frameMaterial}>
        <boxGeometry args={[item.width + 0.02, screenHeight + 0.02, 0.005]} />
      </mesh>
      {hasStand && (
        <>
          {/* Stand */}
          <mesh position={[0, standHeight / 2, 0]} castShadow material={frameMaterial}>
            <boxGeometry args={[0.04, standHeight, item.depth]} />
          </mesh>
          {/* Stand base */}
          <mesh position={[0, 0.005, 0]} castShadow material={frameMaterial}>
            <boxGeometry args={[item.width * 0.4, 0.01, item.depth]} />
          </mesh>
        </>
      )}
    </group>
  );
}

/**
 * Wall collage: a flat mounted panel divided into a grid of "photos" by
 * the dark frame. The grid count is derived from the aspect ratio so a
 * 1.5×0.6 panel reads as a 5×2 collage automatically. Each cell shows a
 * lightly tinted plane that stands in for a photograph; the user gets a
 * recognisable "photo wall" silhouette without us needing to ship images.
 */
function WallCollageMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  // Pick a grid that roughly matches the panel aspect ratio. Cells are
  // square-ish and bounded so they look like real photo frames.
  const aspect = item.width / item.height;
  const cols = Math.max(2, Math.min(6, Math.round(aspect * 2)));
  const rows = Math.max(1, Math.min(4, Math.round(cols / aspect)));

  const frameMaterial = useMemo(() => getFurnitureMaterial(color, 0.5), [color]);
  // A small palette of warm photo-ish tints — picking via index keeps the
  // result deterministic per cell so it doesn't shimmer between renders.
  const photoMaterials = useMemo(
    () => [
      getFurnitureMaterial('#b6c8d4', 0.4),
      getFurnitureMaterial('#d8c4a0', 0.4),
      getFurnitureMaterial('#a0b88a', 0.4),
      getFurnitureMaterial('#c8a0a0', 0.4),
      getFurnitureMaterial('#9aa0b8', 0.4),
      getFurnitureMaterial('#d0b890', 0.4),
    ],
    [],
  );

  const panelDepth = Math.max(0.005, item.depth);
  const cellMargin = 0.01; // gap between photos within the frame
  const cellW = (item.width - cellMargin * (cols + 1)) / cols;
  const cellH = (item.height - cellMargin * (rows + 1)) / rows;

  return (
    <group>
      {/* Frame backing panel */}
      <mesh position={[0, item.height / 2, 0]} castShadow material={frameMaterial}>
        <boxGeometry args={[item.width, item.height, panelDepth]} />
      </mesh>
      {/* Grid of photo cells, each slightly proud of the frame so the
          dark frame border is visible between them. */}
      {Array.from({ length: rows }).map((_, r) =>
        Array.from({ length: cols }).map((_, c) => {
          const idx = r * cols + c;
          const localX = -item.width / 2 + cellMargin + cellW / 2 + c * (cellW + cellMargin);
          const localY = item.height - cellMargin - cellH / 2 - r * (cellH + cellMargin);
          return (
            <mesh
              key={`cell-${r}-${c}`}
              position={[localX, localY, panelDepth / 2 + 0.001]}
              material={photoMaterials[idx % photoMaterials.length]}
            >
              <boxGeometry args={[cellW, cellH, 0.002]} />
            </mesh>
          );
        }),
      )}
    </group>
  );
}

/**
 * Radiator: a back panel covered by a row of vertical fins. Looks like a
 * standard panel-type room radiator from any angle. The fin count is
 * derived from the width so wider radiators read as having more sections.
 */
function RadiatorMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const bodyMaterial = useMemo(() => getFurnitureMaterial(color, 0.45), [color]);
  // Radiators are typically ~10cm fin pitch.
  const finPitch = 0.1;
  const finCount = Math.max(3, Math.round(item.width / finPitch));
  const finWidth = (item.width / finCount) * 0.65;
  const finDepth = item.depth * 0.85;
  // Back-plate sits flush against the wall (negative Z is "into the wall"
  // for wall-mounted items, but radiators are free-standing — keep the
  // panel centered along Z).
  const panelDepth = item.depth * 0.25;

  return (
    <group>
      {/* Back panel */}
      <mesh position={[0, item.height / 2, -item.depth / 2 + panelDepth / 2]} castShadow material={bodyMaterial}>
        <boxGeometry args={[item.width, item.height, panelDepth]} />
      </mesh>
      {/* Vertical fins */}
      {Array.from({ length: finCount }).map((_, i) => {
        const offsetX = -item.width / 2 + (i + 0.5) * (item.width / finCount);
        return (
          <mesh
            key={i}
            position={[offsetX, item.height / 2, 0]}
            castShadow
            material={bodyMaterial}
          >
            <boxGeometry args={[finWidth, item.height * 0.95, finDepth]} />
          </mesh>
        );
      })}
      {/* Top cap (the slotted grille typical of panel radiators) */}
      <mesh position={[0, item.height - 0.005, 0]} castShadow material={bodyMaterial}>
        <boxGeometry args={[item.width, 0.01, item.depth]} />
      </mesh>
    </group>
  );
}

/**
 * Curtain: a slim rod across the top with pleated fabric panels hanging
 * from it. Left and right panels are independently drawn aside via the
 * `leftOpen` and `rightOpen` metadata values (0 = fully closed, 1 = fully
 * retracted to the outer edge). A legacy symmetric `openAmount` field is
 * honoured as a fallback so older curtain rows keep their appearance.
 *
 * Rendering is identical to the symmetric version in structure — two
 * panels of pleated strips — except each panel consults its own open
 * value so the user can tie back just one side.
 */
function CurtainMesh({ item, color: _defaultColor }: { readonly item: FurnitureItem; readonly color: string }) {
  // Read state from metadata with safe defaults for legacy rows.
  const fabricColor = getCurtainFabricColor(item.metadata);
  const leftOpen = getCurtainLeftOpen(item.metadata);
  const rightOpen = getCurtainRightOpen(item.metadata);

  const fabricMaterial = useMemo(() => getFurnitureMaterial(fabricColor, 0.95), [fabricColor]);
  const rodMaterial = useMemo(() => getFurnitureMaterial('#8a7a5c', 0.4), []);

  // Rod sits just above the top of the curtain, 2cm thick.
  const rodRadius = 0.012;
  const rodY = item.height + 0.02;
  const fabricHeight = item.height;
  const panelDepth = 0.02;
  const pleatDepth = 0.025;

  // Total pleats across the full (closed) width; split into two equal halves.
  const totalPleats = Math.max(8, Math.round(item.width / 0.08));
  const pleatsPerPanel = Math.max(1, Math.floor(totalPleats / 2));

  // Each panel's visible width is derived from its own open value. A small
  // residual (~5% of half-width) keeps the bunched stack visible when a
  // panel is fully retracted instead of collapsing into zero-width geometry.
  const minPanelWidth = item.width * 0.05;
  const halfWidth = item.width / 2;
  const leftPanelWidth = Math.max(minPanelWidth, halfWidth * (1 - leftOpen));
  const rightPanelWidth = Math.max(minPanelWidth, halfWidth * (1 - rightOpen));
  const leftPleatWidth = leftPanelWidth / pleatsPerPanel;
  const rightPleatWidth = rightPanelWidth / pleatsPerPanel;

  const leftOuterX = -halfWidth;
  const rightOuterX = halfWidth;

  return (
    <group>
      {/* Horizontal rod runs the full width regardless of open state. */}
      <mesh
        position={[0, rodY, -item.depth / 2 + rodRadius]}
        rotation={[0, 0, Math.PI / 2]}
        castShadow
        material={rodMaterial}
      >
        <cylinderGeometry args={[rodRadius, rodRadius, item.width + 0.1, 8]} />
      </mesh>
      {/* Rod finials (end caps) */}
      {[-1, 1].map((side) => (
        <mesh
          key={`finial-${side}`}
          position={[side * (halfWidth + 0.05), rodY, -item.depth / 2 + rodRadius]}
          castShadow
          material={rodMaterial}
        >
          <sphereGeometry args={[rodRadius * 1.8, 10, 8]} />
        </mesh>
      ))}
      {/* Left panel pleats grow inward from the left outer edge. */}
      {Array.from({ length: pleatsPerPanel }).map((_, i) => {
        const localX = leftOuterX + (i + 0.5) * leftPleatWidth;
        const isFront = i % 2 === 0;
        const zOffset = isFront ? pleatDepth / 2 : -pleatDepth / 2;
        return (
          <mesh
            key={`pleat-L-${i}`}
            position={[localX, fabricHeight / 2, zOffset]}
            castShadow
            material={fabricMaterial}
          >
            <boxGeometry args={[leftPleatWidth * 0.95, fabricHeight, panelDepth]} />
          </mesh>
        );
      })}
      {/* Right panel pleats grow inward from the right outer edge. */}
      {Array.from({ length: pleatsPerPanel }).map((_, i) => {
        const localX = rightOuterX - (i + 0.5) * rightPleatWidth;
        const isFront = i % 2 === 0;
        const zOffset = isFront ? pleatDepth / 2 : -pleatDepth / 2;
        return (
          <mesh
            key={`pleat-R-${i}`}
            position={[localX, fabricHeight / 2, zOffset]}
            castShadow
            material={fabricMaterial}
          >
            <boxGeometry args={[rightPleatWidth * 0.95, fabricHeight, panelDepth]} />
          </mesh>
        );
      })}
    </group>
  );
}

/**
 * Crib (baby bed): solid floor panel with a mattress, surrounded by a
 * ring of vertical slats that give cribs their characteristic look.
 * Slat density scales with width so wider cribs get more rails without
 * looking sparse.
 */
function CribMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const frameMaterial = useSurfaceMaterial(item, color, 0.55, item.width, item.depth);
  const mattressMaterial = useMemo(() => getFurnitureMaterial('#f4eadf', 0.9), []);

  const mattressThick = 0.08;
  const mattressY = 0.25; // top of the mattress sits just below cot top rail
  const mattressLift = mattressY;
  // Slats
  const slatRadius = 0.01;
  const slatHeight = item.height - mattressLift - 0.04;
  const slatsPerSide = Math.max(6, Math.round((item.width - 0.1) / 0.08));
  const slatsPerEnd = Math.max(4, Math.round((item.depth - 0.1) / 0.08));
  // Top rail, bottom rail, corner posts
  const railThick = 0.02;

  const cornerPosts = ([-1, 1] as const).flatMap((sx) =>
    ([-1, 1] as const).map((sz) => ({ sx, sz })),
  );

  return (
    <group>
      {/* Bottom rail ring (sits just above the mattress top) */}
      <mesh
        position={[0, mattressLift + railThick / 2, -item.depth / 2 + railThick / 2]}
        castShadow
        material={frameMaterial}
      >
        <boxGeometry args={[item.width, railThick, railThick]} />
      </mesh>
      <mesh
        position={[0, mattressLift + railThick / 2, item.depth / 2 - railThick / 2]}
        castShadow
        material={frameMaterial}
      >
        <boxGeometry args={[item.width, railThick, railThick]} />
      </mesh>
      <mesh
        position={[-item.width / 2 + railThick / 2, mattressLift + railThick / 2, 0]}
        castShadow
        material={frameMaterial}
      >
        <boxGeometry args={[railThick, railThick, item.depth]} />
      </mesh>
      <mesh
        position={[item.width / 2 - railThick / 2, mattressLift + railThick / 2, 0]}
        castShadow
        material={frameMaterial}
      >
        <boxGeometry args={[railThick, railThick, item.depth]} />
      </mesh>
      {/* Top rail ring */}
      <mesh position={[0, item.height - railThick / 2, -item.depth / 2 + railThick / 2]} castShadow material={frameMaterial}>
        <boxGeometry args={[item.width, railThick, railThick]} />
      </mesh>
      <mesh position={[0, item.height - railThick / 2, item.depth / 2 - railThick / 2]} castShadow material={frameMaterial}>
        <boxGeometry args={[item.width, railThick, railThick]} />
      </mesh>
      <mesh position={[-item.width / 2 + railThick / 2, item.height - railThick / 2, 0]} castShadow material={frameMaterial}>
        <boxGeometry args={[railThick, railThick, item.depth]} />
      </mesh>
      <mesh position={[item.width / 2 - railThick / 2, item.height - railThick / 2, 0]} castShadow material={frameMaterial}>
        <boxGeometry args={[railThick, railThick, item.depth]} />
      </mesh>
      {/* Corner posts */}
      {cornerPosts.map(({ sx, sz }) => (
        <mesh
          key={`post-${sx}-${sz}`}
          position={[sx * (item.width / 2 - railThick / 2), item.height / 2, sz * (item.depth / 2 - railThick / 2)]}
          castShadow
          material={frameMaterial}
        >
          <boxGeometry args={[railThick, item.height, railThick]} />
        </mesh>
      ))}
      {/* Long-side slats (front + back) */}
      {([-1, 1] as const).map((sz) =>
        Array.from({ length: slatsPerSide }).map((_, i) => {
          const localX = -item.width / 2 + railThick + (i + 0.5) * ((item.width - railThick * 2) / slatsPerSide);
          return (
            <mesh
              key={`slat-lr-${sz}-${i}`}
              position={[localX, mattressLift + railThick + slatHeight / 2, sz * (item.depth / 2 - railThick / 2)]}
              castShadow
              material={frameMaterial}
            >
              <cylinderGeometry args={[slatRadius, slatRadius, slatHeight, 6]} />
            </mesh>
          );
        }),
      )}
      {/* Short-end slats (left + right) */}
      {([-1, 1] as const).map((sx) =>
        Array.from({ length: slatsPerEnd }).map((_, i) => {
          const localZ = -item.depth / 2 + railThick + (i + 0.5) * ((item.depth - railThick * 2) / slatsPerEnd);
          return (
            <mesh
              key={`slat-fb-${sx}-${i}`}
              position={[sx * (item.width / 2 - railThick / 2), mattressLift + railThick + slatHeight / 2, localZ]}
              castShadow
              material={frameMaterial}
            >
              <cylinderGeometry args={[slatRadius, slatRadius, slatHeight, 6]} />
            </mesh>
          );
        }),
      )}
      {/* Mattress */}
      <mesh position={[0, mattressLift - mattressThick / 2, 0]} castShadow material={mattressMaterial}>
        <boxGeometry args={[item.width - railThick * 2 - 0.01, mattressThick, item.depth - railThick * 2 - 0.01]} />
      </mesh>
    </group>
  );
}

/**
 * Dressing table (vanity): a desk-like base with drawers and an upright
 * mirror mounted at the back. Depth is typically shallow (~40cm) and the
 * mirror takes up roughly half the total height. Height of the desk slab
 * is derived as a fraction of the total so the mirror always reads as an
 * upright panel.
 */
function DressingTableMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const bodyMaterial = useSurfaceMaterial(item, color, 0.5, item.width, item.depth);
  const mirrorMaterial = useMemo(() => getFurnitureMaterial('#b8d0d8', 0.05), []);
  const mirrorFrameMaterial = useMemo(() => getFurnitureMaterial(color, 0.4), [color]);

  const deskHeight = Math.min(0.8, item.height * 0.45);
  const mirrorHeight = item.height - deskHeight;
  const mirrorWidth = item.width * 0.7;
  const mirrorThickness = 0.015;
  const frameThickness = 0.025;

  return (
    <group>
      {/* Desk body — a closed box representing drawers */}
      <mesh position={[0, deskHeight / 2, 0]} castShadow material={bodyMaterial}>
        <boxGeometry args={[item.width, deskHeight, item.depth]} />
      </mesh>
      {/* Drawer divider lines on the front face */}
      <mesh
        position={[0, deskHeight * 0.5, item.depth / 2 + 0.001]}
        material={mirrorFrameMaterial}
      >
        <boxGeometry args={[item.width * 0.9, 0.005, 0.002]} />
      </mesh>
      {/* Mirror frame (slightly larger than the mirror itself) */}
      <mesh
        position={[0, deskHeight + mirrorHeight / 2, -item.depth / 2 + frameThickness / 2]}
        castShadow
        material={mirrorFrameMaterial}
      >
        <boxGeometry args={[mirrorWidth + frameThickness * 2, mirrorHeight, frameThickness]} />
      </mesh>
      {/* Mirror surface */}
      <mesh
        position={[0, deskHeight + mirrorHeight / 2, -item.depth / 2 + frameThickness + mirrorThickness / 2]}
        material={mirrorMaterial}
      >
        <boxGeometry args={[mirrorWidth, mirrorHeight - frameThickness, mirrorThickness]} />
      </mesh>
    </group>
  );
}

/**
 * PC tower (desktop computer case): a tall rectangular box with a subtle
 * front-panel accent and a small power LED. Depth > width > short so it
 * reads as a standing tower regardless of exact dimensions.
 */
function PcTowerMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const bodyMaterial = useMemo(() => getFurnitureMaterial(color, 0.45), [color]);
  const accentMaterial = useMemo(() => getFurnitureMaterial('#141418', 0.3), []);
  const ledMaterial = useMemo(() => {
    const mat = new THREE.MeshStandardMaterial({
      color: '#4cc9ff',
      emissive: '#4cc9ff',
      emissiveIntensity: 0.8,
      roughness: 0.2,
    });
    return mat;
  }, []);

  const accentDepth = 0.002;

  return (
    <group>
      {/* Main body */}
      <mesh position={[0, item.height / 2, 0]} castShadow material={bodyMaterial}>
        <boxGeometry args={[item.width, item.height, item.depth]} />
      </mesh>
      {/* Front-panel darker inset (slightly recessed visually) */}
      <mesh position={[0, item.height / 2, item.depth / 2 + accentDepth / 2]} material={accentMaterial}>
        <boxGeometry args={[item.width * 0.85, item.height * 0.92, accentDepth]} />
      </mesh>
      {/* Power LED */}
      <mesh
        position={[item.width * 0.3, item.height * 0.88, item.depth / 2 + accentDepth + 0.0005]}
        material={ledMaterial}
      >
        <boxGeometry args={[0.012, 0.004, 0.001]} />
      </mesh>
    </group>
  );
}

/**
 * Plant / flower: terracotta pot with foliage on top. Rendering switches
 * between a few styles based on `metadata.variant`:
 *   - `flower` → short pot with a coloured blossom sphere (uses
 *     `metadata.flowerColor` or a warm default)
 *   - `tall`   → tall pot with an elongated green foliage cylinder capped
 *     by a sphere (ficus / indoor tree look)
 *   - `bush` (default) → pot with a spherical green foliage ball
 * No new data model is required — the variant is set at placement time
 * via the FurnitureDef's `defaultMetadata`.
 */
function PlantMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const variant = ((item.metadata?.['variant'] as string | undefined) ?? 'bush').toLowerCase();
  const flowerColorRaw = item.metadata?.['flowerColor'];
  const flowerColor =
    typeof flowerColorRaw === 'string' && /^#[0-9a-fA-F]{6}$/.test(flowerColorRaw)
      ? flowerColorRaw
      : '#e05570';

  const potHeight = Math.min(item.height * 0.35, 0.25);
  const potRadiusTop = Math.min(item.width, item.depth) / 2;
  const potRadiusBottom = potRadiusTop * 0.75;
  const foliageBottom = potHeight;
  const foliageHeight = item.height - potHeight;

  const potMaterial = useMemo(() => getFurnitureMaterial('#a85a3a', 0.8), []);
  const soilMaterial = useMemo(() => getFurnitureMaterial('#3a2a1a', 0.95), []);
  const leafMaterial = useMemo(() => getFurnitureMaterial(color, 0.85), [color]);
  const flowerMaterial = useMemo(() => getFurnitureMaterial(flowerColor, 0.7), [flowerColor]);

  return (
    <group>
      {/* Terracotta pot */}
      <mesh position={[0, potHeight / 2, 0]} castShadow material={potMaterial}>
        <cylinderGeometry args={[potRadiusTop, potRadiusBottom, potHeight, 16]} />
      </mesh>
      {/* Soil disk just below the pot rim */}
      <mesh position={[0, potHeight - 0.005, 0]} material={soilMaterial}>
        <cylinderGeometry args={[potRadiusTop * 0.9, potRadiusTop * 0.9, 0.01, 16]} />
      </mesh>
      {/* Foliage */}
      {variant === 'tall' ? (
        <>
          {/* Elongated trunk + top sphere */}
          <mesh
            position={[0, foliageBottom + foliageHeight * 0.5, 0]}
            castShadow
            material={leafMaterial}
          >
            <cylinderGeometry args={[potRadiusTop * 0.55, potRadiusTop * 0.7, foliageHeight, 14]} />
          </mesh>
          <mesh
            position={[0, foliageBottom + foliageHeight, 0]}
            castShadow
            material={leafMaterial}
          >
            <sphereGeometry args={[potRadiusTop * 0.9, 16, 12]} />
          </mesh>
        </>
      ) : variant === 'flower' ? (
        <>
          {/* Short stem */}
          <mesh
            position={[0, foliageBottom + foliageHeight * 0.3, 0]}
            castShadow
            material={leafMaterial}
          >
            <cylinderGeometry args={[potRadiusTop * 0.25, potRadiusTop * 0.35, foliageHeight * 0.6, 12]} />
          </mesh>
          {/* Blossom */}
          <mesh
            position={[0, foliageBottom + foliageHeight * 0.75, 0]}
            castShadow
            material={flowerMaterial}
          >
            <sphereGeometry args={[Math.min(potRadiusTop, foliageHeight * 0.35), 16, 12]} />
          </mesh>
        </>
      ) : (
        // Default: leafy bush — a single foliage sphere just above the pot
        <mesh
          position={[0, foliageBottom + foliageHeight * 0.55, 0]}
          castShadow
          material={leafMaterial}
        >
          <sphereGeometry args={[Math.min(potRadiusTop * 1.2, foliageHeight * 0.55), 16, 12]} />
        </mesh>
      )}
    </group>
  );
}

/**
 * Mirror: a framed reflective panel. Two variants selected via
 * `metadata.variant`:
 *
 *   - `wall` (default): thin wall-mounted panel. Only the front-facing
 *     mirror and the frame are drawn — no stand. The item's `depth`
 *     should be small (~3cm) and it's placed flush against a wall, so
 *     elevation controls where on the wall it hangs.
 *
 *   - `floor`: free-standing full-length mirror with an A-frame stand
 *     behind it. A tilt is NOT applied — the mirror sits upright against
 *     the stand, and the stand legs splay backward into the `depth`
 *     dimension so the mesh remains within the item's bounding box.
 *     Elevation should be 0 (on the floor).
 *
 * The frame color is a wood tone by default; the mirror surface itself
 * uses a cool pale blue/grey material with low roughness so it reads as
 * glass without requiring real reflections.
 */
function MirrorMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const variantRaw = item.metadata?.['variant'];
  const variant = typeof variantRaw === 'string' ? variantRaw.toLowerCase() : 'wall';
  const isFloor = variant === 'floor';

  const frameMaterial = useMemo(() => getFurnitureMaterial(color, 0.5), [color]);
  const mirrorMaterial = useMemo(() => getFurnitureMaterial('#c8dce3', 0.05), []);

  // Mirror sits at the back of the bounding box so the frame protrudes
  // forward and the stand (for floor variant) can live in front of it.
  const frameThickness = 0.03;
  const panelThickness = 0.015;
  const frameInset = 0.025; // frame width around the mirror edge

  // The glass occupies most of the height/width, with the frame wrapping it.
  const glassWidth = item.width - frameInset * 2;
  const glassHeight = item.height - frameInset * 2;

  // Wall variant: mirror sits at the back face of the bounding box so it
  // reads as flush against a wall. Floor variant: the mirror is pushed
  // slightly forward to leave room for the stand legs behind it.
  const mirrorZ = isFloor
    ? -item.depth / 2 + frameThickness + 0.005
    : -item.depth / 2 + frameThickness + panelThickness / 2;

  return (
    <group>
      {/* Frame — a rectangular ring around the mirror. Built from four
          rail meshes so the centre stays open for the glass. */}
      {/* Top rail */}
      <mesh
        position={[0, item.height - frameInset / 2, mirrorZ - panelThickness / 2]}
        castShadow
        material={frameMaterial}
      >
        <boxGeometry args={[item.width, frameInset, frameThickness]} />
      </mesh>
      {/* Bottom rail */}
      <mesh
        position={[0, frameInset / 2, mirrorZ - panelThickness / 2]}
        castShadow
        material={frameMaterial}
      >
        <boxGeometry args={[item.width, frameInset, frameThickness]} />
      </mesh>
      {/* Left rail */}
      <mesh
        position={[-item.width / 2 + frameInset / 2, item.height / 2, mirrorZ - panelThickness / 2]}
        castShadow
        material={frameMaterial}
      >
        <boxGeometry args={[frameInset, item.height - frameInset * 2, frameThickness]} />
      </mesh>
      {/* Right rail */}
      <mesh
        position={[item.width / 2 - frameInset / 2, item.height / 2, mirrorZ - panelThickness / 2]}
        castShadow
        material={frameMaterial}
      >
        <boxGeometry args={[frameInset, item.height - frameInset * 2, frameThickness]} />
      </mesh>
      {/* Glass */}
      <mesh position={[0, item.height / 2, mirrorZ]} material={mirrorMaterial}>
        <boxGeometry args={[glassWidth, glassHeight, panelThickness]} />
      </mesh>

      {/* Floor stand — A-frame behind the mirror. Two splayed legs +
          a crossbar at the top that the mirror rests against. Only
          rendered for the floor variant. */}
      {isFloor && (
        <>
          {(() => {
            const legThickness = 0.025;
            // Legs go from the back-bottom (at mirrorZ - 0.01) splayed out
            // to near the front of the bounding box at ground level.
            const legTopY = item.height * 0.75;
            const legTopZ = mirrorZ - 0.005;
            const legBottomZ = item.depth / 2 - legThickness;
            const legLength = Math.hypot(legTopY, legBottomZ - legTopZ);
            const angle = Math.atan2(legBottomZ - legTopZ, -legTopY); // tilt around X
            return (
              <>
                {/* Back crossbar connecting the tops of the two legs — a
                    short horizontal rail the mirror rests against. */}
                <mesh
                  position={[0, legTopY, legTopZ]}
                  castShadow
                  material={frameMaterial}
                >
                  <boxGeometry args={[item.width * 0.3, legThickness, legThickness]} />
                </mesh>
                {/* Two splayed legs */}
                {[-1, 1].map((side) => (
                  <mesh
                    key={`leg-${side}`}
                    position={[
                      side * item.width * 0.15,
                      legTopY / 2 + (legTopY * 0) / 2,
                      (legTopZ + legBottomZ) / 2,
                    ]}
                    rotation={[angle + Math.PI, 0, 0]}
                    castShadow
                    material={frameMaterial}
                  >
                    <boxGeometry args={[legThickness, legLength, legThickness]} />
                  </mesh>
                ))}
              </>
            );
          })()}
        </>
      )}
    </group>
  );
}

/**
 * Digital piano: a slim keyboard chassis with an optional X-frame stand.
 *
 * The stand is controlled via `metadata.hasStand`: when true (default),
 * the chassis sits elevated on a pair of X-shaped legs and the total
 * `item.height` includes the stand. When false, the chassis rests on
 * whatever surface the piano is placed on (i.e., starts at `y=0` in the
 * item's local frame) and the full height is the chassis height.
 *
 * The keyboard's top surface shows a row of alternating white/black
 * keys so the instrument reads as a piano from any angle.
 */
function DigitalPianoMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const hasStandRaw = item.metadata?.['hasStand'];
  const hasStand = typeof hasStandRaw === 'boolean' ? hasStandRaw : true;

  const chassisMaterial = useMemo(() => getFurnitureMaterial(color, 0.4), [color]);
  const standMaterial = useMemo(() => getFurnitureMaterial('#15151a', 0.5), []);
  const whiteKeyMaterial = useMemo(() => getFurnitureMaterial('#f4f0e8', 0.35), []);
  const blackKeyMaterial = useMemo(() => getFurnitureMaterial('#0a0a0e', 0.2), []);

  // Split the total height between chassis and stand. Chassis is ~15cm
  // regardless; the rest is stand (or zero if no stand).
  const chassisHeight = 0.15;
  const standHeight = hasStand ? Math.max(0, item.height - chassisHeight) : 0;
  const chassisBottom = standHeight;
  const chassisCenterY = chassisBottom + chassisHeight / 2;

  // Keys sit on top of the chassis, inset slightly from the edges.
  const keyAreaWidth = item.width * 0.95;
  const keyAreaDepth = item.depth * 0.6;
  const keyAreaX0 = -keyAreaWidth / 2;
  const keyAreaZ = item.depth * 0.05; // shifted toward the front (player-facing)
  const whiteKeyTopY = chassisBottom + chassisHeight + 0.005;
  const whiteKeyHeight = 0.015;
  // Number of white keys scales with width; a typical 88-key digital
  // piano is ~1.27m wide with 52 white keys, so we target roughly one
  // white key every 2.4cm.
  const whiteKeyCount = Math.max(12, Math.round(item.width / 0.024));
  const whiteKeyWidth = keyAreaWidth / whiteKeyCount;

  // Black keys appear on 5 out of every 7 white key positions (after
  // white indices 0, 1, 3, 4, 5). We draw one shorter + slightly
  // raised on top of the white key row.
  const blackKeyPositions: number[] = [];
  for (let i = 0; i < whiteKeyCount - 1; i++) {
    const mod = i % 7;
    if (mod !== 2 && mod !== 6) {
      blackKeyPositions.push(i);
    }
  }

  return (
    <group>
      {/* Stand (X-frame legs + horizontal crossbar). Only when hasStand. */}
      {hasStand && standHeight > 0.05 && (
        <>
          {/* Two splayed legs at the left and right of the chassis,
              forming an X when viewed from the front. Simplified to two
              diagonal bars on each side rather than a true X, which is
              hard to render with a single box. */}
          {[-1, 1].map((side) => (
            <mesh
              key={`leg-${side}`}
              position={[side * (item.width / 2 - 0.1), standHeight / 2, 0]}
              castShadow
              material={standMaterial}
            >
              <boxGeometry args={[0.04, standHeight, item.depth * 0.6]} />
            </mesh>
          ))}
          {/* Horizontal crossbar near the bottom for stability */}
          <mesh
            position={[0, 0.05, 0]}
            castShadow
            material={standMaterial}
          >
            <boxGeometry args={[item.width - 0.25, 0.025, 0.025]} />
          </mesh>
        </>
      )}
      {/* Chassis (the piano body) */}
      <mesh position={[0, chassisCenterY, 0]} castShadow material={chassisMaterial}>
        <boxGeometry args={[item.width, chassisHeight, item.depth]} />
      </mesh>
      {/* White key row */}
      {Array.from({ length: whiteKeyCount }).map((_, i) => {
        const localX = keyAreaX0 + (i + 0.5) * whiteKeyWidth;
        return (
          <mesh
            key={`wk-${i}`}
            position={[localX, whiteKeyTopY + whiteKeyHeight / 2, keyAreaZ]}
            material={whiteKeyMaterial}
          >
            <boxGeometry args={[whiteKeyWidth * 0.92, whiteKeyHeight, keyAreaDepth]} />
          </mesh>
        );
      })}
      {/* Black keys sit on the gaps between adjacent white keys */}
      {blackKeyPositions.map((i) => {
        const localX = keyAreaX0 + (i + 1) * whiteKeyWidth;
        return (
          <mesh
            key={`bk-${i}`}
            position={[localX, whiteKeyTopY + whiteKeyHeight + 0.005, keyAreaZ - keyAreaDepth * 0.2]}
            material={blackKeyMaterial}
          >
            <boxGeometry args={[whiteKeyWidth * 0.55, 0.012, keyAreaDepth * 0.55]} />
          </mesh>
        );
      })}
    </group>
  );
}

/**
 * Audio speaker with two variants selected via `metadata.variant`:
 *
 *   - `shelf` (default): compact bookshelf speaker — a squat rectangular
 *     cabinet with one tweeter dome and one woofer cone on the front face.
 *     Typically placed on a shelf, desk, or stand. Height ≤ 0.5 m.
 *
 *   - `floor`: full-range tower / floor-standing speaker — a tall, slim
 *     cabinet with a tweeter, a midrange, and a woofer stacked on the
 *     front. Sits directly on the floor. Height usually > 0.8 m.
 *
 * The cabinet is a single box, the drivers are flat discs pressed
 * against the front face. The driver layout is derived from the height
 * so both variants read correctly as speakers regardless of exact
 * preset dimensions.
 */
function SpeakerMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const variantRaw = item.metadata?.['variant'];
  const variant = typeof variantRaw === 'string' ? variantRaw.toLowerCase() : 'shelf';
  const isFloor = variant === 'floor';

  const cabinetMaterial = useMemo(() => getFurnitureMaterial(color, 0.55), [color]);
  // Subtle accent for driver cones (warm off-black so they read as
  // distinct from the cabinet even on a dark cabinet).
  const driverMaterial = useMemo(() => getFurnitureMaterial('#2a2520', 0.3), []);
  const tweeterMaterial = useMemo(() => getFurnitureMaterial('#3a3430', 0.15), []);

  // Cabinet
  const cabinetHeight = item.height;
  const frontZ = item.depth / 2 + 0.001;

  // Driver sizing — scale with cabinet width so drivers always fit
  // comfortably within the front face and look proportional.
  const maxDriverRadius = item.width * 0.4;
  const tweeterRadius = Math.min(0.025, maxDriverRadius * 0.3);
  const midRadius = Math.min(0.05, maxDriverRadius * 0.55);
  const wooferRadius = Math.min(0.09, maxDriverRadius * 0.85);
  const driverThickness = 0.006;

  // Vertical placement: tweeter near the top, woofer near the bottom,
  // midrange between (floor variant only). All positions are in the
  // cabinet's local frame where y=0 is the floor.
  const topY = cabinetHeight - tweeterRadius - 0.03;
  const bottomY = wooferRadius + 0.04;
  const midY = (topY + bottomY) / 2;

  return (
    <group>
      {/* Cabinet body */}
      <mesh position={[0, cabinetHeight / 2, 0]} castShadow material={cabinetMaterial}>
        <boxGeometry args={[item.width, cabinetHeight, item.depth]} />
      </mesh>
      {/* Tweeter (top) — small recessed disc */}
      <mesh
        position={[0, topY, frontZ]}
        rotation={[Math.PI / 2, 0, 0]}
        castShadow
        material={tweeterMaterial}
      >
        <cylinderGeometry args={[tweeterRadius, tweeterRadius, driverThickness, 16]} />
      </mesh>
      {/* Midrange — only on floor towers, sits between tweeter and woofer */}
      {isFloor && (
        <mesh
          position={[0, midY, frontZ]}
          rotation={[Math.PI / 2, 0, 0]}
          castShadow
          material={driverMaterial}
        >
          <cylinderGeometry args={[midRadius, midRadius, driverThickness, 16]} />
        </mesh>
      )}
      {/* Woofer — larger driver near the bottom */}
      <mesh
        position={[0, bottomY, frontZ]}
        rotation={[Math.PI / 2, 0, 0]}
        castShadow
        material={driverMaterial}
      >
        <cylinderGeometry args={[wooferRadius, wooferRadius, driverThickness, 18]} />
      </mesh>
      {/* Subtle phase plug in the centre of the woofer (tiny dome) */}
      <mesh
        position={[0, bottomY, frontZ + driverThickness / 2 + 0.002]}
        castShadow
        material={tweeterMaterial}
      >
        <sphereGeometry args={[wooferRadius * 0.2, 10, 8]} />
      </mesh>
    </group>
  );
}

/**
 * Office chair: wheeled 5-star base → gas-lift post → seat pan → tall
 * backrest → two short armrests. Proportions are driven by `item.height`
 * (total top-of-backrest) and `item.width` (seat pan width), so the
 * same mesh looks correct across preset sizes from low task chair to
 * tall executive chair. The seat height is derived as a fraction of
 * total so the backrest is always recognisably taller than the seat.
 */
function OfficeChairMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
  const uphMaterial = useMemo(() => getFurnitureMaterial(color, 0.75), [color]);
  const frameMaterial = useMemo(() => getFurnitureMaterial('#1a1a1e', 0.4), []);
  const casterMaterial = useMemo(() => getFurnitureMaterial('#141418', 0.3), []);

  const seatHeight = item.height * 0.42;
  const seatPanThickness = 0.05;
  const backrestBottom = seatHeight + seatPanThickness;
  const backrestHeight = item.height - backrestBottom;
  const seatWidth = item.width;
  const seatDepth = item.depth * 0.85;

  // 5-star base. The arms splay out radially from a central hub; each
  // arm carries a caster at its outer end. We render the arms as thin
  // boxes oriented toward the outer caster position.
  const baseRadius = Math.min(item.width, item.depth) * 0.55;
  const hubRadius = 0.035;
  const armThickness = 0.02;
  const casterRadius = 0.022;
  const armCount = 5;

  // Gas-lift cylinder from hub up to just under the seat
  const postRadius = 0.025;
  const postHeight = seatHeight - 0.05; // leave room for the hub

  return (
    <group>
      {/* 5-star base — central hub + 5 splayed arms + casters */}
      <mesh position={[0, 0.03, 0]} castShadow material={frameMaterial}>
        <cylinderGeometry args={[hubRadius, hubRadius * 0.8, 0.04, 12]} />
      </mesh>
      {Array.from({ length: armCount }).map((_, i) => {
        const angle = (i / armCount) * Math.PI * 2;
        const midR = baseRadius * 0.5;
        const cx = Math.cos(angle) * midR;
        const cz = Math.sin(angle) * midR;
        return (
          <group key={`arm-${i}`}>
            {/* Arm — rotated around Y so its long axis points outward */}
            <mesh
              position={[cx, 0.02, cz]}
              rotation={[0, -angle, 0]}
              castShadow
              material={frameMaterial}
            >
              <boxGeometry args={[baseRadius, armThickness, armThickness * 1.6]} />
            </mesh>
            {/* Caster (sphere) at the outer end of the arm */}
            <mesh
              position={[Math.cos(angle) * baseRadius, casterRadius, Math.sin(angle) * baseRadius]}
              castShadow
              material={casterMaterial}
            >
              <sphereGeometry args={[casterRadius, 12, 10]} />
            </mesh>
          </group>
        );
      })}
      {/* Gas-lift post */}
      <mesh position={[0, 0.05 + postHeight / 2, 0]} castShadow material={frameMaterial}>
        <cylinderGeometry args={[postRadius * 0.85, postRadius, postHeight, 12]} />
      </mesh>
      {/* Seat pan */}
      <mesh position={[0, seatHeight + seatPanThickness / 2, 0]} castShadow material={uphMaterial}>
        <boxGeometry args={[seatWidth, seatPanThickness, seatDepth]} />
      </mesh>
      {/* Backrest — taller than it is wide, slightly set back from the seat */}
      <mesh
        position={[0, backrestBottom + backrestHeight / 2, -seatDepth / 2 + 0.03]}
        castShadow
        material={uphMaterial}
      >
        <boxGeometry args={[seatWidth * 0.9, backrestHeight, 0.06]} />
      </mesh>
      {/* Armrests — two short horizontal pads at ~elbow height */}
      {([-1, 1] as const).map((side) => {
        const armY = seatHeight + seatPanThickness + 0.18;
        const armX = side * (seatWidth / 2 + 0.015);
        return (
          <group key={`armrest-${side}`}>
            {/* Vertical support from seat level up to the pad */}
            <mesh
              position={[armX, seatHeight + seatPanThickness + 0.09, 0]}
              castShadow
              material={frameMaterial}
            >
              <boxGeometry args={[0.02, 0.18, 0.03]} />
            </mesh>
            {/* Horizontal pad */}
            <mesh position={[armX, armY, 0]} castShadow material={uphMaterial}>
              <boxGeometry args={[0.04, 0.025, seatDepth * 0.7]} />
            </mesh>
          </group>
        );
      })}
    </group>
  );
}

function getFurnitureComponent(type: FurnitureType) {
  switch (type) {
    case 'BED': return BedMesh;
    case 'CRIB': return CribMesh;
    case 'DESK': return DeskMesh;
    case 'WARDROBE': return WardrobeMesh;
    case 'SOFA': return SofaMesh;
    case 'TABLE': return TableMesh;
    case 'CHAIR': return ChairMesh;
    case 'OFFICE_CHAIR': return OfficeChairMesh;
    case 'BOOKCASE': return BookcaseMesh;
    case 'TV': return TvMesh;
    case 'PC_TOWER': return PcTowerMesh;
    case 'AC_UNIT': return SimpleBoxMesh;
    case 'RADIATOR': return RadiatorMesh;
    case 'WALL_COLLAGE': return WallCollageMesh;
    case 'CURTAIN': return CurtainMesh;
    case 'DRESSING_TABLE': return DressingTableMesh;
    case 'PLANT': return PlantMesh;
    case 'MIRROR': return MirrorMesh;
    case 'DIGITAL_PIANO': return DigitalPianoMesh;
    case 'SPEAKER': return SpeakerMesh;
    case 'SHELF':
    case 'NIGHTSTAND':
    case 'DRESSER':
    case 'OTHER':
    default:
      return SimpleBoxMesh;
  }
}

export function FurnitureMesh({ item, isSelected, onSelect, globalOpacity = 1 }: FurnitureMeshProps) {
  const Component = useMemo(() => getFurnitureComponent(item.type), [item.type]);
  const color = isSelected ? SELECTED_COLOR : FURNITURE_COLORS[item.type];

  // (item.x, item.y) is the anchored point on the rotated visual. Use the
  // rotation-aware helper so "left" tracks the visual left edge of the
  // rotated box. Reduces to (0, 0) for the default middle/middle anchor.
  const offset = rotatedAnchorOffsetToCenter(
    item.positionAnchor,
    item.width,
    item.depth,
    item.rotation,
  );
  const centerX = item.x + offset.dx;
  const centerY = item.y + offset.dy;

  // The inner mesh components draw from shared, module-level materials so
  // that identical furniture (e.g. three radiators of the same color) can
  // reuse GPU uploads. That means mutating `material.opacity` on one item
  // would ghost every other item that references the same singleton.
  //
  // To give each item its own opacity we clone the shared materials at
  // mount time (and whenever the inner component re-renders with new
  // materials — e.g. when width/height/type/color change), keep the clones
  // in a ref so we can dispose them on unmount, and mutate the clones'
  // opacity whenever the effective opacity changes. The clones are owned
  // by THIS FurnitureMesh instance exclusively.
  //
  // Effective opacity = per-item opacity × global furniture opacity.
  // Using the product means per-item and global both compose correctly
  // and the global slider no longer needs to mutate shared singletons.
  const groupRef = useRef<THREE.Group>(null);
  const clonedMaterialsRef = useRef<THREE.Material[]>([]);
  const effectiveOpacity = (item.opacity ?? 1) * globalOpacity;
  const shouldCastShadow = effectiveOpacity > SHADOW_OPACITY_THRESHOLD;

  // Clone materials after the inner component has mounted / re-rendered.
  // Re-running on dimension / type / color / rotation / variant changes is
  // important because the inner component may emit fresh mesh instances
  // that reference the shared pool again. Each run disposes the previous
  // set of clones before making new ones.
  useEffect(() => {
    const group = groupRef.current;
    if (!group) return;

    // Dispose any clones from a previous render before creating new ones.
    for (const m of clonedMaterialsRef.current) {
      m.dispose();
    }
    const cloned: THREE.Material[] = [];

    group.traverse((obj) => {
      if (!(obj as THREE.Mesh).isMesh) return;
      const mesh = obj as THREE.Mesh;
      if (!mesh.material) return;
      if (Array.isArray(mesh.material)) {
        const replacements = mesh.material.map((m) => {
          const c = m.clone();
          cloned.push(c);
          return c;
        });
        mesh.material = replacements;
      } else {
        const c = mesh.material.clone();
        cloned.push(c);
        mesh.material = c;
      }
      mesh.castShadow = shouldCastShadow;
    });

    clonedMaterialsRef.current = cloned;

    return () => {
      for (const m of cloned) m.dispose();
    };
    // `shouldCastShadow` is intentionally included so the traversal also
    // updates castShadow on newly-cloned meshes after an opacity change
    // that crosses the threshold.
  }, [
    shouldCastShadow,
    item.type,
    item.width,
    item.depth,
    item.height,
    color,
    // Re-clone when curtain/plant/mirror variant changes, because the
    // inner component may swap between branches that emit different
    // meshes (different material pools).
    item.metadata,
  ]);

  // Apply the effective opacity to the clones whenever it changes.
  // This runs both after the cloning effect above (via the `cloned`
  // array in the ref) and on every subsequent opacity change, without
  // re-cloning materials.
  useEffect(() => {
    for (const mat of clonedMaterialsRef.current) {
      // MeshStandardMaterial is the only material type the inner
      // components create. Cast lets us mutate opacity fields uniformly.
      const m = mat as THREE.MeshStandardMaterial;
      m.transparent = effectiveOpacity < 1;
      m.opacity = effectiveOpacity;
      m.depthWrite = effectiveOpacity >= 1;
      m.needsUpdate = true;
    }
  }, [effectiveOpacity]);

  return (
    <group
      ref={groupRef}
      position={[centerX, item.elevationFromFloor, centerY]}
      rotation={[0, -degToRad(item.rotation), 0]}
      onClick={onSelect ? (e) => { e.stopPropagation(); onSelect(item.id); } : undefined}
    >
      <Component item={item} color={color} />
    </group>
  );
}
