#!/usr/bin/env python3
"""Generate the RP2040 breakout (Adom-molecule design rules).

v3: replaces the v2 standard pin headers with Adom Medium Contacts around the
perimeter + 4 Adom Medium Pins in the corners (per the adom-molecules and
kicad-to-molecule skills). 40×24 mm board on the 8 mm medium-grid; corner
machine pins are 1 mm in from each corner. RP2040 + 3× decoupling caps + 12
MHz crystal + 2× crystal load caps + USB-C receptacle, all placed without
overlap. USB-C sits on the LEFT short edge with its opening facing outward
(pin 1 in the +X direction toward the chip).

No nets are emitted — the goal is geometric placement + clean 3D render.
A user can connect the schematic in KiCad later if they want a working
breakout.

Run:
    python plugins/kicad/tour-pack-rp2040/generate_breakout.py
"""
from __future__ import annotations
import re
import sys
import uuid
from pathlib import Path

HERE = Path(__file__).parent
KICAD_ROOT = Path(r"C:/Program Files/KiCad/10.0/share/kicad")
KICAD_VERSION = "10"

# ── Board geometry ──────────────────────────────────────────────────────
# 40×24 mm — both fit the medium-board class (≤60 mm). Centred at
# (130, 110) so the board outline is (110, 98) → (150, 122).
BOARD_W, BOARD_H = 40.0, 24.0
BX_MIN, BY_MIN = 130.0 - BOARD_W / 2, 110.0 - BOARD_H / 2
BX_MAX, BY_MAX = 130.0 + BOARD_W / 2, 110.0 + BOARD_H / 2
EDGE_OFFSET = 1.0    # mm — corner-pin offset per kicad-to-molecule medium rules
GRID = 8.0           # mm — Adom medium grid

# ── Helpers ─────────────────────────────────────────────────────────────


def extract_symbol_def(lib_path: Path, sym_name: str,
                       qualify_with_lib: str | None = None) -> str:
    txt = lib_path.read_text(encoding="utf-8")
    needle = f'(symbol "{sym_name}"'
    i = txt.find(needle)
    if i < 0:
        raise RuntimeError(f"Symbol {sym_name!r} not found in {lib_path}")
    depth = 0
    end = i
    for j in range(i, len(txt)):
        if txt[j] == "(":
            depth += 1
        elif txt[j] == ")":
            depth -= 1
            if depth == 0:
                end = j + 1
                break
    block = txt[i:end]
    if qualify_with_lib:
        block = block.replace(needle, f'(symbol "{qualify_with_lib}:{sym_name}"', 1)
    return block


def extract_footprint_body(fp_path: Path, instance_name: str) -> str:
    txt = fp_path.read_text(encoding="utf-8")
    txt = re.sub(
        r'^\(footprint "[^"]+"',
        f'(footprint "AdomRP2040:{instance_name}"',
        txt,
        count=1,
    )
    return txt


def place_footprint(fp_body: str, x: float, y: float, rot: float = 0) -> str:
    """Inject (uuid …) + (at X Y rot) into a footprint body."""
    inst_uuid = str(uuid.uuid4())
    insertion = f'\n\t(uuid "{inst_uuid}")\n\t(at {x} {y}{(" " + str(rot)) if rot else ""})'
    lines = fp_body.splitlines()
    for i, line in enumerate(lines):
        if line.strip().startswith('(layer "F.Cu")'):
            lines.insert(i + 1, insertion.lstrip("\n"))
            break
    return "\n".join(lines)


def model_block(model_ref: str | None) -> str:
    """Emit a (model …) block. `model_ref` is either a KiCad-std relative path
    like 'Capacitor_SMD.3dshapes/C_0402_1005Metric.step' (resolved via
    ${KICAD<n>_3DMODEL_DIR}) OR an in-pack path starting with `pack:`.
    Returns "" if model_ref is None."""
    if not model_ref:
        return ""
    if model_ref.startswith("pack:"):
        # Relative to this footprint .pretty/ → ../AdomRP2040.3dshapes/<name>
        rel = model_ref[len("pack:"):]
        full = f"${{KIPRJMOD}}/../AdomRP2040.3dshapes/{rel}"
    else:
        full = f"${{KICAD{KICAD_VERSION}_3DMODEL_DIR}}/{model_ref}"
    return (
        f'\n\t(model "{full}"\n'
        f'\t\t(offset (xyz 0 0 0))\n'
        f'\t\t(scale (xyz 1 1 1))\n'
        f'\t\t(rotate (xyz 0 0 0))\n'
        f'\t)'
    )


