from pathlib import Path
import pcbnew as k,wx,json,xml.etree.ElementTree as E,shutil,hashlib
app=wx.App(False);quiet=wx.LogNull()
P=Path(__file__).resolve().parent; NAME='drill_motor_a85_cutoff'
P.mkdir(exist_ok=True)

source=(P/'baseline-A82.kicad_pcb').read_text()
def balanced_end(text,a):
 depth=0;quoted=False;escaped=False
 for i in range(a,len(text)):
  ch=text[i]
  if quoted:
   if escaped:escaped=False
   elif ch=='\\':escaped=True
   elif ch=='"':quoted=False
  elif ch=='"':quoted=True
  elif ch=='(':depth+=1
  elif ch==')':
   depth-=1
   if depth==0:return i+1
 raise ValueError('unbalanced')
import re
removed=[]
for tag in ['segment','via']:
 for match in reversed(list(re.finditer(r'\('+tag+r'\s',source))):
  st=match.start();en=balanced_end(source,st);block=source[st:en]
  if '(net "/DRV_RUN_REQ")' not in block:continue
  removed.append({'type':tag,'sha256':hashlib.sha256(block.encode()).hexdigest(),'source':block})
  source=source[:st]+source[en:]
(P/'stripped-raw-run.kicad_pcb').write_text(source)
b=k.LoadBoard(str(P/'stripped-raw-run.kicad_pcb'));oldcount=len(b.GetTracks())+len(removed);oldfp=len(b.GetFootprints())
root=E.parse(P/'netlist-A85.xml').getroot();rootuuid='8bddf17b-6b2d-4f6e-849c-d7c3b90fe3ea'
existing_names={p.GetNetname() for f in b.GetFootprints() for p in f.Pads()} | {t.GetNetname() for t in b.GetTracks()}
netobj={name:b.FindNet(name) for name in existing_names if name}
code=max(n.GetNetCode() for n in netobj.values())
pinmeta={}
for n in root.findall('nets/net'):
 name=n.get('name')
 if name not in netobj:
  code+=1;net=k.NETINFO_ITEM(b,name,code);b.Add(net);netobj[name]=net
 for x in n.findall('node'):pinmeta[x.get('ref'),x.get('pin')]=(name,x.get('pinfunction',''),x.get('pintype',''))
# Baseline component placements and all non-RUN copper remain unchanged.
for f in b.GetFootprints():
 for pad in f.Pads():
  key=(f.GetReference(),pad.GetNumber())
  if key in pinmeta:
   net,func,typ=pinmeta[key];pad.SetNet(netobj[net]);pad.SetPinFunction(func);pad.SetPinType(typ)
pos={'U2':(24,65),'U3':(39,65),'U4':(59,65),'U5':(72,65),'U6':(83,65),'U7':(94,65),'U8':(110,65),'U9':(121,65),'U10':(133,65),'J5':(50,70),
'R16':(14,63),'R17':(14,67),'R18':(14,71),'R19':(59,60),'R20':(133,60),'R21':(52,66),'R22':(59,70),'C19':(63,70),'C20':(24,71),'C21':(39,71),
'C22':(24,60),'C23':(39,60),'C24':(63,60),'C25':(72,60),'C26':(83,60),'C27':(94,60),'C28':(110,60),'C29':(121,60),'C30':(137,60)}
added=[]
for c in root.findall('components/comp'):
 ref=c.get('ref')
 if ref not in pos:continue
 fpname=c.findtext('footprint');lib,item=fpname.split(':');f=k.FootprintLoad(str(P/'MotorStage.pretty') if lib=='MotorStage' else '/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints/'+lib+'.pretty',item);assert f
 f.SetReference(ref);f.SetValue(c.findtext('value'));f.SetFPID(k.LIB_ID(lib,item));f.SetPath(k.KIID_PATH('/'+rootuuid+'/'+c.findtext('tstamps')))
 f.SetPosition(k.VECTOR2I(k.FromMM(pos[ref][0]),k.FromMM(pos[ref][1])));f.SetOrientationDegrees(0)
 f.Reference().SetTextSize(k.VECTOR2I(k.FromMM(.8),k.FromMM(.8)));f.Reference().SetTextThickness(k.FromMM(.12));f.Reference().SetPosition(k.VECTOR2I(k.FromMM(pos[ref][0]),k.FromMM(pos[ref][1]+(4 if ref.startswith('U') else -1.7))))
 f.Value().SetVisible(False)
 for pad in f.Pads():
  name,func,typ=pinmeta[ref,pad.GetNumber()];pad.SetNet(netobj[name]);pad.SetPinFunction(func);pad.SetPinType(typ)
 b.Add(f);added.append(ref)
# Resolve silk collisions without reducing electrical clearance rules.
for f in b.GetFootprints():
 if f.GetReference() in ['C20','C21','R22','C19']:
  f.Reference().SetPosition(k.VECTOR2I(f.GetPosition().x,f.GetPosition().y+k.FromMM(2)))
 if f.GetReference()=='J5':f.Reference().SetPosition(k.VECTOR2I(k.FromMM(50),k.FromMM(75)))
for d in b.GetDrawings():
 if isinstance(d,k.PCB_TEXT) and 'DRILL_MOTOR EVT CARRIER' in d.GetText():
  d.SetPosition(k.VECTOR2I(k.FromMM(90),k.FromMM(77)))
# Lock baseline copper against autorouter modification. UUID/geometry are recorded.
for t in b.GetTracks():t.SetLocked(True)
b.BuildConnectivity();fill=k.ZONE_FILLER(b);fill.Fill(b.Zones())
k.SaveBoard(str(P/(NAME+'.kicad_pcb')),b)
assert k.ExportSpecctraDSN(b,str(P/'routing-input-A86.dsn'))
(P/'placement-A86.json').write_text(json.dumps({'baseline_tracks_vias':oldcount,'removed_RAW_RUN_tracks_vias':removed,'retained_baseline_tracks_vias':len(b.GetTracks()),'baseline_footprints':oldfp,'added_footprints':added,'positions_mm':pos,'outline_mm':[180,80],'outline_changed':False,'scope':'existing oversized engineering fixture, product housing fit NOT validated','PCB_routing_complete':False,'physical_validation':False,'manufacturing':'HOLD'},indent=2)+'\n')
print('Added',len(added),'removed raw RUN copper',len(removed),'retained locked copper',len(b.GetTracks()),'outline unchanged 180x80mm')
