"""Annotated route figures + refined metrics (opposite-turn jogs, detour ratio per signal net)."""
import sys, math, json, collections
import pcbnew
import matplotlib; matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Polygon

NONSIG = ('GND', 'BAT', 'PHA', 'PHB', 'PHC', 'PHASE', '3V3', 'GVDD', '10V')
def signal(n): return not any(k in n.upper() for k in NONSIG)

def load(path):
    b = pcbnew.LoadBoard(path); mm = pcbnew.ToMM
    segs, vias = [], []
    for t in b.GetTracks():
        if t.GetClass() == 'PCB_VIA':
            vias.append(dict(net=t.GetNetname(), p=(mm(t.GetPosition().x), mm(t.GetPosition().y)), d=mm(t.GetWidth(pcbnew.F_Cu))))
        elif t.GetClass() == 'PCB_TRACK':
            segs.append(dict(net=t.GetNetname(), layer=t.GetLayerName(), w=mm(t.GetWidth()),
                             a=(mm(t.GetStart().x), mm(t.GetStart().y)), b=(mm(t.GetEnd().x), mm(t.GetEnd().y))))
    pads = collections.defaultdict(list)
    for fp in b.GetFootprints():
        for p in fp.Pads():
            if p.GetNetname(): pads[p.GetNetname()].append((mm(p.GetPosition().x), mm(p.GetPosition().y)))
    bb = b.GetBoardEdgesBoundingBox()
    box = (mm(bb.GetX()), mm(bb.GetY()), mm(bb.GetWidth()), mm(bb.GetHeight()))
    zones = []
    for z in b.Zones():
        if z.GetIsRuleArea(): continue
        for li in (pcbnew.F_Cu, pcbnew.B_Cu):
            if not z.IsOnLayer(li): continue
            fp = z.GetFilledPolysList(li)
            for k in range(fp.OutlineCount()):
                o = fp.Outline(k)
                zones.append((li, [(mm(o.CPoint(i).x), mm(o.CPoint(i).y)) for i in range(o.PointCount())]))
    return segs, vias, pads, box, zones

def mst(pts):
    if len(pts) < 2: return 0
    inside = {0}; tot = 0
    d = {i: math.dist(pts[0], pts[i]) for i in range(1, len(pts))}
    while d:
        j = min(d, key=d.get); tot += d.pop(j); inside.add(j)
        for i in d: d[i] = min(d[i], math.dist(pts[j], pts[i]))
    return tot

def metrics(segs, vias, pads):
    ends = collections.defaultdict(list)
    for i, g in enumerate(segs):
        g['len'] = math.dist(g['a'], g['b'])
        if g['len'] < 1e-4: continue
        dx, dy = g['b'][0]-g['a'][0], g['b'][1]-g['a'][1]
        ang = math.degrees(math.atan2(dy, dx)) % 180
        g['dev'] = min(ang % 45, 45 - ang % 45)
        for k in ('a', 'b'):
            ends[(g['net'], g['layer'], round(g[k][0], 3), round(g[k][1], 3))].append(i)
    def other(g, p):
        return g['b'] if math.dist(g['a'], p) < 1e-3 else g['a']
    def turn_at(i, p):
        lst = ends.get((segs[i]['net'], segs[i]['layer'], round(p[0], 3), round(p[1], 3)), [])
        if len(lst) != 2: return None
        j = lst[0] if lst[1] == i else lst[1]
        q0, q1 = other(segs[i], p), other(segs[j], p)
        u = (p[0]-q0[0], p[1]-q0[1]); v = (q1[0]-p[0], q1[1]-p[1])
        cr = u[0]*v[1]-u[1]*v[0]; dt = u[0]*v[0]+u[1]*v[1]
        return math.degrees(math.atan2(cr, dt))
    right, jogs = [], []
    for key, lst in ends.items():
        if len(lst) == 2:
            i = lst[0]; t = turn_at(i, (key[2], key[3]))
            if t is not None and abs(abs(t)-90) < 2: right.append((key[2], key[3]))
    for i, g in enumerate(segs):
        if g['len'] < 1e-4 or g['len'] > 1.0: continue
        ta, tb = turn_at(i, g['a']), turn_at(i, g['b'])
        if ta is None or tb is None or abs(ta) < 1 or abs(tb) < 1: continue
        # walking a->b: turn at a is entering, at b leaving; opposite sign = a sideways step (jog)
        ta2 = -ta  # turn_at measured with this segment as the incoming one; flip for a
        if (ta2 > 0) != (tb > 0): jogs.append(((g['a'][0]+g['b'][0])/2, (g['a'][1]+g['b'][1])/2))
    L = collections.Counter(); 
    for g in segs: L[g['net']] += g['len']
    det = {}
    for n, l in L.items():
        if signal(n) and len(pads.get(n, [])) >= 2:
            m = mst(pads[n]); det[n] = (round(l, 1), round(m, 1), round(l/max(m, 0.1), 2))
    sv = collections.Counter(v['net'] for v in vias if signal(v['net']))
    sig_len = sum(l for n, l in L.items() if signal(n))
    return dict(off=[g for g in segs if g.get('dev', 0) > 1.0], right=right, jogs=jogs, detour=det,
                signal_vias=sum(sv.values()), signal_nets_with_vias=len(sv), signal_len=round(sig_len, 1),
                signal_nets=len([n for n in L if signal(n)]))

