#!/usr/bin/env python3
"""Cross-check the OpenSCAD case against KiCad's official Raspberry Pi Zero
footprint (Module:Raspberry_Pi_Zero_Socketed_THT_FaceDown_MountingHoles).

Usage:  python3 kicad/check_dimensions.py [path/to/footprint.kicad_mod]

With no argument it uses the installed KiCad library if present, otherwise the
copy in kicad/lib/ (downloaded from gitlab.com/kicad/libraries/kicad-footprints).

The footprint is drawn face-down with the long axis along KiCad Y. The script
maps it into the case's "hole frame":
    x = along the long edge, 0 at the camera-end hole, + toward the SD/pin-1 end
    y = across the board, 0 on the header-side hole line
"""
import math, re, sys, pathlib

HERE = pathlib.Path(__file__).resolve().parent
SCAD = HERE.parent / "pi_zero_relay_case.scad"
FP_NAME = "Raspberry_Pi_Zero_Socketed_THT_FaceDown_MountingHoles.kicad_mod"
CANDIDATES = [
    pathlib.Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints/Module.pretty") / FP_NAME,
    HERE / "lib" / FP_NAME,
]
TOL = 0.05  # mm
NUM = r"([-\d.]+)"


def scad_value(name):
    m = re.search(rf"(?<![\w]){name}\s*=\s*([-\d.]+)\s*;", SCAD.read_text())
    if not m:
        sys.exit(f"could not find '{name}' in {SCAD.name}")
    return float(m.group(1))


def shapes(txt, kind, layer):
    # split into one block per graphic item so a match never spans two items
    for block in re.split(r"\n\s*\((?=fp_|pad|property|model)", txt):
        if block.startswith(kind) and f'(layer "{layer}")' in block:
            yield [(float(a), float(b)) for a, b in re.findall(rf"\((?:start|end|mid)\s+{NUM}\s+{NUM}\)", block)]


def main():
    path = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else next((p for p in CANDIDATES if p.exists()), None)
    if not path or not path.exists():
        sys.exit("KiCad footprint not found. Install KiCad or put the .kicad_mod in kicad/lib/.")
    txt = path.read_text()

    pads = []
    for m in re.finditer(rf'\(pad\s+"([^"]*)"\s+(\w+)\s+\w+\s*\(at\s+{NUM}\s+{NUM}', txt):
        pads.append(dict(name=m.group(1), kind=m.group(2), x=float(m.group(3)), y=float(m.group(4))))
    holes = [p for p in pads if p["kind"] == "np_thru_hole"]
    pins = {p["name"]: p for p in pads if p["name"].isdigit()}
    print(f"Footprint: {path.name}\n  {len(pins)} header pins, {len(holes)} mounting holes")

    # Board outline = bounding box of the four rounded-corner arcs on F.Fab
    arc_pts = [pt for s in shapes(txt, "fp_arc", "F.Fab") for pt in s]
    bx0, bx1 = min(p[0] for p in arc_pts), max(p[0] for p in arc_pts)
    by0, by1 = min(p[1] for p in arc_pts), max(p[1] for p in arc_pts)

    hx = sorted({h["x"] for h in holes}); hy = sorted({h["y"] for h in holes})
    pin_cx = sum(p["x"] for p in pins.values()) / len(pins)
    head_col = min(hx, key=lambda c: abs(c - pin_cx))          # header-side hole line (KiCad x)
    p1 = pins["1"]
    cam_row = max(hy, key=lambda r: abs(r - p1["y"]))          # hole row at the far end from pin 1

    def to_case(kx, ky):   # KiCad footprint coords -> case hole frame
        return abs(ky - cam_row), abs(kx - head_col)

    # Connectors drawn on F.Fab outside the board edge
    ports = {}
    for seg in shapes(txt, "fp_line", "F.Fab"):
        (x0, y0), (x1, y1) = seg[0], seg[-1]
        if min(x0, x1) > bx1 + 0.1 and x0 == x1:                  # beyond the port edge
            ports.setdefault(round(x0, 2), []).append((min(y0, y1), max(y0, y1)))
        if min(y0, y1) > by1 + 0.1 and y0 == y1:                  # beyond the camera end
            ports.setdefault("cam", []).append((min(x0, x1), max(x0, x1)))
    port_centres_x = sorted(to_case(0, (a + b) / 2)[0] for k, v in ports.items() if k != "cam" for a, b in v)
    cam = ports.get("cam", [(0, 0)])[0]
    cam_y = to_case((cam[0] + cam[1]) / 2, 0)[1]

    p1x, p1y = to_case(p1["x"], p1["y"])
    rows = [
        ("Hole spacing, long axis",        scad_value("hole_dx"),        hy[-1] - hy[0]),
        ("Hole spacing, short axis",       scad_value("pi_hole_dy"),     hx[-1] - hx[0]),
        ("Board length",                   scad_value("pi_w"),           by1 - by0),
        ("Board width",                    scad_value("pi_d"),           bx1 - bx0),
        ("Hole inset from board edge",     scad_value("pi_hole_inset"),  abs(head_col - bx0)),
        ("Header centre line (y)",         0.0,                          abs(pin_cx - head_col)),
        ("Pin 1 position x",               53.13,                        p1x),
        ("Pin 1 row offset |y|",           1.27,                         p1y),
        ("Micro-USB PWR centre x",         scad_value("usb_pwr_x"),      port_centres_x[0]),
        ("Micro-USB data centre x",        scad_value("usb_otg_x"),      port_centres_x[1]),
        ("Mini-HDMI centre x",             scad_value("hdmi_x"),         port_centres_x[2]),
        ("Camera connector centre y",      scad_value("csi_y"),          cam_y),
    ]

    ok = True
    print(f"\n  {'check':32} {'scad':>8} {'kicad':>8}  result")
    for label, s, k in rows:
        good = abs(s - k) <= TOL
        ok &= good
        print(f"  {label:32} {s:8.2f} {k:8.2f}  {'OK' if good else 'MISMATCH'}")
    print("\n  Not in the KiCad footprint, so not checked here: micro-SD position, all relay-board data.")
    print("  Relay-board values come from Waveshare's dimension drawing (65.15 x 56.21, holes 58 x 49).")
    print("\nALL CHECKS PASS" if ok else "\nSOME CHECKS FAILED")
    sys.exit(0 if ok else 1)


if __name__ == "__main__":
    main()
