#!/usr/bin/env python3
"""kicad_board_map.py - render geom.json (from kicad_board_geom.py) as an SVG board map:
outline, copper, silkscreen, vias, pads coloured by functional section.

Usage: python3 kicad_board_map.py <geom.json> <F|B> <out.svg> [blocks.json]
blocks.json: {"section-id": ["R1","U2",...], ...} - optional; palette assigned in key order.
"""
import json,math,sys,itertools
G=json.load(open(sys.argv[1]))
side=sys.argv[2] if len(sys.argv)>2 else 'F'
out=sys.argv[3] if len(sys.argv)>3 else f'board-{side}.svg'
B=json.load(open(sys.argv[4])) if len(sys.argv)>4 else {}
BLK={r:k for k,v in B.items() for r in v}
PALETTE=['#f5a524','#f97316','#3b82f6','#a855f7','#06b6d4','#22c55e','#ef4444','#64748b',
         '#eab308','#14b8a6','#8b5cf6','#f43f5e']
COL={k:PALETTE[i%len(PALETTE)] for i,k in enumerate(B.keys())}
mode='map' if B else 'bare' 

xs=[];ys=[]
for k,d in G['edges']:
    if k=='L': xs+=[d[0],d[2]];ys+=[d[1],d[3]]
    elif k=='A': xs+=[d[0],d[2],d[4]];ys+=[d[1],d[3],d[5]]
    elif k=='R': xs+=[d[0],d[2]];ys+=[d[1],d[3]]
    elif k=='P': xs+=[p[0] for p in d];ys+=[p[1] for p in d]
X0,X1,Y0,Y1=min(xs),max(xs),min(ys),max(ys)
PAD=3; W=X1-X0+2*PAD; H=Y1-Y0+2*PAD
S=8   # px per mm
mir = (side=='B')
def T(x,y):
    X = (X1-x) if mir else (x-X0)
    return ((X+PAD)*S, (y-Y0+PAD)*S)

o=[]
o.append(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W*S:.1f} {H*S:.1f}" width="{W*S:.0f}" height="{H*S:.0f}" font-family="ui-sans-serif,system-ui,sans-serif">')
o.append('<defs><linearGradient id="fr4" x1="0" y1="0" x2="0" y2="1">'
         '<stop offset="0" stop-color="#0f3d2e"/><stop offset="1" stop-color="#0a2d22"/></linearGradient>'
         '<filter id="sh" x="-20%" y="-20%" width="140%" height="140%">'
         '<feDropShadow dx="0" dy="3" stdDeviation="4" flood-color="#000" flood-opacity=".45"/></filter></defs>')

# --- board outline path
def edge_path():
    segs=[]
    for k,d in G['edges']:
        if k=='L': segs.append([(d[0],d[1]),(d[2],d[3])])
        elif k=='A':
            ax,ay,mx,my,bx,by=d
            dd=2*(ax*(my-by)+mx*(by-ay)+bx*(ay-my))
            if abs(dd)<1e-9: segs.append([(ax,ay),(bx,by)]); continue
            ux=((ax*ax+ay*ay)*(my-by)+(mx*mx+my*my)*(by-ay)+(bx*bx+by*by)*(ay-my))/dd
            uy=((ax*ax+ay*ay)*(bx-mx)+(mx*mx+my*my)*(ax-bx)+(bx*bx+by*by)*(mx-ax))/dd
            r=math.hypot(ax-ux,ay-uy)
            a1=math.atan2(ay-uy,ax-ux);am=math.atan2(my-uy,mx-ux);a2=math.atan2(by-uy,bx-ux)
            while am-a1>math.pi: am-=2*math.pi
            while am-a1<-math.pi: am+=2*math.pi
            while a2-am>math.pi: a2-=2*math.pi
            while a2-am<-math.pi: a2+=2*math.pi
            N=14; segs.append([(ux+r*math.cos(a1+(a2-a1)*t/N), uy+r*math.sin(a1+(a2-a1)*t/N)) for t in range(N+1)])
        elif k=='R':
            x0,y0,x1,y1=d; segs.append([(x0,y0),(x1,y0),(x1,y1),(x0,y1),(x0,y0)])
        elif k=='P': segs.append([tuple(p) for p in d]+[tuple(d[0])])
    # chain segments into a loop
    used=[False]*len(segs); chain=list(segs[0]); used[0]=True
    for _ in range(len(segs)):
        best=None;bd=1e9
        for i,sg in enumerate(segs):
            if used[i]: continue
            for endp,pts in ((sg[0],sg),(sg[-1],list(reversed(sg)))):
                dd=math.dist(chain[-1],endp)
                if dd<bd: bd=dd;best=(i,pts)
        if best is None or bd>0.6: break
        used[best[0]]=True; chain+=best[1][1:]
    return 'M'+' L'.join(f'{T(*p)[0]:.2f},{T(*p)[1]:.2f}' for p in chain)+' Z'
