#!/usr/bin/env python3
"""Build kicad/pi_zero_relay_stack.kicad_pcb: a dimension-reference board
showing the Pi Zero (KiCad library footprint), the Waveshare RPi Relay Board
outline, and the case walls, all in the same frame as the OpenSCAD model.

Run with KiCad's bundled Python (it has the pcbnew module):
  ~/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/Current/bin/python3 \
      kicad/make_reference_board.py

Layers:
  Edge.Cuts  case outer outline        Cmts.User  case inner wall + wall openings
  Dwgs.User  relay board outline       Eco1.User  relays, terminals, LEDs, IDC
  F.Fab etc. Pi Zero footprint (from KiCad's Module library)
Aux (drill/place) origin = the case's hole-frame origin, so the measure tool
and DXF export (--use-drill-origin) read the same numbers as the .scad file.
"""
import math, pathlib, re, sys
import pcbnew

HERE = pathlib.Path(__file__).resolve().parent
SCAD = (HERE.parent / "pi_zero_relay_case.scad").read_text()
OUT = HERE / "pi_zero_relay_stack.kicad_pcb"
LIBROOT = pathlib.Path.home() / "Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints"
if not LIBROOT.exists():
    LIBROOT = pathlib.Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints")


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


# ---- values from the .scad (single source of truth) ----
hole_dx, pi_hole_dy, hat_hole_dy = v("hole_dx"), v("pi_hole_dy"), v("hat_hole_dy")
pi_w, pi_d, hat_w, hat_d, board_r = v("pi_w"), v("pi_d"), v("hat_w"), v("hat_d"), v("board_r")
pi_x0 = pi_y0 = -v("pi_hole_inset")
hat_x0, hat_y0 = -v("hat_hole_inset_x0"), -v("hat_hole_inset_y0")
clear_xy, wall, outer_r = v("clear_xy"), v("wall"), v("outer_r")
in_x0 = min(pi_x0, hat_x0) - clear_xy
in_x1 = max(pi_x0 + pi_w, hat_x0 + hat_w) + clear_xy
in_y0 = min(pi_y0, hat_y0) - clear_xy
in_y1 = max(pi_y0 + pi_d, hat_y0 + hat_d) + clear_xy
term_x = [(3.5, 20.1), (21.3, 37.3), (38.5, 55.0)]   # mirrors term_x in the scad
leds = [(20.1, 10.5, 4, 9), (33.7, 10.5, 4, 9), (47.4, 10.5, 4, 9), (53.5, 13.3, 4, 6)]

ORIGIN = (100.0, 100.0)   # KiCad position of the hole-frame origin


def K(x, y):  # case frame (y up) -> KiCad (y down), as VECTOR2I
    return pcbnew.VECTOR2I(pcbnew.FromMM(ORIGIN[0] + x), pcbnew.FromMM(ORIGIN[1] - y))


board = pcbnew.BOARD()


def seg(a, b, layer, w=0.12):
    s = pcbnew.PCB_SHAPE(board); s.SetShape(pcbnew.SHAPE_T_SEGMENT)
    s.SetStart(K(*a)); s.SetEnd(K(*b)); s.SetLayer(layer); s.SetWidth(pcbnew.FromMM(w)); board.Add(s)


def arc(c, r, a0, a1, layer, w=0.12):
    pt = lambda a: (c[0] + r * math.cos(math.radians(a)), c[1] + r * math.sin(math.radians(a)))
    s = pcbnew.PCB_SHAPE(board); s.SetShape(pcbnew.SHAPE_T_ARC)
    s.SetArcGeometry(K(*pt(a0)), K(*pt((a0 + a1) / 2)), K(*pt(a1)))
    s.SetLayer(layer); s.SetWidth(pcbnew.FromMM(w)); board.Add(s)


def rrect(x0, y0, w, d, r, layer, lw=0.12):
    x1, y1 = x0 + w, y0 + d
    if r <= 0:
        for a, b in [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)), ((x1, y1), (x0, y1)), ((x0, y1), (x0, y0))]:
            seg(a, b, layer, lw)
        return
    seg((x0 + r, y0), (x1 - r, y0), layer, lw); seg((x1, y0 + r), (x1, y1 - r), layer, lw)
    seg((x1 - r, y1), (x0 + r, y1), layer, lw); seg((x0, y1 - r), (x0, y0 + r), layer, lw)
    arc((x1 - r, y0 + r), r, -90, 0, layer, lw); arc((x1 - r, y1 - r), r, 0, 90, layer, lw)
    arc((x0 + r, y1 - r), r, 90, 180, layer, lw); arc((x0 + r, y0 + r), r, 180, 270, layer, lw)


def text(s, x, y, layer, size=1.2):
    t = pcbnew.PCB_TEXT(board); t.SetText(s); t.SetPosition(K(x, y)); t.SetLayer(layer)
    t.SetTextSize(pcbnew.VECTOR2I(pcbnew.FromMM(size), pcbnew.FromMM(size)))
    t.SetTextThickness(pcbnew.FromMM(size * 0.15)); board.Add(t)


