#!/usr/bin/env python3
"""Clean-sheet renderer for the SN65BC184 USB-C RS-485 interface.

This renderer intentionally uses real component symbols and connected wires,
with off-page tags only where signal flow crosses a functional block.
"""

from __future__ import annotations

import argparse
import csv
import os
from dataclasses import dataclass
from pathlib import Path

os.environ.setdefault("MPLCONFIGDIR", "/tmp/schemdraw-matplotlib-cache")

import schemdraw
import schemdraw.elements as elm

ROOT = Path(__file__).resolve().parent
INK = "#1b2430"
GRID = "#dce3ea"
MUTED = "#667085"
ACCENT = "#155eef"
POWER = "#b42318"
GROUND = "#344054"


@dataclass(frozen=True)
class PinDef:
    number: str
    name: str
    side: str


@dataclass(frozen=True)
class Component:
    ref: str
    value: str
    package: str
    description: str
    pins: tuple[PinDef, ...]


def pins(*items: tuple[str, str, str]) -> tuple[PinDef, ...]:
    return tuple(PinDef(*item) for item in items)


COMPONENTS: tuple[Component, ...] = (
    Component(
        "J1",
        "USB-C 5V",
        "16-pin receptacle",
        "USB Type-C sink, 5 V power only",
        pins(
            ("A4/B4/A9/B9", "VBUS", "right"),
            ("A5", "CC1", "right"),
            ("B5", "CC2", "right"),
            ("A6/B6", "DP", "right"),
            ("A7/B7", "DN", "right"),
            ("SHELL", "SHIELD", "right"),
            ("A1/B1/A12/B12", "GND", "right"),
        ),
    ),
    Component("F1", "0.5A", "PTC 6V", "Resettable VBUS fuse", pins(("1", "IN", "left"), ("2", "OUT", "right"))),
    Component("D1", "SMBJ5.0A", "SMB", "VBUS transient clamp", pins(("1", "K", "top"), ("2", "A", "bottom"))),
    Component("C1", "10uF", "0805", "VBUS bulk capacitor", pins(("1", "1", "top"), ("2", "2", "bottom"))),
    Component("C2", "100nF", "0603", "VBUS bypass", pins(("1", "1", "top"), ("2", "2", "bottom"))),
    Component(
        "U1",
        "AMS1117-3.3",
        "SOT-223",
        "1 A fixed 3.3 V LDO; Pin 4/tab is electrically VOUT",
        pins(
            ("3", "VIN", "left"),
            ("2", "VOUT", "right"),
            ("1", "GND", "bottom"),
            ("4/TAB", "VOUT_TAB", "top"),
        ),
    ),
    Component("C3", "22uF", "3528 tantalum", "AMS1117 output stability capacitor", pins(("1", "1", "top"), ("2", "2", "bottom"))),
    Component("C4", "100nF", "0603", "LDO output high-frequency bypass", pins(("1", "1", "top"), ("2", "2", "bottom"))),
    Component("R5", "1k", "0603", "Power LED current limit", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("D3", "GREEN", "0603", "3.3 V power LED", pins(("1", "A", "left"), ("2", "K", "right"))),
    Component("R1", "1M", "0603", "USB shield to GND", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("C7", "4.7nF", "0603 1kV", "USB shield AC return", pins(("1", "1", "top"), ("2", "2", "bottom"))),
    Component("R2", "5.1k", "0603", "USB-C CC1 Rd", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("R3", "5.1k", "0603", "USB-C CC2 Rd", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component(
        "J2",
        "LVTTL",
        "1x5 2.54mm",
        "Logic-side interface connector",
        pins(
            ("1", "GND", "right"),
            ("2", "V33", "right"),
            ("3", "TXD", "right"),
            ("4", "RXD", "right"),
            ("5", "DIR", "right"),
        ),
    ),
    Component(
        "U3",
        "74LVC1G04",
        "SC-70-5",
        "Direction inverter",
        pins(
            ("1", "A", "left"),
            ("4", "Y", "right"),
            ("3", "NC", "right"),
            ("5", "VCC", "top"),
            ("2", "GND", "bottom"),
        ),
    ),
    Component("C6", "100nF", "0603", "U3 bypass", pins(("1", "1", "top"), ("2", "2", "bottom"))),
    Component("R4", "10k", "0603", "DIR power-up pulldown", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component(
        "U2",
        "SN65BC184",
        "SOIC-8",
        "3.3 V half-duplex RS-485 transceiver",
        pins(
            ("1", "RO", "left"),
            ("2", "RE_N", "left"),
            ("3", "DE", "left"),
            ("4", "DI", "left"),
            ("8", "VCC", "top"),
            ("5", "GND", "bottom"),
            ("6", "A", "right"),
            ("7", "B", "right"),
        ),
    ),
    Component("C5", "100nF", "0603", "U2 bypass at VCC/GND", pins(("1", "1", "top"), ("2", "2", "bottom"))),
    Component("R6", "10R", "0603 pulse-proof", "A-line series resistor", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("R7", "10R", "0603 pulse-proof", "B-line series resistor", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component(
        "D2",
        "PESD2CAN",
        "SOT-23",
        "RS-485 connector ESD clamp",
        pins(("1", "A", "top"), ("2", "B", "top"), ("3", "GND", "bottom")),
    ),
    Component(
        "J3",
        "RS-485",
        "3-pin 5.08mm",
        "A(+), B(-), signal ground",
        pins(("1", "A", "left"), ("2", "B", "left"), ("3", "GND", "left")),
    ),
    Component("R8", "120R", "0603", "Optional bus termination", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("JP1", "TERM", "1x2 2.54mm", "End-node termination jumper", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("R9", "560R", "0603", "Fail-safe bias pull-up", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("JP2", "BIAS_A", "1x2 2.54mm", "Enable fail-safe bias to A", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("R10", "560R", "0603", "Fail-safe bias pull-down", pins(("1", "1", "left"), ("2", "2", "right"))),
    Component("JP3", "BIAS_B", "1x2 2.54mm", "Enable fail-safe bias to B", pins(("1", "1", "left"), ("2", "2", "right"))),
)

PART_BY_REF = {component.ref: component for component in COMPONENTS}

NETS: dict[str, tuple[str, ...]] = {
    "VBUS_RAW": ("J1.VBUS", "F1.IN"),
    "+5V": ("F1.OUT", "D1.K", "C1.1", "C2.1", "U1.VIN"),
    "+3V3": ("U1.VOUT", "U1.VOUT_TAB", "C3.1", "C4.1", "R5.1", "C5.1", "C6.1", "U2.VCC", "U3.VCC", "J2.V33", "R9.1"),
    "GND": (
        "J1.GND", "D1.A", "C1.2", "C2.2", "U1.GND", "C3.2", "C4.2", "D3.K", "R1.2", "C7.2",
        "R2.2", "R3.2", "J2.GND", "U3.GND", "C6.2", "R4.2", "U2.GND", "C5.2", "D2.GND",
        "J3.GND", "JP3.2",
    ),
    "CC1": ("J1.CC1", "R2.1"),
    "CC2": ("J1.CC2", "R3.1"),
    "USB_SHIELD": ("J1.SHIELD", "R1.1", "C7.1"),
    "PWR_LED": ("R5.2", "D3.A"),
    "RO_3V3": ("U2.RO", "J2.RXD"),
    "DI_3V3": ("U2.DI", "J2.TXD"),
    "DIR_3V3": ("J2.DIR", "U2.DE", "U3.A", "R4.1"),
    "RX_EN_N": ("U2.RE_N", "U3.Y"),
    "U2_A_NODE": ("U2.A", "R6.1"),
    "U2_B_NODE": ("U2.B", "R7.1"),
    "BUS_A": ("R6.2", "R8.1", "JP2.2", "D2.A", "J3.A"),
    "BUS_B": ("R7.2", "JP1.2", "R10.1", "D2.B", "J3.B"),
    "TERM_SW": ("R8.2", "JP1.1"),
    "BIAS_A_SW": ("R9.2", "JP2.1"),
    "BIAS_B_SW": ("R10.2", "JP3.1"),
}

NO_CONNECTS = ("J1.DP", "J1.DN", "U3.NC")


def validate_design() -> list[str]:
    errors: list[str] = []
    pin_aliases: dict[str, str] = {}
    logical_pins: set[str] = set()
    for component in COMPONENTS:
        for pin in component.pins:
            logical_id = f"{component.ref}.{pin.number}"
            logical_pins.add(logical_id)
            pin_aliases[logical_id] = logical_id
            pin_aliases[f"{component.ref}.{pin.name}"] = logical_id

    assigned: dict[str, str] = {}
    for net, members in NETS.items():
        if len(members) < 2:
            errors.append(f"Net {net} has fewer than two members")
        for member in members:
            if member not in pin_aliases:
                errors.append(f"Unknown pin {member} on net {net}")
                continue
            logical_id = pin_aliases[member]
            previous = assigned.get(logical_id)
            if previous is not None and previous != net:
                errors.append(f"{member} is on both {previous} and {net}")
            assigned[logical_id] = net

    for member in NO_CONNECTS:
        logical_id = pin_aliases.get(member)
        if logical_id is None:
            errors.append(f"Unknown no-connect pin {member}")
        elif logical_id in assigned:
            errors.append(f"No-connect pin {member} is assigned to {assigned[logical_id]}")
        else:
            assigned[logical_id] = "<NC>"

    missing = sorted(logical_pins - set(assigned))
    if missing:
        errors.append("Unassigned pins: " + ", ".join(missing))
    return errors


def write_netlist(output_dir: Path) -> None:
    with (output_dir / "netlist.csv").open("w", newline="", encoding="utf-8") as stream:
        writer = csv.writer(stream)
        writer.writerow(("Net", "Connections"))
        for net in sorted(NETS):
            writer.writerow((net, " ".join(NETS[net])))


def write_bom(output_dir: Path) -> None:
    with (output_dir / "bom.csv").open("w", newline="", encoding="utf-8") as stream:
        writer = csv.writer(stream)
        writer.writerow(("Reference", "Value", "Package", "Description"))
        for component in sorted(COMPONENTS, key=lambda item: item.ref):
            writer.writerow((component.ref, component.value, component.package, component.description))


def make_ic(
    drawing: schemdraw.Drawing,
    component,
    x: float,
    y: float,
    width: float,
    height: float,
    pin_layout: list[tuple[str, str, float]],
    *,
    show_package: bool = True,
    show_numbers: bool = True,
):
    by_name = {pin.name: pin for pin in component.pins}
    drawing += (
        elm.Rect((0, 0), (width, height), fill="white", lw=1.35)
        .at((x, y))
        .color(INK)
    )
    anchors: dict[str, tuple[float, float]] = {}
    lead = 0.40

    for name, side, position in pin_layout:
        pin_number = by_name[name].number
        if side == "left":
            boundary = (x, y + height * position)
            anchor = (boundary[0] - lead, boundary[1])
            inside_align = "left"
            number_align = "right"
        elif side == "right":
            boundary = (x + width, y + height * position)
            anchor = (boundary[0] + lead, boundary[1])
            inside_align = "right"
            number_align = "left"
        elif side == "top":
            boundary = (x + width * position, y + height)
            anchor = (boundary[0], boundary[1] + lead)
            inside_align = "center"
            number_align = "center"
        else:
            boundary = (x + width * position, y)
            anchor = (boundary[0], boundary[1] - lead)
            inside_align = "center"
            number_align = "center"

        wire(drawing, anchor, boundary, color=INK, width=1.15)
        anchors[name] = anchor

        if side == "left":
            drawing += elm.Label().at((x + 0.10, boundary[1])).label(
                name, loc="left", fontsize=9.5, color=INK
            )
        elif side == "right":
            drawing += elm.Label().at((x + width - 0.10, boundary[1])).label(
                name, loc="right", fontsize=9.5, color=INK
            )
        elif side == "top":
            drawing += elm.Label().at((boundary[0], y + height - 0.10)).label(
                name, loc="top", fontsize=9.5, color=INK
            )
        else:
            drawing += elm.Label().at((boundary[0], y + 0.10)).label(
                name, loc="bottom", fontsize=9.5, color=INK
            )

        if show_numbers:
            if side == "left":
                num_point = (boundary[0] - 0.10, boundary[1] + 0.10)
                num_loc = "right"
            elif side == "right":
                num_point = (boundary[0] + 0.10, boundary[1] + 0.10)
                num_loc = "left"
            else:
                num_point = (boundary[0] + 0.10, anchor[1])
                num_loc = number_align
            drawing += elm.Label().at(num_point).label(
                pin_number, loc=num_loc, fontsize=6.8, color=MUTED
            )

    drawing += elm.Label().at((x, y + height + 0.63)).label(
        f"{component.ref}  {component.value}",
        loc="left",
        fontsize=10.5,
        color=INK,
    )
    if show_package:
        drawing += elm.Label().at((x, y + height + 0.37)).label(
            component.package,
            loc="left",
            fontsize=8,
            color=MUTED,
        )
    return anchors


def make_connector(
    drawing: schemdraw.Drawing,
    component,
    x: float,
    y: float,
    width: float,
    height: float,
    pin_layout: list[tuple[str, str, float]],
):
    """Draw a compact connector body with labels outside the contacts."""
    by_name = {pin.name: pin for pin in component.pins}
    drawing += (
        elm.Rect((0, 0), (width, height), fill="white", lw=1.35)
        .at((x, y))
        .color(INK)
    )
    anchors: dict[str, tuple[float, float]] = {}
    lead = 0.52

    for name, side, position in pin_layout:
        if side == "right":
            boundary = (x + width, y + height * position)
            anchor = (boundary[0] + lead, boundary[1])
            label_point = (x - 0.12, boundary[1])
            label_loc = "right"
        else:
            boundary = (x, y + height * position)
            anchor = (boundary[0] - lead, boundary[1])
            label_point = (x + width + 0.12, boundary[1])
            label_loc = "left"

        drawing += elm.Dot(radius=0.07, open=True).at(boundary).color(INK)
        wire(drawing, boundary, anchor, color=INK, width=1.15)
        anchors[name] = anchor
        drawing += elm.Label().at(label_point).label(
            name, loc=label_loc, fontsize=9, color=INK
        )
        number_point = (
            (boundary[0] + 0.12, boundary[1] + 0.10)
            if side == "right"
            else (boundary[0] - 0.12, boundary[1] + 0.10)
        )
        number_loc = "left" if side == "right" else "right"
        drawing += elm.Label().at(number_point).label(
            by_name[name].number, loc=number_loc, fontsize=5.8, color=MUTED
        )

    drawing += elm.Label().at((x, y + height + 0.62)).label(
        f"{component.ref}  {component.value}",
        loc="left",
        fontsize=10,
        color=INK,
    )
    drawing += elm.Label().at((x, y + height + 0.34)).label(
        component.package,
        loc="left",
        fontsize=7.5,
        color=MUTED,
    )
    return anchors


def wire(
    drawing: schemdraw.Drawing,
    *points: tuple[float, float],
    color: str = INK,
    width: float = 1.15,
) -> None:
    for start, end in zip(points, points[1:]):
        drawing += (
            elm.Line()
            .at(start)
            .to(end)
            .color(color)
            .linewidth(width)
        )


def junction(drawing: schemdraw.Drawing, point: tuple[float, float]) -> None:
    drawing += elm.Dot().at(point).color(INK)


def ground(drawing: schemdraw.Drawing, point: tuple[float, float]) -> None:
    drawing += elm.Ground().at(point).down().color(GROUND)


def tag(
    drawing: schemdraw.Drawing,
    point: tuple[float, float],
    text: str,
    direction: str = "right",
    *,
    width: float | None = None,
    color: str = INK,
):
    if width is None:
        width = max(0.75, 0.22 * len(text) + 0.32)
    element = elm.Tag(width=width, height=0.42, fontsize=8.5).at(point)
    element = getattr(element, direction)()
    element = element.color(color)
    drawing += element.label(text, fontsize=8.5, color=color)
    return element


def two_pin(
    drawing: schemdraw.Drawing,
    element_type,
    ref: str,
    value: str,
    point: tuple[float, float],
    direction: str,
    length: float,
    label_loc: str = "top",
):
    element = element_type().at(point).color(INK)
    element = getattr(element, direction)().length(length)
    drawing += element.label(f"{ref} {value}", loc=label_loc, fontsize=8.5)
    return element


def no_connect(drawing: schemdraw.Drawing, point: tuple[float, float], side: str) -> None:
    if side == "right":
        tip = (point[0] + 0.42, point[1])
    elif side == "left":
        tip = (point[0] - 0.42, point[1])
    elif side == "top":
        tip = (point[0], point[1] + 0.42)
    else:
        tip = (point[0], point[1] - 0.42)
    wire(drawing, point, tip, color=POWER, width=1.0)
    drawing += elm.Line().at((tip[0] - 0.09, tip[1] - 0.09)).to(
        (tip[0] + 0.09, tip[1] + 0.09)
    ).color(POWER)
    drawing += elm.Line().at((tip[0] - 0.09, tip[1] + 0.09)).to(
        (tip[0] + 0.09, tip[1] - 0.09)
    ).color(POWER)


def section_title(drawing: schemdraw.Drawing, x: float, y: float, text: str) -> None:
    drawing += elm.Label().at((x, y)).label(
        text, loc="left", fontsize=10.5, color=MUTED
    )


def draw_sheet(output: Path) -> None:
    canvas = "svg" if output.suffix.lower() == ".svg" else "matplotlib"
    drawing = schemdraw.Drawing(canvas=canvas, show=False)
    drawing.config(unit=1.2, fontsize=11, lw=1.3)

    drawing += elm.Label().at((0, 23.3)).label(
        "USB-C 5 V / 3.3 V LVTTL TO RS-485",
        loc="left", fontsize=22, color=INK,
    )
    drawing += elm.Label().at((0, 22.65)).label(
        "SN65BC184  •  half-duplex  •  non-isolated  •  AMS1117-3.3 SOT-223",
        loc="left", fontsize=10, color=MUTED,
    )
    drawing += elm.Line().at((0, 22.15)).to((30.0, 22.15)).color(GRID)
    section_title(drawing, 0, 21.55, "USB-C INPUT / PROTECTION / 3.3 V REGULATION")

    # USB-C connector.  All connector pins are on one side for a compact symbol.
    j1 = make_connector(
        drawing, PART_BY_REF["J1"], 1.2, 15.9, 1.25, 4.7,
        [
            ("GND", "right", 0.10),
            ("SHIELD", "right", 0.25),
            ("DN", "right", 0.39),
            ("DP", "right", 0.50),
            ("CC2", "right", 0.62),
            ("CC1", "right", 0.75),
            ("VBUS", "right", 0.91),
        ],
    )
    fuse = elm.Fuse().at(j1["VBUS"]).right().length(1.1).color(INK)
    drawing += fuse.label("F1  0.5 A", loc="top", fontsize=7)
    plus5 = fuse.end
    wire(drawing, plus5, (10.4, plus5[1]), color=POWER)
    tag(drawing, (5.7, plus5[1]), "+5V", "up", color=POWER)

    d1 = two_pin(drawing, elm.Diode, "D1", "SMBJ5.0A", (6.05, plus5[1] - 0.55), "down", 1.25, "left")
    wire(drawing, (6.05, plus5[1]), d1.start, color=POWER)
    ground(drawing, d1.end)
    c1 = two_pin(drawing, elm.Capacitor, "C1", "10 uF", (7.10, plus5[1] - 0.55), "down", 1.25, "left")
    wire(drawing, (7.10, plus5[1]), c1.start, color=POWER)
    ground(drawing, c1.end)
    c2 = two_pin(drawing, elm.Capacitor, "C2", "100 nF", (8.15, plus5[1] - 0.55), "down", 1.25, "right")
    wire(drawing, (8.15, plus5[1]), c2.start, color=POWER)
    ground(drawing, c2.end)

    u1 = make_ic(
        drawing, PART_BY_REF["U1"], 11.0, 18.0, 3.4, 2.8,
        [
            ("VIN", "left", 0.65),
            ("VOUT", "right", 0.65),
            ("GND", "bottom", 0.50),
            ("VOUT_TAB", "top", 0.50),
        ],
    )
    wire(drawing, (10.4, plus5[1]), (10.4, u1["VIN"][1]), color=POWER)

    # The SOT-223 tab and Pin 2 are the same output node.
    out_node = (18.3, u1["VOUT"][1])
    tab_join = (15.0, u1["VOUT"][1])
    wire(drawing, u1["VOUT"], out_node, color=POWER)
    wire(
        drawing,
        u1["VOUT_TAB"],
        (u1["VOUT_TAB"][0], 21.35),
        (tab_join[0], 21.35),
        tab_join,
        color=POWER,
    )
    junction(drawing, tab_join)
    tag(drawing, out_node, "+3V3", "right", color=POWER)
    ground(drawing, u1["GND"])

    c3 = two_pin(drawing, elm.Capacitor, "C3", "22 uF", (14.8, u1["VOUT"][1] - 0.55), "down", 1.25, "left")
    wire(drawing, (14.8, u1["VOUT"][1]), c3.start, color=POWER)
    ground(drawing, c3.end)
    c4 = two_pin(drawing, elm.Capacitor, "C4", "100 nF", (15.9, u1["VOUT"][1] - 0.55), "down", 1.25, "right")
    wire(drawing, (15.9, u1["VOUT"][1]), c4.start, color=POWER)
    ground(drawing, c4.end)

    # USB-C CC and shield networks are kept below the main 5 V rail.
    wire(drawing, j1["CC1"], (4.25, j1["CC1"][1]), (4.25, 16.15))
    r2 = two_pin(drawing, elm.Resistor, "R2", "5.1 k", (4.25, 16.15), "down", 1.0, "left")
    ground(drawing, r2.end)
    wire(drawing, j1["CC2"], (4.85, j1["CC2"][1]), (4.85, 15.85))
    r3 = two_pin(drawing, elm.Resistor, "R3", "5.1 k", (4.85, 15.85), "down", 1.0, "right")
    ground(drawing, r3.end)

    wire(drawing, j1["SHIELD"], (5.75, j1["SHIELD"][1]), (5.75, 14.55))
    r1 = two_pin(drawing, elm.Resistor, "R1", "1 M", (5.75, 14.55), "right", 1.0, "top")
    ground(drawing, r1.end)
    junction(drawing, (5.75, 14.55))
    c7 = two_pin(drawing, elm.Capacitor, "C7", "4.7 nF", (5.75, 14.55), "down", 1.0, "left")
    ground(drawing, c7.end)
    no_connect(drawing, j1["DP"], "right")
    no_connect(drawing, j1["DN"], "right")
    ground(drawing, j1["GND"])

    r5 = two_pin(drawing, elm.Resistor, "R5", "1 k", (19.8, 20.85), "right", 1.0, "top")
    tag(drawing, r5.start, "+3V3", "left", color=POWER)
    led = elm.LED().at(r5.end).right().length(0.85).color(INK)
    drawing += led.label("D3", loc="top", fontsize=7)
    ground(drawing, led.end)

    drawing += elm.Line().at((0, 12.55)).to((30.0, 12.55)).color(GRID)
    section_title(drawing, 0, 12.00, "3.3 V LVTTL INTERFACE / DIRECTION CONTROL")
    section_title(drawing, 12.1, 12.00, "SN65BC184 TRANSCEIVER")
    section_title(drawing, 22.2, 12.00, "RS-485 BUS / PROTECTION / TERMINAL")

    # Logic connector and direction control.
    j2 = make_connector(
        drawing, PART_BY_REF["J2"], 1.2, 4.8, 1.2, 4.2,
        [
            ("GND", "right", 0.12),
            ("V33", "right", 0.30),
            ("TXD", "right", 0.48),
            ("RXD", "right", 0.66),
            ("DIR", "right", 0.84),
        ],
    )
    ground(drawing, j2["GND"])
    wire(drawing, j2["V33"], (4.05, j2["V33"][1]))
    tag(drawing, (4.05, j2["V33"][1]), "+3V3", "right", color=POWER)
    wire(drawing, j2["TXD"], (4.05, j2["TXD"][1]))
    tag(drawing, (4.05, j2["TXD"][1]), "TXD", "right", width=0.62)
    wire(drawing, j2["RXD"], (4.05, j2["RXD"][1]))
    tag(drawing, (4.05, j2["RXD"][1]), "RXD", "right", width=0.62)

    dir_branch = (4.65, j2["DIR"][1])
    wire(drawing, j2["DIR"], dir_branch, (5.25, j2["DIR"][1]), (5.25, 7.15))
    junction(drawing, dir_branch)
    wire(drawing, dir_branch, (4.65, 3.75))
    r4 = two_pin(drawing, elm.Resistor, "R4", "10 k", (4.65, 3.75), "down", 1.0, "left")
    ground(drawing, r4.end)

    u3 = make_ic(
        drawing, PART_BY_REF["U3"], 7.0, 6.0, 2.2, 2.3,
        [
            ("A", "left", 0.50),
            ("Y", "right", 0.50),
            ("NC", "right", 0.16),
            ("VCC", "top", 0.50),
            ("GND", "bottom", 0.50),
        ],
    )
    wire(drawing, (5.25, 7.15), u3["A"])
    ground(drawing, u3["GND"])
    no_connect(drawing, u3["NC"], "right")
    wire(drawing, u3["VCC"], (u3["VCC"][0], u3["VCC"][1] + 0.38))
    tag(drawing, (u3["VCC"][0], u3["VCC"][1] + 0.38), "+3V3", "left", color=POWER)

    c6 = two_pin(drawing, elm.Capacitor, "C6", "100 nF", (10.65, 7.05), "down", 1.0, "right")
    tag(drawing, c6.start, "+3V3", "left", color=POWER)
    ground(drawing, c6.end)

    # Direction branch is routed below U3 so it cannot cross the receiver path.
    wire(
        drawing,
        dir_branch,
        (4.65, 4.15),
        (11.65, 4.15),
        (11.65, 7.24),
        (12.60, 7.24),
    )

    u2 = make_ic(
        drawing, PART_BY_REF["U2"], 13.0, 4.8, 3.6, 5.2,
        [
            ("RO", "left", 0.87),
            ("RE_N", "left", 0.67),
            ("DE", "left", 0.47),
            ("DI", "left", 0.27),
            ("VCC", "top", 0.50),
            ("GND", "bottom", 0.50),
            ("A", "right", 0.88),
            ("B", "right", 0.28),
        ],
    )
    tag(drawing, u2["RO"], "RXD", "left", width=0.62)
    tag(drawing, u2["DI"], "TXD", "left", width=0.62)
    wire(drawing, u3["Y"], (10.15, u3["Y"][1]), (10.15, u2["RE_N"][1]), u2["RE_N"])
    ground(drawing, u2["GND"])
    wire(drawing, u2["VCC"], (u2["VCC"][0], u2["VCC"][1] + 0.35))
    tag(drawing, (u2["VCC"][0], u2["VCC"][1] + 0.35), "+3V3", "left", color=POWER)
    c5 = two_pin(drawing, elm.Capacitor, "C5", "100 nF", (17.7, 9.2), "down", 1.0, "right")
    tag(drawing, c5.start, "+3V3", "left", color=POWER)
    ground(drawing, c5.end)

    # Wide, independent A/B horizontal trunks.  The physical spacing is 3.1 units.
    a_y = u2["A"][1]
    b_y = u2["B"][1]
    r6 = two_pin(drawing, elm.Resistor, "R6", "10 R", (17.35, a_y), "right", 0.90, "top")
    wire(drawing, u2["A"], r6.start, color=ACCENT)
    r7 = two_pin(drawing, elm.Resistor, "R7", "10 R", (17.35, b_y), "right", 0.90, "bottom")
    wire(drawing, u2["B"], r7.start, color=ACCENT)
    a_bus_start = r6.end
    b_bus_start = r7.end
    a_bus_end = (26.73, a_y)
    b_bus_end = (26.73, b_y)
    wire(drawing, a_bus_start, a_bus_end, color=ACCENT, width=1.45)
    wire(drawing, b_bus_start, b_bus_end, color=ACCENT, width=1.45)
    drawing += elm.Label().at((18.50, a_y + 0.17)).label("A(+)/BUS_A", loc="left", fontsize=6, color=ACCENT)
    drawing += elm.Label().at((18.50, b_y - 0.17)).label("B(-)/BUS_B", loc="left", fontsize=6, color=ACCENT)

    # Termination is a true vertical cross-connection between the separated trunks.
    term_x = 20.25
    junction(drawing, (term_x, a_y))
    junction(drawing, (term_x, b_y))
    wire(drawing, (term_x, a_y), (term_x, 8.85), color=ACCENT)
    r8 = two_pin(
        drawing, elm.Resistor, "R8", "120 R",
        (term_x, 8.85), "down", 0.72, "right",
    )
    jp1 = two_pin(
        drawing, elm.Switch, "JP1", "",
        (term_x, r8.end[1]), "down", 0.72, "right",
    )
    wire(drawing, jp1.end, (term_x, b_y), color=ACCENT)

    # PESD2CAN is drawn between A and B so neither A nor B has to cross the other.
    d2 = make_ic(
        drawing, PART_BY_REF["D2"], 22.15, 7.15, 2.1, 1.20,
        [
            ("A", "top", 0.75),
            ("B", "bottom", 0.25),
            ("GND", "left", 0.50),
        ],
    )
    wire(drawing, d2["A"], (d2["A"][0], a_y), color=ACCENT)
    wire(drawing, d2["B"], (d2["B"][0], b_y), color=ACCENT)
    wire(drawing, d2["GND"], (d2["GND"][0] - 0.35, d2["GND"][1]))
    ground(drawing, (d2["GND"][0] - 0.35, d2["GND"][1]))

    # Fail-safe bias: A is gently pulled up; B is gently pulled down.
    bias_a_node = (24.65, a_y)
    junction(drawing, bias_a_node)
    wire(drawing, bias_a_node, (24.65, 10.85))
    r9 = two_pin(drawing, elm.Resistor, "R9", "560 R", (24.65, 10.85), "left", 0.80, "bottom")
    jp2 = two_pin(drawing, elm.Switch, "JP2", "", (r9.end[0], 10.85), "left", 0.65, "top")
    wire(drawing, jp2.end, (22.45, 10.85))
    tag(drawing, (22.45, 10.85), "+3V3", "left", color=POWER)

    bias_b_node = (25.55, b_y)
    junction(drawing, bias_b_node)
    wire(drawing, bias_b_node, (25.55, 4.85))
    r10 = two_pin(drawing, elm.Resistor, "R10", "560 R", (25.55, 4.85), "left", 0.80, "bottom")
    jp3 = two_pin(drawing, elm.Switch, "JP3", "", (r10.end[0], 4.85), "left", 0.65, "top")
    wire(drawing, jp3.end, (23.35, 4.85))
    ground(drawing, (23.35, 4.85))

    j3 = make_connector(
        drawing, PART_BY_REF["J3"], 27.25, 4.8, 1.2, 5.2,
        [
            ("A", "left", 0.88),
            ("B", "left", 0.28),
            ("GND", "left", 0.58),
        ],
    )
    wire(drawing, a_bus_end, j3["A"], color=ACCENT, width=1.45)
    wire(drawing, b_bus_end, j3["B"], color=ACCENT, width=1.45)
    ground(drawing, j3["GND"])

    # Notes and sheet metadata.
    drawing += elm.Line().at((0, 0.95)).to((30.0, 0.95)).color(GRID)
    drawing += elm.Label().at((0, 0.45)).label("DESIGN NOTES", loc="left", fontsize=9, color=MUTED)
    notes = (
        "1. DIR=0: receive (/RE=0, DE=0). DIR=1: transmit (/RE=1, DE=1). R4 defaults DIR low.",
        "2. Fit R8/JP1 termination only at both physical cable ends.",
        "3. Fit R9/JP2 and R10/JP3 bias links at one master only.",
        "4. AMS1117 SOT-223: 1=GND, 2=VOUT, 3=VIN, TAB=VOUT. Solder TAB to copper.",
    )
    for index, note in enumerate(notes):
        drawing += elm.Label().at((0, -0.02 - 0.43 * index)).label(
            note, loc="left", fontsize=8.2, color=INK
        )
    drawing += elm.Label().at((30.0, 23.25)).label("SHEET 01", loc="right", fontsize=8.5, color=MUTED)
    drawing += elm.Label().at((30.0, 22.85)).label("REV B  •  2026-11-09", loc="right", fontsize=8.5, color=MUTED)

    drawing.save(str(output), dpi=220)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--format", choices=("svg", "png", "pdf"), default="svg")
    parser.add_argument("--output-dir", type=Path, default=ROOT / "output")
    args = parser.parse_args()

    errors = validate_design()
    if errors:
        raise SystemExit("\n".join(errors))

    args.output_dir.mkdir(parents=True, exist_ok=True)
    output = args.output_dir / f"rs485_sn65bc184_v2.{args.format}"
    draw_sheet(output)
    write_netlist(args.output_dir)
    write_bom(args.output_dir)
    print(f"Generated schematic: {output}")
    print(f"Generated netlist:   {args.output_dir / 'netlist.csv'}")
    print(f"Generated BOM:       {args.output_dir / 'bom.csv'}")


if __name__ == "__main__":
    main()
