#!/usr/bin/env python3
"""build_gps.py — generate the CM5-carrier GPS + PPS distribution sheet.

Same connectivity model as build_power.py: every pin gets a coincident named
label / GND symbol / no_connect; nets form by name (grid-snapped 1.27mm).

Frozen spec (CM5_Carrier_Pinout_BOM.md §6, CM5_Carrier_Design.md §5a):
  J_GPS(5-pin): 1=PPS  2=NMEA_RX(GPS TX -> CM5 GPIO5/UART2 RX)
                3=NMEA_TX(CM5 GPIO4/UART2 TX -> GPS RX)  4=+3V3_AUX  5=GND
  GPS_PPS_RAW -> TVS -> 74LVC1G17 Schmitt (3V3) -> PPS_CLEAN
    -> Rs 33R -> SYNC_OUT (CM5 pin 18, Ethernet_SYNC_OUT, 3.3V bidi, cfg input)
    -> J_PPS1 (UWB tap) / J_PPS2 (scope tap), 2-pin each
  Board is always a PPS SINK (GPS-sourced).

Cross-sheet nets are GLOBAL: +3V3_AUX (Power sheet flag drives it), and
GPIO4/GPIO5/SYNC_OUT (their CM5_GPIO local labels were converted to global —
net names unchanged; nothing else in the design used those names).
J_GPS pins 1/2 are re-typed 'output' (the GPS module drives PPS and TX into
the board) so the analyzer's no_driver gate sees real drivers (E-key lesson).
"""
import re, sys, os, uuid as uuidlib

ROOT_UUID = "e63e39d7-6ac0-4ffd-8aa3-1841a4541b55"
PROJECT = "CM5IO"
KSS = "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"
HERE = os.path.dirname(os.path.abspath(__file__))
CARRIER = os.path.join(HERE, "..", "carrier")

def U(label): return str(uuidlib.uuid5(uuidlib.NAMESPACE_DNS, "cm5carrier-gps-" + label))
SHEET_SYMBOL_UUID = U("sheet-symbol")
SHEET_FILE_UUID = U("sheet-file")

def g(v): return round(round(v / 1.27) * 1.27, 2)

def paren_end(t, s):
    d = 0
    for i in range(s, len(t)):
        if t[i] == '(': d += 1
        elif t[i] == ')':
            d -= 1
            if d == 0: return i + 1
    raise ValueError("unbalanced")

def get_symbol(text, name):
    m = re.search(r'\(symbol\s+"' + re.escape(name) + r'"', text)
    return text[m.start():paren_end(text, m.start())] if m else None

def lib_pins(seg):
    out = {}
    for pm in re.finditer(r'\(pin\b', seg):
        s = pm.start(); ps = seg[s:paren_end(seg, s)]
        at = re.search(r'\(at\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\)', ps)
        nu = re.search(r'\(number\s+"([^"]+)"', ps)
        if at and nu:
            out.setdefault(nu.group(1), (float(at.group(1)), float(at.group(2)), float(at.group(3))))
    return out

def set_pin_type(seg, number, newtype):
    for pm in re.finditer(r'\(pin\s+(\w+)\s+(\w+)', seg):
        s = pm.start(); blk = seg[s:paren_end(seg, s)]
        if re.search(r'\(number\s+"' + re.escape(number) + r'"', blk):
            head = re.match(r'\(pin\s+\w+', blk).group(0)
            return seg[:s] + blk.replace(head, f'(pin {newtype}', 1) + seg[s + len(blk):]
    return seg

# ---- lib_symbol sources -----------------------------------------------------------
carrier_hs = open(os.path.join(CARRIER, "CM5_HighSpeed.kicad_sch")).read()
LIBS = {}
def add_from(lib_id, srctext, srcname=None, retype=None):
    srcname = srcname or lib_id.split(':')[-1]
    blk = get_symbol(srctext, lib_id) or get_symbol(srctext, srcname)
    if not blk: sys.exit("missing lib_symbol " + lib_id)
    if ('"' + lib_id + '"') not in blk[:60]:
        blk = blk.replace('(symbol "' + srcname + '"', '(symbol "' + lib_id + '"', 1)
    for num, ty in (retype or {}).items():
        blk = set_pin_type(blk, num, ty)
    LIBS[lib_id] = (blk, lib_pins(blk))

