# SPDX-License-Identifier: Apache-2.0
"""Where each board sits and turns, and where its pads are: one frame for the wire router, the checks and the site.

A board's *local frame* is the frame its pads are given in:
  - a board with a vendor model: the model's own frame, as in vendor/models/<key>/<key>_pcb.stl (min corner at 0);
  - any other part: its layout box, from the box's min corner, components up.
`frame(c)` maps local → world for a layout.json comp. It is the frame the site draws the vendor model in (the site
reads it from wires.json, so the two can't drift), and `review_geometry`-style checks test it against the packer's
mounting holes: a frame that put the holes anywhere but on their bosses would draw the board, and its pads, wrong.

Pads: PADS[key] lists each pad or pin a wire or cable reaches: its silkscreen name, local x, y, the face it is soldered
from, its kind (th: a through-hole pad; smd: a surface pad; plug: a connector a cable plugs into, with the direction it
opens; lead: a wire or leg that comes off the part), and its source. Pads come from the vendor's own board files where
they exist (Eagle .brd, STEP); where they don't, the source says how they were placed.
"""
from __future__ import annotations

import math

import numpy as np

# ---------------------------------------------------------------- vendor models (site/build.py embeds the same keys)
VENDOR = {
    "radxa pcb": "radxa_zero3e",
    "bq24074 charger (Adafruit 4755)": "charger_bq24074",
    "USB-C breakout (charge port)": "usbc_4090",
    "5 V boost U3V40F5": "boost_u3v40f5",
    "mic head (SPH0645)": "mic_sph0645",
    "mic tail (SPH0645)": "mic_sph0645",
    "NFC tag ST25DV64KC": "nfc_st25dv",
    "ATECC608 secure element": "atecc608",
}
# each vendor model's size (x, y) in its own frame, from its STLs (pcb ∪ parts)
VENDOR_XY = {"radxa_zero3e": (65.0, 31.51), "charger_bq24074": (41.33, 33.02), "usbc_4090": (20.32, 13.97),
             "boost_u3v40f5": (15.24, 15.24), "mic_sph0645": (16.51, 12.7), "nfc_st25dv": (25.4, 25.4), "atecc608": (25.4, 17.78),
             "ideal diode LM66200 (Adafruit 5830)": (16.5, 10.0)}   # a part drawn from its box, whose box the packer may turn
# vendor-frame offset applied before anything else: the Radxa's model starts 1.5 mm before its PCB in y and has the
# board's underside parts below z 3.66 (vendor/models/radxa_zero3e/radxa_zero3e_info.json, pcb_bounds)
VENDOR_SHIFT = {"radxa_zero3e": (0.0, -1.5, -3.66)}
# a board on symmetric holes can be fitted either way round: the way that suits its wires (ADR-0021). The clock turned
# 180° puts its header on its +Y edge, toward the lid connector and the hinge (Rafa: "spin the real-time clock 180 degrees
# so the cables flow easier")
TURN_OF = {"RTC PCF8523 + CR1220 (STEMMA QT)": 180}
FLIPPED = ("SPH0645",)   # mounted upside down: the mics hang from the roof with their port facing it (ADR-0018)


def key_of(name: str) -> str:
    return VENDOR.get(name, name)


def frame(c: dict) -> np.ndarray:
    """4 × 4 matrix, a board's local frame → world, for layout.json comp c."""
    (sx, sy, sz), at = c["size"], c["at"]
    k = key_of(c["name"])
    M = np.eye(4)
    turn = (c.get("mount") or {}).get("turn", TURN_OF.get(c["name"]))
    if turn is not None:                                             # the packer says which way round it put the board
        T = {0: [[1, 0, 0, 0], [0, 1, 0, 0]], 90: [[0, -1, 0, sx], [1, 0, 0, 0]], 180: [[-1, 0, 0, sx], [0, -1, 0, sy]],
             270: [[0, 1, 0, 0], [-1, 0, 0, sy]]}[turn]
        M = np.array(T + [[0, 0, 1, 0], [0, 0, 0, 1]], float) @ M
    elif k in VENDOR_XY:
        M[:3, 3] = VENDOR_SHIFT.get(k, (0, 0, 0))
        vx, vy = VENDOR_XY[k]
        if (vx > vy) != (sx > sy) and abs(vx - vy) > 1:            # placed turned 90°: (x, y) → (sx − y, x)
            T = np.array([[0, -1, 0, sx], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], float)
            M = T @ M
    if any(f in c["name"] for f in FLIPPED):                         # turned over about x: (x, y, z) → (x, sy − y, sz − z)
        M = np.array([[1, 0, 0, 0], [0, -1, 0, sy], [0, 0, -1, sz], [0, 0, 0, 1]], float) @ M
    W = np.eye(4); W[:3, 3] = at
    return W @ M


