"""Route remaining SYS_3V3/PD_3V3 branches while honoring native track/via rule areas. Native DRC and physical qualification remain separate."""
from pathlib import Path
import argparse,pcbnew,json,hashlib,math,heapq,collections,shutil
import wx
wx.Log.EnableLogging(False);app=wx.App(False)
p=argparse.ArgumentParser();p.add_argument('--board',required=True);p.add_argument('--output',required=True);p.add_argument('--report',required=True);p.add_argument('--check-rules-only',action='store_true');a=p.parse_args();b=pcbnew.LoadBoard(a.board)
sha=lambda p:hashlib.sha256(Path(p).read_bytes()).hexdigest();before=sha(a.board);mm=pcbnew.ToMM;xy=lambda p:(mm(p.x),mm(p.y));vec=lambda q:pcbnew.VECTOR2I(*(round(x*pcbnew.FromMM(1)) for x in q));layers=[pcbnew.F_Cu,pcbnew.B_Cu];pitch=.1;width=.25;via_diameter=.6;via_drill=.3;switch=set('SYS_SW1 SYS_SW2 LED_SW AUX_SW SW1 SW2 HIDRV1 HIDRV2 LODRV1 LODRV2 G_H1 G_H2 G_L1 G_L2'.split());pads=[];holes=[];shapes=[];idx=collections.defaultdict(list);targets={};new=[];rows=[];held=list(b.GetTracks());rule_areas=[]
def keys(bb):
 for x in range(math.floor(bb[0]),math.floor(bb[2])+1):
  for y in range(math.floor(bb[1]),math.floor(bb[3])+1):yield x,y
def addshape(net,s,bb,ls,kind,ref=''):
 r=(net,s,bb,set(ls),kind,ref);shapes.append(r)
 for k in keys([bb[0]-2.0,bb[1]-2.0,bb[2]+2.0,bb[3]+2.0]):idx[k].append(r)
for f in b.GetFootprints():
 for pad in f.Pads():
  z=pad.GetBoundingBox();bb=[mm(z.GetX()),mm(z.GetY()),mm(z.GetRight()),mm(z.GetBottom())];net=pad.GetNetname();pads.append((net,pad,bb));active=[l for l in layers if pad.IsOnLayer(l)]
  targets[(f.GetReference(),pad.GetNumber())]=pad
  if active:addshape(net,pad.GetEffectivePolygon(active[0]),bb,active,'pad',f.GetReference())
  if pad.GetDrillSize().x:holes.append((xy(pad.GetPosition()),max(xy(pad.GetDrillSize()))/2,net))
for t in held:
 via=isinstance(t,pcbnew.PCB_VIA);q=xy(t.GetStart());z=xy(t.GetEnd());r=mm(t.GetWidth(t.GetLayer()) if via else t.GetWidth())/2;bb=[min(q[0],z[0])-r,min(q[1],z[1])-r,max(q[0],z[0])+r,max(q[1],z[1])+r];net=t.GetNetname();addshape(net,t.GetEffectiveShape(),bb,layers if via else [t.GetLayer()],'via' if via else 'track')
 if via:holes.append((q,mm(t.GetDrillValue())/2,net))
# Preserve fixed supply/switch copper. GND pours are regenerated after routing; In1.Cu stays reserved. All old GND pad contacts must pass the independent graph audit.
for zone in b.Zones():
 if zone.GetIsRuleArea():
  rule_areas.append((zone.GetZoneName(),zone.Outline(),set(zone.GetLayerSet().Seq()),zone.GetDoNotAllowTracks(),zone.GetDoNotAllowVias()))
  continue
 if zone.GetLayer() not in layers or zone.GetNetname()=='GND':continue
 poly=zone.GetFilledPolysList(zone.GetLayer())
 for i in range(poly.OutlineCount()):
  shape=pcbnew.SHAPE_POLY_SET();shape.AddOutline(poly.COutline(i))
  for j in range(poly.HoleCount(i)):shape.AddHole(poly.CHole(i,j),0)
  outline=poly.COutline(i);points=[xy(outline.CPoint(k)) for k in range(outline.PointCount())]
  bb=[min(p[0] for p in points),min(p[1] for p in points),max(p[0] for p in points),max(p[1] for p in points)]
  addshape(zone.GetNetname(),shape,bb,[zone.GetLayer()],'zone')

