#!/usr/bin/env python3
"""kicad_board_geom.py - parse a .kicad_pcb into geom.json (outline, footprints+pads with
transforms applied, silkscreen, tracks, vias). Stdlib-only s-expression parse; no KiCad needed.
Feeds kicad_board_map.py (SVG board maps) and the Path B README builder.

Usage: python3 kicad_board_geom.py <board.kicad_pcb> [out_geom.json]
"""
import re,json,math,sys
SRC=sys.argv[1]
OUT=sys.argv[2] if len(sys.argv)>2 else 'geom.json'
s=open(SRC,encoding='utf8',errors='replace').read()

def toks(txt,i):
    """return (list, next_index) parsing one s-expr starting at txt[i]=='('"""
    assert txt[i]=='('
    out=[];i+=1;cur=''
    while i<len(txt):
        c=txt[i]
        if c=='(':
            sub,i=toks(txt,i); 
            if cur: out.append(cur); cur=''
            out.append(sub); continue
        if c==')':
            if cur: out.append(cur)
            return out,i+1
        if c=='"':
            j=i+1;buf=''
            while txt[j]!='"' or txt[j-1]=='\\':
                buf+=txt[j];j+=1
            if cur: out.append(cur);cur=''
            out.append(('STR',buf)); i=j+1; continue
        if c.isspace():
            if cur: out.append(cur);cur=''
            i+=1;continue
        cur+=c;i+=1
    return out,i

def find_all(node,name):
    for c in node:
        if isinstance(c,list) and c and c[0]==name: yield c
def get(node,name):
    for c in find_all(node,name): return c
    return None
def sval(x): return x[1] if isinstance(x,tuple) else x
def nums(node,n=2,start=1):
    return [float(sval(v)) for v in node[start:start+n]]

print('parsing…',file=sys.stderr)
root,_=toks(s,s.index('(kicad_pcb'))
print('top-level children:',len(root),file=sys.stderr)

def xf(px,py,ox,oy,rot):
    a=math.radians(rot)
    return (ox+px*math.cos(a)+py*math.sin(a), oy-px*math.sin(a)+py*math.cos(a))

edges=[];silk=[];pads=[];fabtext=[];courts=[]
def layer_of(n):
    l=get(n,'layer')
    return sval(l[1]) if l else ''
def layers_of(n):
    l=get(n,'layers')
    return [sval(v) for v in l[1:]] if l else []

def arc_pts(x1,y1,xm,ym,x2,y2,steps=16):
    # circle through 3 points
    ax,ay,bx,by,cx,cy=x1,y1,xm,ym,x2,y2
    d=2*(ax*(by-cy)+bx*(cy-ay)+cx*(ay-by))
    if abs(d)<1e-9: return [(x1,y1),(x2,y2)]
    ux=((ax*ax+ay*ay)*(by-cy)+(bx*bx+by*by)*(cy-ay)+(cx*cx+cy*cy)*(ay-by))/d
    uy=((ax*ax+ay*ay)*(cx-bx)+(bx*bx+by*by)*(ax-cx)+(cx*cx+cy*cy)*(bx-ax))/d
    r=math.hypot(ax-ux,ay-uy)
    a1=math.atan2(ay-uy,ax-ux);am=math.atan2(by-uy,bx-ux);a2=math.atan2(cy-uy,cx-ux)
    def norm(a,b):
        while b-a>math.pi: b-=2*math.pi
        while b-a<-math.pi: b+=2*math.pi
        return b
    am=norm(a1,am); a2=norm(am,a2)
    return [(ux+r*math.cos(a1+(a2-a1)*t/steps), uy+r*math.sin(a1+(a2-a1)*t/steps)) for t in range(steps+1)]

