from pathlib import Path
import csv,json,re,hashlib,collections
import wx
app=wx.App(False);quiet=wx.LogNull()
import pcbnew
p=Path(__file__).resolve().parent;b=pcbnew.LoadBoard(str(p/'drill_motor_a85_cutoff.kicad_pcb'))
expected_sha='603f212f336f297203eecc4c71457d8e33366f52bb97036da824215e28e9c22b'
assert hashlib.sha256((p/'drill_motor_a85_cutoff.kicad_pcb').read_bytes()).hexdigest()==expected_sha
bom=list(csv.DictReader((p/'motor-A92-candidate-BOM.csv').open(encoding='utf-8-sig')))
fitted={r['Ref'] for r in bom if r['DNP'].lower()=='false'}
a=list(csv.DictReader((p/'placement/all-fitted.csv').open()));s=list(csv.DictReader((p/'placement/smd-fitted.csv').open()))
assert {r['Ref'] for r in a}==fitted and len(a)==67
assert len(s)==61 and {r['Ref'] for r in a}-{r['Ref'] for r in s}=={'C9','J1','J2','J3','J4','J5'}
fps={f.GetReference():f for f in b.GetFootprints()}
for r in a:
 f=fps[r['Ref']];pos=f.GetPosition()
 assert abs(float(r['PosX'])-pcbnew.ToMM(pos.x))<0.000002
 assert abs(float(r['PosY'])+pcbnew.ToMM(pos.y))<0.000002
 assert abs(float(r['Rot'])-f.GetOrientationDegrees())<0.000002
 assert r['Side']=='top'
def read_drill(name):
 tools={};hits=[];tool=None
 for line in (p/'drill'/name).read_text().splitlines():
  m=re.fullmatch(r'T(\d+)C([\d.]+)',line)
  if m: tools[int(m[1])]=float(m[2]);continue
  m=re.fullmatch(r'T(\d+)',line)
  if m:tool=int(m[1]);continue
  m=re.fullmatch(r'X(-?[\d.]+)Y(-?[\d.]+)',line)
  if m:hits.append((round(float(m[1]),3),round(float(m[2]),3),tools[tool]))
 return collections.Counter(hits)
pth=read_drill('drill_motor_a85_cutoff-PTH.drl');npth=read_drill('drill_motor_a85_cutoff-NPTH.drl')
exp_pth=[];exp_npth=[];headers=[]
for ref,f in fps.items():
 for pad in f.Pads():
  ds=pad.GetDrillSize()
  if not ds.x:continue
  assert ds.x==ds.y
  pos=pad.GetPosition();entry=(pcbnew.ToMM(pos.x),-pcbnew.ToMM(pos.y),round(pcbnew.ToMM(ds.x),3))
  (exp_npth if pad.GetAttribute()==pcbnew.PAD_ATTRIB_NPTH else exp_pth).append(entry)
  if ref in {'J1','J2','J3','J4','J5'}:headers.append(entry)
for track in b.GetTracks():
 if isinstance(track,pcbnew.PCB_VIA):
  pos=track.GetPosition();exp_pth.append((pcbnew.ToMM(pos.x),-pcbnew.ToMM(pos.y),round(pcbnew.ToMM(track.GetDrillValue()),3)))
def match_holes(observed,expected):
 remaining=list(observed.elements());max_error=0
 assert len(remaining)==len(expected)
 for x,y,d in expected:
  matches=[(i,h) for i,h in enumerate(remaining) if abs(h[2]-d)<0.000001 and abs(h[0]-x)<=0.000501 and abs(h[1]-y)<=0.000501]
  assert len(matches)==1, (x,y,d,matches)
  i,h=matches[0];max_error=max(max_error,abs(h[0]-x),abs(h[1]-y));remaining.pop(i)
 assert not remaining
 return max_error
max_error=max(match_holes(pth,exp_pth),match_holes(npth,exp_npth))
assert len(headers)==29 and all(x[2]==1.2 for x in headers) and sum(n for (x,y,d),n in pth.items() if d==1.2)==29
g=list((p/'gerbers').glob('*'));assert len(g)==12
for f in g:
 if f.suffix=='.gbrjob':continue
 t=f.read_text();assert '%MOMM*%' in t and '%TF.SameCoordinates,Original*%' in t and 'M02*' in t
result={'status':'LOCAL_EXPORT_READBACK_PASS_FACTORY_REVIEW_ONLY','source_board_sha256':expected_sha,'gerber_layers':11,'gerber_job_files':1,'pth_holes':sum(pth.values()),'npth_holes':sum(npth.values()),'every_drill_coordinate_matches_native_board_within_0_000501mm':True,'max_coordinate_rounding_error_mm':max_error,'header_1_2mm_holes':29,'fitted_placement_rows':len(a),'SMT_rows':len(s),'TH_refs':['C9','J1','J2','J3','J4','J5'],'placement_native_position_rotation_side':'PASS','R3_DNP_excluded':True,'manufacturing_release':False,'factory_drill_compensation_confirmed':False,'effect_validation':'NOT_VERIFIED'}
(p/'CAM-readback-A95.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result,indent=2))