def candidates(q,z):
 found={}
 for k in keys([min(q[0],z[0]),min(q[1],z[1]),max(q[0],z[0]),max(q[1],z[1])]):
  for r in idx[k]:found[id(r)]=r
 return found.values()
def near(bb,q,z,r):return min(q[0],z[0])-r<=bb[2] and max(q[0],z[0])+r>=bb[0] and min(q[1],z[1])-r<=bb[3] and max(q[1],z[1])+r>=bb[1]
def routing_width(q,z,l,net):
 return width
cache={};vcache={};front_only_supply_region=False;forced_layer=None;padrefs={p.m_Uuid.AsString():f.GetReference() for f in b.GetFootprints() for p in f.Pads()}
def lineok(q,z,l,net):
 key=(q,z,l,net)
 if key in cache:return cache[key]
 rw=routing_width(q,z,l,net);edge=.31+rw/2;ok=all(edge<=v[0]<=140-edge and edge<=v[1]<=100-edge for v in [q,z]);seg=pcbnew.SEG(vec(q),vec(z))
 if ok:ok=not any(no_tracks and l in ls and poly.Collide(seg,pcbnew.FromMM(rw/2+.001)) for _,poly,ls,no_tracks,no_vias in rule_areas)
 if ok:
  for n,s,bb,ls,kind,ref in candidates(q,z):
   if n==net or l not in ls:continue
   local=all(62.0-1e-8<=v[0]<=68.3+1e-8 and 55.6-1e-8<=v[1]<=61.3+1e-8 for v in [q,z]);# IC bootstrap/mode pins share the primary package pad pitch with the switch pin.
   # Keep 0.75 outside that package region; use the actual 0.18 native rule inside.
   package_local=any((n in ns or net in ns) and all(box[0]<=v[0]<=box[2] and box[1]<=v[1]<=box[3] for v in [q,z]) for ns,box in [(set(['LED_SW']),[9.0,24.5,17.5,29.1]),(set(['AUX_SW']),[24.0,26.5,32.5,30.5]),(set(['SW1','SW2','HIDRV1','HIDRV2','LODRV1','LODRV2']),[43.0,14.0,50.0,22.0])])
   clear=.75 if (n in switch or net in switch) and ref not in {'U305','C317','C318'} and not local and not package_local else .21
   if near(bb,q,z,clear+rw/2) and s.Collide(seg,pcbnew.FromMM(clear+rw/2)):ok=False;break
 if ok:
  for h,r,n in holes:
   if n!=net and near([h[0]-r,h[1]-r,h[0]+r,h[1]+r],q,z,.25+rw/2) and pcbnew.SHAPE_CIRCLE(vec(h),pcbnew.FromMM(r)).Collide(seg,pcbnew.FromMM(.25+rw/2)):ok=False;break
 cache[key]=ok;return ok
def viaok(q,net):
 key=(q,net)
 if key in vcache:return vcache[key]
 edge=.31+via_diameter/2;ok=edge<q[0]<140-edge and edge<q[1]<100-edge;v=vec(q);local=62.0<=q[0]<=68.3 and 55.6<=q[1]<=61.3
 if ok:ok=not any(no_vias and ls and poly.Collide(v,pcbnew.FromMM(via_diameter/2+.001)) for _,poly,ls,no_tracks,no_vias in rule_areas)
 if ok:
  for n,shape,bb,ls,kind,ref in candidates(q,q):
   if kind!='pad':continue
   r=via_drill/2+.251 if n==net else max(via_drill/2+.251,.75+via_diameter/2 if n in switch and ref not in {'U305','C317','C318'} else .21+via_diameter/2)
   if near(bb,q,q,r) and shape.Collide(v,pcbnew.FromMM(r)):ok=False;break
 if ok:
  for n,s,bb,ls,kind,ref in candidates(q,q):
   r=.75+via_diameter/2 if n in switch and not local else .21+via_diameter/2
   if kind!='pad' and n!=net and near(bb,q,q,r) and s.Collide(v,pcbnew.FromMM(r)):ok=False;break
 if ok:ok=not any(math.dist(q,h)<r+via_drill/2+.25 for h,r,n in holes)
 vcache[key]=ok;return ok