def apply(M: np.ndarray, p) -> list:
    return [float(v) for v in (M @ np.array([p[0], p[1], p[2], 1.0]))[:3]]


def direction(M: np.ndarray, d) -> list:
    v = M[:3, :3] @ np.array(d, float)
    return [float(x) for x in v / (np.linalg.norm(v) or 1)]


# ---------------------------------------------------------------- pads
# PCB faces in each local frame (z of the bottom and top of the board). Vendor models: from their info.json pcb_z
# (the Radxa's after its shift). Every other part: its box, a 1.6 mm board at the bottom, as the site draws it.
PCB_Z = {"radxa_zero3e": (3.66, 5.26), "charger_bq24074": (0.0, 1.57), "usbc_4090": (0.0, 1.57), "boost_u3v40f5": (0.0, 1.02),
         "mic_sph0645": (0.05, 1.55), "nfc_st25dv": (0.0, 1.6), "atecc608": (0.0, 1.6)}
DIRS = {"top": (0, 0, 1), "bottom": (0, 0, -1), "+x": (1, 0, 0), "-x": (-1, 0, 0), "+y": (0, 1, 0), "-y": (0, -1, 0)}
LIFT = {"th": 2.5, "smd": 2.5, "plug": 4.5, "lead": 2.0, "pin": 2.0}   # how far a wire runs straight off its pad before it turns

# PADS[key][pad] = (x, y, z or a face, the side it is soldered from (or a plug opens to), kind, silkscreen label, source).
# z: "top"/"bottom" puts the pad on that face of the board; a number is a local z. Filled below, per board.
PADS: dict[str, dict[str, tuple]] = {}
TITLE = {"radxa pcb": "Radxa ZERO 3E", "bq24074 charger (Adafruit 4755)": "charger (Adafruit 4755)", "USB-C breakout (charge port)": "USB-C port (Adafruit 4090)",
         "5 V boost U3V40F5": "5 V boost (Pololu U3V40F5)", "power switch Pololu 2810": "power switch module (Pololu 2810)",
         "ideal diode LM66200 (Adafruit 5830)": "ideal diode (Adafruit 5830)", "mic head (SPH0645)": "head mic (SPH0645)", "mic tail (SPH0645)": "tail mic (SPH0645)",
         "NFC tag ST25DV64KC": "NFC tag (SparkFun ST25DV64KC)", "ATECC608 secure element": "secure element (Adafruit 4314)",
         "RTC PCF8523 + CR1220 (STEMMA QT)": "clock (Adafruit 5189)", "NFC icon LED (NeoPixel Mini)": "tap-icon LED (NeoPixel Mini)",
         "vibration disc (ERM)": "vibration motor (Adafruit 1201)", "power switch": "power slide switch", "mic switch": "mic slide switch",
         "eye switch (tactile, in cradle)": "eye button (tactile switch)", "mic-live dot (sequin)": "eye light (LED sequin)",
         "lid connector (12-way)": "lid connector (Harwin G125)", "battery": "battery", "strip": "light-ring strip, DIN end"}


class PadEnd:
    """A wire's end: the pad it is soldered to (or the socket it plugs into), in world coordinates."""
    def __init__(self, part, pad, label, at, d, kind, src):
        self.part, self.pad, self.label, self.at, self.dir, self.kind, self.src = part, pad, label, at, d, kind, src
        self.lift = LIFT.get(kind, 2.0)
        self.joint = {"plug": "plug", "lead": "solder (wire to wire, heat-shrink)", "pin": "solder (wire to leg)"}.get(kind, "solder")


