#!/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3
"""Build a separate, routed-review PCB candidate from the compact lamp board."""
from __future__ import annotations

import hashlib
import json
from pathlib import Path

import wx
app = wx.App(False)
import pcbnew

OUT = Path(__file__).resolve().parent
SOURCE = OUT.parent / "compact_bench_suite/lamp_power/lamp_power_evt_a1_compact_bench.kicad_pcb"
FOOTPRINT_DIR = OUT / "Lamp_Power.pretty"
FOOTPRINT_NAME = "LMR51450_WSON_DRR0012"
BOARD_PATH = OUT / "lamp_buck_lmr51450_a1.kicad_pcb"
DSN_PATH = OUT / "lamp_buck_lmr51450_a1.dsn"
MM = pcbnew.FromMM
FP_CACHE = {}
NET_CACHE = {}
NEW_U1_PAD_CACHE = {}


def vec(x: float, y: float):
    return pcbnew.VECTOR2I(MM(x), MM(y))


def get_fp(board, ref: str):
    if ref in FP_CACHE:
        return FP_CACHE[ref]
    for footprint in list(board.GetFootprints()):
        footprint = pcbnew.Cast_to_FOOTPRINT(footprint)
        if footprint.GetReference() == ref:
            FP_CACHE[ref] = footprint
            return footprint
    raise KeyError(f"missing footprint {ref}")


def get_pad(footprint, number: str):
    for pad in footprint.Pads():
        if pad.GetNumber() == str(number):
            return pad
    raise KeyError(f"missing {footprint.GetReference()}.{number}")


