#!/usr/bin/env python3
"""Compare the exported schematic pin nets with the KiCad PCB candidate."""
from __future__ import annotations

import json
from pathlib import Path
import sys
import xml.etree.ElementTree as ET

import wx
import pcbnew


# Keep the wx application alive while pcbnew loads the board. Without a strong
# reference, wx.App can be garbage-collected and KiCad emits a traits assertion.
_wx_app = wx.App(False)


HERE = Path(__file__).resolve().parent
NETLIST = HERE / "camera_evb_a0.xml"
BOARD_FILE = HERE / "camera_evb_a0.kicad_pcb"
REPORT = HERE / "camera_evb_a0_netcheck.json"
MECHANICAL_PAD_ALLOWLIST = {("J1", "MP")}


def main() -> int:
    root = ET.parse(NETLIST).getroot()
    schematic_nets: dict[tuple[str, str], str | None] = {}
    for net in root.findall("./nets/net"):
        name = net.attrib["name"].lstrip("/")
        no_connect_net = name.startswith("unconnected-(")
        for node in net.findall("node"):
            key = (node.attrib["ref"], node.attrib["pin"])
            value = None if no_connect_net else name
            if key in schematic_nets and schematic_nets[key] != value:
                raise RuntimeError(f"Pin appears on multiple schematic nets: {key}")
            schematic_nets[key] = value

    board = pcbnew.LoadBoard(str(BOARD_FILE))
    board_pads: dict[tuple[str, str], str | None] = {}
    pad_instance_count = 0
    mechanical_pad_instance_count = 0
    for footprint in board.GetFootprints():
        ref = footprint.GetReference()
        for pad in footprint.Pads():
            pad_instance_count += 1
            key = (ref, pad.GetNumber())
            if key in MECHANICAL_PAD_ALLOWLIST and not pad.GetNetname():
                mechanical_pad_instance_count += 1
            net_name = pad.GetNetname() or None
            board_pads[key] = net_name

    missing_on_board = sorted(set(schematic_nets) - set(board_pads))
    absent_from_schematic = sorted(set(board_pads) - set(schematic_nets))
    allowed_mechanical = sorted(set(absent_from_schematic) & MECHANICAL_PAD_ALLOWLIST)
    unexpected_board_pads = sorted(set(absent_from_schematic) - MECHANICAL_PAD_ALLOWLIST)
    mismatches = [
        {"ref": ref, "pin": pin, "schematic": schematic_nets.get((ref, pin)),
         "pcb": board_pads.get((ref, pin))}
        for ref, pin in sorted(set(schematic_nets) & set(board_pads))
        if schematic_nets[(ref, pin)] != board_pads[(ref, pin)]
    ]

    trace_lengths: dict[str, float] = {}
    track_count = 0
    for item in board.GetTracks():
        if item.GetClass() != "PCB_TRACK":
            continue
        track_count += 1
        name = item.GetNetname()
        trace_lengths[name] = trace_lengths.get(name, 0.0) + pcbnew.ToMM(item.GetLength())

    pairs = {
        "CSI0_D1": ("CAM_CSI0_D1_N", "CAM_CSI0_D1_P"),
        "CSI0_CLK": ("CAM_CSI0_CLK_N", "CAM_CSI0_CLK_P"),
        "CSI0_D0": ("CAM_CSI0_D0_N", "CAM_CSI0_D0_P"),
    }
    pair_lengths = {}
    for pair, (negative, positive) in pairs.items():
        n_len = round(trace_lengths.get(negative, 0.0), 4)
        p_len = round(trace_lengths.get(positive, 0.0), 4)
        pair_lengths[pair] = {
            "negative_mm": n_len,
            "positive_mm": p_len,
            "skew_mm": round(abs(n_len - p_len), 4),
        }

    result = {
        "status": "PASS" if not missing_on_board and not unexpected_board_pads
                  and not mismatches and all(v["skew_mm"] <= 0.15 for v in pair_lengths.values())
                  else "FAIL",
        "tool": "KiCad pcbnew Python API + KiCad 10.0.6 exported kicadxml netlist",
        "schematic_pin_nodes": len(schematic_nets),
        "pcb_footprint_pads": len(board_pads),
        "pcb_physical_pad_instances": pad_instance_count,
        "allowed_mechanical_pads_without_schematic_symbol": allowed_mechanical,
        "allowed_unnetted_mechanical_pad_instances": mechanical_pad_instance_count,
        "connected_schematic_pins": sum(v is not None for v in schematic_nets.values()),
        "explicit_no_connect_pins": sum(v is None for v in schematic_nets.values()),
        "tracks": track_count,
        "missing_on_board": missing_on_board,
        "unexpected_board_pads_absent_from_schematic": unexpected_board_pads,
        "net_mismatches": mismatches,
        "csi_route_lengths": pair_lengths,
        "limits": {
            "maximum_pair_skew_mm": 0.15,
            "impedance_and_signal_integrity_verified": False,
            "physical_connector_orientation_verified": False,
        },
    }
    REPORT.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return 0 if result["status"] == "PASS" else 1


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