import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

// ===================== Named parameters (millimetres, from sketch) =====================
const MM = 0.001;                       // 1 world unit = 1 metre
const PARAMS = Object.freeze({
  length: 1200,          // top length (front view dimension)
  depth: 400,            // top depth (side view dimension)
  topThickness: 30,      // top: 1200 x 400 x 30
  totalHeight: 450,      // floor to top surface
  legHeight: 420,        // leg height (420 + 30 = 450)
  legSection: 50,        // legs 50 x 50 in section
  legInset: 40,          // leg outer face inset from each adjacent top edge
  lengthMin: 900,
  lengthMax: 1600,
  lengthStep: 10,
  explodeOffset: 250,    // exploded view raises top 0.25 m
  woodColour: '#b5814f', // assumption: not specified in sketch
});
if (PARAMS.legHeight + PARAMS.topThickness !== PARAMS.totalHeight) {
  console.warn('Sketch dimensions inconsistent: legHeight + topThickness != totalHeight');
}
// Framing parameters (screen pixels)
const FRAME = Object.freeze({
  panelGap: 8,            // gap between controls panel and usable region
  marginMin: 24,          // minimum comfortable margin around the bench
  marginFraction: 0.07,   // margin as fraction of smaller usable dimension
  visibleMargin: 8,       // tolerance used when checking a user-adjusted view
  minRegion: 120,         // smallest usable region before falling back to full viewport
});
// Default view direction (assumption: three-quarter view from front-right, above)
const VIEW_DIR = new THREE.Vector3(0.9, 0.6, 1.35).normalize();

// ===================== Starter renderer / camera / controls =====================
const viewport = document.querySelector('#viewport');
const controlsPanel = document.querySelector('#controls');
const scene = new THREE.Scene();
scene.background = new THREE.Color('#eef1f5');
const camera = new THREE.PerspectiveCamera(40,1,0.01,100);
camera.position.set(2,1.6,2);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
viewport.appendChild(renderer.domElement);
const orbit = new OrbitControls(camera,renderer.domElement);
orbit.target.set(0,0,0);
orbit.enableDamping = true;
scene.add(new THREE.HemisphereLight(0xffffff,0x64748b,2.5));
const light = new THREE.DirectionalLight(0xffffff,3);
light.position.set(3,5,4);
scene.add(light);

const grid = new THREE.GridHelper(4, 40, 0x9aa8b8, 0xd3dae3);
grid.name = 'floor-grid';
scene.add(grid);

// ===================== State =====================
const state = {
  length: PARAMS.length,
  colour: PARAMS.woodColour,
  exploded: false,
};
let userAdjustedView = false;   // set once the user orbits/zooms/pans

// ===================== Bench geometry =====================
const woodMaterial = new THREE.MeshStandardMaterial({color: state.colour, roughness: 0.65, metalness: 0});
woodMaterial.name = 'wood';

const bench = new THREE.Group();
bench.name = 'bench';
scene.add(bench);

function makeTopGeometry(lengthMm) {
  return new THREE.BoxGeometry(lengthMm*MM, PARAMS.topThickness*MM, PARAMS.depth*MM);
}

const top = new THREE.Mesh(makeTopGeometry(state.length), woodMaterial);
top.name = 'top';
bench.add(top);

const legGeometry = new THREE.BoxGeometry(PARAMS.legSection*MM, PARAMS.legHeight*MM, PARAMS.legSection*MM);
const legDefs = [
  {name:'leg-front-left',  sx:-1, sz: 1},
  {name:'leg-front-right', sx: 1, sz: 1},
  {name:'leg-back-left',   sx:-1, sz:-1},
  {name:'leg-back-right',  sx: 1, sz:-1},
];
const legs = legDefs.map(def => {
  const m = new THREE.Mesh(legGeometry, woodMaterial);
  m.name = def.name;
  m.userData.sx = def.sx;
  m.userData.sz = def.sz;
  bench.add(m);
  return m;
});

function setTopLength(lengthMm) {
  const old = top.geometry;
  top.geometry = makeTopGeometry(lengthMm);
  old.dispose();
}

function layout() {
  const L = state.length, D = PARAMS.depth, s = PARAMS.legSection, inset = PARAMS.legInset;
  const lx = (L/2 - inset - s/2) * MM;
  const lz = (D/2 - inset - s/2) * MM;
  const ly = (PARAMS.legHeight/2) * MM;
  for (const leg of legs) leg.position.set(leg.userData.sx*lx, ly, leg.userData.sz*lz);
  top.position.set(0,
    (PARAMS.legHeight + PARAMS.topThickness/2 + (state.exploded ? PARAMS.explodeOffset : 0)) * MM,
    0);
  bench.updateMatrixWorld(true);
}

