import { useMemo } from 'react';
import * as THREE from 'three';
import type { Point, Wall, WallFinish, WallOpening } from '@house-plan-maker/shared';
import {
  splitWallAroundOpenings,
  wallRotationY,
  wallSegmentCenter3D,
  wallNormal,
  type WallSegment,
} from './utils/wallGeometry';
import { getWallPbr } from './utils/pbrTextures';

interface WallMeshProps {
  readonly wall: Wall;
  readonly openings: readonly WallOpening[];
  readonly wallHeight: number;
  readonly wallColor?: string;
  readonly wallFinish?: WallFinish;
  readonly selectedIds: ReadonlySet<string>;
  readonly onSelect?: (id: string) => void;
  /**
   * Centroid of the room polygon (in 2D editor coords). The wall's
   * `startX/Y..endX/Y` line represents the **inner** edge of the wall (this
   * matches how the 2D WallLayer renders walls — it draws an outer boundary
   * offset outward by `thickness`). In 3D, however, a box geometry is
   * centered on the line by default, so the wall would bleed `thickness/2`
   * into the room and collide with furniture sitting against it. We push
   * each wall segment outward (away from the centroid) by `thickness/2` so
   * the inner face stays on the wall line.
   */
  readonly roomCentroid?: Point;
}

const DEFAULT_WALL_COLOR = '#f0ebe3';
const WALL_SELECTED_COLOR = '#b8d4e3';

// ── PAINT material cache (one per color) ──
//
// PAINT is the only finish that uses `wallColor`; the textured finishes
// (PLASTER/BRICK/CONCRETE/WOOD_PANEL/WALLPAPER) ignore color and apply a
// shared PBR material loaded lazily from getWallPbr().
const paintMaterialCache = new Map<string, THREE.MeshStandardMaterial>();

function getPaintMaterial(color: string): THREE.MeshStandardMaterial {
  let mat = paintMaterialCache.get(color);
  if (!mat) {
    mat = new THREE.MeshStandardMaterial({ color, roughness: 0.85, side: THREE.DoubleSide });
    paintMaterialCache.set(color, mat);
  }
  return mat;
}

const wallSelectedMaterial = new THREE.MeshStandardMaterial({
  color: WALL_SELECTED_COLOR,
  roughness: 0.7,
  side: THREE.DoubleSide,
});

/**
 * Build a BoxGeometry for a wall segment and rescale its UVs so a textured
 * wall finish tiles every `tileMeters` of physical surface instead of
 * stretching one tile across the whole segment. The default BoxGeometry has
 * UVs in the 0..1 range per face — multiplying by `(width/tile, height/tile)`
 * gives the desired tile density and `wrapS/wrapT = RepeatWrapping` on the
 * texture handles the modulo. The UV scale is baked into the geometry so a
 * single shared material instance can serve walls of any size.
 */
function buildSegmentGeometry(
  width: number,
  height: number,
  thickness: number,
  tileMeters: number | null,
): THREE.BoxGeometry {
  const geometry = new THREE.BoxGeometry(width, height, thickness);
  if (tileMeters != null) {
    const u = Math.max(1, width / tileMeters);
    const v = Math.max(1, height / tileMeters);
    const uv = geometry.attributes.uv;
    if (uv) {
      for (let i = 0; i < uv.count; i++) {
        uv.setXY(i, uv.getX(i) * u, uv.getY(i) * v);
      }
      uv.needsUpdate = true;
    }
  }
  return geometry;
}

function WallSegmentMesh({
  wall,
  segment,
  thickness,
  material,
  tileMeters,
  isSelected,
  onSelect,
  outwardOffset,
}: {
  readonly wall: Wall;
  readonly segment: WallSegment;
  readonly thickness: number;
  readonly material: THREE.MeshStandardMaterial;
  readonly tileMeters: number | null;
  readonly isSelected: boolean;
  readonly onSelect?: (id: string) => void;
  /** [dx, 0, dz] offset to push the box from "centered on wall line" to "outer side of wall line". */
  readonly outwardOffset: readonly [number, number, number];
}) {
  const segmentWidth = segment.endAlongWall - segment.startAlongWall;
  const segmentHeight = segment.topY - segment.bottomY;

  const center = useMemo<[number, number, number]>(() => {
    const [x, y, z] = wallSegmentCenter3D(wall, segment);
    return [x + outwardOffset[0], y, z + outwardOffset[2]];
  }, [wall, segment, outwardOffset]);

  const rotY = useMemo(() => wallRotationY(wall), [wall]);

  const geometry = useMemo(
    () => buildSegmentGeometry(segmentWidth, segmentHeight, thickness, tileMeters),
    [segmentWidth, segmentHeight, thickness, tileMeters],
  );

  if (segmentWidth <= 0 || segmentHeight <= 0) return null;

  return (
    <mesh
      position={center}
      rotation={[0, rotY, 0]}
      castShadow
      receiveShadow
      material={isSelected ? wallSelectedMaterial : material}
      geometry={geometry}
      onClick={onSelect ? (e) => { e.stopPropagation(); onSelect(wall.id); } : undefined}
    />
  );
}

export function WallMesh({ wall, openings, wallHeight, wallColor = DEFAULT_WALL_COLOR, wallFinish = 'PAINT', selectedIds, onSelect, roomCentroid }: WallMeshProps) {
  const segments = useMemo(
    () => splitWallAroundOpenings(wall, openings, wallHeight),
    [wall, openings, wallHeight],
  );

  // Resolve the finish to a material + tile size. PAINT uses the per-color
  // cache (no UV rescale needed); textured finishes load a shared PBR set.
  const { material, tileMeters } = useMemo(() => {
    if (wallFinish === 'PAINT') {
      return { material: getPaintMaterial(wallColor), tileMeters: null as number | null };
    }
    const pbr = getWallPbr(wallFinish);
    return { material: pbr.material, tileMeters: pbr.tileMeters };
  }, [wallFinish, wallColor]);

  // Compute the outward (away-from-room-centroid) offset along the wall's
  // perpendicular normal. Without this the wall box straddles the wall line
  // and the inner half-thickness collides with furniture placed against the
  // wall. The 2D editor draws walls extending entirely outward from the
  // shape — this matches that semantic.
  const outwardOffset = useMemo<[number, number, number]>(() => {
    if (!roomCentroid) return [0, 0, 0];
    const { nx, ny } = wallNormal(wall);
    if (nx === 0 && ny === 0) return [0, 0, 0];
    // Wall midpoint, used to decide which side of the wall is "outside".
    const midX = (wall.startX + wall.endX) / 2;
    const midY = (wall.startY + wall.endY) / 2;
    // Vector from centroid to wall midpoint = outward direction (pre-normal-projection).
    const outX = midX - roomCentroid.x;
    const outY = midY - roomCentroid.y;
    // Sign of normal-along-outward tells us whether to flip.
    const dot = nx * outX + ny * outY;
    const sign = dot >= 0 ? 1 : -1;
    const half = wall.thickness / 2;
    // 2D y-axis maps to 3D z-axis.
    return [sign * nx * half, 0, sign * ny * half];
  }, [wall, roomCentroid]);

  const isSelected = selectedIds.has(wall.id);

  return (
    <group>
      {segments.map((segment, i) => (
        <WallSegmentMesh
          key={`${wall.id}-seg-${i}`}
          wall={wall}
          segment={segment}
          thickness={wall.thickness}
          material={material}
          tileMeters={tileMeters}
          isSelected={isSelected}
          onSelect={onSelect}
          outwardOffset={outwardOffset}
        />
      ))}
    </group>
  );
}
