"""CNC plywood base/bed and printed motion parts for the cord-driven plotter.

Run this file to export the shared parts, CNC STEP/DXF, printed adapters and assembly.
DXF files describe geometry in mm, not machine-ready router toolpaths.
"""
from pathlib import Path
import csv, itertools, json, math
from collections import Counter
import cadquery as cq
import ezdxf
import trimesh
import build_plotter as m

OUT=Path(__file__).resolve().parent.parent/'cnc'


def nut_pockets(obj, points):
    for x,y in points:
        pocket=cq.Workplane('XY').polygon(6,6.8).extrude(3.1).translate((x,y,21.9))
        obj=obj.cut(pocket)
    return obj


end=m.holes(m.box(36,70,15,y=15,z=17.5),[(-10,25),(10,25)],4.4)
end=m.holes(end,[(0,-15)],3.4)
m.register('cnc_end_riser',nut_pockets(end,[(0,-15)]))
points=[(0,y) for y in [-26,-16,16,26]]
centre=m.holes(m.box(32,64,15,z=17.5),[(-10,0),(10,0)],4.4)
m.register('cnc_centre_riser',nut_pockets(m.holes(centre,points),points))
m.register('cnc_bed_spacer',m.cyl(12,16,z=78).cut(m.cyl(3.4,18,z=77)),'bed')
m.register('cnc_corner_foot',m.holes(m.box(28,28,8,z=-6),[(0,0)],4.4))
base_holes=[(x,y) for x in [-150,150] for y in [-185,185,23,83]]+[(x,y) for x in [-10,10] for y in [-185,0,185]]
base=m.box(326,400,12,z=4).edges('|Z').fillet(6)
base=m.holes(base,base_holes,4.4)
base=base.cut(m.box(52,52,16,x=-46,y=-135,z=4))
bed_points=list(itertools.product([-12,12],[-16,16]))
bed=m.holes(m.box(240,190,6,z=97).edges('|Z').fillet(4),bed_points)
for x,y in bed_points:bed=bed.cut(m.cyl(6.4,2.31,x,y,97.7))
WOOD={'cnc_base':base,'cnc_bed':bed}
ADAPTERS={'cnc_end_riser':2,'cnc_centre_riser':1,'cnc_bed_spacer':4,'cnc_corner_foot':4}


def construct(x=0,y=0):
    m.construct(x,y)
    remove={'base_side_front','base_side_middle','base_side_rear','base_cross_half','base_join_strap',
            'bed_left_front','bed_left_rear','bed_right_front','bed_right_rear','bed_join_strap'}
    m.INSTANCES[:]=[i for i in m.INSTANCES if i['part'] not in remove]
    m.add('cnc_base',kind='cnc',shape=base,color=(.67,.48,.29))
    m.add('cnc_bed',(0,y,0),kind='cnc',moving='y',shape=bed,color=(.83,.69,.48))
    for sign in [-1,1]:m.add('cnc_end_riser',(0,sign*160,0),0 if sign>0 else 180)
    m.add('cnc_centre_riser')
    for xx,yy in bed_points:m.add('cnc_bed_spacer',(xx,y+yy,0),moving='y')
    for xx,yy in itertools.product([-150,150],[-185,185]):m.add('cnc_corner_foot',(xx,yy,0))
    return m.INSTANCES


def dxf(name, shape, depth):
    doc=ezdxf.new('R2010');doc.units=ezdxf.units.MM;space=doc.modelspace()
    for layer,color in [('OUTLINE',7),('DRILL_THROUGH',3),('POCKET_2_3_MM',1),('NOTES',8)]:doc.layers.new(layer,dxfattribs={'color':color})
    w,h,r=(326,400,6) if name=='cnc_base' else (240,190,4)
    # Rounded rectangle, represented by tangent lines and exact quarter arcs.
    for start,end in [((-w/2+r,-h/2),(w/2-r,-h/2)),((w/2,-h/2+r),(w/2,h/2-r)),((w/2-r,h/2),(-w/2+r,h/2)),((-w/2,h/2-r),(-w/2,-h/2+r))]:
        space.add_line(start,end,dxfattribs={'layer':'OUTLINE'})
    for center,a,b in [((w/2-r,-h/2+r),270,360),((w/2-r,h/2-r),0,90),((-w/2+r,h/2-r),90,180),((-w/2+r,-h/2+r),180,270)]:
        space.add_arc(center,r,a,b,dxfattribs={'layer':'OUTLINE'})
    if name=='cnc_base':
        space.add_lwpolyline([(-72,-161),(-20,-161),(-20,-109),(-72,-109)],close=True,dxfattribs={'layer':'OUTLINE'})
        points=base_holes;diameter=4.4
    else:points=bed_points;diameter=3.4
    for x,y in points:
        space.add_circle((x,y),diameter/2,dxfattribs={'layer':'DRILL_THROUGH'})
        if name=='cnc_bed':space.add_circle((x,y),3.2,dxfattribs={'layer':'POCKET_2_3_MM'})
    space.add_text(f'{name} | mm | stock {depth} mm | top view | CAM required',dxfattribs={'height':4,'layer':'NOTES','insert':(-w/2,h/2+12)})
    doc.saveas(OUT/(name+'.dxf'))