if a.check_rules_only:
 checks=[]
 named=[x for x in rule_areas if x[0]=='PWR_SP01_BACK_STOP_KEEPOUT']
 assert len(named)==1
 _,poly,ls,nt,nv=named[0]
 probes=[('back_center_track',pcbnew.B_Cu,(4.0,63.8),(5.0,63.8),True),('front_center_track',pcbnew.F_Cu,(4.0,63.8),(5.0,63.8),False),('back_edge_overlap_track',pcbnew.B_Cu,(7.3,63.6),(7.3,64.0),True),('back_clear_track',pcbnew.B_Cu,(7.6,63.6),(7.6,64.0),False)]
 for name,layer,q,z,expected in probes:
  blocked=nt and layer in ls and poly.Collide(pcbnew.SEG(vec(q),vec(z)),pcbnew.FromMM(.251))
  assert bool(blocked)==expected,name
  checks.append({'name':name,'blocked':bool(blocked),'expected':expected})
 for name,q,expected in [('center_through_via',(4.5,63.8),True),('edge_overlap_through_via',(7.4,63.8),True),('clear_through_via',(7.6,63.8),False)]:
  blocked=nv and bool(ls) and poly.Collide(vec(q),pcbnew.FromMM(.301))
  assert bool(blocked)==expected,name
  checks.append({'name':name,'blocked':bool(blocked),'expected':expected})
 Path(a.report).write_text(json.dumps({'input_sha256':before,'source_sha256':sha(__file__),'checks':checks,'native_rule_areas_indexed':len(rule_areas),'board_modified':False},indent=2)+'\n')
 print('NATIVE_RULE_INDEX_PROBES_PASS',len(checks),flush=True)
 raise SystemExit(0)

node_xy=lambda n:(n[0]*pitch,n[1]*pitch)
def seeds(p,net):
 q=xy(p.GetPosition());x,y=round(q[0]/pitch),round(q[1]/pitch);l=layers.index(pcbnew.F_Cu if p.IsOnLayer(pcbnew.F_Cu) else pcbnew.B_Cu);out={};poly=p.GetEffectivePolygon(layers[l]);size=xy(p.GetSize());radius=math.ceil((max(size)/2+width/2+.05)/pitch)
 for dx in range(-radius,radius+1):
  for dy in range(-radius,radius+1):
   n=(x+dx,y+dy,l);z=node_xy(n)
   if poly.Collide(vec(z),pcbnew.FromMM(routing_width(z,z,layers[l],net)/2-.02)) and lineok(z,z,layers[l],net):out[n]=0
 return out
def build_goal_tree(points,depth=0):
 if not points:return None
 axis=depth%2;ordered=sorted(points,key=lambda p:p[axis]);mid=len(ordered)//2
 return (ordered[mid],axis,build_goal_tree(ordered[:mid],depth+1),build_goal_tree(ordered[mid+1:],depth+1))
def nearest_goal_squared(tree,q,best=float('inf')):
 if tree is None:return best
 p,axis,left,right=tree;dd=(q[0]-p[0])**2+(q[1]-p[1])**2;best=min(best,dd)
 if best==0:return 0
 delta=q[axis]-p[axis];near,far=(left,right) if delta<0 else (right,left)
 best=nearest_goal_squared(near,q,best)
 if delta*delta<best:best=nearest_goal_squared(far,q,best)
 return best