def make_uuid() -> str:
    return str(uuid.uuid4())


# ── BOM ─────────────────────────────────────────────────────────────────
#
# Each entry: (ref, value, sym_lib, sym_name, fp_lib, fp_name, model_ref,
#              sch_x, sch_y, sch_rot, pcb_x, pcb_y, pcb_rot)
#
# sch_*: schematic placement (free-form A4)
# pcb_*: board placement (mm, board-coords). No overlaps audited below.

# Compute perimeter machine-contact positions on the 8 mm grid.
# Adom convention: corner MPs at (offset, offset) etc.; intermediate MCs sit on
# the 8 mm grid between them. For a 40 × 24 board with offset 1 mm and grid 8,
# the available top-edge X positions are 119, 127, 135, 143 (4 contacts, all
# inside the 38 mm corner-to-corner span 111 → 149).
TOP_CONTACTS_X = [119.0, 127.0, 135.0, 143.0]
BOT_CONTACTS_X = [119.0, 127.0, 135.0, 143.0]

# Corner positions per the kicad-to-molecule convention (CCW from front-left).
CORNER_MP = {
    "MP1": (BX_MIN + EDGE_OFFSET, BY_MAX - EDGE_OFFSET),  # front-left  (111, 121)
    "MP2": (BX_MIN + EDGE_OFFSET, BY_MIN + EDGE_OFFSET),  # back-left   (111, 99)
    "MP3": (BX_MAX - EDGE_OFFSET, BY_MIN + EDGE_OFFSET),  # back-right  (149, 99)
    "MP4": (BX_MAX - EDGE_OFFSET, BY_MAX - EDGE_OFFSET),  # front-right (149, 121)
}

# ── Active component placement (chip + caps + crystal + USB-C) ─────────
# Coordinates chosen so no component bounding box overlaps another.
# QFN-56 body 7×7 mm (pads ~9.5×9.5 mm).