def _strip_frame(L):
    """The strip's DIN end: local x along the strip (towards the first LED), y into the cavity, z up."""
    x, y, a = L["strip"]["leds"][0][:3]
    t = np.array([math.cos(a), math.sin(a), 0.0])
    inward = np.array([-x, -y, 0.0]); inward /= np.linalg.norm(inward)
    if np.dot(t[:2], np.array(L["strip"]["leds"][1][:2]) - np.array([x, y])) < 0:
        t = -t
    M = np.eye(4)
    M[:3, 0], M[:3, 1], M[:3, 2] = t, inward, (0, 0, 1)
    M[:3, 3] = (x, y, L["strip"]["z"])
    return M


def comp_of(L, part):
    return next((c for c in L["comps"] if c["name"] == part), None)


def frame_of(L, part):
    return _strip_frame(L) if part == "strip" else frame(comp_of(L, part))


def local_pad(L, part, pad):
    key = "strip" if part == "strip" else key_of(part)
    x, y, z, side, kind, label, src = PADS[key][pad]
    if isinstance(z, str):
        z0, z1 = PCB_Z.get(key, (0.0, 1.6))
        z = z1 if z == "top" else z0
    return (x, y, z), DIRS[side], kind, label, src


def pad_end(L, part, pad) -> PadEnd:
    p, d, kind, label, src = local_pad(L, part, pad)
    M = frame_of(L, part)
    return PadEnd(part, pad, label, [round(v, 3) for v in apply(M, p)], [round(v, 4) for v in direction(M, d)], kind, src)


def boards_json(L, users: dict) -> dict:
    """For the site: each board's frame (local → its box, row-major) and every pad, world-placed, with what joins it."""
    out = {}
    names = [c["name"] for c in L["comps"] if key_of(c["name"]) in PADS] + (["strip"] if "strip" in PADS else [])
    for n in names:
        M = frame_of(L, n)
        c = comp_of(L, n)
        rel = M.copy()
        if c:
            rel[:3, 3] -= np.array(c["at"])
        key = "strip" if n == "strip" else key_of(n)
        pads = []
        for pad in PADS[key]:
            e = pad_end(L, n, pad)
            w = users.get((n, pad), [])
            pads.append({"name": pad, "label": e.label, "at": e.at, "dir": e.dir, "kind": e.kind, "used": bool(w), "wires": w, "src": e.src})
        rec = {"title": TITLE.get(n, n), "frame": [round(float(v), 5) for v in rel.flatten()], "pads": pads}
        if key in BODY:
            rec["body"] = BODY[key]
        out[n] = rec
    return out




# ---------------------------------------------------------------- the pad tables
A_CH = "Adafruit-BQ24074-PCB, Adafruit_BQ24074.brd (github.com/adafruit/Adafruit-BQ24074-PCB); model x = brd x + 3.23"
A_USB = "Adafruit-USB-C-Downstream-Breakout .brd, rev B (github.com/adafruit/Adafruit-USB-C-Downstream-Breakout)"
A_MIC = ("Adafruit I2S mic breakout .brd (github.com/adafruit/Adafruit-I2S-Microphone-Breakout-PCB): the real 3421 carries the mic on its "
         "underside; it hangs port-up, turned 180°, and the pads are placed for that")
A_ID = "Adafruit-LM66200-PCB .brd (github.com/adafruit/Adafruit-LM66200-PCB)"
A_RTC = "Adafruit-PCF8523-RTC-Breakout-PCB, 'Adafrruit PCF8523 QT.brd' (github.com/adafruit/Adafruit-PCF8523-RTC-Breakout-PCB)"
A_SE = "Adafruit-ATECC608-PCB .brd (vendor/models/atecc608/src.brd); model = brd + (6.096, 14.542)"
S_NFC = "SparkFun_Qwiic_RFID_Tag_ST25DV64KC .brd (vendor/models/nfc_st25dv/src.brd); labels read from the label shapes' names"
P_BO = "Pololu 4012: holes from its STEP (vendor/models/boost_u3v40f5), names from Pololu's connection photo and dimension PDF (0J1844)"
P_PO = "Pololu 2810: holes from Pololu's STEP and dimension PDF; names from Pololu's product page"
RADXA = "Radxa ZERO 3E DXF (vendor/radxa-zero-3e/dxf): pin 1 at the low-x end of the inner row; functions from Radxa's pinout table"
FN = {1: "3V3", 2: "5V", 3: "SDA", 5: "SCL", 6: "GND", 9: "GND", 12: "I2S BCLK", 14: "GND", 16: "GPIO", 17: "3V3", 18: "GPIO",
      19: "SPI MOSI", 20: "GND", 35: "I2S LRCK", 38: "I2S SDI", 39: "GND"}