def search(starts,goals,net,limits):
 if not starts or not goals:return None,0
 gs=list({node_xy(n) for n in goals});gl={n[2] for n in goals};goal_tree=build_goal_tree(gs)
 def h(n):
  return math.sqrt(nearest_goal_squared(goal_tree,node_xy(n)))+(0 if n[2] in gl else 2)
 heap=[];d=dict(starts);parent={n:None for n in starts};serial=0
 for n,v in starts.items():serial+=1;heapq.heappush(heap,(v+1.6*h(n),serial,v,n))
 visited=0
 while heap:
  _,_,cost,n=heapq.heappop(heap)
  if cost>d[n]+1e-9:continue
  visited+=1
  if n in goals:
   path=[n]
   while parent[path[-1]] is not None:path.append(parent[path[-1]])
   return list(reversed(path)),visited
  if visited>80000:break
  q=node_xy(n)
  for dx,dy in [(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)]:
   v=(n[0]+dx,n[1]+dy,n[2]);z=node_xy(v)
   if front_only_supply_region and 30<=z[0]<=50 and 10<=z[1]<=27 and v[2]!=0:continue
   cost2=cost+math.hypot(dx,dy)*pitch*(1.03 if n[2]!=2 else 1)
   if not(limits[0]<=z[0]<=limits[2] and limits[1]<=z[1]<=limits[3]) or cost2>=d.get(v,float('inf')) or not lineok(q,z,layers[n[2]],net):continue
   d[v]=cost2;parent[v]=n;serial+=1;heapq.heappush(heap,(cost2+1.6*h(v),serial,cost2,v))
  if forced_layer is None and viaok(q,net):
   for l in range(len(layers)):
    v=(n[0],n[1],l)
    if front_only_supply_region and 30<=q[0]<=50 and 10<=q[1]<=27 and l!=0:continue
    cost2=cost+3
    if l!=n[2] and cost2<d.get(v,float('inf')):d[v]=cost2;parent[v]=n;serial+=1;heapq.heappush(heap,(cost2+1.6*h(v),serial,cost2,v))
 return None,visited
added_vias=set()
def track(q,z,l,net):
 # Native DRC requires endpoints on existing same-net vias to meet their centers.
 ends=[q,z]
 for i,point in enumerate(ends):
  for n,shape,bb,ls,kind,ref in candidates(point,point):
   if n==net and kind=='via' and l in ls:
    center=((bb[0]+bb[2])/2,(bb[1]+bb[3])/2)
    if math.dist(point,center)<1e-7 or shape.Collide(vec(point),0):
     ends[i]=center;break
 q,z=ends
 if math.dist(q,z)<1e-6:return
 if not lineok(q,z,l,net):
  # Retain the exact integer-grid edges accepted by A* if joining them into
  # one long segment changes the collision predicate (no relaxed clearance).
  count=round(max(abs(z[0]-q[0]),abs(z[1]-q[1]))/pitch)
  assert count>1,'Untested edge: '+str((q,z,l,net,width))
  edges=[(q[0]+(z[0]-q[0])*i/count,q[1]+(z[1]-q[1])*i/count) for i in range(count+1)]
  assert all(lineok(aa,bb,l,net) for aa,bb in zip(edges,edges[1:])), 'Compressed path cannot be restored faithfully'
  for aa,bb in zip(edges,edges[1:]):track(aa,bb,l,net)
  return

 t=pcbnew.PCB_TRACK(b);t.SetStart(vec(q));t.SetEnd(vec(z));rw=routing_width(q,z,l,net);t.SetWidth(pcbnew.FromMM(rw));t.SetLayer(l);t.SetNet(b.FindNet(net));b.Add(t);new.append(t);objects[t.m_Uuid.AsString()]=t;addshape(net,t.GetEffectiveShape(),[min(q[0],z[0])-rw/2,min(q[1],z[1])-rw/2,max(q[0],z[0])+rw/2,max(q[1],z[1])+rw/2],[l],'track');cache.clear();rows.append({'kind':'track','net':net,'layer':b.GetLayerName(l),'start_mm':list(q),'end_mm':list(z),'width_mm':rw})