ACTIVE_PARTS = [
    # (ref, value, sym_lib, sym_name, fp_lib, fp_name, model_ref,
    #  sch_x, sch_y, sch_rot, pcb_x, pcb_y, pcb_rot)
    # The RP2040 sits centre-left of the chip area to leave space for the
    # crystal on its right and the USB-C on the board's left edge.
    ("U1", "RP2040",
     "AdomRP2040", "RP2040_AdomTour",
     "AdomRP2040", "QFN-56_AdomRP2040",
     None,
     150, 100, 0,
     130, 110, 0),

    # 3× 100 nF / 1 µF decoupling caps, kept clear of the chip body (chip
    # body extends ~4.75 mm from centre in each direction including pads).
    # Place them in the gap between the chip and the top-edge contacts row.
    ("C1", "100nF",
     "Device", "C",
     "Capacitor_SMD", "C_0402_1005Metric",
     "Capacitor_SMD.3dshapes/C_0402_1005Metric.step",
     115, 65, 0,
     126.5, 102.5, 0),
    ("C2", "100nF",
     "Device", "C",
     "Capacitor_SMD", "C_0402_1005Metric",
     "Capacitor_SMD.3dshapes/C_0402_1005Metric.step",
     130, 65, 0,
     133.5, 102.5, 0),
    ("C3", "1uF",
     "Device", "C",
     "Capacitor_SMD", "C_0402_1005Metric",
     "Capacitor_SMD.3dshapes/C_0402_1005Metric.step",
     145, 65, 0,
     130, 117, 0),

    # 12 MHz crystal next to the chip (right side, clear of the QFN pads).
    # The 3225 crystal is 3.2 × 2.5 mm body; load caps tuck above/below.
    ("Y1", "12MHz",
     "Device", "Crystal",
     "Crystal", "Crystal_SMD_3225-4Pin_3.2x2.5mm",
     "Crystal.3dshapes/Crystal_SMD_3225-4Pin_3.2x2.5mm.step",
     180, 100, 0,
     140, 110, 0),
    ("C4", "22pF",
     "Device", "C",
     "Capacitor_SMD", "C_0402_1005Metric",
     "Capacitor_SMD.3dshapes/C_0402_1005Metric.step",
     180, 130, 0,
     140, 106, 0),
    ("C5", "22pF",
     "Device", "C",
     "Capacitor_SMD", "C_0402_1005Metric",
     "Capacitor_SMD.3dshapes/C_0402_1005Metric.step",
     180, 145, 0,
     140, 114, 0),

    # USB-C receptacle on the LEFT short edge — opening pointing OUTWARD
    # (west / -X) so a cable plugged in from the left actually mates.
    # v3.1 fix: rot=270 (not 90 — v3 had it backwards) and shifted west
    # so the opening sits flush with the board's left edge.
    #
    # Amphenol 12401610E4-2A footprint (rot=0) extents: X = [-5.39, 5.39],
    # Y = [-6.36, 6.14], pads-cluster at y=-5.02 (BACK of the connector).
    # The receptacle opening is at the +Y extreme. After rot=270, the
    # opening lies at -X relative to the placement origin — facing west,
    # which is what we want for cable insertion from outside the board.
    #
    # Placement: centre x=114 — the opening edge sits ~2 mm past the
    # board's left edge (at x=110) for cable clearance, while the
    # mounting tabs / SMT pads at centre.x + 5.02 ≈ 119 are pushed
    # deeper into the FR4 so the through-hole tabs root cleanly.
    ("J1", "USB-C",
     "Connector", "USB_C_Receptacle_USB2.0_16P",
     "Connector_USB", "USB_C_Receptacle_Amphenol_12401610E4-2A",
     "Connector_USB.3dshapes/USB_C_Receptacle_Amphenol_12401610E4-2A.step",
     90, 100, 0,
     114, 110, 270),
]

