#!/usr/bin/env python3
"""Pad-level netlist + layout stats from a .kicad_pcb (KiCad 7-10, stdlib only).

Usage: kicad_netlist.py <board.kicad_pcb>

Prints: components (ref/value/footprint/layer/DNP-attrs), per-net pin lists
with pin functions, pads with no net, per-net routed track length + via count.
Handles both pad-net forms: (net N "name") pre-v10, (net "name") v10+.
"""
import math, re, sys
from collections import defaultdict

def parse(text):
    # quote-aware tokenizer: pin names may contain parentheses
    toks = re.findall(r'"(?:[^"\\]|\\.)*"|\(|\)|[^\s()"]+', text)
    stack = [[]]
    for t in toks:
        if t == '(':
            new = []
            stack[-1].append(new)
            stack.append(new)
        elif t == ')':
            stack.pop()
        else:
            if t.startswith('"'):
                t = t[1:-1].replace('\\"', '"').replace('\\\\', '\\')
            stack[-1].append(t)
    return stack[0]

def first(node, tag):
    for c in node:
        if isinstance(c, list) and c and c[0] == tag:
            return c
    return None

def atom(node, tag, idx=1):
    n = first(node, tag)
    return n[idx] if n and len(n) > idx and not isinstance(n[idx], list) else None

def pad_net(pad):
    n = first(pad, 'net')
    if not n:
        return None
    return n[2] if len(n) >= 3 else n[1]   # (net N "name") | (net "name")

def refkey(ref):
    m = re.match(r'([A-Za-z_#]+)(\d*)', ref)
    return (m.group(1), int(m.group(2) or 0))

def main(path):
    root = parse(open(path, encoding='utf-8').read())[0]
    comps, net_pins, no_net = [], defaultdict(list), []
    for fp in (c for c in root if isinstance(c, list) and c and c[0] == 'footprint'):
        props = {p[1]: p[2] for p in fp
                 if isinstance(p, list) and p and p[0] == 'property'
                 and len(p) >= 3 and not isinstance(p[2], list)}
        ref = props.get('Reference', '?')
        attrs = first(fp, 'attr')
        flags = [a for a in (attrs[1:] if attrs else []) if not isinstance(a, list)]
        comps.append((ref, props.get('Value', '?'), fp[1], atom(fp, 'layer') or '?', ','.join(flags)))
        for pad in (c for c in fp if isinstance(c, list) and c and c[0] == 'pad'):
            net, fn = pad_net(pad), atom(pad, 'pinfunction')
            tag = f"{ref}.{pad[1]}" + (f"({fn})" if fn else "")
            if net:
                net_pins[net].append(tag)
            elif pad[2] in ('smd', 'thru_hole'):
                no_net.append(tag)

    netlen, vias = defaultdict(float), defaultdict(int)
    for c in root:
        if isinstance(c, list) and c and c[0] == 'segment':
            s, e = first(c, 'start'), first(c, 'end')
            netlen[atom(c, 'net')] += math.hypot(
                float(e[1]) - float(s[1]), float(e[2]) - float(s[2]))
        elif isinstance(c, list) and c and c[0] == 'via':
            vias[atom(c, 'net')] += 1

    print(f"== COMPONENTS ({len(comps)}) ==")
    for ref, val, fpname, layer, flags in sorted(comps, key=lambda x: refkey(x[0])):
        print(f"{ref:10s} {val[:38]:38s} {fpname[:52]:52s} {layer} {flags}")
    print(f"\n== NETS ({len(net_pins)}): pins | routed mm | vias ==")
    for net in sorted(net_pins):
        print(f"{net}  [{len(net_pins[net])} pins, {netlen.get(net, 0):.1f}mm, {vias.get(net, 0)} vias]")
        print("    " + " ".join(sorted(net_pins[net])))
    print(f"\n== PADS WITH NO NET ({len(no_net)}) ==")
    for t in no_net:
        print("  " + t)

if __name__ == '__main__':
    if len(sys.argv) != 2:
        sys.exit(__doc__)
    main(sys.argv[1])