for lid in ["Device:R", "Device:C", "power:GND"]:
    add_from(lid, carrier_hs)
add_from("Device:D_TVS", open(f"{KSS}/Device.kicad_sym").read(), "D_TVS")
# GPS module DRIVES pins 1 (PPS) and 2 (its TX) -> 'output' for driver-aware checks
add_from("Connector_Generic:Conn_01x05", open(f"{KSS}/Connector_Generic.kicad_sym").read(),
         "Conn_01x05", retype={'1': 'output', '2': 'output'})
add_from("Connector_Generic:Conn_01x02", open(f"{KSS}/Connector_Generic.kicad_sym").read(), "Conn_01x02")
add_from("74xGxx:74LVC1G17", open(f"{KSS}/74xGxx.kicad_sym").read(), "74LVC1G17")

GLOBAL_NETS = {"+3V3_AUX", "GPIO4", "GPIO5", "SYNC_OUT"}
HIER_NETS = set()

# ---- placement engine (same as build_power.py, + ('nc',) pins) --------------------
body = []
def emit_label(net, x, y, ang=0):
    body.append(f'\t(label "{net}"\n\t\t(at {x} {y} {ang})\n'
                f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("l-"+net+f"-{x}-{y}")}")\n\t)\n')
def emit_glabel(net, x, y, ang=0):
    body.append(f'\t(global_label "{net}"\n\t\t(shape input)\n\t\t(at {x} {y} {ang})\n'
                f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("gl-"+net+f"-{x}-{y}")}")\n\t)\n')
def emit_nc(x, y, tag):
    body.append(f'\t(no_connect (at {x} {y}) (uuid "{U("nc-"+tag)}"))\n')
def emit_gnd(x, y, tag):
    body.append(f'\t(symbol\n\t\t(lib_id "power:GND")\n\t\t(at {x} {y} 0)\n\t\t(unit 1)\n'
                f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("gnd-"+tag)}")\n'
                f'\t\t(property "Reference" "#PWR{tag}" (at {x} {y+3} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
                f'\t\t(property "Value" "GND" (at {x} {y+2} 0) (effects (font (size 1.27 1.27))))\n'
                f'\t\t(property "Footprint" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
                f'\t\t(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
                f'\t\t(pin "1" (uuid "{U("gp-"+tag)}"))\n'
                f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "#PWR{tag}") (unit 1))))\n\t)\n')

def place(lib_id, ref, value, footprint, ox, oy, pin_nets):
    ox, oy = g(ox), g(oy)
    _, pins = LIBS[lib_id]
    extra = ('\t\t(property "Description" "" (at %g %g 0) (effects (font (size 1.27 1.27)) (hide yes)))\n' % (ox, oy))
    sym = (f'\t(symbol\n\t\t(lib_id "{lib_id}")\n\t\t(at {ox} {oy} 0)\n\t\t(unit 1)\n'
           f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("sym-"+ref)}")\n'
           f'\t\t(property "Reference" "{ref}" (at {ox+2.54} {oy-1} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
           f'\t\t(property "Value" "{value}" (at {ox+2.54} {oy+1} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
           f'\t\t(property "Footprint" "{footprint}" (at {ox} {oy} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
           f'\t\t(property "Datasheet" "" (at {ox} {oy} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n' + extra)
    for num in sorted(pins, key=lambda n: int(re.sub(r'\D', '', n) or 0)):
        sym += f'\t\t(pin "{num}" (uuid "{U("p-"+ref+"-"+num)}"))\n'
    sym += f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "{ref}") (unit 1))))\n\t)\n'
    body.append(sym)
    for num, net in pin_nets.items():
        lx, ly, _ = pins[num]; px, py = round(ox + lx, 2), round(oy - ly, 2)
        if net == ('gnd',): emit_gnd(px, py, ref + num)
        elif net == ('nc',): emit_nc(px, py, ref + num)
        elif isinstance(net, tuple) and net[0] == 'global': emit_glabel(net[1], px, py)
        elif net in GLOBAL_NETS: emit_glabel(net, px, py)
        else: emit_label(net, px, py)
    return ox, oy