PADS["radxa_zero3e"] = {f"pin {n}": (8.32 + ((n - 1) // 2) * 2.54, 1.5 + 25.405 + ((n - 1) % 2) * 2.54, "top", "top", "th",
                                     f"{n}" + (f" · {FN[n]}" if n in FN else ""), RADXA) for n in range(1, 41)}

ch = lambda x: x + 3.23159
PADS["charger_bq24074"] = {
    **{nm: (ch(x), 2.54, "top", "top", "th", lb, A_CH) for nm, x, lb in (
        ("GND 3", 31.75, "GND"), ("LIPO", 29.21, "LIPO"), ("GND 2", 26.67, "GND"), ("OUT", 24.13, "OUT"), ("CHG", 21.59, "CHG"),
        ("PGOOD", 19.05, "PGOOD"), ("CE", 16.51, "CE"), ("ISET", 13.97, "ISET"), ("THERM", 11.43, "THERM"), ("GND", 8.89, "GND"),
        ("VBUS", 6.35, "VBUS"))},
    # "LiPo Batt" and "Load Out": JST-PH, opening +x; pin 1 +, pin 2 −
    "BATT +": (ch(37.54), 9.795, 4.0, "+x", "plug", "LiPo Batt +", A_CH), "BATT −": (ch(37.54), 11.795, 4.0, "+x", "plug", "LiPo Batt −", A_CH),
    "LOAD +": (ch(37.54), 21.225, 4.0, "+x", "plug", "Load Out +", A_CH), "LOAD −": (ch(37.54), 23.225, 4.0, "+x", "plug", "Load Out −", A_CH),
    "THERM jumper": (ch(15.37), 6.35, "top", "top", "smd", "THM jumper: cut", A_CH),
}
PADS["usbc_4090"] = {nm: (x, 2.54, "top", "top", "th", nm, A_USB) for nm, x in (
    ("GND", 19.05), ("VBUS", 16.51), ("SBU2", 13.97), ("CC1", 11.43), ("D-", 8.89), ("D+", 6.35), ("SBU1", 3.81), ("CC2", 1.27))}
PADS["boost_u3v40f5"] = {nm: (x, 1.27, "top", "top", "th", lb, P_BO) for nm, x, lb in (
    ("EN", 1.27, "EN"), ("VIN", 3.81, "VIN"), ("VIN 2", 6.35, "VIN"), ("GND", 8.89, "GND"), ("GND 2", 11.43, "GND"), ("VOUT", 13.97, "VOUT"))}
PADS["power switch Pololu 2810"] = {
    **{nm: (1.27, y, "top", "top", "th", lb, P_PO) for nm, y, lb in (("VIN", 13.97, "VIN"), ("VIN 2", 11.43, "VIN"), ("GND", 8.89, "GND"), ("GND 2", 6.35, "GND"), ("ON", 3.81, "ON"))},
    **{nm: (13.97, y, "top", "top", "th", lb, P_PO) for nm, y, lb in (("VOUT", 13.97, "VOUT"), ("VOUT 2", 11.43, "VOUT"), ("GND 3", 8.89, "GND"), ("GND 4", 6.35, "GND"), ("GND 5", 3.81, "GND"), ("SW", 1.27, "SW"))},
    **{nm: (x, 5.715, "top", "top", "th", lb, P_PO + "; which outer hole is NC and which GND is unverified") for nm, x, lb in (("ext NC", 5.08, "NC"), ("ext SW", 7.62, "SW"), ("ext GND", 10.16, "GND"))},
}
PADS["ideal diode LM66200 (Adafruit 5830)"] = {nm: (x, 2.54, "top", "top", "th", lb, A_ID) for nm, x, lb in (
    ("VIN 1", 1.905, "VIN 1"), ("VIN 2", 4.445, "VIN 2"), ("GND", 6.985, "GND"), ("Vout", 9.525, "Vout"), ("On", 12.065, "On (active low)"), ("Sta", 14.605, "Sta"))}
# the mic proxy is drawn turned over (frame()), so a pad at brd x sits at local 17.526 − x; its "top" faces the cavity
PADS["mic_sph0645"] = {nm: (17.526 - x, 2.54, "top", "top", "th", nm, A_MIC) for nm, x in (
    ("3V", 15.621), ("GND", 13.081), ("BCLK", 10.541), ("DOUT", 8.001), ("LRCL", 5.461), ("SEL", 2.921))}
QT_Z = 2.6   # the middle of a JST-SH socket's mouth above the board
PADS["atecc608"] = {
    **{nm: (x, 2.54, "top", "top", "th", nm, A_SE) for nm, x in (("Vcc", 8.89), ("GND", 11.43), ("SCL", 13.97), ("SDA", 16.51))},
    "QT left": (0.46, 8.89, QT_Z, "-x", "plug", "STEMMA QT", A_SE), "QT right": (24.95, 8.89, QT_Z, "+x", "plug", "STEMMA QT", A_SE)}
PADS["RTC PCF8523 + CR1220 (STEMMA QT)"] = {
    **{nm: (x, 2.54, "top", "top", "th", nm, A_RTC) for nm, x in (("VIN", 7.62), ("GND", 10.16), ("SCL", 12.7), ("SDA", 15.24), ("SQW", 17.78))},
    "QT left": (0.45, 8.84, QT_Z + 1.6, "-x", "plug", "STEMMA QT", A_RTC), "QT right": (24.95, 8.94, QT_Z + 1.6, "+x", "plug", "STEMMA QT", A_RTC)}
# the tag hangs components-up under the roof (its holes say so); its header is soldered from below
PADS["nfc_st25dv"] = {
    **{nm: (x, 1.27, "bottom", "bottom", "th", lb, S_NFC) for nm, x, lb in (("GND", 6.35, "GND"), ("3V3", 8.89, "3V3"), ("SDA", 11.43, "SDA"), ("SCL", 13.97, "SCL"), ("GPO", 16.51, "GPO (active low)"), ("VEH", 19.05, "VEH"))},
    "Qwiic J2": (0.48, 8.89, QT_Z, "-x", "plug", "Qwiic", S_NFC), "Qwiic J1": (24.92, 8.89, QT_Z, "+x", "plug", "Qwiic", S_NFC)}
SEQ = "Adafruit LED Sequin .brd (vendor/models/sequin/src.brd): + and − are plated vias in bare copper at ±3.556 mm"
PADS["mic-live dot (sequin)"] = {"+": (4.5 - 3.556, 2.0, 0.0, "bottom", "th", "+", SEQ), "−": (4.5 + 3.556, 2.0, 0.0, "bottom", "th", "−", SEQ)}
NEO = "Adafruit 1612 product photos (no board file published): six pads on the back, 2 columns 4.0 mm apart, rows 2.0 mm apart; unverified"
PADS["NFC icon LED (NeoPixel Mini)"] = {nm: (4.55 + dx, 4.55 + dy, 0.0, "-x" if dx < 0 else "+x", "smd", lb, NEO + "; its wires are tacked flat across the back, under the strap's foam") for nm, dx, dy, lb in (
    ("+5V", -2.0, 2.0, "5V"), ("DIN", -2.0, 0.0, "Din"), ("GND", -2.0, -2.0, "gnd"), ("+5V out", 2.0, 2.0, "+"), ("DOUT", 2.0, 0.0, "Dout"), ("GND out", 2.0, -2.0, "−"))}
STR = "Adafruit 2969 photo: three pads at each cut line about 2.2 mm apart, on the LED face; which is which is unverified (check the silkscreen)"
PADS["strip"] = {nm: (-3.47, 0.6, z, "+y", "smd", nm, STR) for nm, z in (("+5V", 2.2), ("DIN", 0.0), ("GND", -2.2))}
CK = ("C&K OS102011MS2QN1 datasheet (OS series, p. I-37, 8 Aug 17): 3 pins at 2.00 mm, 2 is common, 2.80 mm out of the body, "
      "trimmed to 1.5 mm; the switch lies on its back, so they point into the case and the wires rise from their tips")
LEG = {"power switch": 1.5, "mic switch": 1.5}   # a pad at the tip of a leg this long: the leg itself must fit too
for sw in ("power switch", "mic switch"):
    PADS[sw] = {nm: (4.3 + dx, 4.0 + 1.5, 2.15, "top", "pin", lb, CK) for nm, dx, lb in (("1", -2.0, "pin 1"), ("COM", 0.0, "pin 2 (common)"), ("3", 2.0, "pin 3"))}
DTS = "Diptronics DTS-6 datasheet: legs 6.5 × 4.5 mm; the two legs 6.5 mm apart are joined, the switch closes across the 4.5 mm"
PADS["eye switch (tactile, in cradle)"] = {nm: (3.8 + dx, 3.8 + dy, 0.0, "bottom", "pin", f"leg {nm}", DTS) for nm, dx, dy in (
    ("1", -3.25, -2.25), ("2", 3.25, -2.25), ("3", -3.25, 2.25), ("4", 3.25, 2.25))}
ERMS = ("Adafruit 1201: red is +, blue is −, both leave through one tab (Adafruit, maker datasheet); the PN2222A rides in heat-shrink on "
        "the blue lead (wiring.md §5). Where the tab points is the builder's choice: here, towards the Radxa")
PADS["vibration disc (ERM)"] = {nm: (5.0 + dx, -1.0, 1.35, "-y", "lead", lb, ERMS) for nm, dx, lb in (
    ("+", -2.0, "red lead (+)"), ("Q1 B", 0.0, "PN2222A base"), ("Q1 E", 2.0, "PN2222A emitter"))}
BATS = "EREMIT 4000 mAh: where its JST-PH lead and the taped thermistor leave the cell is the builder's choice: here, the end by the charger (unverified)"
PADS["battery"] = {"+": (91.0, 25.5, 6.0, "+x", "lead", "cell lead + (red)", BATS), "−": (91.0, 27.5, 6.0, "+x", "lead", "cell lead −", BATS),
                   "RT1 a": (91.0, 38.0, 6.0, "+x", "lead", "thermistor", BATS), "RT1 b": (91.0, 40.0, 6.0, "+x", "lead", "thermistor", BATS)}
# ADR-0020: the Harwin G125 pair, mated, lying flat in its box: 9.6 long (the mating axis, x) × 11.45 wide × 4.9 deep.
# Contacts 1–6 on one row, 7–12 on the other, 1 opposite 7, 1.25 mm apart both ways (Harwin C125 spec, Appendix 1);
# 26 AWG PTFE pigtails leave straight out of each rear face: the lid half at −x, the chassis half at +x. All 24 are white.
HAR = "Harwin G125 (C125 spec, Appendix 1): contacts 1–6 and 7–12 in two rows, 1 opposite 7; every pigtail is white 26 AWG PTFE: label them"
LID_LINES = {1: "GND", 2: "mic rail", 3: "BCLK", 4: "LRCL", 5: "DOUT", 6: "eye", 7: "icon 5 V", 8: "icon DIN", 9: "icon DOUT", 10: "3V3", 11: "SDA", 12: "SCL"}
_hb = (22.0 - 9.6) / 2
PADS["lid connector (12-way)"] = {
    f"{end}{n}": (_hb if end == "l" else 22.0 - _hb, 6.0 + (((n - 1) % 6) - 2.5) * 1.25, 3.0 + (0.625 if n <= 6 else -0.625),
                  "-x" if end == "l" else "+x", "lead", f"{n} · {LID_LINES[n]}", HAR)
    for end in ("l", "c") for n in range(1, 13)}
BODY = {"lid connector (12-way)": {"at": [_hb, (12.0 - 11.45) / 2, 3.0 - 2.45], "size": [9.6, 11.45, 4.9]}}