def export():
    for folder in ['stl','step']:(OUT/folder).mkdir(parents=True,exist_ok=True)
    parts=[]
    for name,quantity in ADAPTERS.items():
        obj=m.PARTS[name]['shape'];bb=obj.val().BoundingBox();obj=obj.translate((-bb.xmin,-bb.ymin,-bb.zmin));bb=obj.val().BoundingBox()
        assert obj.val().isValid() and len(obj.solids().vals())==1
        cq.exporters.export(obj,str(OUT/'stl'/(name+'.stl')),tolerance=.08)
        cq.exporters.export(obj,str(OUT/'step'/(name+'.step')))
        mesh=trimesh.load_mesh(OUT/'stl'/(name+'.stl'));assert mesh.is_watertight
        sizes=[round(bb.xlen,2),round(bb.ylen,2),round(bb.zlen,2)];assert max(sizes[:2])<=190 and sizes[2]<=200
        parts.append({'part':name,'quantity':quantity,'size_mm':sizes,'valid':True,'watertight':True})
    for name,shape in WOOD.items():
        assert shape.val().isValid() and len(shape.solids().vals())==1
        cq.exporters.export(shape,str(OUT/'step'/(name+'.step')));dxf(name,shape,12 if name=='cnc_base' else 6)
    construct();assy=cq.Assembly(name='Rwanda_cord_plotter_CNC');view=[]
    for i in m.INSTANCES:
        assy.add(i['shape'],name=i['name'],color=cq.Color(*i['color']))
        vertices,triangles=i['shape'].val().tessellate(.3,.5) if i['kind']=='cord' else i['shape'].val().tessellate(.18,.18)
        view.append({**{key:i[key] for key in ['name','part','label','kind','moving','color','cord_axis','cord_end','cord_segment'] if key in i},'vertices':[[round(v.x,3),round(v.y,3),round(v.z,3)] for v in vertices],'triangles':triangles})
    assy.export(str(OUT/'step/plotter_cnc_assembly.step'));assy.export(str(OUT/'plotter_cnc_assembly.glb'),tolerance=.2,angularTolerance=.5)
    (OUT/'assembly-mesh.json').write_text(json.dumps({'units':'mm','parts':view},separators=(',',':')))
    quantities=Counter(i['part'] for i in m.INSTANCES if i['kind']=='printed')
    with (OUT/'print-list.csv').open('w',newline='') as f:
        w=csv.writer(f,lineterminator='\n');w.writerow(['part','quantity','STL'])
        for name,count in sorted(quantities.items()):
            w.writerow([name,count,('stl/' if name in ADAPTERS else '../stl/')+name+'.stl'])
    checks=[]
    for x,y in itertools.product([-100,0,100],[-75,0,75]):
        construct(x,y);collisions=[]
        for a,b in itertools.combinations(m.INSTANCES,2):
            if a['kind'] not in ['printed','cnc'] or b['kind'] not in ['printed','cnc']:continue
            if (x or y) and a['moving']==b['moving']:continue
            ba=a['shape'].val().BoundingBox();bb=b['shape'].val().BoundingBox()
            if min(min(ba.xmax,bb.xmax)-max(ba.xmin,bb.xmin),min(ba.ymax,bb.ymax)-max(ba.ymin,bb.ymin),min(ba.zmax,bb.zmax)-max(ba.zmin,bb.zmin))<=.01:continue
            volume=a['shape'].intersect(b['shape']).val().Volume()
            if volume>.1:collisions.append({'a':a['name'],'b':b['name'],'volume_mm3':round(volume,3)})
        checks.append({'x':x,'bed_y':y,'collisions':collisions});print('CNC motion',x,y,'collisions',len(collisions),flush=True)
    report={'units':'mm','status':'CAD prototype; not fabricated','stock_mm':{'base':12,'bed':6},'printed_adapters':parts,'motion_checks':checks,'scope':'Checks printed and plywood solids at nine X/Y poses. Hardware, cutting feeds, cord behaviour and physical accuracy are not validated.'}
    (OUT/'cad-validation.json').write_text(json.dumps(report,indent=2)+'\n')
    with (OUT/'adapter-print-list.csv').open('w',newline='') as f:
        w=csv.writer(f,lineterminator='\n');w.writerow(['part','quantity','x_mm','y_mm','z_mm','STL'])
        for part in parts:w.writerow([part['part'],part['quantity'],*part['size_mm'],'stl/'+part['part']+'.stl'])
    assert not any(c['collisions'] for c in checks), 'Resolve CNC collisions before fabrication.'
    print('Exported plywood machine, DXFs and four adapter types.',flush=True)


if __name__=='__main__':
    import argparse
    parser=argparse.ArgumentParser(description='Build the plywood-base cord-driven pen plotter.')
    parser.add_argument('--out',type=Path,default=OUT,help='CNC output folder; shared parts are written in its parent.')
    args=parser.parse_args();OUT=args.out.resolve()
    m.export_shared_parts(OUT.parent)
    export()