EP=edge_path()
o.append(f'<path d="{EP}" fill="url(#fr4)" stroke="#134e37" stroke-width="1.2" filter="url(#sh)"/>')
o.append(f'<clipPath id="brd"><path d="{EP}"/></clipPath><g clip-path="url(#brd)">')

# --- tracks
tl = 'F.Cu' if side=='F' else 'B.Cu'
for (d,w,L) in G['tracks']:
    if L!=tl: continue
    a=T(d[0],d[1]);b=T(d[2],d[3])
    o.append(f'<line x1="{a[0]:.2f}" y1="{a[1]:.2f}" x2="{b[0]:.2f}" y2="{b[1]:.2f}" stroke="#b78227" stroke-opacity=".55" stroke-width="{w*S:.2f}" stroke-linecap="round"/>')
for v in G['vias']:
    p=T(v[0],v[1]); o.append(f'<circle cx="{p[0]:.2f}" cy="{p[1]:.2f}" r="{v[2]*S:.2f}" fill="#8a6420" fill-opacity=".6"/>')
o.append('</g>')

# --- block wash
if mode=='map':
    import collections
    bb=collections.defaultdict(lambda:[1e9,1e9,-1e9,-1e9])
    for f in G['fps']:
        k=BLK.get(f['ref'])
        if not k: continue
        for pd in f['pads']:
            r=max(pd['w'],pd['h'])/2
            b=bb[k]
            b[0]=min(b[0],pd['x']-r);b[1]=min(b[1],pd['y']-r);b[2]=max(b[2],pd['x']+r);b[3]=max(b[3],pd['y']+r)

# --- pads
for f in G['fps']:
    if f['layer']!=('F.Cu' if side=='F' else 'B.Cu') and not any(pd['drill'] for pd in f['pads']): continue
    k=BLK.get(f['ref']); c=COL.get(k,'#c9a227')
    for pd in f['pads']:
        if not (tl in pd['layers'] or '*.Cu' in pd['layers']): continue
        x,y=T(pd['x'],pd['y']); w,h=pd['w']*S,pd['h']*S
        rot=-pd['rot'] if not mir else pd['rot']
        rx = min(w,h)*0.22 if pd['shape'] in('roundrect','rect') else min(w,h)/2
        if pd['shape']=='circle': rx=min(w,h)/2
        o.append(f'<rect x="{-w/2:.2f}" y="{-h/2:.2f}" width="{w:.2f}" height="{h:.2f}" rx="{rx:.2f}" '
                 f'fill="{c}" fill-opacity="{0.95 if mode=="map" else 0.85}" transform="translate({x:.2f},{y:.2f}) rotate({rot:.1f})"/>')

# --- silkscreen
sl='F.SilkS' if side=='F' else 'B.SilkS'
o.append('<g stroke="#e9eef2" stroke-opacity=".7" fill="none" stroke-width="0.9" stroke-linecap="round">')
for f in G['fps']:
    for k,d in f['silk']:
        if k=='L':
            a=T(d[0],d[1]);b=T(d[2],d[3]); o.append(f'<path d="M{a[0]:.2f},{a[1]:.2f}L{b[0]:.2f},{b[1]:.2f}"/>')
        elif k=='P':
            pts=' '.join(f'{T(*p)[0]:.2f},{T(*p)[1]:.2f}' for p in d); o.append(f'<polygon points="{pts}" fill="#e9eef2" fill-opacity=".5" stroke="none"/>')
        elif k=='C':
            p=T(d[0],d[1]); o.append(f'<circle cx="{p[0]:.2f}" cy="{p[1]:.2f}" r="{d[2]*S:.2f}"/>')
        elif k=='A':
            a=T(d[0],d[1]);m=T(d[2],d[3]);b=T(d[4],d[5])
            o.append(f'<path d="M{a[0]:.2f},{a[1]:.2f}Q{m[0]:.2f},{m[1]:.2f} {b[0]:.2f},{b[1]:.2f}"/>')
o.append('</g>')

# --- refdes labels for the map
if mode=='map':
    o.append('<g font-size="7" text-anchor="middle" fill="#eaf2ee" font-weight="600">')
    BIG={'U2','U1','U3','U4','U5','U6','J1','J2','J3','J6','J16','J23','SW1','SW2','SW3','Y1','F1','F2','D15','D16','C9'}
    for f in G['fps']:
        if f['ref'] in BIG or f['ref'].startswith('J') and f['ref'][1:].isdigit() and 7<=int(f['ref'][1:])<=13:
            x,y=T(f['x'],f['y']); o.append(f'<text x="{x:.1f}" y="{y-6:.1f}" stroke="#07231a" stroke-width="2" paint-order="stroke">{f["ref"]}</text>')
    o.append('</g>')
o.append('</svg>')
open(out,'w').write('\n'.join(o))
print('wrote',out, f'{W*S:.0f}x{H*S:.0f}px  ({X1-X0:.1f}x{Y1-Y0:.1f} mm)')