# ---- board graphics
for n in root:
    if not isinstance(n,list) or not n: continue
    t=n[0]
    if t in ('gr_line','gr_arc','gr_rect','gr_circle','gr_poly'):
        if layer_of(n)!='Edge.Cuts': continue
        if t=='gr_line': edges.append(('L',nums(get(n,'start'))+nums(get(n,'end'))))
        elif t=='gr_arc': edges.append(('A',nums(get(n,'start'))+nums(get(n,'mid'))+nums(get(n,'end'))))
        elif t=='gr_rect': edges.append(('R',nums(get(n,'start'))+nums(get(n,'end'))))
        elif t=='gr_circle': edges.append(('C',nums(get(n,'center'))+nums(get(n,'end'))))
        elif t=='gr_poly':
            pts=get(n,'pts'); edges.append(('P',[nums(p) for p in find_all(pts,'xy')]))

fps=[]
for n in find_all(root,'footprint'):
    at=get(n,'at'); ox,oy=nums(at); rot=float(sval(at[3])) if len(at)>3 else 0.0
    ref=''
    for p in find_all(n,'property'):
        if sval(p[1])=='Reference': ref=sval(p[2])
    lay=layer_of(n)
    fp={'ref':ref,'x':ox,'y':oy,'rot':rot,'layer':lay,'pads':[],'silk':[],'court':[]}
    for pd in find_all(n,'pad'):
        pat=get(pd,'at'); px,py=nums(pat); prot=float(sval(pat[3])) if len(pat)>3 else 0.0
        sz=get(pd,'size'); w,h=nums(sz) if sz else (0.5,0.5)
        shape=sval(pd[3]) if len(pd)>3 else 'rect'
        lys=layers_of(pd)
        gx,gy=xf(px,py,ox,oy,rot)
        fp['pads'].append({'x':gx,'y':gy,'w':w,'h':h,'rot':rot+prot,'shape':shape,'layers':lys,
                           'drill':bool(get(pd,'drill'))})
    for g in n:
        if not isinstance(g,list) or not g: continue
        gt=g[0]
        if gt in ('fp_line','fp_arc','fp_rect','fp_circle','fp_poly'):
            L=layer_of(g)
            tgt = fp['silk'] if L in ('F.SilkS','B.SilkS') else (fp['court'] if L in ('F.CrtYd','B.CrtYd') else None)
            if tgt is None: continue
            if gt=='fp_line':
                a=nums(get(g,'start'));b=nums(get(g,'end'))
                tgt.append(('L',[*xf(*a,ox,oy,rot),*xf(*b,ox,oy,rot)]))
            elif gt=='fp_arc':
                a=nums(get(g,'start'));m=nums(get(g,'mid'));b=nums(get(g,'end'))
                tgt.append(('A',[*xf(*a,ox,oy,rot),*xf(*m,ox,oy,rot),*xf(*b,ox,oy,rot)]))
            elif gt=='fp_rect':
                a=nums(get(g,'start'));b=nums(get(g,'end'))
                c=[(a[0],a[1]),(b[0],a[1]),(b[0],b[1]),(a[0],b[1])]
                tgt.append(('P',[xf(*p,ox,oy,rot) for p in c]))
            elif gt=='fp_circle':
                c=nums(get(g,'center'));e=nums(get(g,'end'))
                tgt.append(('C',[*xf(*c,ox,oy,rot),math.dist(c,e)]))
            elif gt=='fp_poly':
                pts=get(g,'pts'); tgt.append(('P',[xf(*nums(p),ox,oy,rot) for p in find_all(pts,'xy')]))
    fps.append(fp)

# ---- tracks
tracks=[]
for n in find_all(root,'segment'):
    tracks.append((nums(get(n,'start'))+nums(get(n,'end')), float(sval(get(n,'width')[1])), layer_of(n)))
vias=[]
for n in find_all(root,'via'):
    vias.append(nums(get(n,'at'))+[float(sval(get(n,'size')[1]))/2])
print(f'edges={len(edges)} fps={len(fps)} tracks={len(tracks)} vias={len(vias)}',file=sys.stderr)
json.dump({'edges':edges,'fps':fps,'tracks':tracks,'vias':vias},open(OUT,'w'))
print('wrote',OUT)