// Worst-case envelope over every allowed state: max length, exploded.
const ENVELOPE = new THREE.Box3(
  new THREE.Vector3(-PARAMS.lengthMax/2*MM, 0, -PARAMS.depth/2*MM),
  new THREE.Vector3( PARAMS.lengthMax/2*MM, (PARAMS.totalHeight + PARAMS.explodeOffset)*MM, PARAMS.depth/2*MM)
);
const ENVELOPE_CENTER = ENVELOPE.getCenter(new THREE.Vector3());

function boxCorners(box) {
  const out = [];
  for (const x of [box.min.x, box.max.x])
    for (const y of [box.min.y, box.max.y])
      for (const z of [box.min.z, box.max.z]) out.push(new THREE.Vector3(x, y, z));
  return out;
}
const ENVELOPE_CORNERS = boxCorners(ENVELOPE);

// ===================== Usable (unobstructed) screen region =====================
let viewW = 1, viewH = 1;
let freeRect = {left:0, top:0, right:1, bottom:1};

function computeFreeRect() {
  const vr = viewport.getBoundingClientRect();
  const W = vr.width, H = vr.height;
  const full = {left:0, top:0, right:W, bottom:H};
  const pr = controlsPanel.getBoundingClientRect();
  const p = {left:pr.left - vr.left, top:pr.top - vr.top, right:pr.right - vr.left, bottom:pr.bottom - vr.top};
  const overlaps = pr.width > 0 && pr.height > 0 && p.left < W && p.right > 0 && p.top < H && p.bottom > 0;
  if (!overlaps) return full;
  const g = FRAME.panelGap;
  const candidates = [
    {left:0, top:0, right:W, bottom:p.top - g},          // above panel
    {left:0, top:p.bottom + g, right:W, bottom:H},       // below panel
    {left:0, top:0, right:p.left - g, bottom:H},         // left of panel
    {left:p.right + g, top:0, right:W, bottom:H},        // right of panel
  ].map(r => ({...r, w:r.right - r.left, h:r.bottom - r.top}))
   .filter(r => r.w >= FRAME.minRegion && r.h >= FRAME.minRegion);
  if (!candidates.length) return full;
  // The bench projects wide; prefer the region in which it can be drawn largest.
  const score = r => Math.min(r.w, r.h*2.2) + 1e-6*r.w*r.h;
  candidates.sort((a, b) => score(b) - score(a));
  const best = candidates[0];
  return {left:best.left, top:best.top, right:best.right, bottom:best.bottom};
}

function comfortableMargin() {
  const m = Math.min(freeRect.right - freeRect.left, freeRect.bottom - freeRect.top);
  return Math.round(Math.max(FRAME.marginMin, FRAME.marginFraction*m));
}

function resize(){
  const width=viewport.clientWidth,height=viewport.clientHeight;
  viewW = Math.max(1, width); viewH = Math.max(1, height);
  camera.aspect=viewW/viewH;
  renderer.setSize(viewW,viewH);
  freeRect = computeFreeRect();
  // Move the projection centre to the centre of the unobstructed region.
  const cx = (freeRect.left + freeRect.right)/2;
  const cy = (freeRect.top + freeRect.bottom)/2;
  camera.setViewOffset(viewW, viewH, viewW/2 - cx, viewH/2 - cy, viewW, viewH);
  camera.updateProjectionMatrix();
}

// ===================== Projection-based fitting =====================
const _p = new THREE.Vector3();
function projectedRect(corners) {
  camera.updateMatrixWorld();
  let minX=Infinity, minY=Infinity, maxX=-Infinity, maxY=-Infinity;
  for (const c of corners) {
    _p.copy(c).applyMatrix4(camera.matrixWorldInverse);
    if (-_p.z < camera.near*2 || -_p.z > camera.far*0.95) return null;
    _p.copy(c).project(camera);
    const sx = (_p.x + 1)/2*viewW, sy = (1 - _p.y)/2*viewH;
    minX=Math.min(minX,sx); maxX=Math.max(maxX,sx);
    minY=Math.min(minY,sy); maxY=Math.max(maxY,sy);
  }
  return {left:minX, top:minY, right:maxX, bottom:maxY};
}
function rectFits(r, f, margin) {
  return !!r && r.left >= f.left + margin && r.right <= f.right - margin &&
         r.top >= f.top + margin && r.bottom <= f.bottom - margin;
}

function flushDamping() {
  orbit.enableDamping = false;
  orbit.update();          // applies and clears any residual motion
  orbit.enableDamping = true;
}