def emit(path,p,net):
 start=path[0];prev=start;direction=None
 for n in path[1:]:
  if n[2]!=prev[2]:
   track(node_xy(start),node_xy(prev),layers[prev[2]],net);q=node_xy(n)
   if (q,net) not in added_vias:
    v=pcbnew.PCB_VIA(b);v.SetPosition(vec(q));v.SetWidth(pcbnew.FromMM(via_diameter));v.SetDrill(pcbnew.FromMM(via_drill));v.SetViaType(pcbnew.VIATYPE_THROUGH);v.SetLayerPair(pcbnew.F_Cu,pcbnew.B_Cu);v.SetNet(b.FindNet(net));b.Add(v);new.append(v);objects[v.m_Uuid.AsString()]=v;added_vias.add((q,net));rows.append({'kind':'via','net':net,'center_mm':list(q),'diameter_mm':via_diameter,'drill_mm':via_drill});holes.append((q,via_drill/2,net));addshape(net,v.GetEffectiveShape(),[q[0]-via_diameter/2,q[1]-via_diameter/2,q[0]+via_diameter/2,q[1]+via_diameter/2],layers,'via');vcache.clear();cache.clear()
   start=n;prev=n;direction=None;continue
  nd=(n[0]-prev[0],n[1]-prev[1])
  if direction is not None and nd!=direction:track(node_xy(start),node_xy(prev),layers[prev[2]],net);start=prev
  direction=nd;prev=n
 track(node_xy(start),node_xy(prev),layers[prev[2]],net)

def addvia(q,net):
 v=pcbnew.PCB_VIA(b);v.SetPosition(vec(q));v.SetWidth(pcbnew.FromMM(via_diameter));v.SetDrill(pcbnew.FromMM(via_drill));v.SetViaType(pcbnew.VIATYPE_THROUGH);v.SetLayerPair(pcbnew.F_Cu,pcbnew.B_Cu);v.SetNet(b.FindNet(net));b.Add(v);new.append(v);objects[v.m_Uuid.AsString()]=v;added_vias.add((q,net));rows.append({'kind':'via','net':net,'center_mm':list(q),'diameter_mm':via_diameter,'drill_mm':via_drill});holes.append((q,via_drill/2,net));addshape(net,v.GetEffectiveShape(),[q[0]-via_diameter/2,q[1]-via_diameter/2,q[0]+via_diameter/2,q[1]+via_diameter/2],layers,'via');cache.clear();vcache.clear()


pitch=.1
objects={p.m_Uuid.AsString():p for _,p,_ in pads};objects.update({t.m_Uuid.AsString():t for t in held})
# Explicit-pair drivers derive connectivity from actual native copper, not a previous DRC.
allowed=set('SYS_3V3 PD_3V3'.split())
def points(item,net):
 if isinstance(item,pcbnew.PAD):
  # Through-hole pads conduct on all active layers; SMD seeds stay on their face.
  out=seeds(item,net)
  if item.GetDrillSize().x:
   base=list(out)
   for n in base:
    for li,l in enumerate(layers):
     if item.IsOnLayer(l) and lineok(node_xy(n),node_xy(n),l,net):out[(n[0],n[1],li)]=0
  return out
 via=isinstance(item,pcbnew.PCB_VIA);q=xy(item.GetStart());z=xy(item.GetEnd());active=layers if via else [item.GetLayer()];out={};span=math.dist(q,z);num=max(1,math.ceil(span/pitch))
 for k in range(num+1):
  t=k/num;v=[q[i]+t*(z[i]-q[i]) for i in range(2)];gx,gy=[round(x/pitch) for x in v]
  for dx in [-1,0,1]:
   for dy in [-1,0,1]:
    node=(gx+dx,gy+dy,0);v=node_xy(node)
    if not item.GetEffectiveShape().Collide(vec(v),0):continue
    for l in active:
     if lineok(v,v,l,net):out[(node[0],node[1],layers.index(l))]=0
 return out


checks=[]
for net,start,end in [('/PANEL_SCL',('U5','13'),('J5','3')),('/PANEL_SDA',('U5','14'),('J5','4')),('/CTRL_3V3',('J4','1'),('J5','1')),('/GND',('J4','2'),('J5','2'))]:
 cache.clear();vcache.clear();path,visits=search(points(targets[start],net),points(targets[end],net),net,[76,43,138,90])
 print(net,'found',path is not None,'visits',visits,flush=True);assert path,net
 emit(path,None,net);checks.append({'net':net,'visits':visits})
for z in b.Zones():
 if not z.GetIsRuleArea():z.UnFill()
b.BuildConnectivity();pcbnew.ZONE_FILLER(b).Fill(b.Zones());pcbnew.SaveBoard(a.output,b)
Path(a.report).write_text(json.dumps({'routing':checks,'added_copper':rows,'native_validation_pending':True},indent=2))
