"""Reference drilling fixture. CAD dimensions are millimetres."""
from pathlib import Path
import math,json
import cadquery as cq
ROOT=Path(__file__).resolve().parent

def block(x,y,z,cx=0,cy=0,z0=0):return cq.Workplane('XY').box(x,y,z,centered=(True,True,False)).translate((cx,cy,z0))
def cylinder(d,h,x,y,z):return cq.Workplane('XY').circle(d/2).extrude(h).translate((x,y,z))
def load():return json.loads((ROOT/'inputs.json').read_text())
def validate(c):
 if c['guide_bottom'] <= c['plate_bottom']+c['plate_thickness']+5:raise ValueError('At least 5 mm chip clearance is required')
 if c['round_pin_diameter']>=c['datum_hole_diameter']:raise ValueError('Datum pin must clear datum hole')
 if len(c['variants'])!=3:raise ValueError('The fixture has three named reference variants')
 for v in c['variants']:
  if v['width']>145 or v['height']>85:raise ValueError('Plate exceeds the post/clamp envelope')
  for x,y in v['holes']:
   if abs(x)+c['bushing_od']/2>43 or abs(y)+c['bushing_od']/2>29:raise ValueError('Bushing exceeds the guide window')

def build(c,variant):
 validate(c);v=next(x for x in c['variants'] if x['name']==variant);p={}
 p['base']=block(*c['base'])
 for x,y in [(-80,-50),(-80,50),(80,-50),(80,50),(0,-50),(0,50)]:p['base']=p['base'].cut(cylinder(5,12,x,y,0))
 for x in [-40,40]:p['base']=p['base'].cut(cylinder(5.98,12,x,0,0))
 for i,(x,y) in enumerate([(-45,-27),(45,-27),(0,28)]):p['support_pad_'+str(i+1)]=cylinder(12,4,x,y,12)
 plate=block(v['width'],v['height'],c['plate_thickness'],z0=c['plate_bottom'])
 for x in [-40,40]:plate=plate.cut(cylinder(c['datum_hole_diameter'],8,x,0,16))
 p['workpiece_before']=plate
 after=plate
 for x,y in v['holes']:after=after.cut(cylinder(c['drill_diameter'],8,x,y,16))
 p['round_locator']=cylinder(5.98,20,-40,0,8)
 diamond=cylinder(5.98,20,40,0,8).intersect(block(c['diamond_pin_width_x'],8,20,40,0,8));p['diamond_locator']=diamond
 for i,(x,y) in enumerate([(-80,-50),(-80,50),(80,-50),(80,50)]):
  p['post_'+str(i+1)]=cylinder(16,24,x,y,12).cut(cylinder(5,24,x,y,12))
 frame=block(180,120,12,z0=36).cut(block(90,60,12,z0=36)).cut(block(100.02,70.02,8,z0=40))
 for x,y in [(-80,-50),(-80,50),(80,-50),(80,50)]:frame=frame.cut(cylinder(6.5,12,x,y,36))
 insert=block(100,70,8,z0=40)
 for x in [-46.5,46.5]:
  for y in [-31.5,31.5]:
   frame=frame.cut(cylinder(2.5,4,x,y,36));insert=insert.cut(cylinder(3.4,8,x,y,40))
 for i,(x,y) in enumerate(v['holes']):
  insert=insert.cut(cylinder(10,8,x,y,40))
  # The flange stops downward movement; a separate cap stops pullout.
  # Two M3 x 0.5 threads in the insert retain each cap independently of the fit.
  bush=cylinder(10,12,x,y,36).union(cylinder(14,2,x,y,48))
  p['drill_bushing_'+str(i+1)]=bush.cut(cylinder(4.2,14,x,y,36))
  cap=block(30,14,3,x,y,50).cut(cylinder(4.5,3,x,y,50))
  for offset in [-10.5,10.5]:
   insert=insert.cut(cylinder(2.5,6,x+offset,y,42))
   cap=cap.union(cylinder(5,2,x+offset,y,48))
   cap=cap.cut(cylinder(3.4,5,x+offset,y,48))
  p['bushing_retainer_'+str(i+1)]=cap
 p['guide_frame']=frame;p['guide_insert_'+variant]=insert
 for side in [-1,1]:
  clamp=block(20,20,8,0,side*50,16).union(block(20,10,4,0,side*39,24))
  clamp=clamp.cut(cylinder(6.5,15,0,side*50,16));p['toe_clamp_'+str(side)]=clamp
  p['clamp_heel_'+str(side)]=block(20,12,4,0,side*54,12).cut(cylinder(6.5,4,0,side*50,12))
 return p,after

def tolerance(c,v):
 t=c['tolerance'];radial=(t['datum_hole_max']-t['pin_min'])/2
 r=max(math.hypot(x+40,y) for x,y in v['holes']);rotation=2*radial/c['datum_pitch'];angular=r*rotation
 terms={'datum_translation':radial,'rotation_at_farthest_hole':angular,**{k:t[k] for k in ['locator_position_radial','insert_seating_radial','bushing_position_radial','drill_bushing_radial_clearance','drill_walk_allowance']}}
 return {'variant':v['name'],'worst_case_radial_mm':sum(terms.values()),'allowed_radial_mm':t['allowed_radial_error'],'radial_margin_mm':t['allowed_radial_error']-sum(terms.values()),'rotation_rad':rotation,'terms':terms}

def checks(c):
 records=[]
 def check(name,value,limit,ok):
  records.append({'name':name,'actual':value,'criterion':limit,'passed':bool(ok)})
 for v in c['variants']:
  parts,after=build(c,v['name']);tol=tolerance(c,v)
  check(v['name']+' tolerance stack',round(tol['worst_case_radial_mm'],6),'<= 0.20 mm radial',tol['radial_margin_mm']>=0)
  expected=3*math.pi*(c['drill_diameter']/2)**2*c['plate_thickness'];removed=parts['workpiece_before'].val().Volume()-after.val().Volume()
  check(v['name']+' drilled volume',round(removed,5),round(expected,5),abs(removed-expected)<1e-5)
  collisions={}
  for key in ['round_locator','diamond_locator','guide_frame','guide_insert_'+v['name'],'toe_clamp_-1','toe_clamp_1']:
   overlap=parts['workpiece_before'].val().intersect(parts[key].val()).Volume();collisions[key]=overlap
  check(v['name']+' workpiece intersections',max(collisions.values()),'< 1e-6 mm3',max(collisions.values())<1e-6)
  clearance=c['guide_bottom']-(c['plate_bottom']+c['plate_thickness'])
  check(v['name']+' drill chip gap',clearance,'>= 5 mm',clearance>=5)
  # A straight drill of diameter 4.2 mm is intentionally coincident with each bushing bore.
  obstruction=0
  for x,y in v['holes']:
   sweep=cylinder(4.19,36,x,y,24)
   for key in ['guide_frame','guide_insert_'+v['name']]+[k for k in parts if 'clamp' in k or 'retainer' in k]:obstruction+=sweep.val().intersect(parts[key].val()).Volume()
  check(v['name']+' drill travel',obstruction,'< 1e-6 mm3 obstruction',obstruction<1e-6)
  check(v['name']+' valid B-rep solids',sum(q.val().isValid() for q in parts.values()),len(parts),all(q.val().isValid() for q in parts.values()))
 free=(c['datum_hole_diameter']-c['diamond_pin_width_x'])/2
 check('Diamond locator pitch relief',round(free,3),'>= 0.10 mm per side',free>=c['tolerance']['datum_pitch_error'])
 return records
