#!/usr/bin/env python3
# audit_clearance.py - Gate 1: same-layer different-net clearance audit.
# Spatial hash (2mm bins); segment-segment and via-everything checks.
import pcbnew, math, sys
from collections import defaultdict
ToMM=pcbnew.ToMM
def segdist(ax,ay,bx,by,cx,cy,dx,dy):
    # min distance between segments AB and CD
    def d2(px,py,qx,qy): return (px-qx)**2+(py-qy)**2
    def ptseg(px,py,ax,ay,bx,by):
        vx,vy=bx-ax,by-ay; L=vx*vx+vy*vy
        if L<1e-12: return math.sqrt(d2(px,py,ax,ay))
        t=max(0,min(1,((px-ax)*vx+(py-ay)*vy)/L))
        return math.sqrt(d2(px,py,ax+t*vx,ay+t*vy))
    # check intersection
    def ccw(ax,ay,bx,by,cx,cy): return (cy-ay)*(bx-ax)-(by-ay)*(cx-ax)
    if (ccw(ax,ay,bx,by,cx,cy)*ccw(ax,ay,bx,by,dx,dy)<0 and
        ccw(cx,cy,dx,dy,ax,ay)*ccw(cx,cy,dx,dy,bx,by)<0): return 0.0
    return min(ptseg(ax,ay,cx,cy,dx,dy),ptseg(bx,by,cx,cy,dx,dy),
               ptseg(cx,cy,ax,ay,bx,by),ptseg(dx,dy,ax,ay,bx,by))
def audit(path, clearance=0.20, via_clear=0.20):
    b=pcbnew.LoadBoard(path)
    tracks=[]; vv=[]
    for t in b.GetTracks():
        if t.Type()==pcbnew.PCB_TRACE_T:
            tracks.append((t.GetNetname(),t.GetLayer(),ToMM(t.GetStart().x),ToMM(t.GetStart().y),
                           ToMM(t.GetEnd().x),ToMM(t.GetEnd().y),ToMM(t.GetWidth())))
        else:
            vv.append((t.GetNetname(),ToMM(t.GetPosition().x),ToMM(t.GetPosition().y),ToMM(t.GetWidth())))
    # pads as obstacles too (SMD F.Cu, THT all)
    pads=[]
    for f in b.GetFootprints():
        for p in f.Pads():
            pos=p.GetPosition(); sz=p.GetSize()
            r=max(ToMM(sz.x),ToMM(sz.y))/2
            layers='ALL' if p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH,pcbnew.PAD_ATTRIB_NPTH) else 'F'
            pads.append((p.GetNetname(),layers,ToMM(pos.x),ToMM(pos.y),r))
    H=defaultdict(list)
    def key(x,y): return (int(x/2),int(y/2))
    for i,(nm,l,x1,y1,x2,y2,w) in enumerate(tracks):
        for kx in range(int(min(x1,x2)/2)-1,int(max(x1,x2)/2)+2):
            for ky in range(int(min(y1,y2)/2)-1,int(max(y1,y2)/2)+2):
                H[(kx,ky)].append(i)
    viol=[]
    seen=set()
    for cell,ids in H.items():
        for a in range(len(ids)):
            for c in range(a+1,len(ids)):
                i,j=ids[a],ids[c]
                if (i,j) in seen: continue
                seen.add((i,j))
                t1=tracks[i]; t2=tracks[j]
                if t1[0]==t2[0] or t1[1]!=t2[1]: continue
                d=segdist(t1[2],t1[3],t1[4],t1[5],t2[2],t2[3],t2[4],t2[5])
                gap=d-(t1[6]+t2[6])/2
                if gap<clearance: viol.append(('TT',t1[0],t2[0],t1[2],t1[3],gap))
    # via vs tracks (all layers) and via-via
    VH=defaultdict(list)
    for i,(nm,x,y,w) in enumerate(vv): VH[key(x,y)].append(i)
    for i,(nm,l,x1,y1,x2,y2,w) in enumerate(tracks):
        for kx in range(int(min(x1,x2)/2)-1,int(max(x1,x2)/2)+2):
            for ky in range(int(min(y1,y2)/2)-1,int(max(y1,y2)/2)+2):
                for j in VH.get((kx,ky),[]):
                    vn,vx,vy,vw=vv[j]
                    if vn==nm: continue
                    # via spans F..B: conflicts with any copper layer track
                    def ptseg(px,py,ax,ay,bx,by):
                        ux,uy=bx-ax,by-ay; L=ux*ux+uy*uy
                        if L<1e-12: return math.hypot(px-ax,py-ay)
                        t=max(0,min(1,((px-ax)*ux+(py-ay)*uy)/L))
                        return math.hypot(px-(ax+t*ux),py-(ay+t*uy))
                    d=ptseg(vx,vy,x1,y1,x2,y2)-vw/2-w/2
                    if d<via_clear: viol.append(('VT',vn,nm,vx,vy,d))
    for cell,ids in VH.items():
        for a in range(len(ids)):
            for c in range(a+1,len(ids)):
                i,j=ids[a],ids[c]
                v1,v2=vv[i],vv[j]
                if v1[0]==v2[0]: continue
                d=math.hypot(v1[1]-v2[1],v1[2]-v2[2])-(v1[3]+v2[3])/2
                if d<via_clear: viol.append(('VV',v1[0],v2[0],v1[1],v1[2],d))
    # dedupe
    out=[]
    sk=set()
    for v in viol:
        k=(v[0],v[1],v[2],round(v[3],1),round(v[4],1))
        if k in sk: continue
        sk.add(k); out.append(v)
    return out
if __name__=='__main__':
    v=audit(sys.argv[1] if len(sys.argv)>1 else '/tmp/phyllo2.kicad_pcb')
    print("clearance violations:",len(v))
    from collections import Counter
    print(Counter(x[0] for x in v))
    for x in v[:15]: print("  %s %s|%s at (%.1f,%.1f) gap=%.3f"%x)
    sys.exit(0 if not v else 1)