function fitCamera(direction) {
  flushDamping();
  const dir = direction.clone().normalize();
  const target = ENVELOPE_CENTER.clone();
  const margin = comfortableMargin();
  camera.zoom = 1;
  camera.updateProjectionMatrix();
  const place = d => {
    camera.position.copy(target).addScaledVector(dir, d);
    camera.lookAt(target);
  };
  const fits = d => { place(d); return rectFits(projectedRect(ENVELOPE_CORNERS), freeRect, margin); };
  let lo = 0.2, hi = 2;
  while (!fits(hi) && hi < 60) hi *= 1.5;
  for (let i = 0; i < 40; i++) {
    const mid = (lo + hi)/2;
    if (fits(mid)) hi = mid; else lo = mid;
  }
  place(hi);
  orbit.target.copy(target);
  orbit.update();
}

const _benchBox = new THREE.Box3();
function benchCorners() {
  bench.updateMatrixWorld(true);
  return boxCorners(_benchBox.setFromObject(bench));
}

// Keeps a user-adjusted view unless the bench would be clipped/covered.
function ensureVisible() {
  if (!userAdjustedView) { fitCamera(VIEW_DIR); return; }
  if (!rectFits(projectedRect(benchCorners()), freeRect, FRAME.visibleMargin)) {
    const dir = camera.position.clone().sub(orbit.target);
    fitCamera(dir.lengthSq() > 1e-8 ? dir : VIEW_DIR);
  }
}

function applyLayout() {
  resize();
  ensureVisible();
}
let layoutPending = false;
function scheduleLayout() {
  if (layoutPending) return;
  layoutPending = true;
  requestAnimationFrame(() => { layoutPending = false; applyLayout(); });
}

orbit.addEventListener('start', () => { userAdjustedView = true; });

// ===================== UI =====================
const lengthInput = document.querySelector('#length');
const lengthOut = document.querySelector('#lengthOut');
const colourInput = document.querySelector('#colour');
const explodedInput = document.querySelector('#exploded');
const resetBtn = document.querySelector('#reset');
const dimL = document.querySelector('#dimL');
const dimD = document.querySelector('#dimD');
const dimH = document.querySelector('#dimH');
const statusEl = document.querySelector('#status');

lengthInput.min = PARAMS.lengthMin;
lengthInput.max = PARAMS.lengthMax;
lengthInput.step = PARAMS.lengthStep;

const fmt = mm => `${Math.round(mm)} mm`;

function updateReadout() {
  lengthOut.textContent = fmt(state.length);
  lengthInput.setAttribute('aria-valuetext', fmt(state.length));
  dimL.textContent = fmt(state.length);
  dimD.textContent = fmt(PARAMS.depth);
  dimH.textContent = fmt(PARAMS.totalHeight);
  // Fixed single-line text so the panel height does not change.
  statusEl.textContent = state.exploded
    ? `Exploded: top +${PARAMS.explodeOffset} mm`
    : 'Assembled view';
}

function syncInputs() {
  lengthInput.value = String(state.length);
  colourInput.value = state.colour;
  explodedInput.checked = state.exploded;
}

lengthInput.addEventListener('input', () => {
  const v = Math.min(PARAMS.lengthMax, Math.max(PARAMS.lengthMin,
    Math.round(Number(lengthInput.value)/PARAMS.lengthStep)*PARAMS.lengthStep));
  if (v === state.length) return;
  state.length = v;
  setTopLength(v);
  layout();
  updateReadout();
  ensureVisible();
});

colourInput.addEventListener('input', () => {
  state.colour = colourInput.value;
  woodMaterial.color.set(state.colour);
});

explodedInput.addEventListener('change', () => {
  state.exploded = explodedInput.checked;
  layout();
  updateReadout();
  ensureVisible();
});

function resetAll() {
  state.length = PARAMS.length;
  state.colour = PARAMS.woodColour;
  state.exploded = false;
  setTopLength(state.length);
  woodMaterial.color.set(state.colour);
  layout();
  syncInputs();
  updateReadout();
  userAdjustedView = false;
  resize();
  fitCamera(VIEW_DIR);
}
resetBtn.addEventListener('click', resetAll);

// ===================== Layout observers / loop =====================
addEventListener('resize', scheduleLayout);
if ('ResizeObserver' in window) {
  const ro = new ResizeObserver(scheduleLayout);
  ro.observe(viewport);
  ro.observe(controlsPanel);
}

layout();
syncInputs();
updateReadout();
resize();
fitCamera(VIEW_DIR);

// Inspection hook for evaluation.
window.benchDebug = {
  scene, bench, top