// Validate the exported geometry and the controls used by the website, without a browser.
import { readFileSync, existsSync } from 'node:fs';
import assert from 'node:assert/strict';
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const loader=new GLTFLoader();
async function read(path){const buffer=readFileSync(path);return (await loader.parseAsync(buffer.buffer.slice(buffer.byteOffset,buffer.byteOffset+buffer.byteLength),'')).scene;}
const main=await read('public/models/mezzo.glb');
for(const name of ['cartridge_compute','cartridge_project','pause_rocker','media_previous','media_play','media_next','bay_02_shutters','rear_connections','service_lid','lid_clamp','lid_clamp_screw','heel_left','heel_right','memory_a','memory_b','cartridge_audio_mini','cursor_left','cursor_right','cursor_up','cursor_down'])assert(main.getObjectByName(name),`Missing ${name}`);
for(const name of ['bay_01_shutters','bay_01_door_front','bay_01_door_rear','bay_02_door_front','bay_02_door_rear','service_electronics','top_grille','rear_exhaust_left','rear_exhaust_right'])assert(main.getObjectByName(name),`Missing ${name}`);
main.updateMatrixWorld(true);
// The pad and directional keys must be exposed above their continuous bezel.
for(const [x,y,control] of [[14.17,-3.0,'touchpad_surface'],[14.17,-5.98,'cursor_up'],[14.17,-7.33,'cursor_down'],[12.15,-6.63,'cursor_left'],[16.19,-6.63,'cursor_right']]){
  const ray=new THREE.Raycaster(new THREE.Vector3(x*.01,1,-y*.01),new THREE.Vector3(0,-1,0));
  let hit=ray.intersectObject(main,true)[0]?.object;
  while(hit&&hit.name!==control)hit=hit.parent;
  assert(hit,`${control} is covered by another surface`);
}
// These points were visibly obscured by the flat internal titanium spine.
for(const [x,y] of [[14.17,-8.3],[17.1,-7.5],[-5,-8.65]]){
  const ray=new THREE.Raycaster(new THREE.Vector3(x*.01,1,-y*.01),new THREE.Vector3(0,-1,0));
  const hit=ray.intersectObject(main,true)[0];assert(hit);
  assert.equal(hit.object.material.name,'Graphite / fine textured polymer',`Internal plate protrudes through deck at ${x}, ${y}`);
  assert(Math.abs(hit.point.y-(2.50+.063*y+.10)*.01)<1e-6,'Incorrect deck clearance');
}
const deck=.0414196, seated=.0404196;
const seatedGuard=new THREE.Box3().setFromObject(main.getObjectByName('vector_connector_guard'));
assert(seatedGuard.min.y<deck-.015,'Cartridge sleeve must enter the bay');
assert(seatedGuard.max.y>deck,'Sleeve must join the case above the opening');
for(const side of ['left','right']){
  const exhaust=main.getObjectByName('rear_exhaust_'+side);
  const aft=new THREE.Vector3(0,0,1).transformDirection(exhaust.matrixWorld);
  assert(aft.z<-.85&&Math.abs(aft.x)<1e-6,'Jet outlet must face the back');
  const bounds=new THREE.Box3().setFromObject(exhaust);
  assert(bounds.max.y<.037,'Jet outlet must be below the top grille');
}
for(const [i,name] of ['rear_usb_c_1','rear_usb_c_2','rear_usb_c_3','rear_usb_c_4','rear_usb_c_5','rear_lan','rear_hdmi','rear_usb_a','rear_audio'].entries()){
  const port=main.getObjectByName(name);assert(port,`Missing ${name}`);
  const size=new THREE.Box3().setFromObject(port).getSize(new THREE.Vector3());
  if(i<5)assert(size.x>=.00979,'USB-C ports must retain their full physical width');
}
// Both leaves share the bezel datum and tuck beside the inserted sleeve.
main.getObjectByName('cartridge_compute').visible=false;
for(const slot of [1,2]){
  for(const side of ['front','rear'])main.getObjectByName(`bay_0${slot}_door_${side}`).rotation.x=0;
  main.updateMatrixWorld(true);
  const cy=slot===1?7.30:11.10;
  for(const dy of [-.48,.48]){
    const ray=new THREE.Raycaster(new THREE.Vector3(.0846,1,-(cy+dy)*.01),new THREE.Vector3(0,-1,0));
    const hit=ray.intersectObject(main,true).find(h=>visible(h.object));assert(hit);
    assert(Math.abs(hit.point.y-(deck+.00075))<1e-6,'Empty bay doors must close flush');
    let object=hit.object;while(object&&!object.name.startsWith(`bay_0${slot}_door_`))object=object.parent;
    assert(object,'Empty bay is not covered by its paired doors');
  }
  const rimRay=new THREE.Raycaster(new THREE.Vector3(.1506,1,-cy*.01),new THREE.Vector3(0,-1,0));
  const rim=rimRay.intersectObject(main,true).find(h=>visible(h.object));
  assert(Math.abs(rim.point.y-(deck+.00075))<1e-6,'Doors and bezel must share a flush surface');
  for(const [side,sign] of [['front',-1],['rear',1]])main.getObjectByName(`bay_0${slot}_door_${side}`).rotation.x=sign*Math.PI/2;
  main.updateMatrixWorld(true);
  const guard=seatedGuard.clone();if(slot===2)guard.translate(new THREE.Vector3(0,0,-.038));
  for(const side of ['front','rear']){
    const door=new THREE.Box3().setFromObject(main.getObjectByName(`bay_0${slot}_door_${side}`));
    assert(!door.intersectsBox(guard),'Open door intersects the protective sleeve');
  }
}
for(const depth of [.05,.08,.118]){
  const ray=new THREE.Raycaster(new THREE.Vector3(0,1,-depth),new THREE.Vector3(0,-1,0));
  const hit=ray.intersectObject(main,true)[0];assert(hit);assert(Math.abs(hit.point.y-.0414196)<1e-6,`Rear deck height ${hit.point.y}`);
  const normal=hit.face.normal.clone().transformDirection(hit.object.matrixWorld);assert(normal.distanceTo(new THREE.Vector3(0,1,0))<1e-6,'Deck is not horizontal');
}
const modules=new Map();
for(const id of ['vector','companion','sound','origin','batt','world','audio','mix']){
  const root=await read(`public/models/cartridge-${id}.glb`);root.updateMatrixWorld(true);modules.set(id,root);
  const size=new THREE.Box3().setFromObject(root).getSize(new THREE.Vector3());assert(size.x>.07&&size.x<(id==='mix'?.30:.14),`${id}: incorrect export units`);
  if(['vector','companion','sound','origin','batt'].includes(id)){
    const guard=root.getObjectByName(id+'_connector_guard'),contacts=root.getObjectByName(id+'_connector_contacts');
    assert(guard&&contacts,`${id}: missing protected connector assembly`);
    const casing=new THREE.Box3().setFromObject(guard),contactBox=new THREE.Box3().setFromObject(contacts);
    assert(contactBox.min.y-casing.min.y>.005,`${id}: contacts should be recessed at least 5 mm`);
    assert(contactBox.min.x>casing.min.x&&contactBox.max.x<casing.max.x,`${id}: casing must protect the contact ends`);
    const ray=new THREE.Raycaster(new THREE.Vector3(0,-.006,.04),new THREE.Vector3(0,0,-1));
    let hit=ray.intersectObject(root,true)[0]?.object;
    while(hit&&hit!==guard)hit=hit.parent;
    assert(hit,`${id}: contacts are exposed from the side`);
  }
  root.traverse(o=>{if(o.isMesh){const position=o.geometry.getAttribute('position');for(const v of position.array)assert(Number.isFinite(v));}});
  for(const name of id==='origin'?['origin_mode_button',...Array.from({length:4},(_,i)=>`origin_led_${i}`)]:id==='batt'?['batt_check_button',...Array.from({length:5},(_,i)=>`batt_led_${i}`)]:[])assert(root.getObjectByName(name),`${id}: missing ${name}`);
  if(['origin','batt'].includes(id)){
    const button=root.getObjectByName(id==='origin'?'origin_mode_button':'batt_check_button');
    const center=new THREE.Box3().setFromObject(button).getCenter(new THREE.Vector3());
    const ray=new THREE.Raycaster(center.clone().add(new THREE.Vector3(1,0,0)),new THREE.Vector3(-1,0,0));
    assert.equal(ray.intersectObject(root,true)[0]?.object.name,button.name,`${id}: side control is obstructed`);
    const shape=new THREE.Box3().setFromObject(button).getSize(new THREE.Vector3());assert(Math.abs(shape.y-shape.z)<1e-6,`${id}: button should be square`);
  }
  const lights=[];root.traverse(o=>{if(o.name.startsWith('activity_led_'))lights.push(o);});assert.equal(lights.length,id==='mix'?0:1,`${id}: expected one front activity light`);
  assert(existsSync(`public/images/cartridge-${id}.webp`));
  console.log(`${id}: ${(size.x*1000).toFixed(1)} × ${(size.y*1000).toFixed(1)} × ${(size.z*1000).toFixed(1)} mm; geometry and controls verified`);
}
// The user's core requirement: controls remain reachable with both bays occupied.
function visible(object){for(let o=object;o;o=o.parent)if(!o.visible)return false;return true;}
main.getObjectByName('cartridge_compute').visible=false;
for(const controlId of ['origin','batt'])for(const slot of [0,1])for(const neighbour of ['vector','companion','sound','origin','batt'].filter(id=>id!==controlId)){
  const scene=new THREE.Group();scene.add(main.clone(true));
  const control=modules.get(controlId).clone(true),other=modules.get(neighbour).clone(true);
  control.position.set(.0846,seated,slot===0?-.073:-.111);other.position.set(.0846,seated,slot===0?-.111:-.073);scene.add(control,other);scene.updateMatrixWorld(true);
  const name=controlId==='origin'?'origin_mode_button':controlId==='batt'?'batt_check_button':'sound_monitor_wheel';
  const target=control.getObjectByName(name),center=new THREE.Box3().setFromObject(target).getCenter(new THREE.Vector3());
  const ray=new THREE.Raycaster(center.clone().add(new THREE.Vector3(1,0,0)),new THREE.Vector3(-1,0,0));
  const hit=ray.intersectObject(scene,true).find(h=>visible(h.object));
  let object=hit?.object,found=false;while(object){if(object===target){found=true;break;}object=object.parent;}
  assert(found,`${name} blocked in bay ${slot+1} by ${neighbour}: ${hit?.object.name}`);
}
const world=modules.get('world');assert(world.getObjectByName('world_epaper_label'),'Missing e-paper label');
world.traverse(o=>{if(o.isMesh&&o.material.name.includes('e-paper'))assert(o.material.emissive.getHex()===0 || o.material.emissiveIntensity===0,'E-paper should be unlit');});
const vectorSize=new THREE.Box3().setFromObject(modules.get('vector')).getSize(new THREE.Vector3());
const soundSize=new THREE.Box3().setFromObject(modules.get('sound')).getSize(new THREE.Vector3());
assert(vectorSize.distanceTo(soundSize)<.0005,'SOUND and VECTOR must share an envelope');
assert(vectorSize.distanceTo(new THREE.Box3().setFromObject(modules.get('companion')).getSize(new THREE.Vector3()))<.0005,'COMPANION must share the ASX envelope');
for(const id of ['vector','sound'])assert(!modules.get(id).getObjectByName('sound_monitor_wheel'));
for(const n of ['route_led_1','route_led_2'])assert(main.getObjectByName(n),`Missing ${n}`);
const lid=main.getObjectByName('service_lid');
const screw=main.getObjectByName('lid_clamp_screw');
let root=screw;while(root&&root!==lid)root=root.parent;assert.equal(root,lid,'Clamp screw must stay captive to lid');
console.log('Cursor clearance, rear-facing exhausts, flush paired doors, protected contacts, insertion clearance, service groups and cartridge controls verified.');