def draw(name, path, out):
    segs, vias, pads, box, zones = load(path)
    m = metrics(segs, vias, pads)
    fig, ax = plt.subplots(figsize=(9, 9), dpi=130)
    fig.patch.set_facecolor('#0f1115'); ax.set_facecolor('#0f1115')
    for li, poly in zones:
        ax.add_patch(Polygon(poly, closed=True, fc=('#5a3a20' if li == pcbnew.F_Cu else '#1f3550'), ec='none', alpha=0.35, zorder=0))
    for g in segs:
        col = '#e0a050' if g['layer'] == 'F.Cu' else '#5aa0ff'
        ax.plot([g['a'][0], g['b'][0]], [g['a'][1], g['b'][1]], color=col, lw=max(0.6, g['w']*4), solid_capstyle='round', alpha=0.85, zorder=2)
    for g in m['off']:
        ax.plot([g['a'][0], g['b'][0]], [g['a'][1], g['b'][1]], color='#ff2d55', lw=max(1.4, g['w']*5), solid_capstyle='round', zorder=4)
    for v in vias:
        sigv = signal(v['net'])
        ax.add_patch(Circle(v['p'], max(v['d']/2, 0.25), fc=('#ffffff' if sigv else '#777777'), ec='none', zorder=5))
    for (x, y) in m['right']:
        ax.add_patch(Circle((x, y), 0.45, fc='none', ec='#ffd60a', lw=1.6, zorder=6))
    for (x, y) in m['jogs']:
        ax.plot(x, y, marker='x', color='#00e5ff', ms=5, mew=1.4, zorder=6)
    x0, y0, w, h = box
    ax.set_xlim(x0-0.5, x0+w+0.5); ax.set_ylim(y0+h+0.5, y0-0.5); ax.set_aspect('equal'); ax.axis('off')
    ax.plot([x0, x0+w, x0+w, x0, x0], [y0, y0, y0+h, y0+h, y0], color='#888', lw=0.8)
    ax.set_title(name, color='white', fontsize=14, loc='left')
    fig.text(0.02, 0.015, 'orange F.Cu  blue B.Cu  RED off-45° segment  YELLOW ring 90° corner  CYAN x short jog  WHITE dot signal via  grey dot power/GND via',
             color='#cccccc', fontsize=7.5)
    fig.savefig(out, facecolor=fig.get_facecolor(), bbox_inches='tight'); plt.close(fig)
    return m

if __name__ == '__main__':
    res = {}
    for name, path, out in [('AI Flow (full AI) — board-19', 'boards/aiflow.kicad_pcb', 'fig_aiflow.png'),
                            ('Hands-on (Noah) — am32-esc-v1', 'boards/noah.kicad_pcb', 'fig_noah.png')]:
        m = draw(name, path, out)
        top = sorted(m['detour'].items(), key=lambda kv: -kv[1][2])[:8]
        res[name] = dict(off_angle=len(m['off']), right=len(m['right']), jogs=len(m['jogs']), signal_vias=m['signal_vias'],
                         signal_nets_with_vias=m['signal_nets_with_vias'], signal_len=m['signal_len'], signal_nets=m['signal_nets'],
                         median_detour=sorted(d[2] for d in m['detour'].values())[len(m['detour'])//2], worst_detour=top)
    print(json.dumps(res, indent=1))