# Corner Adom machine pins (no signal). The ADOM_MECHANICAL_PIN symbol fits.
CORNER_PARTS = [
    (ref, "MP",
     "AdomRP2040", "ADOM_MECHANICAL_PIN",
     "AdomRP2040", "ADOM_MEDIUM_PIN_SHORTER_v1",
     "pack:MachinePin_Medium_Short.step",
     # Sch placement off to the right of the active sketch
     220 + (i % 2) * 12, 60 + (i // 2) * 14, 0,
     CORNER_MP[ref][0], CORNER_MP[ref][1], 0)
    for i, ref in enumerate(CORNER_MP)
]

# Perimeter Adom machine contacts (signal-carrying). Numbered MC1..MCn
# counterclockwise from front-left along the top edge first, then the
# right side (if any), then the bottom.
CONTACT_PARTS: list[tuple] = []
mc_idx = 1
# Top edge (y = BY_MIN + EDGE_OFFSET = 99)
for x in TOP_CONTACTS_X:
    ref = f"MC{mc_idx}"
    CONTACT_PARTS.append((
        ref, "MC",
        "AdomRP2040", "ADOM_SIGNAL_PIN",
        "AdomRP2040", "ADOM_MEDIUM_CONTACT",
        "pack:MediumMachineContact v1.step",
        45 + (mc_idx - 1) * 10, 170, 0,
        x, BY_MIN + EDGE_OFFSET, 0,
    ))
    mc_idx += 1
# Bottom edge (y = BY_MAX - EDGE_OFFSET = 121) — reverse so MCs flow CCW
for x in reversed(BOT_CONTACTS_X):
    ref = f"MC{mc_idx}"
    CONTACT_PARTS.append((
        ref, "MC",
        "AdomRP2040", "ADOM_SIGNAL_PIN",
        "AdomRP2040", "ADOM_MEDIUM_CONTACT",
        "pack:MediumMachineContact v1.step",
        45 + (mc_idx - 9) * 10, 195, 0,
        x, BY_MAX - EDGE_OFFSET, 0,
    ))
    mc_idx += 1

PARTS = ACTIVE_PARTS + CORNER_PARTS + CONTACT_PARTS


# ── Overlap audit ───────────────────────────────────────────────────────


def audit_overlaps() -> list[str]:
    """Conservative bounding-box overlap check on PCB placements."""
    # Approximate footprint extents (mm).  These are total pad+body extent
    # measured from the placement origin (which is usually the footprint centre).
    bbox = {
        "QFN-56_AdomRP2040": (5.5, 5.5),
        "C_0402_1005Metric": (1.5, 0.9),
        "Crystal_SMD_3225-4Pin_3.2x2.5mm": (2.0, 1.6),
        "USB_C_Receptacle_Amphenol_12401610E4-2A": (5.0, 5.5),
        "ADOM_MEDIUM_PIN_SHORTER_v1": (0.9, 0.9),
        "ADOM_MEDIUM_CONTACT": (0.7, 0.7),
    }
    issues = []
    placed = []
    for p in PARTS:
        (ref, _v, _sl, _sn, _fl, fp_name, _mref,
         _sx, _sy, _srot, px, py, prot) = p
        ext = bbox.get(fp_name, (1.0, 1.0))
        # Swap extents for 90/270 rotation
        if prot in (90, 270, -90):
            ext = (ext[1], ext[0])
        placed.append((ref, px, py, ext))
    for i in range(len(placed)):
        ra, ax, ay, (aw, ah) = placed[i]
        for j in range(i + 1, len(placed)):
            rb, bx, by, (bw, bh) = placed[j]
            if abs(ax - bx) < (aw + bw) and abs(ay - by) < (ah + bh):
                # Skip corner-MP vs contact (tiny overlap is OK between them)
                if ra.startswith("MP") and rb.startswith("MC"):
                    continue
                if ra.startswith("MC") and rb.startswith("MP"):
                    continue
                issues.append(f"{ra} ↔ {rb}: bbox overlap at ({px}={ax}/{ay}) ({by})")
    return issues


# ── Symbol instance emitter ────────────────────────────────────────────


def emit_symbol_instance(part: tuple, sch_uuid: str) -> str:
    (ref, value, sym_lib, sym_name, fp_lib, fp_name, _mref,
     sx, sy, srot, _px, _py, _prot) = part
    inst_uuid = make_uuid()
    pin_lines = "\n".join(
        f'\t\t(pin "{i+1}" (uuid "{make_uuid()}"))' for i in range(30)
    )
    fp_prop = f"{fp_lib}:{fp_name}"
    return f"""\t(symbol
\t\t(lib_id "{sym_lib}:{sym_name}")
\t\t(at {sx} {sy} {srot})
\t\t(unit 1)
\t\t(exclude_from_sim no)
\t\t(in_bom yes)
\t\t(on_board yes)
\t\t(dnp no)
\t\t(uuid "{inst_uuid}")
\t\t(property "Reference" "{ref}"
\t\t\t(at {sx + 2.54} {sy - 5} 0)
\t\t\t(effects (font (size 1.27 1.27)))
\t\t)
\t\t(property "Value" "{value}"
\t\t\t(at {sx + 2.54} {sy + 5} 0)
\t\t\t(effects (font (size 1.27 1.27)))
\t\t)
\t\t(property "Footprint" "{fp_prop}"
\t\t\t(at {sx} {sy} 0)
\t\t\t(effects (font (size 1.27 1.27)) (hide yes))
\t\t)
\t\t(property "Datasheet" "~"
\t\t\t(at {sx} {sy} 0)
\t\t\t(effects (font (size 1.27 1.27)) (hide yes))
\t\t)
\t\t(property "Description" "{value}"
\t\t\t(at {sx} {sy} 0)
\t\t\t(effects (font (size 1.27 1.27)) (hide yes))
\t\t)
{pin_lines}
\t\t(instances
\t\t\t(project "rp2040-breakout"
\t\t\t\t(path "/{sch_uuid}"
\t\t\t\t\t(reference "{ref}")
\t\t\t\t\t(unit 1)
\t\t\t\t)
\t\t\t)
\t\t)
\t)"""


def sym_lib_path(sym_lib: str) -> Path:
    if sym_lib == "AdomRP2040":
        return HERE / "AdomRP2040.kicad_sym"
    return KICAD_ROOT / "symbols" / f"{sym_lib}.kicad_sym"


def fp_lib_path(fp_lib: str, fp_name: str) -> Path:
    if fp_lib == "AdomRP2040":
        return HERE / "AdomRP2040.pretty" / f"{fp_name}.kicad_mod"
    return KICAD_ROOT / "footprints" / f"{fp_lib}.pretty" / f"{fp_name}.kicad_mod"


def main() -> int:
    issues = audit_overlaps()
    if issues:
        print("WARN: overlap audit found issues — review before shipping:")
        for it in issues:
            print(f"  • {it}")
    else:
        print(f"overlap audit: clean ({len(PARTS)} components)")

    template_dir = HERE / "template"
    template_dir.mkdir(exist_ok=True)

    # ── lib_symbols block (unique symbols only) ────────────────────────
    seen = set()
    lib_symbol_blocks: list[str] = []
    for p in PARTS:
        key = (p[2], p[3])  # (sym_lib, sym_name)
        if key in seen:
            continue
        seen.add(key)
        lib_symbol_blocks.append(extract_symbol_def(
            sym_lib_path(p[2]), p[3], qualify_with_lib=p[2]))

    sch_uuid = make_uuid()
    sym_instances = "\n".join(emit_symbol_instance(p, sch_uuid) for p in PARTS)

    sch = f"""(kicad_sch
\t(version 20231120)
\t(generator "eeschema")
\t(generator_version "9.0")
\t(uuid "{sch_uuid}")
\t(paper "A4")
\t(title_block
\t\t(title "Adom RP2040 Breakout")
\t\t(date "2026-06-07")
\t\t(comment 1 "RP2040 molecule: 4 corner Adom MPs + 8 perimeter Adom MCs.")
\t\t(comment 2 "Decoupling, crystal, USB-C — placement only, no nets emitted.")
\t)
\t(lib_symbols
{chr(10).join(lib_symbol_blocks)}
\t)
{sym_instances}
\t(sheet_instances
\t\t(path "/"
\t\t\t(page "1")
\t\t)
\t)
\t(embedded_fonts no)
)
"""
    sch_path = template_dir / "rp2040-breakout.kicad_sch"
    sch_path.write_text(sch, encoding="utf-8")
    print(f"wrote {sch_path} ({len(sch):,} bytes)")

    # ── Board ──────────────────────────────────────────────────────────
    fp_blocks = []
    for p in PARTS:
        (_ref, _value, _sl, _sn, fp_lib, fp_name, model_ref,
         _sx, _sy, _srot, px, py, prot) = p
        body = extract_footprint_body(fp_lib_path(fp_lib, fp_name), fp_name)
        # If the source has no (model …) block, append one.
        if "(model " not in body and model_ref:
            body = body.rstrip()
            if body.endswith(")"):
                body = body[:-1] + model_block(model_ref) + "\n)\n"
        body = place_footprint(body, px, py, prot)
        fp_blocks.append(body)

    edge_uuids = [make_uuid() for _ in range(4)]
    pcb = f"""(kicad_pcb
\t(version 20240108)
\t(generator "pcbnew")
\t(generator_version "9.0")
\t(general (thickness 1.6) (legacy_teardrops no))
\t(paper "A4")
\t(layers
\t\t(0 "F.Cu" signal) (31 "B.Cu" signal)
\t\t(32 "B.Adhes" user "B.Adhesive") (33 "F.Adhes" user "F.Adhesive")
\t\t(34 "B.Paste" user) (35 "F.Paste" user)
\t\t(36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen")
\t\t(38 "B.Mask" user "B.Mask") (39 "F.Mask" user "F.Mask")
\t\t(40 "Dwgs.User" user "User.Drawings") (41 "Cmts.User" user "User.Comments")
\t\t(42 "Eco1.User" user "User.Eco1") (43 "Eco2.User" user "User.Eco2")
\t\t(44 "Edge.Cuts" user) (45 "Margin" user)
\t\t(46 "B.CrtYd" user "B.Courtyard") (47 "F.CrtYd" user "F.Courtyard")
\t\t(48 "B.Fab" user) (49 "F.Fab" user)
\t)
\t(setup
\t\t(pad_to_mask_clearance 0)
\t\t(allow_soldermask_bridges_in_footprints no)
\t\t(tenting front back)
\t\t(pcbplotparams
\t\t\t(layerselection 0x00010fc_ffffffff)
\t\t\t(plot_on_all_layers_selection 0x0000000_00000000)
\t\t\t(disableapertmacros no) (usegerberextensions no)
\t\t\t(usegerberattributes yes) (usegerberadvancedattributes yes)
\t\t\t(creategerberjobfile yes)
\t\t\t(dashed_line_dash_ratio 12.000000) (dashed_line_gap_ratio 3.000000)
\t\t\t(svgprecision 4) (plotframeref no) (viasonmask no) (mode 1)
\t\t\t(useauxorigin no) (hpglpennumber 1) (hpglpenspeed 20)
\t\t\t(hpglpendiameter 15.000000)
\t\t\t(pdf_front_fp_property_popups yes) (pdf_back_fp_property_popups yes)
\t\t\t(pdf_metadata yes) (pdf_single_document no)
\t\t\t(dxfpolygonmode yes) (dxfimperialunits yes) (dxfusepcbnewfont yes)
\t\t\t(psnegative no) (psa4output no) (plot_black_and_white yes)
\t\t\t(plotinvisibletext no) (sketchpadsonfab no) (plotpadnumbers no)
\t\t\t(hidednponfab no) (sketchdnponfab yes) (crossoutdnponfab yes)
\t\t\t(subtractmaskfromsilk no) (outputformat 1) (mirror no)
\t\t\t(drillshape 1) (scaleselection 1)
\t\t\t(outputdirectory "gerbers/")
\t\t)
\t)
\t(net 0 "")
{chr(10).join(fp_blocks)}
\t(gr_line (start {BX_MIN} {BY_MIN}) (end {BX_MAX} {BY_MIN})
\t\t(stroke (width 0.1) (type default)) (layer "Edge.Cuts") (uuid "{edge_uuids[0]}"))
\t(gr_line (start {BX_MAX} {BY_MIN}) (end {BX_MAX} {BY_MAX})
\t\t(stroke (width 0.1) (type default)) (layer "Edge.Cuts") (uuid "{edge_uuids[1]}"))
\t(gr_line (start {BX_MAX} {BY_MAX}) (end {BX_MIN} {BY_MAX})
\t\t(stroke (width 0.1) (type default)) (layer "Edge.Cuts") (uuid "{edge_uuids[2]}"))
\t(gr_line (start {BX_MIN} {BY_MAX}) (end {BX_MIN} {BY_MIN})
\t\t(stroke (width 0.1) (type default)) (layer "Edge.Cuts") (uuid "{edge_uuids[3]}"))
)
"""
    pcb_path = template_dir / "rp2040-breakout.kicad_pcb"
    pcb_path.write_text(pcb, encoding="utf-8")
    print(f"wrote {pcb_path} ({len(pcb):,} bytes)")

    pro = (
        '{\n'
        '  "board": {"design_settings": {"defaults": {}, "rule_severities": {}, "rules": {}}, "layer_presets": [], "viewports": []},\n'
        '  "boards": [],\n'
        '  "cvpcb": {"equivalence_files": []},\n'
        '  "libraries": {"pinned_footprint_libs": ["AdomRP2040"], "pinned_symbol_libs": ["AdomRP2040"]},\n'
        '  "meta": {"filename": "rp2040-breakout.kicad_pro", "version": 3},\n'
        '  "pcbnew": {"last_paths": {"plot": "gerbers/"}, "page_layout_descr_file": ""},\n'
        '  "schematic": {"annotate_messages": "", "legacy_lib_dir": "", "legacy_lib_list": []},\n'
        '  "sheets": [], "text_variables": {}\n'
        '}\n'
    )
    pro_path = template_dir / "rp2040-breakout.kicad_pro"
    pro_path.write_text(pro, encoding="utf-8")
    print(f"wrote {pro_path}")
    return 0


if __name__ == "__main__":
    sys.exit(main())