def main() -> None:
    board = pcbnew.LoadBoard(str(SOURCE))
    if not board:
        raise RuntimeError(f"failed to load {SOURCE}")
    print("loaded baseline board", flush=True)
    for footprint in list(board.GetFootprints()):
        typed = pcbnew.Cast_to_FOOTPRINT(footprint)
        FP_CACHE[typed.GetReference()] = typed
    for name in board.GetNetsByName().keys():
        text_name = str(name)
        NET_CACHE[text_name] = board.FindNet(text_name)
    new_u1 = pcbnew.FootprintLoad(str(FOOTPRINT_DIR), FOOTPRINT_NAME)
    if not new_u1:
        raise RuntimeError("failed to load candidate LMR51450 footprint")
    for pad in new_u1.Pads():
        NEW_U1_PAD_CACHE[pad.GetNumber()] = pad
    new_u1.SetReference("U1")
    new_u1.SetValue("LMR51450SDRRR")
    new_u1.SetPosition(vec(47.0, 32.0))
    new_u1.SetOrientationDegrees(180.0)
    print("loaded candidate footprint", flush=True)

    # The switch-node power stage is regenerated around the synchronous IC.
    # Do not disturb the original board: this file is a new, isolated candidate.
    tracks_to_remove = {
        "VIN_PD_12_15", "SW", "BOOT", "BUCK_EN_UVLO", "RT_400KHZ",
        "FB", "SYS_5V0_ALWAYS_ON", "COMP", "COMP_C", "LED_PWR_PERMIT",
        "LED_QOD",
    }
    old_ground_anchors = ((38.525, 28.0), (61.0, 44.175))
    def point_mm(point):
        return (pcbnew.ToMM(point.x), pcbnew.ToMM(point.y))

    for track in list(board.GetTracks()):
        remove_ground_branch = False
        if track.GetNetname() == "GND" and not isinstance(track, pcbnew.PCB_VIA):
            start = point_mm(track.GetStart())
            end = point_mm(track.GetEnd())
            in_reworked_zone = (
                44.0 <= min(start[0], end[0]) and max(start[0], end[0]) <= 65.0
                and 26.0 <= min(start[1], end[1]) and max(start[1], end[1]) <= 41.0
            )
            at_old_ground_pad = any(
                max(abs(pt[0] - x), abs(pt[1] - y)) <= 0.01
                for pt in (start, end) for x, y in old_ground_anchors
            )
            remove_ground_branch = in_reworked_zone or at_old_ground_pad
        if track.GetNetname() in tracks_to_remove or remove_ground_branch:
            board.Remove(track)
    print("removed old power tracks", flush=True)

    old_u1 = get_fp(board, "U1")
    board.Remove(old_u1)

    # These parts are now DNP in the LMR51450 circuit and are omitted from the
    # physical PCB to free the switch-node and inductor area.
    for ref in ("D1", "R5", "C9", "C10"):
        board.Remove(get_fp(board, ref))

    # New custom WSON land pattern; VIN exits toward input capacitors and SW
    # exits toward the relocated inductor. Exposed pad and AGND share GND.
    board.Add(new_u1)
    print("replaced U1 and DNP footprints", flush=True)

    # Move the high-frequency input cap, bootstrap cap, inductor, and FB
    # divider to shorten the switching loop and high-impedance sense path.
    placements = {
        "C3": ((42.0, 30.5), 180.0),
        "C4": ((49.5, 29.2), 90.0),
        "L1": ((57.0, 32.0), 0.0),
        "R3": ((40.0, 36.0), 0.0),
        "R4": ((45.0, 36.0), 0.0),
    }
    for ref, (position, angle) in placements.items():
        footprint = get_fp(board, ref)
        footprint.SetPosition(vec(*position))
        footprint.SetOrientationDegrees(angle)

    # Update component labels to match the current schematic candidate.
    for ref, value in {
        "C1": "4.7uF 50V X7R input candidate",
        "C2": "4.7uF 50V X7R input candidate",
        "C3": "100nF 50V X7R high-frequency input bypass",
        "C4": "100nF 10V X7R bootstrap",
        "L1": "4.7uH Isat>=10A RMS>=6A target",
        "R3": "100k 1% FB high side",
        "R4": "19.1k 1% FB low side",
    }.items():
        get_fp(board, ref).SetValue(value)

    # Keep the DNP RT resistor footprint to make the intentionally open RT pin
    # explicit in the candidate PCB's pad-net map.
    old_rt = NET_CACHE.get("RT_400KHZ")
    if old_rt:
        old_rt.SetNetname("RT_OPEN_500KHZ")
        NET_CACHE["RT_OPEN_500KHZ"] = old_rt

    # Map all 13 WSON lands to the updated schematic nets.
    pin_nets = {
        "1": "SW", "2": "SW", "3": "SW", "4": "BOOT", "5": "",
        "6": "RT_OPEN_500KHZ", "7": "FB", "8": "GND", "9": "BUCK_EN_UVLO",
        "10": "VIN_PD_12_15", "11": "VIN_PD_12_15", "12": "VIN_PD_12_15",
        "13": "GND",
    }
    for number, net_name in pin_nets.items():
        pad = NEW_U1_PAD_CACHE[number]
        net = NET_CACHE.get(net_name) if net_name else None
        pad.SetNetCode(net.GetNetCode() if net else 0)

    # Three tented through vias under the exposed pad connect the thermal land
    # to the existing bottom GND plane. Tented vias avoid open via-in-pad wicking.
    for x, y in ((47.0, 31.25), (47.0, 32.0), (47.0, 32.75)):
        via = pcbnew.PCB_VIA(board)
        via.SetPosition(vec(x, y))
        via.SetViaType(pcbnew.VIATYPE_THROUGH)
        via.SetWidth(MM(0.6))
        via.SetDrill(MM(0.3))
        via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
        via.SetFrontTentingMode(pcbnew.TENTING_MODE_TENTED)
        via.SetBackTentingMode(pcbnew.TENTING_MODE_TENTED)
        via.SetNetCode(NET_CACHE["GND"].GetNetCode())
        board.Add(via)
    print("added thermal vias", flush=True)

    # Add project-local library reference and carry over the current board setup.
    (OUT / "fp-lib-table").write_text(
        '(fp_lib_table\n  (lib (name "Lamp_Power") (type "KiCad") '
        '(uri "${KIPRJMOD}/Lamp_Power.pretty") (options "") '
        '(descr "Lamp power candidate footprints"))\n)\n', encoding="utf-8")

    # Refill the original bottom GND zone after adding thermal vias.
    filler = pcbnew.ZONE_FILLER(board)
    filler.Fill(board.Zones())
    print("refilled zones", flush=True)
    if not pcbnew.SaveBoard(str(BOARD_PATH), board):
        raise RuntimeError("failed to save candidate board")
    print("saved candidate board", flush=True)

    if not pcbnew.ExportSpecctraDSN(board, str(DSN_PATH)):
        raise RuntimeError("failed to export Specctra DSN")
    print("exported DSN", flush=True)

    result = {
        "status": "CANDIDATE_BOARD_BUILT_AUTOROUTE_PENDING",
        "source_board": str(SOURCE),
        "candidate_board": str(BOARD_PATH),
        "candidate_dsn": str(DSN_PATH),
        "source_board_sha256": hashlib.sha256(SOURCE.read_bytes()).hexdigest(),
        "candidate_board_sha256": hashlib.sha256(BOARD_PATH.read_bytes()).hexdigest(),
        "candidate_dsn_sha256": hashlib.sha256(DSN_PATH.read_bytes()).hexdigest(),
        "footprint_count": len(list(board.GetFootprints())),
        "removed_dnp_footprints": ["D1", "R5", "C9", "C10"],
        "u1_pad_net_map": pin_nets,
        "u1_center_mm": [47.0, 32.0],
        "u1_orientation_deg": 180,
        "inductor_center_mm": [57.0, 32.0],
        "thermal_vias": 3,
        "board_layers": 2,
        "physical_validation": "NOT_RUN_NO_SAMPLE",
    }
    (OUT / "pcb-build.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()