GND = ('gnd',); NC = ('nc',)

# ---- the GPS + PPS block ----------------------------------------------------------
place("Connector_Generic:Conn_01x05", "J_GPS", "GPS_5pin_PPS_UART", "Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical",
      60, 80, {'1': 'GPS_PPS_RAW', '2': 'GPIO5', '3': 'GPIO4', '4': '+3V3_AUX', '5': GND})
place("Device:D_TVS", "TVS_PPS", "PESD3V3L1BA", "Diode_SMD:D_SOD-323", 85, 95, {'1': 'GPS_PPS_RAW', '2': GND})
# 74LVC1G17: 1=NC 2=A 3=GND 4=Y 5=VCC (KiCad lib types: input/power_in/output/power_in)
place("74xGxx:74LVC1G17", "U_PPS", "74LVC1G17", "Package_TO_SOT_SMD:SOT-23-5",
      110, 80, {'2': 'GPS_PPS_RAW', '4': 'PPS_CLEAN', '5': '+3V3_AUX', '3': GND, '1': NC})
place("Device:C", "C_PPS", "100n", "Capacitor_SMD:C_0402_1005Metric", 100, 95, {'1': '+3V3_AUX', '2': GND})
place("Device:C", "C_GPS", "10u", "Capacitor_SMD:C_0603_1608Metric", 70, 95, {'1': '+3V3_AUX', '2': GND})
# series R guards the LVC1G17 output against a driver fight (CM5 pin 18 is bidi;
# board policy configures it as input — spec Rs 33-100R, pick 33R)
place("Device:R", "R_PPS", "33", "Resistor_SMD:R_0402_1005Metric", 140, 80, {'1': 'PPS_CLEAN', '2': 'SYNC_OUT'})
place("Connector_Generic:Conn_01x02", "J_PPS1", "PPS_tap_UWB", "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
      170, 75, {'1': 'PPS_CLEAN', '2': GND})
place("Connector_Generic:Conn_01x02", "J_PPS2", "PPS_tap_scope", "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
      170, 90, {'1': 'PPS_CLEAN', '2': GND})

# ---- file scaffold ----------------------------------------------------------------
libsyms = "\n".join(blk for blk, _ in LIBS.values())
sheet = ('(kicad_sch\n\t(version 20231120)\n\t(generator "eeschema")\n\t(generator_version "8.0")\n'
         f'\t(uuid "{SHEET_FILE_UUID}")\n\t(paper "A4")\n'
         '\t(title_block\n\t\t(title "CM5 Carrier - GPS + PPS distribution")\n\t\t(rev "A")\n'
         '\t\t(comment 1 "GPS conn (PPS+UART2+3V3_AUX) -> TVS -> LVC1G17 -> CM5 SYNC + 2 taps; PPS sink only")\n\t)\n'
         '\t(lib_symbols\n' + libsyms + '\n\t)\n' + "".join(body) +
         '\t(sheet_instances\n\t\t(path "/"\n\t\t\t(page "6")\n\t\t)\n\t)\n)\n')
assert sheet.count('(') == sheet.count(')'), f"UNBALANCED {sheet.count('(')} vs {sheet.count(')')}"
print("sheet-symbol uuid:", SHEET_SYMBOL_UUID)
print("parens balanced:", sheet.count('('))
out = os.path.join(CARRIER, "GPS_PPS.kicad_sch")
if '--apply' in sys.argv: open(out, 'w').write(sheet); print("WROTE " + out)
else: print("DRY RUN (--apply to write)")