# ---- case outline (Edge.Cuts) and inner wall (Cmts.User) ----
rrect(in_x0 - wall, in_y0 - wall, in_x1 - in_x0 + 2 * wall, in_y1 - in_y0 + 2 * wall, outer_r, pcbnew.Edge_Cuts, 0.1)
rrect(in_x0, in_y0, in_x1 - in_x0, in_y1 - in_y0, max(0.5, outer_r - wall), pcbnew.Cmts_User)
text("CASE INNER WALL", (in_x0 + in_x1) / 2, in_y0 + 1.2, pcbnew.Cmts_User, 1.0)
text("FRONT: relay wire windows + USB power", (in_x0 + in_x1) / 2, in_y1 + wall + 2, pcbnew.Cmts_User, 1.2)
text("REAR: ribbon cable notch", (in_x0 + in_x1) / 2, in_y0 - wall - 2, pcbnew.Cmts_User, 1.2)

# ---- relay board (Dwgs.User / Eco1.User) ----
rrect(hat_x0, hat_y0, hat_w, hat_d, board_r, pcbnew.Dwgs_User)
text(f"Waveshare RPi Relay Board {hat_w} x {hat_d}", 29, hat_y0 + hat_d - 15, pcbnew.Dwgs_User, 1.2)
for i, (a, b) in enumerate(term_x):
    rrect(a, hat_y0 + hat_d - 13.6, b - a, 11.2, 0, pcbnew.Eco1_User)          # screw terminal
    rrect(a, hat_y0 + hat_d - 33.7, b - a, 19.5, 0, pcbnew.Eco1_User)          # relay body
    text(f"CH{i + 1}", (a + b) / 2, hat_y0 + hat_d - 24, pcbnew.Eco1_User, 1.5)
for x, y, w, d in leds:
    rrect(x - w / 2, y - d / 2, w, d, 0, pcbnew.Eco1_User, 0.08)
rrect(29 - 27.95, -3.2, 55.9, 6.4, 0, pcbnew.Eco1_User)                         # IDC ribbon socket
text("40-way IDC ribbon socket", 29, -5, pcbnew.Eco1_User, 1.0)

# ---- Pi Zero from KiCad's library, oriented into the case frame ----
fp = pcbnew.FootprintLoad(str(LIBROOT / "Module.pretty"), "Raspberry_Pi_Zero_Socketed_THT_FaceDown_MountingHoles")
board.Add(fp)
target = {"1": (53.13, 1.27), "2": (53.13, -1.27), "40": (4.87, -1.27)}


def pad_pos(num):
    p = next(p for p in fp.Pads() if p.GetNumber() == num).GetPosition()
    return pcbnew.ToMM(p.x) - ORIGIN[0], ORIGIN[1] - pcbnew.ToMM(p.y)


best = None
for flip in (False, True):
    for rot in (0, 90, 180, 270):
        f2 = pcbnew.FootprintLoad(str(LIBROOT / "Module.pretty"), "Raspberry_Pi_Zero_Socketed_THT_FaceDown_MountingHoles")
        board.Remove(fp); fp = f2; board.Add(fp)
        fp.SetPosition(K(0, 0))
        if flip:
            fp.Flip(K(0, 0), pcbnew.FLIP_DIRECTION_LEFT_RIGHT)
        fp.SetOrientationDegrees(rot)
        dx, dy = (target["1"][0] - pad_pos("1")[0], target["1"][1] - pad_pos("1")[1])
        fp.SetPosition(K(dx, dy))
        err = sum(math.dist(pad_pos(n), t) for n, t in target.items())
        if best is None or err < best[0]:
            best = (err, flip, rot, dx, dy)
err, flip, rot, dx, dy = best
board.Remove(fp)
fp = pcbnew.FootprintLoad(str(LIBROOT / "Module.pretty"), "Raspberry_Pi_Zero_Socketed_THT_FaceDown_MountingHoles")
board.Add(fp); fp.SetPosition(K(0, 0))
if flip:
    fp.Flip(K(0, 0), pcbnew.FLIP_DIRECTION_LEFT_RIGHT)
fp.SetOrientationDegrees(rot); fp.SetPosition(K(dx, dy)); fp.SetReference("PI1")
holes = sorted((round(pcbnew.ToMM(p.GetPosition().x) - ORIGIN[0], 2), round(ORIGIN[1] - pcbnew.ToMM(p.GetPosition().y), 2))
               for p in fp.Pads() if p.GetAttribute() == pcbnew.PAD_ATTRIB_NPTH)
print(f"Pi Zero footprint placed: flip={flip} rot={rot} pin error={err:.3f} mm")
print(f"  Pi Zero holes in case frame: {holes}")
expected = sorted([(0.0, 0.0), (hole_dx, 0.0), (0.0, pi_hole_dy), (hole_dx, pi_hole_dy)])
if any(math.dist(a, b) > 0.02 for a, b in zip(holes, expected)):
    sys.exit(f"  hole mismatch! expected {expected}")
print("  holes match the .scad hole pattern")

# ---- relay board's far-side mounting holes (library footprint) ----
for i, (x, y) in enumerate([(0, hat_hole_dy), (hole_dx, hat_hole_dy)]):
    mh = pcbnew.FootprintLoad(str(LIBROOT / "MountingHole.pretty"), "MountingHole_2.7mm_M2.5")
    mh.SetPosition(K(x, y)); mh.SetReference(f"H{i + 1}"); board.Add(mh)

board.GetDesignSettings().SetAuxOrigin(K(0, 0))
board.GetDesignSettings().SetGridOrigin(K(0, 0))
pcbnew.SaveBoard(str(OUT), board)
print(f"Saved {OUT.name}")
