"""Programmatic LTspice .asc schematic file generation.

Generates graphical schematics (the .asc format LTspice uses for its GUI),
not just text netlists. Components are placed using absolute pin positions
derived from the .asy symbol files and rotated with the standard CCW
transform: R90 applies (px, py) → (-py, px) relative to the component origin.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path

# LTspice grid spacing -- all coordinates should be multiples of this
GRID = 80

_SPICE_SUFFIXES = {
    "T": 1e12, "G": 1e9, "MEG": 1e6, "K": 1e3,
    "M": 1e-3, "U": 1e-6, "N": 1e-9, "P": 1e-12, "F": 1e-15,
}


def _parse_spice_value(value: str) -> float:
    """Convert a SPICE-style value string to a float (e.g., '100k' → 100000.0)."""
    value = value.strip()
    try:
        return float(value)
    except ValueError:
        pass
    upper = value.upper()
    for suffix, mult in sorted(_SPICE_SUFFIXES.items(), key=lambda x: -len(x[0])):
        if upper.endswith(suffix):
            try:
                return float(value[: len(value) - len(suffix)]) * mult
            except ValueError:
                continue
    raise ValueError(f"Cannot parse SPICE value: {value!r}")

# Pin positions (relative to component origin) from LTspice .asy files.
# R0 orientation.  Key = symbol name, value = list of (px, py) per pin.
_PIN_OFFSETS: dict[str, list[tuple[int, int]]] = {
    # 2-pin passives & sources
    "voltage": [(0, 16), (0, 96)],  # pin+ , pin-
    "res": [(16, 16), (16, 96)],  # pinA , pinB
    "cap": [(16, 0), (16, 64)],  # pinA , pinB
    "ind": [(16, 16), (16, 96)],  # pinA , pinB (same body as res)
    "diode": [(16, 0), (16, 64)],  # anode(+) , cathode(-)
    # 3-pin semiconductors
    "npn": [(64, 0), (0, 48), (64, 96)],  # C , B , E
    "pnp": [(64, 0), (0, 48), (64, 96)],  # C , B , E  (same geometry)
    "nmos": [(48, 0), (0, 80), (48, 96)],  # D , G , S
    "pmos": [(48, 0), (0, 80), (48, 96)],  # D , G , S
    # 4-pin MOSFETs (with body)
    "nmos4": [(48, 0), (0, 80), (48, 96), (48, 48)],  # D , G , S , B
    "pmos4": [(48, 0), (0, 80), (48, 96), (48, 48)],  # D , G , S , B
    # 5-pin op-amp (note: negative offsets = pins extend left/above origin)
    "OpAmps/UniversalOpamp2": [
        (-32, 16),  # In+  (non-inverting)
        (-32, -16),  # In-  (inverting)
        (0, -32),  # V+   (positive supply)
        (0, 32),  # V-   (negative supply)
        (32, 0),  # OUT
    ],
}


def _rotate(px: int, py: int, rotation: int) -> tuple[int, int]:
    """Apply LTspice rotation to a relative pin offset.

    LTspice rotations are counterclockwise: (px, py) → (-py, px) for R90.
    """
    if rotation == 0:
        return px, py
    if rotation == 90:
        return -py, px
    if rotation == 180:
        return -px, -py
    if rotation == 270:
        return py, -px
    return px, py


def pin_position(
    symbol: str, pin_index: int, cx: int, cy: int, rotation: int = 0
) -> tuple[int, int]:
    """Compute absolute pin position for a component.

    Args:
        symbol: Base symbol name (``"res"``, ``"cap"``, ``"voltage"``, ...)
        pin_index: 0 for first pin, 1 for second pin, etc.
        cx: Component origin X
        cy: Component origin Y
        rotation: 0, 90, 180, or 270

    Returns:
        Absolute (x, y) of the pin.
    """
    offsets = _PIN_OFFSETS.get(symbol, [(0, 0), (0, 80)])
    px, py = offsets[pin_index % len(offsets)]
    rx, ry = _rotate(px, py, rotation)
    return cx + rx, cy + ry


@dataclass
class _WireEntry:
    x1: int
    y1: int
    x2: int
    y2: int


@dataclass
class _SymbolEntry:
    symbol: str
    name: str
    value: str
    x: int
    y: int
    rotation: int  # 0, 90, 180, 270
    windows: list[str] = field(default_factory=list)


@dataclass
class _FlagEntry:
    x: int
    y: int
    name: str


@dataclass
class _TextEntry:
    x: int
    y: int
    content: str


class AscSchematic:
    """Builder for LTspice .asc schematic files.

    All ``add_*`` methods return ``self`` for chaining::

        sch = (AscSchematic()
            .add_component("res", "R1", "1k", 160, 176, rotation=90)
            .add_wire(80, 176, 160, 176)
            .add_ground(80, 256))
    """

    def __init__(self, sheet_w: int = 880, sheet_h: int = 680) -> None:
        self._sheet_w = sheet_w
        self._sheet_h = sheet_h
        self._wires: list[_WireEntry] = []
        self._symbols: list[_SymbolEntry] = []
        self._flags: list[_FlagEntry] = []
        self._texts: list[_TextEntry] = []

    # -- builder methods -----------------------------------------------------

    def add_component(
        self,
        symbol: str,
        name: str,
        value: str,
        x: int,
        y: int,
        rotation: int = 0,
    ) -> AscSchematic:
        """Place a component symbol.

        Args:
            symbol: LTspice symbol name (``res``, ``cap``, ``voltage``, etc.)
            name: Instance name (``R1``, ``C1``, ``V1``, ...)
            value: Component value (``1k``, ``100n``, ``AC 1``, ...)
            x: X coordinate (should be on 80-pixel grid)
            y: Y coordinate
            rotation: 0, 90, 180, or 270 degrees
        """
        windows: list[str] = []
        # For resistors and inductors placed at R90, shift the WINDOW lines
        # so labels sit neatly above/below the body
        if rotation == 90 and symbol in ("res", "ind", "ind2"):
            windows = [
                "WINDOW 0 0 56 VBottom 2",
                "WINDOW 3 32 56 VTop 2",
            ]
        self._symbols.append(_SymbolEntry(symbol, name, value, x, y, rotation, windows))
        return self

    def add_wire(self, x1: int, y1: int, x2: int, y2: int) -> AscSchematic:
        """Add a wire segment between two points."""
        self._wires.append(_WireEntry(x1, y1, x2, y2))
        return self

    def add_ground(self, x: int, y: int) -> AscSchematic:
        """Place a ground flag (net name ``0``)."""
        self._flags.append(_FlagEntry(x, y, "0"))
        return self

    def add_net_label(self, name: str, x: int, y: int) -> AscSchematic:
        """Place a named net label (e.g., ``out``, ``vdd``)."""
        self._flags.append(_FlagEntry(x, y, name))
        return self

    def add_directive(self, text: str, x: int, y: int) -> AscSchematic:
        """Add a SPICE directive (rendered with ``!`` prefix)."""
        self._texts.append(_TextEntry(x, y, text))
        return self

    # -- output --------------------------------------------------------------

    def render(self) -> str:
        """Render the schematic to an ``.asc`` format string."""
        lines: list[str] = []
        lines.append("Version 4")
        lines.append(f"SHEET 1 {self._sheet_w} {self._sheet_h}")

        for w in self._wires:
            lines.append(f"WIRE {w.x1} {w.y1} {w.x2} {w.y2}")

        for f in self._flags:
            lines.append(f"FLAG {f.x} {f.y} {f.name}")

        for s in self._symbols:
            lines.append(f"SYMBOL {s.symbol} {s.x} {s.y} R{s.rotation}")
            for win in s.windows:
                lines.append(win)
            lines.append(f"SYMATTR InstName {s.name}")
            if s.value:
                lines.append(f"SYMATTR Value {s.value}")

        for t in self._texts:
            lines.append(f"TEXT {t.x} {t.y} Left 2 !{t.content}")

        return "\n".join(lines) + "\n"

    def save(self, path: Path | str) -> Path:
        """Write the schematic to an ``.asc`` file on disk."""
        path = Path(path)
        path.write_text(self.render(), encoding="utf-8")
        return path


# ============================================================================
# Layout helper functions -- auto-placed, ready-to-simulate schematics
# ============================================================================


def generate_rc_lowpass(r: str = "1k", c: str = "100n") -> AscSchematic:
    """Generate an RC lowpass filter schematic with AC analysis.

    Signal flow (left to right)::

        V1 --[R1]-- out --+
                          |
                         [C1]
                          |
                         GND

    Args:
        r: Resistor value (e.g., ``"1k"``, ``"4.7k"``)
        c: Capacitor value (e.g., ``"100n"``, ``"10p"``)

    Returns:
        An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
    """
    sch = AscSchematic()

    # Component origins (all on 16-pixel sub-grid for pin alignment)
    # V1 at (80, 80) R0:  pin+ = (80, 96),   pin- = (80, 176)
    # R1 at (160, 80) R0: pinA = (176, 96),   pinB = (176, 176)
    # C1 at (256, 176) R0: pinA = (272, 176),  pinB = (272, 240)

    v1p = pin_position("voltage", 0, 80, 80)  # (80, 96)
    v1n = pin_position("voltage", 1, 80, 80)  # (80, 176)
    r1a = pin_position("res", 0, 160, 80)  # (176, 96)
    r1b = pin_position("res", 1, 160, 80)  # (176, 176)
    c1a = pin_position("cap", 0, 256, 176)  # (272, 176)
    c1b = pin_position("cap", 1, 256, 176)  # (272, 240)

    # Wires
    sch.add_wire(*v1p, *r1a)  # V1+ to R1 top
    sch.add_wire(*r1b, *c1a)  # R1 bottom to C1 top (junction = "out")

    # Components
    sch.add_component("voltage", "V1", "AC 1", 80, 80)
    sch.add_component("res", "R1", r, 160, 80)
    sch.add_component("cap", "C1", c, 256, 176)

    # Ground flags at V1- and C1 bottom
    sch.add_ground(*v1n)
    sch.add_ground(*c1b)

    # Output net label at the R1-C1 junction
    sch.add_net_label("out", *r1b)

    # Simulation directive
    sch.add_directive(".ac dec 100 1 1meg", 80, 296)

    return sch


def generate_voltage_divider(
    r1: str = "10k",
    r2: str = "10k",
    vin: str = "5",
) -> AscSchematic:
    """Generate a voltage divider schematic with operating-point analysis.

    Topology (vertical)::

        V1+ --[R1]-- out --[R2]-- GND
        V1- = GND

    Args:
        r1: Top resistor value
        r2: Bottom resistor value
        vin: DC input voltage

    Returns:
        An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
    """
    sch = AscSchematic()

    # Vertical layout: V1 on left, R1 and R2 stacked vertically on right
    # V1 at (80, 80) R0:   pin+ = (80, 96),   pin- = (80, 176)
    # R1 at (176, 0) R0:   pinA = (192, 16),   pinB = (192, 96)
    # R2 at (176, 96) R0:  pinA = (192, 112),  pinB = (192, 192)

    v1p = pin_position("voltage", 0, 80, 80)  # (80, 96)
    v1n = pin_position("voltage", 1, 80, 80)  # (80, 176)
    r1a = pin_position("res", 0, 176, 0)  # (192, 16)
    r1b = pin_position("res", 1, 176, 0)  # (192, 96)
    r2a = pin_position("res", 0, 176, 96)  # (192, 112)
    r2b = pin_position("res", 1, 176, 96)  # (192, 192)

    # Wires: V1+ to R1 top, R1 bottom to R2 top (junction = "out")
    sch.add_wire(*v1p, *r1a)  # need to connect (80,96) to (192,16)
    # Route: V1+ up then right then down to R1 top
    sch.add_wire(80, 96, 80, 16)
    sch.add_wire(80, 16, 192, 16)
    # R1 bottom to R2 top
    sch.add_wire(*r1b, *r2a)

    # Components
    sch.add_component("voltage", "V1", vin, 80, 80)
    sch.add_component("res", "R1", r1, 176, 0)
    sch.add_component("res", "R2", r2, 176, 96)

    # Ground
    sch.add_ground(*v1n)
    sch.add_ground(*r2b)

    # Net label at R1-R2 junction
    sch.add_net_label("out", *r1b)

    # Directive
    sch.add_directive(".op", 80, 240)

    return sch


def generate_inverting_amp(
    rin: str = "10k",
    rf: str = "100k",
) -> AscSchematic:
    """Generate an inverting op-amp amplifier schematic.

    Topology::

        V1 --[Rin]--> In-(inv) ---[Rf]--- out
                       In+(non-inv) --> GND
        Supply: Vpos=+15V, Vneg=-15V

    The gain is ``-Rf/Rin``.

    Args:
        rin: Input resistor value
        rf: Feedback resistor value

    Returns:
        An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)
    oa_sym = "OpAmps/UniversalOpamp2"

    # Op-amp U1 at (512, 336) — same position as non-inverting amp
    inp = pin_position(oa_sym, 0, 512, 336)  # In+  = (480, 352)
    inn = pin_position(oa_sym, 1, 512, 336)  # In-  = (480, 320)
    vp = pin_position(oa_sym, 2, 512, 336)   # V+   = (512, 304)
    vn = pin_position(oa_sym, 3, 512, 336)   # V-   = (512, 368)
    out = pin_position(oa_sym, 4, 512, 336)  # OUT  = (544, 336)

    # Signal source V1 at (80, 256)
    v1p = pin_position("voltage", 0, 80, 256)  # (80, 272)
    v1n = pin_position("voltage", 1, 80, 256)  # (80, 352)

    # Rin: horizontal (R90) between V1 and In-
    # Origin (400, 304): pinA=(384,320), pinB=(304,320)
    rin_a = pin_position("res", 0, 400, 304, 90)  # (384, 320)
    rin_b = pin_position("res", 1, 400, 304, 90)  # (304, 320)

    # Rf: horizontal (R90) above the op-amp for feedback
    # Origin (560, 208): pinA=(544,224), pinB=(464,224)
    rf_a = pin_position("res", 0, 560, 208, 90)  # (544, 224)
    rf_b = pin_position("res", 1, 560, 208, 90)  # (464, 224)

    # Supply: Vpos at (688, 176), Vneg at (688, 416)
    vpos_p = pin_position("voltage", 0, 688, 176)  # (688, 192)
    vpos_n = pin_position("voltage", 1, 688, 176)  # (688, 272)
    vneg_p = pin_position("voltage", 0, 688, 416)  # (688, 432)
    vneg_n = pin_position("voltage", 1, 688, 416)  # (688, 512)

    # === WIRING ===
    # V1+ to Rin pinB: route RIGHT first then down (avoids crossing V1-)
    sch.add_wire(v1p[0], v1p[1], rin_b[0], v1p[1])   # (80,272) → (304,272)
    sch.add_wire(rin_b[0], v1p[1], *rin_b)             # (304,272) → (304,320)

    # Rin pinA to In-
    sch.add_wire(*rin_a, inn[0], inn[1])  # (384,320) → (480,320)

    # Rf feedback: OUT up to Rf pinA, Rf pinB left and down to inv junction
    sch.add_wire(out[0], out[1], *rf_a)                  # (544,336) → (544,224)
    sch.add_wire(*rf_b, rin_a[0], rf_b[1])               # (464,224) → (384,224)
    sch.add_wire(rin_a[0], rf_b[1], *rin_a)              # (384,224) → (384,320)

    # Supply wiring
    sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])        # V+ up to (512,192)
    sch.add_wire(vp[0], vpos_p[1], *vpos_p)              # right to Vpos+
    sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])        # V- down to (512,512)
    sch.add_wire(vn[0], vneg_n[1], *vneg_n)              # right to Vneg-

    # === COMPONENTS ===
    sch.add_component("voltage", "V1", "AC 1", 80, 256)
    sch.add_component(oa_sym, "U1", "", 512, 336)
    sch.add_component("res", "Rin", rin, 400, 304, rotation=90)
    sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
    sch.add_component("voltage", "Vpos", "15", 688, 176)
    sch.add_component("voltage", "Vneg", "15", 688, 416)

    # === FLAGS ===
    sch.add_ground(*v1n)
    sch.add_ground(*inp)   # In+ to GND (inverting configuration)
    sch.add_ground(*vpos_n)
    sch.add_ground(*vneg_p)
    sch.add_net_label("out", *out)

    sch.add_directive(".ac dec 100 1 1meg", 80, 560)

    return sch


def generate_non_inverting_amp(
    rin: str = "10k",
    rf: str = "100k",
) -> AscSchematic:
    """Generate a non-inverting op-amp amplifier schematic.

    Topology::

        V1 --> In+
                     U1 --> out
        GND --> Rin --> In-
                         ^--- Rf --- out

        Gain = 1 + Rf/Rin.  Supply: +/-15V

    Args:
        rin: Input resistor (In- to GND)
        rf: Feedback resistor (In- to out)
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)
    oa_sym = "OpAmps/UniversalOpamp2"

    # Op-amp U1 at (512, 336)
    inp = pin_position(oa_sym, 0, 512, 336)  # In+  = (480, 352)
    inn = pin_position(oa_sym, 1, 512, 336)  # In-  = (480, 320)
    vp = pin_position(oa_sym, 2, 512, 336)  # V+   = (512, 304)
    vn = pin_position(oa_sym, 3, 512, 336)  # V-   = (512, 368)
    out = pin_position(oa_sym, 4, 512, 336)  # OUT  = (544, 336)

    # Signal source V1 at (80, 256)
    v1p = pin_position("voltage", 0, 80, 256)  # (80, 272)
    v1n = pin_position("voltage", 1, 80, 256)  # (80, 352)

    # Rin: In- node down to GND.  Want pinA at In- junction (352, 320).
    # res origin so that pinA = (352, 320): origin = (336, 304)
    rin_a = pin_position("res", 0, 336, 304)  # (352, 320)
    rin_b = pin_position("res", 1, 336, 304)  # (352, 400)

    # Rf: horizontal (R90) above the op-amp for feedback path
    # R90 res: pinA offset=(-16,16), pinB offset=(-96,16)
    # Place at origin (560, 208) so: pinA=(544, 224), pinB=(464, 224)
    rf_a = pin_position("res", 0, 560, 208, 90)  # (544, 224)
    rf_b = pin_position("res", 1, 560, 208, 90)  # (464, 224)

    # Supply: Vpos at (688, 176), Vneg at (688, 416)
    vpos_p = pin_position("voltage", 0, 688, 176)  # (688, 192) = vdd
    vpos_n = pin_position("voltage", 1, 688, 176)  # (688, 272)
    vneg_p = pin_position("voltage", 0, 688, 416)  # (688, 432)
    vneg_n = pin_position("voltage", 1, 688, 416)  # (688, 512) = vss

    # === WIRING ===
    # V1+ to In+: go RIGHT then DOWN then RIGHT to avoid crossing both
    # V1- at (80,352) and In- at (480,320).
    # Route: (80,272) → (448,272) → (448,352) → (480,352)
    sch.add_wire(v1p[0], v1p[1], 448, v1p[1])         # (80,272) → (448,272) horiz
    sch.add_wire(448, v1p[1], 448, inp[1])             # (448,272) → (448,352) vert
    sch.add_wire(448, inp[1], inp[0], inp[1])          # (448,352) → (480,352) horiz

    # In- to Rin junction
    sch.add_wire(inn[0], inn[1], rin_a[0], rin_a[1])  # (480,320) → (352,320)

    # Rf feedback: OUT up to Rf pinA, Rf pinB left and down to junction
    sch.add_wire(out[0], out[1], rf_a[0], rf_a[1])  # (544,336) → (544,224) vertical
    sch.add_wire(rf_b[0], rf_b[1], rin_a[0], rf_b[1])  # (464,224) → (352,224) horizontal
    sch.add_wire(rin_a[0], rf_b[1], rin_a[0], rin_a[1])  # (352,224) → (352,320) vertical

    # Supply wiring
    sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])  # V+ up to (512,192)
    sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1])  # right to Vpos+
    sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])  # V- down to (512,512)
    sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1])  # right to Vneg-

    # === COMPONENTS ===
    sch.add_component("voltage", "V1", "AC 1", 80, 256)
    sch.add_component(oa_sym, "U1", "", 512, 336)
    sch.add_component("res", "Rin", rin, 336, 304)
    sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
    sch.add_component("voltage", "Vpos", "15", 688, 176)
    sch.add_component("voltage", "Vneg", "15", 688, 416)

    # === FLAGS ===
    sch.add_ground(*v1n)
    sch.add_ground(*rin_b)
    sch.add_ground(*vpos_n)
    sch.add_ground(*vneg_p)
    sch.add_net_label("out", *out)

    sch.add_directive(".ac dec 100 1 1meg", 80, 560)

    return sch


def generate_common_emitter_amp(
    rc: str = "2.2k",
    rb1: str = "56k",
    rb2: str = "12k",
    re: str = "1k",
    cc_in: str = "10u",
    cc_out: str = "10u",
    ce: str = "47u",
    vcc: str = "12",
    bjt_model: str = "2N2222",
) -> AscSchematic:
    """Generate a common-emitter amplifier schematic.

    Topology::

        Vcc --[RC]--> collector --[CC_out]--> out
        Vcc --[RB1]--> base
        in --[CC_in]--> base
        base --[RB2]--> GND
        emitter --[RE]--> GND
        emitter --[CE]--> GND (bypass)

    Args:
        rc: Collector resistor
        rb1: Base bias resistor (to Vcc)
        rb2: Base bias resistor (to GND)
        re: Emitter resistor
        cc_in: Input coupling capacitor
        cc_out: Output coupling capacitor
        ce: Emitter bypass capacitor
        vcc: Supply voltage
        bjt_model: BJT model name
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)

    # Q1 (NPN) at (400, 288): C=(464, 288), B=(400, 336), E=(464, 384)
    qc = pin_position("npn", 0, 400, 288)  # collector
    qb = pin_position("npn", 1, 400, 288)  # base
    qe = pin_position("npn", 2, 400, 288)  # emitter

    # RC from Vcc rail to collector.
    # res at (448, 192): pinA=(464, 208)=vcc rail, pinB=(464, 288)=collector
    rc_a = pin_position("res", 0, 448, 192)  # (464, 208) = vcc rail

    # RE from emitter to GND.
    # res at (448, 368): pinA=(464, 384)=emitter, pinB=(464, 464)=GND
    re_b = pin_position("res", 1, 448, 368)  # (464, 464) = GND

    # RB1 from Vcc rail to base.  res at (240, 240): pinA=(256, 256), pinB=(256, 336)
    rb1_a = pin_position("res", 0, 240, 240)  # (256, 256)
    rb1_b = pin_position("res", 1, 240, 240)  # (256, 336)

    # RB2 from base to GND.  res at (240, 320): pinA=(256, 336), pinB=(256, 416)
    rb2_b = pin_position("res", 1, 240, 320)  # (256, 416)

    # CC_in (input coupling cap) — VERTICAL (R0) above the base wire.
    # cap R0 at (384, 256): pinA=(400, 256)=input side, pinB=(400, 320)=base side.
    # Critically, neither pin sits on the horizontal RB1-to-base wire at y=336,
    # so the coupling cap is NOT shorted by the bias wire.
    ccin_a = pin_position("cap", 0, 384, 256)  # (400, 256) = input
    ccin_b = pin_position("cap", 1, 384, 256)  # (400, 320) = base side

    # CC_out (output coupling cap) horizontal R90 to the right of collector.
    # cap R90 at (560, 272): pinA=(560, 288)=output, pinB=(496, 288)=collector side
    ccout_a = pin_position("cap", 0, 560, 272, 90)  # (560, 288)
    ccout_b = pin_position("cap", 1, 560, 272, 90)  # (496, 288)

    # CE (emitter bypass cap) parallel to RE.
    # cap at (528, 384): pinA=(544, 384)=emitter, pinB=(544, 448)=GND
    ce_a = pin_position("cap", 0, 528, 384)  # (544, 384)
    ce_b = pin_position("cap", 1, 528, 384)  # (544, 448)

    # Vcc source at (80, 96): pin+=(80, 112), pin-=(80, 192)
    vcc_p = pin_position("voltage", 0, 80, 96)  # (80, 112)
    vcc_n = pin_position("voltage", 1, 80, 96)  # (80, 192)

    # Vin source at (80, 288): pin+=(80, 304), pin-=(80, 384)
    vin_p = pin_position("voltage", 0, 80, 288)  # (80, 304)
    vin_n = pin_position("voltage", 1, 80, 288)  # (80, 384)

    # === WIRING ===
    # Vcc rail at y=208 — route RIGHT from Vcc+ (avoid crossing Vcc- at 80,192)
    vcc_y = rc_a[1]  # 208
    sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1])    # (80,112)→(160,112)
    sch.add_wire(160, vcc_p[1], 160, vcc_y)              # (160,112)→(160,208)
    sch.add_wire(160, vcc_y, rc_a[0], vcc_y)             # (160,208)→(464,208) = RC top
    sch.add_wire(rb1_a[0], rb1_a[1], rb1_a[0], vcc_y)   # RB1 top up to rail
    sch.add_wire(rb1_a[0], vcc_y, rc_a[0], vcc_y)        # connect to rail

    # RB1 bottom → base (horizontal at y=336)
    sch.add_wire(rb1_b[0], rb1_b[1], qb[0], qb[1])

    # CC_in base side → base junction (short vertical drop)
    sch.add_wire(ccin_b[0], ccin_b[1], qb[0], qb[1])   # (400,320)→(400,336)

    # Vin+ → CC_in input side via manhattan L-route
    sch.add_wire(vin_p[0], vin_p[1], ccin_a[0], vin_p[1])  # (80,304)→(400,304)
    sch.add_wire(ccin_a[0], vin_p[1], ccin_a[0], ccin_a[1])  # (400,304)→(400,256)

    # Collector → CC_out left pin
    sch.add_wire(qc[0], qc[1], ccout_b[0], ccout_b[1])

    # Emitter → CE top (bypass cap in parallel with RE)
    sch.add_wire(qe[0], qe[1], ce_a[0], ce_a[1])
    # CE bottom → GND (separate ground flag, no diagonal wire to RE)

    # === COMPONENTS ===
    sch.add_component("npn", "Q1", bjt_model, 400, 288)
    sch.add_component("res", "RC", rc, 448, 192)
    sch.add_component("res", "RE", re, 448, 368)
    sch.add_component("res", "RB1", rb1, 240, 240)
    sch.add_component("res", "RB2", rb2, 240, 320)
    sch.add_component("cap", "CC1", cc_in, 384, 256)
    sch.add_component("cap", "CC2", cc_out, 560, 272, rotation=90)
    sch.add_component("cap", "CE", ce, 528, 384)
    sch.add_component("voltage", "Vcc", vcc, 80, 96)
    sch.add_component("voltage", "Vin", "SINE(0 10m 1k)", 80, 288)

    # === FLAGS ===
    sch.add_ground(*vcc_n)
    sch.add_ground(*vin_n)
    sch.add_ground(*rb2_b)
    sch.add_ground(*re_b)
    sch.add_ground(*ce_b)  # CE bottom to GND directly (no diagonal wire)
    sch.add_net_label("out", *ccout_a)

    sch.add_directive(".tran 5m", 80, 528)

    return sch


def generate_colpitts_oscillator(
    ind: str = "1u",
    c1: str = "100p",
    c2: str = "100p",
    rb: str = "47k",
    rc: str = "1k",
    re: str = "470",
    vcc: str = "12",
    bjt_model: str = "2N2222",
) -> AscSchematic:
    """Generate a Colpitts oscillator schematic.

    Topology::

        Vcc --[RC]--> collector --[L1]--> tank
        Vcc --[RB]--> base
        tank --[C1]--> base
        tank --[C2]--> GND
        emitter --[RE]--> GND
        f ~ 1/(2*pi*sqrt(L*C1*C2/(C1+C2)))
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)

    # Q1 NPN at (400, 288): C=(464,288), B=(400,336), E=(464,384)
    qc = pin_position("npn", 0, 400, 288)
    qb = pin_position("npn", 1, 400, 288)

    # RC: Vcc to collector. res at (448, 192): pinA=(464,208), pinB=(464,288)
    rc_a = pin_position("res", 0, 448, 192)

    # RE: emitter to GND. res at (448, 368): pinA=(464,384), pinB=(464,464)
    re_b = pin_position("res", 1, 448, 368)

    # RB: Vcc to base. res at (240, 240): pinA=(256,256), pinB=(256,336)
    rb_a = pin_position("res", 0, 240, 240)
    rb_b = pin_position("res", 1, 240, 240)

    # Vcc source at (80, 96): pin+=(80,112), pin-=(80,192)
    vcc_p = pin_position("voltage", 0, 80, 96)
    vcc_n = pin_position("voltage", 1, 80, 96)

    # L1 horizontal (R90) from collector to tank node.
    # ind R90 at origin (576, 272):
    #   pinA = origin+(-16,16) = (560, 288) — right/tank side
    #   pinB = origin+(-96,16) = (480, 288) — left/collector side
    l1_a = pin_position("ind", 0, 576, 272, 90)  # (560, 288) = tank
    l1_b = pin_position("ind", 1, 576, 272, 90)  # (480, 288) = near collector

    # C1 from tank down to base (feedback).
    # cap at (544, 288): pinA=(560, 288)=tank (same as l1_a), pinB=(560, 352)
    c1_b = pin_position("cap", 1, 544, 288)  # (560, 352)

    # C2 from tank to GND.
    # cap at (640, 288): pinA=(656, 288), pinB=(656, 352)
    c2_a = pin_position("cap", 0, 640, 288)  # (656, 288)
    c2_b = pin_position("cap", 1, 640, 288)  # (656, 352)

    # === WIRING ===
    vcc_y = rc_a[1]  # 208

    # Vcc rail: route RIGHT from Vcc+ first (avoid crossing Vcc- at 80,192)
    sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1])  # (80,112) → (160,112)
    sch.add_wire(160, vcc_p[1], 160, vcc_y)            # (160,112) → (160,208)
    sch.add_wire(160, vcc_y, rc_a[0], vcc_y)           # (160,208) → RC top
    sch.add_wire(rb_a[0], rb_a[1], rb_a[0], vcc_y)
    sch.add_wire(rb_a[0], vcc_y, rc_a[0], vcc_y)

    # RB bottom to base
    sch.add_wire(rb_b[0], rb_b[1], qb[0], qb[1])

    # Collector to L1 left pin
    sch.add_wire(qc[0], qc[1], l1_b[0], l1_b[1])

    # Tank node: L1 right pin to C1 top (same point at 560,288)
    # Wire from tank to C2 top
    sch.add_wire(l1_a[0], l1_a[1], c2_a[0], c2_a[1])

    # C1 bottom to base: L-route down then left
    sch.add_wire(c1_b[0], c1_b[1], qb[0], c1_b[1])  # horizontal
    sch.add_wire(qb[0], c1_b[1], qb[0], qb[1])  # vertical to base

    # === COMPONENTS ===
    sch.add_component("npn", "Q1", bjt_model, 400, 288)
    sch.add_component("res", "RC", rc, 448, 192)
    sch.add_component("res", "RE", re, 448, 368)
    sch.add_component("res", "RB", rb, 240, 240)
    sch.add_component("ind", "L1", ind, 576, 272, rotation=90)
    sch.add_component("cap", "C1", c1, 544, 288)
    sch.add_component("cap", "C2", c2, 640, 288)
    sch.add_component("voltage", "Vcc", vcc, 80, 96)

    # === FLAGS ===
    sch.add_ground(*vcc_n)
    sch.add_ground(*re_b)
    sch.add_ground(*c2_b)
    sch.add_net_label("out", *qc)

    sch.add_directive(".tran 100u", 80, 528)
    sch.add_directive(".ic V(out)=6", 80, 560)

    return sch


def generate_differential_amp(
    r1: str = "10k",
    r2: str = "10k",
    r3: str = "10k",
    r4: str = "10k",
) -> AscSchematic:
    """Generate a differential amplifier schematic.

    Topology::

        V1 --[R1]--> inv(-)  --[R2]--> out
        V2 --[R3]--> non-inv(+)
                     non-inv(+) --[R4]--> GND
        Supply: +/-15V
        Vout = (R2/R1) * (V2 - V1) when R2/R1 = R4/R3

    Args:
        r1: Input resistor to inverting node
        r2: Feedback resistor
        r3: Input resistor to non-inverting node
        r4: Non-inverting node to GND
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)
    oa_sym = "OpAmps/UniversalOpamp2"

    # U1 at (512, 336): In+=(480,352), In-=(480,320), V+=(512,304), V-=(512,368), OUT=(544,336)
    inp = pin_position(oa_sym, 0, 512, 336)   # (480, 352)
    inn = pin_position(oa_sym, 1, 512, 336)   # (480, 320)
    vp = pin_position(oa_sym, 2, 512, 336)    # (512, 304)
    vn = pin_position(oa_sym, 3, 512, 336)    # (512, 368)
    out = pin_position(oa_sym, 4, 512, 336)   # (544, 336)

    # R1 horizontal (R90) from V1 to inv junction.
    # R90 at (448, 304): pinA=(432,320), pinB=(352,320)
    r1_a = pin_position("res", 0, 448, 304, 90)  # (432, 320) near In-
    r1_b = pin_position("res", 1, 448, 304, 90)  # (352, 320) toward V1

    # R2 horizontal (R90) above op-amp for feedback.
    # R90 at (560, 208): pinA=(544,224), pinB=(464,224)
    r2_a = pin_position("res", 0, 560, 208, 90)  # (544, 224) near OUT
    r2_b = pin_position("res", 1, 560, 208, 90)  # (464, 224)

    # R3 horizontal (R90) from V2 to noninv junction.
    # R90 at (448, 336): pinA=(432,352), pinB=(352,352)
    r3_a = pin_position("res", 0, 448, 336, 90)  # (432, 352) near In+
    r3_b = pin_position("res", 1, 448, 336, 90)  # (352, 352) toward V2

    # R4 vertical from noninv junction to GND.
    # res at (336, 352): pinA=(352,368), pinB=(352,448)
    r4_a = pin_position("res", 0, 336, 352)  # (352, 368)
    r4_b = pin_position("res", 1, 336, 352)  # (352, 448)

    # V1 at (80, 224): pin+=(80,240), pin-=(80,320)
    v1p = pin_position("voltage", 0, 80, 224)
    v1n = pin_position("voltage", 1, 80, 224)

    # V2 at (80, 384): pin+=(80,400), pin-=(80,480)
    v2p = pin_position("voltage", 0, 80, 384)
    v2n = pin_position("voltage", 1, 80, 384)

    # Supply: Vpos at (688, 176), Vneg at (688, 416)
    vpos_p = pin_position("voltage", 0, 688, 176)
    vpos_n = pin_position("voltage", 1, 688, 176)
    vneg_p = pin_position("voltage", 0, 688, 416)
    vneg_n = pin_position("voltage", 1, 688, 416)

    # === WIRING ===
    # Inv path: R1 pinA to In-
    sch.add_wire(r1_a[0], r1_a[1], inn[0], inn[1])

    # V1 to R1 pinB: route RIGHT first (avoid crossing V1- at 80,320)
    sch.add_wire(v1p[0], v1p[1], r1_b[0], v1p[1])  # (80,240) → (352,240)
    sch.add_wire(r1_b[0], v1p[1], r1_b[0], r1_b[1])  # (352,240) → (352,320)

    # R2 feedback: OUT up to R2 pinA, R2 pinB left and down to inv junction
    sch.add_wire(out[0], out[1], r2_a[0], r2_a[1])  # (544,336)→(544,224)
    sch.add_wire(r2_b[0], r2_b[1], r1_a[0], r2_b[1])  # (464,224)→(432,224)
    sch.add_wire(r1_a[0], r2_b[1], r1_a[0], r1_a[1])  # (432,224)→(432,320)

    # Noninv path: R3 pinA to In+
    sch.add_wire(r3_a[0], r3_a[1], inp[0], inp[1])

    # V2 to R3 pinB: route (80,400) → (80,352) → (352,352)
    sch.add_wire(v2p[0], v2p[1], v2p[0], r3_b[1])
    sch.add_wire(v2p[0], r3_b[1], r3_b[0], r3_b[1])

    # R4 from noninv junction to GND
    sch.add_wire(r3_b[0], r3_b[1], r4_a[0], r4_a[1])  # (352,352)→(352,368)

    # Supply wiring
    sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])
    sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1])
    sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])
    sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1])

    # === COMPONENTS ===
    sch.add_component("voltage", "V1", "AC 1", 80, 224)
    sch.add_component("voltage", "V2", "AC 1", 80, 384)
    sch.add_component(oa_sym, "U1", "", 512, 336)
    sch.add_component("res", "R1", r1, 448, 304, rotation=90)
    sch.add_component("res", "R2", r2, 560, 208, rotation=90)
    sch.add_component("res", "R3", r3, 448, 336, rotation=90)
    sch.add_component("res", "R4", r4, 336, 352)
    sch.add_component("voltage", "Vpos", "15", 688, 176)
    sch.add_component("voltage", "Vneg", "15", 688, 416)

    # === FLAGS ===
    sch.add_ground(*v1n)
    sch.add_ground(*v2n)
    sch.add_ground(*r4_b)
    sch.add_ground(*vpos_n)
    sch.add_ground(*vneg_p)
    sch.add_net_label("out", *out)

    sch.add_directive(".ac dec 100 1 1meg", 80, 560)

    return sch


def generate_buck_converter(
    ind: str = "10u",
    c_out: str = "100u",
    r_load: str = "10",
    v_in: str = "12",
    duty_cycle: float = 0.5,
    freq: str = "100k",
    mosfet_model: str = "IRF540N",
    diode_model: str = "1N5819",
) -> AscSchematic:
    """Generate a buck (step-down) converter schematic.

    Topology::

        Vin+ ─── drain
                   M1 (NMOS switch)
                  source ── sw ──[L1]── out ──+
                    |                         |
                   [D1]                 [Cout] [Rload]
                    |                    |      |
                   GND                  GND    GND

    Gate driven by PULSE source at specified frequency and duty cycle.

    Args:
        ind: Inductor value
        c_out: Output capacitor
        r_load: Load resistor
        v_in: Input voltage
        duty_cycle: Switching duty cycle (0.0-1.0)
        freq: Switching frequency
        mosfet_model: NMOS model name
        diode_model: Freewheeling diode model
    """
    sch = AscSchematic(sheet_w=1040, sheet_h=680)

    # Compute PULSE timing
    freq_hz = _parse_spice_value(freq)
    period = 1.0 / freq_hz
    t_on = period * duty_cycle
    t_rise = period * 0.01
    t_fall = t_rise
    pulse_val = (
        f"PULSE(0 {v_in} 0 {t_rise:.4g} {t_fall:.4g} {t_on:.4g} {period:.4g})"
    )

    # Vin at (80, 48): pin+=(80,64), pin-=(80,144)
    vin_p = pin_position("voltage", 0, 80, 48)
    vin_n = pin_position("voltage", 1, 80, 48)

    # Vgate at (80, 256): pin+=(80,272), pin-=(80,352)
    vg_p = pin_position("voltage", 0, 80, 256)
    vg_n = pin_position("voltage", 1, 80, 256)

    # NMOS at (256, 64): D=(304,64), G=(256,144), S=(304,160)
    md = pin_position("nmos", 0, 256, 64)   # drain
    mg = pin_position("nmos", 1, 256, 64)   # gate
    ms = pin_position("nmos", 2, 256, 64)   # source

    # Diode R180 at (320, 224): cathode at sw (auto-connects), anode at GND
    d_anode = pin_position("diode", 0, 320, 224, 180)  # (304, 224)

    # L1 R270 at (288, 176): pinA=(304,160)=sw (auto-connects), pinB=(384,160)=output
    l1_b = pin_position("ind", 1, 288, 176, 270)  # (384, 160)

    # Cout at (368, 160): pinA=(384,160)=output, pinB=(384,224)
    cout_b = pin_position("cap", 1, 368, 160)  # (384, 224)

    # Rload at (448, 144): pinA=(464,160), pinB=(464,240)
    rl_a = pin_position("res", 0, 448, 144)  # (464, 160)
    rl_b = pin_position("res", 1, 448, 144)  # (464, 240)

    # === WIRING ===
    sch.add_wire(vin_p[0], vin_p[1], md[0], md[1])  # Vin+ to drain

    # Gate drive: Vgate+ → route to gate
    sch.add_wire(vg_p[0], vg_p[1], 160, vg_p[1])
    sch.add_wire(160, vg_p[1], 160, mg[1])
    sch.add_wire(160, mg[1], mg[0], mg[1])

    # Output to Rload
    sch.add_wire(l1_b[0], l1_b[1], rl_a[0], rl_a[1])

    # === COMPONENTS ===
    sch.add_component("voltage", "Vin", v_in, 80, 48)
    sch.add_component("voltage", "Vgate", pulse_val, 80, 256)
    sch.add_component("nmos", "M1", mosfet_model, 256, 64)
    sch.add_component("diode", "D1", diode_model, 320, 224, rotation=180)
    sch.add_component("ind", "L1", ind, 288, 176, rotation=270)
    sch.add_component("cap", "Cout", c_out, 368, 160)
    sch.add_component("res", "Rload", r_load, 448, 144)

    # === FLAGS ===
    sch.add_ground(*vin_n)
    sch.add_ground(*vg_n)
    sch.add_ground(*d_anode)
    sch.add_ground(*cout_b)
    sch.add_ground(*rl_b)
    sch.add_net_label("sw", *ms)
    sch.add_net_label("out", *l1_b)

    sch.add_directive(f".tran {period * 200:.4g}", 80, 420)

    return sch


def generate_ldo_regulator(
    r1: str = "10k",
    r2: str = "10k",
    pass_transistor: str = "IRF9540N",
    v_in: str = "8",
    v_ref: str = "2.5",
) -> AscSchematic:
    """Generate a simple LDO voltage regulator schematic.

    Topology::

        Vin ──[PMOS source]──[PMOS drain]── out ──+
                 gate                              |
                  |                          [R1] [Cout] [Rload]
               [Op-amp OUT]                   |     |      |
               + = Vref                      fb    GND    GND
               - = fb                         |
                                             [R2]
                                              |
                                             GND

        Vout = Vref * (1 + R1/R2)

    Args:
        r1: Top feedback resistor (out to fb)
        r2: Bottom feedback resistor (fb to GND)
        pass_transistor: PMOS model name
        v_in: Input voltage
        v_ref: Reference voltage
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)
    oa_sym = "OpAmps/UniversalOpamp2"

    # PMOS M1 at (400, 48): D=(448,48)=out, G=(400,128)=gate, S=(448,144)=vin
    m_d = pin_position("pmos", 0, 400, 48)   # drain (448, 48) = output
    m_g = pin_position("pmos", 1, 400, 48)   # gate  (400, 128)
    m_s = pin_position("pmos", 2, 400, 48)   # source (448, 144) = vin

    # Vin at (80, 48): pin+=(80,64), pin-=(80,144)
    vin_p = pin_position("voltage", 0, 80, 48)
    vin_n = pin_position("voltage", 1, 80, 48)

    # Vref at (80, 336): pin+=(80,352), pin-=(80,432)
    vref_p = pin_position("voltage", 0, 80, 336)
    vref_n = pin_position("voltage", 1, 80, 336)

    # Op-amp U1 at (304, 304): In+=(272,320), In-=(272,288), V+=(304,272),
    #   V-=(304,336), OUT=(336,304)
    oa_inp = pin_position(oa_sym, 0, 304, 304)   # (272, 320) = vref
    oa_inn = pin_position(oa_sym, 1, 304, 304)   # (272, 288) = fb
    oa_vp = pin_position(oa_sym, 2, 304, 304)    # (304, 272) = V+
    oa_vn = pin_position(oa_sym, 3, 304, 304)    # (304, 336) = V-
    oa_out = pin_position(oa_sym, 4, 304, 304)   # (336, 304) = gate drive

    # R1 from output down to fb node. res at (560, 160): pinA=(576,176), pinB=(576,256)
    r1_a = pin_position("res", 0, 560, 160)  # (576, 176)
    r1_b = pin_position("res", 1, 560, 160)  # (576, 256) = fb

    # R2 from fb to GND. res at (560, 256): pinA=(576,272), pinB=(576,352)
    r2_a = pin_position("res", 0, 560, 256)  # (576, 272) = fb
    r2_b = pin_position("res", 1, 560, 256)  # (576, 352) = GND

    # Cout at (640, 48): pinA=(656,48)=out, pinB=(656,112)=GND
    cout_a = pin_position("cap", 0, 640, 48)  # (656, 48)
    cout_b = pin_position("cap", 1, 640, 48)  # (656, 112)

    # Rload at (720, 48): pinA=(736,64), pinB=(736,144)
    rl_a = pin_position("res", 0, 720, 48)   # (736, 64)
    rl_b = pin_position("res", 1, 720, 48)   # (736, 144)

    # === WIRING ===
    # Vin to PMOS source: (80,64) right to (448,64), down to source (448,144)
    sch.add_wire(vin_p[0], vin_p[1], m_s[0], vin_p[1])  # horizontal
    sch.add_wire(m_s[0], vin_p[1], m_s[0], m_s[1])       # vertical to source

    # PMOS drain (output) right to R1, Cout, Rload
    # Drain at (448, 48). Wire right to Cout pinA (656, 48)
    sch.add_wire(m_d[0], m_d[1], cout_a[0], cout_a[1])
    # Continue to Rload: (656,48) → (736,48), down to pinA (736,64)
    sch.add_wire(cout_a[0], cout_a[1], rl_a[0], cout_a[1])
    sch.add_wire(rl_a[0], cout_a[1], rl_a[0], rl_a[1])
    # R1 top: down from output rail. (576,48) → (576,176)
    sch.add_wire(m_d[0], m_d[1], r1_a[0], m_d[1])  # (448,48)→(576,48)
    sch.add_wire(r1_a[0], m_d[1], r1_a[0], r1_a[1])  # (576,48)→(576,176)

    # Op-amp output to PMOS gate
    sch.add_wire(oa_out[0], oa_out[1], m_g[0], oa_out[1])  # horizontal
    sch.add_wire(m_g[0], oa_out[1], m_g[0], m_g[1])  # vertical to gate

    # Vref to op-amp In+
    sch.add_wire(vref_p[0], vref_p[1], vref_p[0], oa_inp[1])
    sch.add_wire(vref_p[0], oa_inp[1], oa_inp[0], oa_inp[1])

    # Feedback (fb) to op-amp In-: R1 bottom/R2 top junction at (576,256/272)
    # Wire from fb junction to In- (272, 288)
    sch.add_wire(r2_a[0], r2_a[1], r2_a[0], oa_inn[1])  # (576,272)→(576,288)
    sch.add_wire(r2_a[0], oa_inn[1], oa_inn[0], oa_inn[1])  # →(272,288)

    # Op-amp supply: V+ to Vin rail, V- to GND
    sch.add_wire(oa_vp[0], oa_vp[1], oa_vp[0], vin_p[1])  # V+ up to y=64
    sch.add_wire(oa_vp[0], vin_p[1], vin_p[0], vin_p[1])  # left to Vin+

    # === COMPONENTS ===
    sch.add_component("pmos", "M1", pass_transistor, 400, 48)
    sch.add_component(oa_sym, "U1", "", 304, 304)
    sch.add_component("voltage", "Vin", v_in, 80, 48)
    sch.add_component("voltage", "Vref", v_ref, 80, 336)
    sch.add_component("res", "R1", r1, 560, 160)
    sch.add_component("res", "R2", r2, 560, 256)
    sch.add_component("cap", "Cout", "10u", 640, 48)
    sch.add_component("res", "Rload", "100", 720, 48)

    # === FLAGS ===
    sch.add_ground(*vin_n)
    sch.add_ground(*vref_n)
    sch.add_ground(*r2_b)
    sch.add_ground(*cout_b)
    sch.add_ground(*rl_b)
    sch.add_ground(oa_vn[0], oa_vn[1])
    sch.add_net_label("out", *m_d)
    sch.add_net_label("fb", *r1_b)

    sch.add_directive(".tran 10m", 80, 500)

    return sch


def generate_h_bridge(
    v_supply: str = "12",
    r_load: str = "10",
    mosfet_model: str = "IRF540N",
) -> AscSchematic:
    """Generate an H-bridge motor driver schematic.

    Topology (two half-bridges with load between them)::

        Vcc ──┬── M1_D     M2_D ──┬── Vcc
              M1(fwd)       M2(rev)
        outA ─┤── M1_S     M2_S ──├─ outB
              │                    │
              ├──── [Rload] ──────┤
              │                    │
        outA ─┤── M3_D     M4_D ──├─ outB
              M3(rev)       M4(fwd)
        GND ──┴── M3_S     M4_S ──┴── GND

    M1+M4 driven together (forward), M2+M3 driven together (reverse).
    Complementary PULSE gate drives with dead time.

    Args:
        v_supply: Supply voltage
        r_load: Load resistance
        mosfet_model: NMOS model name (used for all 4 switches)
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)

    # PULSE gate drive parameters
    period = "1m"
    t_on = "450u"
    t_dead = "25u"
    fwd_pulse = f"PULSE(0 {v_supply} {t_dead} 10n 10n {t_on} {period})"
    rev_pulse = f"PULSE(0 {v_supply} 525u 10n 10n {t_on} {period})"

    # --- Left column (A side) ---
    # M1 (high-A) at (320, 48): D=(368,48), G=(320,128), S=(368,144)
    m1_d = pin_position("nmos", 0, 320, 48)   # (368, 48) = vcc
    m1_g = pin_position("nmos", 1, 320, 48)   # (320, 128) = gate_fwd
    m1_s = pin_position("nmos", 2, 320, 48)   # (368, 144) = outA

    # M3 (low-A) at (320, 240): D=(368,240), G=(320,320), S=(368,336)
    m3_d = pin_position("nmos", 0, 320, 240)  # (368, 240) = outA
    m3_g = pin_position("nmos", 1, 320, 240)  # (320, 320) = gate_rev
    m3_s = pin_position("nmos", 2, 320, 240)  # (368, 336) = GND

    # --- Right column (B side) ---
    # M2 (high-B) at (560, 48): D=(608,48), G=(560,128), S=(608,144)
    m2_d = pin_position("nmos", 0, 560, 48)   # (608, 48) = vcc
    m2_g = pin_position("nmos", 1, 560, 48)   # (560, 128) = gate_rev
    m2_s = pin_position("nmos", 2, 560, 48)   # (608, 144) = outB

    # M4 (low-B) at (560, 240): D=(608,240), G=(560,320), S=(608,336)
    m4_d = pin_position("nmos", 0, 560, 240)  # (608, 240) = outB
    m4_g = pin_position("nmos", 1, 560, 240)  # (560, 320) = gate_fwd
    m4_s = pin_position("nmos", 2, 560, 240)  # (608, 336) = GND

    # Rload R90 between outA and outB at y=192
    # R90 at (544, 176): pinA=(528,192), pinB=(448,192)
    rl_a = pin_position("res", 0, 544, 176, 90)  # (528, 192)
    rl_b = pin_position("res", 1, 544, 176, 90)  # (448, 192)

    # Sources
    # Vsupply at (80, 48): pin+=(80,64), pin-=(80,144)
    vs_p = pin_position("voltage", 0, 80, 48)
    vs_n = pin_position("voltage", 1, 80, 48)

    # Vg_fwd at (80, 272): pin+=(80,288), pin-=(80,368)
    vgf_p = pin_position("voltage", 0, 80, 272)
    vgf_n = pin_position("voltage", 1, 80, 272)

    # Vg_rev at (80, 432): pin+=(80,448), pin-=(80,528)
    vgr_p = pin_position("voltage", 0, 80, 432)
    vgr_n = pin_position("voltage", 1, 80, 432)

    # === WIRING ===
    # Vcc rail: Vsupply+ to M1_D to M2_D
    sch.add_wire(vs_p[0], vs_p[1], vs_p[0], m1_d[1])   # (80,64)→(80,48)
    sch.add_wire(vs_p[0], m1_d[1], m1_d[0], m1_d[1])    # (80,48)→(368,48)
    sch.add_wire(m1_d[0], m1_d[1], m2_d[0], m2_d[1])    # (368,48)→(608,48)

    # Left column vertical: M1_S → outA junction → M3_D
    sch.add_wire(m1_s[0], m1_s[1], m1_s[0], 192)        # (368,144)→(368,192)
    sch.add_wire(m1_s[0], 192, m3_d[0], m3_d[1])        # (368,192)→(368,240)

    # Right column vertical: M2_S → outB junction → M4_D
    sch.add_wire(m2_s[0], m2_s[1], m2_s[0], 192)        # (608,144)→(608,192)
    sch.add_wire(m2_s[0], 192, m4_d[0], m4_d[1])        # (608,192)→(608,240)

    # Rload connections
    sch.add_wire(m1_s[0], 192, rl_b[0], rl_b[1])        # outA→Rload pinB
    sch.add_wire(rl_a[0], rl_a[1], m2_s[0], 192)        # Rload pinA→outB

    # Gate connections via net labels (cleaner than long wires)
    # gate_fwd: M1_G, M4_G, Vg_fwd+
    # gate_rev: M2_G, M3_G, Vg_rev+

    # === COMPONENTS ===
    sch.add_component("voltage", "Vsupply", v_supply, 80, 48)
    sch.add_component("voltage", "Vg_fwd", fwd_pulse, 80, 272)
    sch.add_component("voltage", "Vg_rev", rev_pulse, 80, 432)
    sch.add_component("nmos", "M1", mosfet_model, 320, 48)
    sch.add_component("nmos", "M2", mosfet_model, 560, 48)
    sch.add_component("nmos", "M3", mosfet_model, 320, 240)
    sch.add_component("nmos", "M4", mosfet_model, 560, 240)
    sch.add_component("res", "Rload", r_load, 544, 176, rotation=90)

    # === FLAGS ===
    sch.add_ground(*vs_n)
    sch.add_ground(*vgf_n)
    sch.add_ground(*vgr_n)
    sch.add_ground(*m3_s)
    sch.add_ground(*m4_s)
    sch.add_net_label("gate_fwd", *m1_g)
    sch.add_net_label("gate_fwd", *m4_g)
    sch.add_net_label("gate_fwd", *vgf_p)
    sch.add_net_label("gate_rev", *m2_g)
    sch.add_net_label("gate_rev", *m3_g)
    sch.add_net_label("gate_rev", *vgr_p)
    sch.add_net_label("outA", *m1_s)
    sch.add_net_label("outB", *m2_s)

    sch.add_directive(".tran 5m", 80, 592)

    return sch


def generate_sallen_key_lowpass(
    r1: str = "10k",
    r2: str = "10k",
    c1: str = "10n",
    c2: str = "10n",
) -> AscSchematic:
    """Generate a Sallen-Key lowpass filter schematic (unity gain).

    Topology::

        V1 --[R1]-- n1 --[R2]-- n2 (In+)
                     |                    --> U1 --> out
                    [C1]--> out           In- --> out (unity gain)
                     |
                    [C2]
                     |
                    GND

    The C1 connects n1 to out (feedback). C2 connects n2 to GND.
    f_c = 1 / (2*pi*sqrt(R1*R2*C1*C2)).  Supply: +/-15V.

    Args:
        r1: First series resistor
        r2: Second series resistor
        c1: Feedback capacitor (n1 to out)
        c2: Shunt capacitor (n2 to GND)
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)
    oa_sym = "OpAmps/UniversalOpamp2"

    # Op-amp U1 at (576, 336)
    inp = pin_position(oa_sym, 0, 576, 336)   # In+  = (544, 352)
    inn = pin_position(oa_sym, 1, 576, 336)   # In-  = (544, 320)
    vp = pin_position(oa_sym, 2, 576, 336)    # V+   = (576, 304)
    vn = pin_position(oa_sym, 3, 576, 336)    # V-   = (576, 368)
    out = pin_position(oa_sym, 4, 576, 336)   # OUT  = (608, 336)

    # V1 at (80, 256): pin+=(80,272), pin-=(80,352)
    v1p = pin_position("voltage", 0, 80, 256)
    v1n = pin_position("voltage", 1, 80, 256)

    # R1 horizontal (R90) at origin (256, 256): pinA=(240,272), pinB=(160,272)
    r1_a = pin_position("res", 0, 256, 256, 90)  # (240, 272) = n1 side
    r1_b = pin_position("res", 1, 256, 256, 90)  # (160, 272) = input side

    # R2 horizontal (R90) at origin (416, 256): pinA=(400,272), pinB=(320,272)
    r2_a = pin_position("res", 0, 416, 256, 90)  # (400, 272) = n2 side
    r2_b = pin_position("res", 1, 416, 256, 90)  # (320, 272) = n1 side

    # C1 vertical: connects n1 (junction R1_a/R2_b at x=272-ish) to out
    # We route n1 down to C1, then C1 bottom goes right to out.
    # C1 at (304, 336): pinA=(320, 336), pinB=(320, 400)
    # Actually let's place C1 horizontal (R90) above the feedback path.
    # Better: C1 as a horizontal cap (R90) from n1 to out.
    # cap R90: pinA offset (16,0)-> (0,16), pinB offset (16,64)->(-64,16)
    # At origin (560, 192): pinA=(560,208), pinB=(496,208)
    c1_a = pin_position("cap", 0, 560, 192, 90)  # (560, 208) = out side
    c1_b = pin_position("cap", 1, 560, 192, 90)  # (496, 208) = n1 side

    # C2 vertical at (480, 352): pinA=(496,352), pinB=(496,416)
    c2_a = pin_position("cap", 0, 480, 352)  # (496, 352)
    c2_b = pin_position("cap", 1, 480, 352)  # (496, 416)

    # Supply: Vpos at (752, 176), Vneg at (752, 416)
    vpos_p = pin_position("voltage", 0, 752, 176)  # (752, 192)
    vpos_n = pin_position("voltage", 1, 752, 176)  # (752, 272)
    vneg_p = pin_position("voltage", 0, 752, 416)  # (752, 432)
    vneg_n = pin_position("voltage", 1, 752, 416)  # (752, 512)

    # === WIRING ===
    # V1+ to R1 pinB
    sch.add_wire(*v1p, *r1_b)

    # R1 pinA to R2 pinB (n1 junction at roughly x=272..320)
    # n1 junction: R1_a at (240,272), R2_b at (320,272) -- wire between
    sch.add_wire(*r1_a, *r2_b)

    # R2 pinA to In+ : (400,272) down to (400,352) then right to (544,352)
    sch.add_wire(r2_a[0], r2_a[1], r2_a[0], inp[1])
    sch.add_wire(r2_a[0], inp[1], inp[0], inp[1])

    # n2 junction to C2: from R2_a/In+ junction at (400,352) left to C2 pinA (496,352)
    # Actually, n2 is at the In+ side. C2 from n2 to GND.
    # n2 is where R2 meets In+. Let's tap off at (496,352) for C2.
    sch.add_wire(r2_a[0], inp[1], c2_a[0], c2_a[1])

    # C1 feedback: n1 to out via C1
    # n1 is at the R1_a / R2_b junction. Wire from n1 up to C1 pinB.
    # n1 at y=272, C1 pinB at (496, 208)
    # Route from (240,272) up to (240,208) then right to (496,208)
    sch.add_wire(r1_a[0], r1_a[1], r1_a[0], c1_b[1])
    sch.add_wire(r1_a[0], c1_b[1], c1_b[0], c1_b[1])

    # C1 pinA to out: (560,208) down to out (608,336)
    sch.add_wire(c1_a[0], c1_a[1], out[0], c1_a[1])
    sch.add_wire(out[0], c1_a[1], out[0], out[1])

    # Unity gain feedback: out to In-
    # (608,336) up to (608,320) ... In- is at (544,320)
    # Route: out (608,336) left to (544,336) up... no.
    # Actually wire from In- (544,320) right and down to out (608,336).
    # Route: (544,320) right to (620,320), down to (620,336), left to (608,336)
    # Simpler: go below. (608,336) to (608,400), left to (544,400), up to (544,320)
    sch.add_wire(out[0], out[1], out[0], 400)
    sch.add_wire(out[0], 400, inn[0], 400)
    sch.add_wire(inn[0], 400, inn[0], inn[1])

    # Supply wiring
    sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])
    sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1])
    sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])
    sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1])

    # === COMPONENTS ===
    sch.add_component("voltage", "V1", "AC 1", 80, 256)
    sch.add_component(oa_sym, "U1", "", 576, 336)
    sch.add_component("res", "R1", r1, 256, 256, rotation=90)
    sch.add_component("res", "R2", r2, 416, 256, rotation=90)
    sch.add_component("cap", "C1", c1, 560, 192, rotation=90)
    sch.add_component("cap", "C2", c2, 480, 352)
    sch.add_component("voltage", "Vpos", "15", 752, 176)
    sch.add_component("voltage", "Vneg", "15", 752, 416)

    # === FLAGS ===
    sch.add_ground(*v1n)
    sch.add_ground(*c2_b)
    sch.add_ground(*vpos_n)
    sch.add_ground(*vneg_p)
    sch.add_net_label("out", *out)
    sch.add_net_label("n1", r1_a[0], r1_a[1])
    sch.add_net_label("n2", r2_a[0], r2_a[1])

    sch.add_directive(".ac dec 100 1 1meg", 80, 560)

    return sch


def generate_boost_converter(
    ind: str = "10u",
    c_out: str = "100u",
    r_load: str = "50",
    v_in: str = "5",
    duty_cycle: float = 0.5,
    freq: str = "100k",
) -> AscSchematic:
    """Generate a boost (step-up) converter schematic.

    Topology::

        Vin --[L1]--> sw_node --[D1]--> out
                         |                |
                       [MOSFET]     [Cout] [Rload]
                       D=sw            |      |
                       G=gate         GND    GND
                       S=GND

    Gate driven by PULSE at switching frequency and duty cycle.
    Vout_ideal = Vin / (1 - duty_cycle).

    Args:
        ind: Inductor value
        c_out: Output capacitor
        r_load: Load resistor
        v_in: Input voltage
        duty_cycle: Switching duty cycle (0.0-1.0)
        freq: Switching frequency
    """
    sch = AscSchematic(sheet_w=1040, sheet_h=680)

    # Compute PULSE timing
    freq_hz = _parse_spice_value(freq)
    period = 1.0 / freq_hz
    t_on = period * duty_cycle
    t_rise = period * 0.01
    t_fall = t_rise
    pulse_val = (
        f"PULSE(0 {v_in} 0 {t_rise:.4g} {t_fall:.4g} {t_on:.4g} {period:.4g})"
    )

    mosfet_model = "IRF540N"
    diode_model = "1N5819"

    # Vin at (80, 48): pin+=(80,64), pin-=(80,144)
    vin_p = pin_position("voltage", 0, 80, 48)
    vin_n = pin_position("voltage", 1, 80, 48)

    # Vgate at (80, 256): pin+=(80,272), pin-=(80,352)
    vg_p = pin_position("voltage", 0, 80, 256)
    vg_n = pin_position("voltage", 1, 80, 256)

    # L1 horizontal (R90) at (192, 48):
    # ind R90: pinA offset (16,16)->(-16,16), pinB offset (16,96)->(-96,16)
    # At (192, 48): pinA = (176, 64), pinB = (96, 64)
    # Actually re-derive: R90 transform: (px,py) -> (-py, px)
    # pinA raw = (16,16) -> R90 -> (-16, 16). origin(192,48) + (-16,16) = (176, 64)
    # pinB raw = (16,96) -> R90 -> (-96, 16). origin(192,48) + (-96,16) = (96, 64)
    # So pinB is on the Vin side, pinA on the sw side. Good.
    l1_a = pin_position("ind", 0, 192, 48, 90)  # (176, 64) = sw side
    l1_b = pin_position("ind", 1, 192, 48, 90)  # (96, 64) = vin side

    # NMOS at (224, 112): D=(272,112)=sw, G=(224,192), S=(272,208)=GND
    # Actually let's position so drain lines up with L1 pinA.
    # L1_a = (176, 64). NMOS drain should be at (176, something).
    # nmos D offset = (48, 0). So origin = (176-48, y) = (128, y).
    # Want drain at y ~ 112 range. Put origin at (128, 112): D=(176,112).
    # Then wire from L1_a (176,64) down to D (176,112).
    md = pin_position("nmos", 0, 128, 112)   # drain (176, 112)
    mg = pin_position("nmos", 1, 128, 112)   # gate (128, 192)
    ms = pin_position("nmos", 2, 128, 112)   # source (176, 208)

    # Diode from sw_node to output.
    # sw_node is at L1_a (176,64). Diode horizontal.
    # diode R90: anode offset (16,0)->(0,16), cathode offset (16,64)->(-64,16)
    # At origin (320, 48): anode=(320,64), cathode=(256,64)
    # We want anode at sw side, cathode at out side. But R90 has cathode to left.
    # Use R270 instead: anode (16,0)->R270->(0,-16), cathode (16,64)->R270->(64,-16)
    # At origin (240, 80): anode=(240,64), cathode=(304,64)
    # Wire from sw (176,64) right to anode (240,64). Cathode (304,64) = output side.
    d_anode = pin_position("diode", 0, 240, 80, 270)   # (240, 64)
    d_cathode = pin_position("diode", 1, 240, 80, 270)  # (304, 64)

    # Wire from L1_a (176,64) to sw junction, then to diode anode (240,64)
    # And from diode cathode (304,64) to out

    # Cout at (384, 64): pinA=(400,64), pinB=(400,128)
    cout_a = pin_position("cap", 0, 384, 64)  # (400, 64)
    cout_b = pin_position("cap", 1, 384, 64)  # (400, 128)

    # Rload at (464, 48): pinA=(480,64), pinB=(480,144)
    rl_a = pin_position("res", 0, 464, 48)  # (480, 64)
    rl_b = pin_position("res", 1, 464, 48)  # (480, 144)

    # === WIRING ===
    # Vin+ to L1 pinB
    sch.add_wire(*vin_p, *l1_b)

    # L1 pinA (sw) to MOSFET drain
    sch.add_wire(l1_a[0], l1_a[1], md[0], md[1])

    # L1 pinA (sw) to diode anode
    sch.add_wire(l1_a[0], l1_a[1], d_anode[0], d_anode[1])

    # Diode cathode to Cout and Rload (output rail)
    sch.add_wire(d_cathode[0], d_cathode[1], cout_a[0], cout_a[1])
    sch.add_wire(cout_a[0], cout_a[1], rl_a[0], rl_a[1])

    # Gate drive: Vgate+ to MOSFET gate
    sch.add_wire(vg_p[0], vg_p[1], mg[0], vg_p[1])
    sch.add_wire(mg[0], vg_p[1], mg[0], mg[1])

    # === COMPONENTS ===
    sch.add_component("voltage", "Vin", v_in, 80, 48)
    sch.add_component("voltage", "Vgate", pulse_val, 80, 256)
    sch.add_component("ind", "L1", ind, 192, 48, rotation=90)
    sch.add_component("nmos", "M1", mosfet_model, 128, 112)
    sch.add_component("diode", "D1", diode_model, 240, 80, rotation=270)
    sch.add_component("cap", "Cout", c_out, 384, 64)
    sch.add_component("res", "Rload", r_load, 464, 48)

    # === FLAGS ===
    sch.add_ground(*vin_n)
    sch.add_ground(*vg_n)
    sch.add_ground(*ms)
    sch.add_ground(*cout_b)
    sch.add_ground(*rl_b)
    sch.add_net_label("sw", l1_a[0], l1_a[1])
    sch.add_net_label("out", d_cathode[0], d_cathode[1])

    sch.add_directive(f".tran {period * 200:.4g}", 80, 420)

    return sch


def generate_instrumentation_amp(
    r1: str = "10k",
    r2: str = "10k",
    r3: str = "10k",
    r_gain: str = "10k",
) -> AscSchematic:
    """Generate a 3-opamp instrumentation amplifier schematic.

    Stage 1 (input buffers with gain)::

        Vin+ --> In+(X1) --> out1
        Vin- --> In+(X2) --> out2
        R1 from out1 to In-(X1), R1b from out2 to In-(X2)
        Rgain between In-(X1) and In-(X2)

    Stage 2 (difference amp)::

        out1 --[R2]--> In-(X3) --[R3]--> Vout
        out2 --[R2b]--> In+(X3)
        In+(X3) --[R3b]--> GND

    Gain = (1 + 2*R1/Rgain) * (R3/R2).  Supply: +/-15V.

    Args:
        r1: Stage 1 feedback resistor
        r2: Stage 2 input resistor
        r3: Stage 2 feedback resistor
        r_gain: Gain-setting resistor between X1 and X2 inverting inputs
    """
    sch = AscSchematic(sheet_w=1600, sheet_h=1040)
    oa_sym = "OpAmps/UniversalOpamp2"

    # === STAGE 1 ===
    # X1 at (480, 208)
    x1_inp = pin_position(oa_sym, 0, 480, 208)   # In+ = (448, 224)
    x1_inn = pin_position(oa_sym, 1, 480, 208)   # In- = (448, 192)
    x1_vp = pin_position(oa_sym, 2, 480, 208)    # V+  = (480, 176)
    x1_vn = pin_position(oa_sym, 3, 480, 208)    # V-  = (480, 240)
    x1_out = pin_position(oa_sym, 4, 480, 208)   # OUT = (512, 208)

    # X2 at (480, 480)
    x2_inp = pin_position(oa_sym, 0, 480, 480)   # In+ = (448, 496)
    x2_inn = pin_position(oa_sym, 1, 480, 480)   # In- = (448, 464)
    x2_vp = pin_position(oa_sym, 2, 480, 480)    # V+  = (480, 448)
    x2_vn = pin_position(oa_sym, 3, 480, 480)    # V-  = (480, 512)
    x2_out = pin_position(oa_sym, 4, 480, 480)   # OUT = (512, 480)

    # R1: feedback from X1 out to X1 In- (horizontal R90)
    # Place at (576, 96): pinA=(560,112), pinB=(480,112)
    # We want to connect out1 (512,208) up to (512,112) left to (560,112)=R1_a
    # and R1_b (480,112) left to (448,112) down to In- (448,192)
    r1_a = pin_position("res", 0, 576, 96, 90)   # (560, 112)
    r1_b = pin_position("res", 1, 576, 96, 90)   # (480, 112)

    # R1b: feedback from X2 out to X2 In- (horizontal R90)
    # Place at (576, 544): pinA=(560,560), pinB=(480,560)
    r1b_a = pin_position("res", 0, 576, 544, 90)  # (560, 560)
    r1b_b = pin_position("res", 1, 576, 544, 90)  # (480, 560)

    # Rgain: vertical between X1 In- junction and X2 In- junction
    # X1 In- at y=192 area, X2 In- at y=464 area
    # We'll connect to the R1/R1b feedback junctions.
    # Place Rgain at (400, 208): pinA=(416,224), pinB=(416,304)
    # Actually we need it to span from node_a (X1 In- junction) to node_b (X2 In- junction).
    # The junctions are at the R1_b / R1b_b x positions.
    # Let's use net labels for cleanliness: "node_a" at R1_b junction, "node_b" at R1b_b junction.
    # Rgain vertical res at (336, 256): pinA=(352, 272), pinB=(352, 352)
    rg_a = pin_position("res", 0, 336, 256)  # (352, 272) = node_a side
    rg_b = pin_position("res", 1, 336, 256)  # (352, 352) = node_b side

    # === STAGE 2 ===
    # X3 at (880, 352)
    x3_inp = pin_position(oa_sym, 0, 880, 352)   # In+ = (848, 368)
    x3_inn = pin_position(oa_sym, 1, 880, 352)   # In- = (848, 336)
    x3_vp = pin_position(oa_sym, 2, 880, 352)    # V+  = (880, 320)
    x3_vn = pin_position(oa_sym, 3, 880, 352)    # V-  = (880, 384)
    x3_out = pin_position(oa_sym, 4, 880, 352)   # OUT = (912, 352)

    # R2: out1 to X3 In- (horizontal R90)
    # Place at (768, 320): pinA=(752,336), pinB=(672,336)
    r2_a = pin_position("res", 0, 768, 320, 90)  # (752, 336) near X3 In-
    r2_b = pin_position("res", 1, 768, 320, 90)  # (672, 336) from out1

    # R3: X3 In- to Vout (horizontal R90, above X3 for feedback)
    # Place at (960, 240): pinA=(944, 256), pinB=(864, 256)
    r3_a = pin_position("res", 0, 960, 240, 90)  # (944, 256) near Vout
    r3_b = pin_position("res", 1, 960, 240, 90)  # (864, 256) near In-

    # R2b: out2 to X3 In+ (horizontal R90)
    # Place at (768, 352): pinA=(752,368), pinB=(672,368)
    r2b_a = pin_position("res", 0, 768, 352, 90)  # (752, 368) near X3 In+
    r2b_b = pin_position("res", 1, 768, 352, 90)  # (672, 368) from out2

    # R3b: X3 In+ to GND (vertical)
    # Place at (832, 416): pinA=(848,432), pinB=(848,512)
    r3b_a = pin_position("res", 0, 832, 416)  # (848, 432)
    r3b_b = pin_position("res", 1, 832, 416)  # (848, 512)

    # Sources
    # V1 at (80, 160): pin+=(80,176), pin-=(80,256)
    v1p = pin_position("voltage", 0, 80, 160)
    v1n = pin_position("voltage", 1, 80, 160)

    # V2 at (80, 432): pin+=(80,448), pin-=(80,528)
    v2p = pin_position("voltage", 0, 80, 432)
    v2n = pin_position("voltage", 1, 80, 432)

    # Supply: Vpos at (1056, 176), Vneg at (1056, 480)
    vpos_p = pin_position("voltage", 0, 1056, 176)
    vpos_n = pin_position("voltage", 1, 1056, 176)
    vneg_p = pin_position("voltage", 0, 1056, 480)
    vneg_n = pin_position("voltage", 1, 1056, 480)

    # === WIRING ===
    # V1+ to X1 In+
    sch.add_wire(v1p[0], v1p[1], v1p[0], x1_inp[1])
    sch.add_wire(v1p[0], x1_inp[1], x1_inp[0], x1_inp[1])

    # V2+ to X2 In+
    sch.add_wire(v2p[0], v2p[1], v2p[0], x2_inp[1])
    sch.add_wire(v2p[0], x2_inp[1], x2_inp[0], x2_inp[1])

    # R1 feedback: X1 out (512,208) up to (512,112), right to R1_a (560,112)
    sch.add_wire(x1_out[0], x1_out[1], x1_out[0], r1_a[1])
    sch.add_wire(x1_out[0], r1_a[1], r1_a[0], r1_a[1])
    # R1_b (480,112) down to X1 In- (448,192): (480,112) left to (448,112), down to (448,192)
    sch.add_wire(r1_b[0], r1_b[1], x1_inn[0], r1_b[1])
    sch.add_wire(x1_inn[0], r1_b[1], x1_inn[0], x1_inn[1])

    # R1b feedback: X2 out (512,480) down to (512,560), right to R1b_a (560,560)
    sch.add_wire(x2_out[0], x2_out[1], x2_out[0], r1b_a[1])
    sch.add_wire(x2_out[0], r1b_a[1], r1b_a[0], r1b_a[1])
    # R1b_b (480,560) down to X2 In- (448,464): (480,560) left to (448,560), up to (448,464)
    sch.add_wire(r1b_b[0], r1b_b[1], x2_inn[0], r1b_b[1])
    sch.add_wire(x2_inn[0], r1b_b[1], x2_inn[0], x2_inn[1])

    # Rgain: node_a (junction at X1 In-) to Rgain pinA, node_b (X2 In-) to Rgain pinB
    # Route from X1 In- junction (448,192) left to (352,192), down to Rg_a (352,272)
    sch.add_wire(x1_inn[0], x1_inn[1], rg_a[0], x1_inn[1])
    sch.add_wire(rg_a[0], x1_inn[1], rg_a[0], rg_a[1])
    # Route from X2 In- junction (448,464) left to (352,464), up to Rg_b (352,352)
    sch.add_wire(x2_inn[0], x2_inn[1], rg_b[0], x2_inn[1])
    sch.add_wire(rg_b[0], x2_inn[1], rg_b[0], rg_b[1])

    # Stage 2 wiring
    # out1 (512,208) to R2_b (672,336): route (512,208) right to (672,208), down to (672,336)
    sch.add_wire(x1_out[0], x1_out[1], r2_b[0], x1_out[1])
    sch.add_wire(r2_b[0], x1_out[1], r2_b[0], r2_b[1])
    # R2_a (752,336) to X3 In- (848,336)
    sch.add_wire(r2_a[0], r2_a[1], x3_inn[0], x3_inn[1])

    # out2 (512,480) to R2b_b (672,368): route (512,480) right to (672,480), up to (672,368)
    sch.add_wire(x2_out[0], x2_out[1], r2b_b[0], x2_out[1])
    sch.add_wire(r2b_b[0], x2_out[1], r2b_b[0], r2b_b[1])
    # R2b_a (752,368) to X3 In+ (848,368)
    sch.add_wire(r2b_a[0], r2b_a[1], x3_inp[0], x3_inp[1])

    # R3 feedback: X3 out (912,352) up to (912,256), left to R3_a (944,256)
    sch.add_wire(x3_out[0], x3_out[1], x3_out[0], r3_a[1])
    sch.add_wire(x3_out[0], r3_a[1], r3_a[0], r3_a[1])
    # R3_b (864,256) down to X3 In- junction: (864,256) left to (848,256), down to (848,336)
    sch.add_wire(r3_b[0], r3_b[1], x3_inn[0], r3_b[1])
    sch.add_wire(x3_inn[0], r3_b[1], x3_inn[0], x3_inn[1])

    # R3b: X3 In+ to GND via R3b
    sch.add_wire(x3_inp[0], x3_inp[1], r3b_a[0], r3b_a[1])

    # Supply wiring (use net labels for cleanliness with 3 opamps)
    # Just wire X1, X2, X3 supply pins to net labels

    # === COMPONENTS ===
    sch.add_component("voltage", "V1", "AC 1", 80, 160)
    sch.add_component("voltage", "V2", "0", 80, 432)
    sch.add_component(oa_sym, "X1", "", 480, 208)
    sch.add_component(oa_sym, "X2", "", 480, 480)
    sch.add_component(oa_sym, "X3", "", 880, 352)
    sch.add_component("res", "R1", r1, 576, 96, rotation=90)
    sch.add_component("res", "R1b", r1, 576, 544, rotation=90)
    sch.add_component("res", "Rgain", r_gain, 336, 256)
    sch.add_component("res", "R2", r2, 768, 320, rotation=90)
    sch.add_component("res", "R3", r3, 960, 240, rotation=90)
    sch.add_component("res", "R2b", r2, 768, 352, rotation=90)
    sch.add_component("res", "R3b", r3, 832, 416)
    sch.add_component("voltage", "Vpos", "15", 1056, 176)
    sch.add_component("voltage", "Vneg", "15", 1056, 480)

    # === FLAGS ===
    sch.add_ground(*v1n)
    sch.add_ground(*v2n)
    sch.add_ground(*r3b_b)
    sch.add_ground(*vpos_n)
    sch.add_ground(*vneg_p)
    # Supply net labels for all three opamps
    sch.add_net_label("vdd", *x1_vp)
    sch.add_net_label("vdd", *x2_vp)
    sch.add_net_label("vdd", *x3_vp)
    sch.add_net_label("vdd", *vpos_p)
    sch.add_net_label("vss", *x1_vn)
    sch.add_net_label("vss", *x2_vn)
    sch.add_net_label("vss", *x3_vn)
    sch.add_net_label("vss", *vneg_n)
    sch.add_net_label("vout", *x3_out)

    sch.add_directive(".ac dec 100 1 1meg", 80, 640)

    return sch


def generate_current_mirror(
    r_ref: str = "10k",
    r_load: str = "1k",
    vcc: str = "12",
) -> AscSchematic:
    """Generate a BJT current mirror schematic.

    Topology::

        Vcc --[Rref]--> collector_Q1 = base_Q1 = base_Q2
                         emitter_Q1 = GND
        Vcc --[Rload]--> collector_Q2
                          emitter_Q2 = GND

    Q1 is diode-connected (collector tied to base).
    I_ref ~ (Vcc - Vbe) / Rref, I_load ~ I_ref.

    Args:
        r_ref: Reference resistor
        r_load: Load resistor
        vcc: Supply voltage
    """
    sch = AscSchematic(sheet_w=880, sheet_h=680)
    bjt_model = "2N2222"

    # Q1 (NPN, diode-connected) at (256, 288): C=(320,288), B=(256,336), E=(320,384)
    q1c = pin_position("npn", 0, 256, 288)  # collector (320, 288)
    q1b = pin_position("npn", 1, 256, 288)  # base (256, 336)
    q1e = pin_position("npn", 2, 256, 288)  # emitter (320, 384)

    # Q2 (NPN) at (448, 288): C=(512,288), B=(448,336), E=(512,384)
    q2c = pin_position("npn", 0, 448, 288)  # collector (512, 288)
    q2b = pin_position("npn", 1, 448, 288)  # base (448, 336)
    q2e = pin_position("npn", 2, 448, 288)  # emitter (512, 384)

    # Rref from Vcc to Q1 collector. res at (304, 192): pinA=(320,208)=vcc, pinB=(320,288)=Q1c
    rref_a = pin_position("res", 0, 304, 192)  # (320, 208)

    # Rload from Vcc to Q2 collector. res at (496, 192): pinA=(512,208)=vcc, pinB=(512,288)=Q2c
    rload_a = pin_position("res", 0, 496, 192)  # (512, 208)

    # Vcc source at (80, 96): pin+=(80,112), pin-=(80,192)
    vcc_p = pin_position("voltage", 0, 80, 96)
    vcc_n = pin_position("voltage", 1, 80, 96)

    # Vcc rail at y=208
    vcc_y = rref_a[1]  # 208

    # === WIRING ===
    # Vcc rail
    sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1])  # (80,112)->(160,112)
    sch.add_wire(160, vcc_p[1], 160, vcc_y)            # (160,112)->(160,208)
    sch.add_wire(160, vcc_y, rload_a[0], vcc_y)        # (160,208)->(512,208) spans both

    # Diode connection: Q1 collector to Q1 base
    # Q1c = (320,288), Q1b = (256,336)
    # Route: (320,288) left to (256,288) down to (256,336)
    sch.add_wire(q1c[0], q1c[1], q1b[0], q1c[1])
    sch.add_wire(q1b[0], q1c[1], q1b[0], q1b[1])

    # Base connection: Q1 base to Q2 base
    sch.add_wire(q1b[0], q1b[1], q2b[0], q2b[1])

    # === COMPONENTS ===
    sch.add_component("npn", "Q1", bjt_model, 256, 288)
    sch.add_component("npn", "Q2", bjt_model, 448, 288)
    sch.add_component("res", "Rref", r_ref, 304, 192)
    sch.add_component("res", "Rload", r_load, 496, 192)
    sch.add_component("voltage", "Vcc", vcc, 80, 96)

    # === FLAGS ===
    sch.add_ground(*vcc_n)
    sch.add_ground(*q1e)
    sch.add_ground(*q2e)
    sch.add_net_label("mirror", *q1b)
    sch.add_net_label("out", *q2c)

    sch.add_directive(".op", 80, 448)
    sch.add_directive(".tran 1m", 80, 480)

    return sch


def generate_transimpedance_amp(
    rf: str = "100k",
    cf: str = "1p",
    i_source: str = "1u",
) -> AscSchematic:
    """Generate a transimpedance amplifier (TIA) schematic.

    Topology::

        I1 (current src, AC) --> In- (inv)
                                       --> U1 --> out
        GND --> In+ (noninv)
        Rf from In- to out (feedback)
        Cf from In- to out (parallel with Rf, for stability)

    Vout = -I_in * Rf (at low frequencies).
    Bandwidth limited by Cf. Supply: +/-15V.

    Args:
        rf: Feedback resistor
        cf: Feedback capacitor (for stability)
        i_source: AC current source magnitude
    """
    sch = AscSchematic(sheet_w=1200, sheet_h=880)
    oa_sym = "OpAmps/UniversalOpamp2"

    # Op-amp U1 at (512, 336)
    inp = pin_position(oa_sym, 0, 512, 336)   # In+  = (480, 352)
    inn = pin_position(oa_sym, 1, 512, 336)   # In-  = (480, 320)
    vp = pin_position(oa_sym, 2, 512, 336)    # V+   = (512, 304)
    vn = pin_position(oa_sym, 3, 512, 336)    # V-   = (512, 368)
    out = pin_position(oa_sym, 4, 512, 336)   # OUT  = (544, 336)

    # Current source I1 at (320, 224) (vertical, like voltage source)
    # Using "voltage" pin positions since current source shares the same symbol geometry
    # I1 at origin (320, 224): pin+ = (320, 240), pin- = (320, 320)
    # Current flows from pin+ to pin- inside the source (conventional).
    # We want current into In- node: connect pin+ to In- node, pin- to GND.
    i1p = pin_position("voltage", 0, 320, 224)  # (320, 240)
    i1n = pin_position("voltage", 1, 320, 224)  # (320, 320)

    # Rf horizontal (R90) above op-amp for feedback
    # At (560, 208): pinA=(544,224), pinB=(464,224)
    rf_a = pin_position("res", 0, 560, 208, 90)  # (544, 224) near out
    rf_b = pin_position("res", 1, 560, 208, 90)  # (464, 224) near In-

    # Cf horizontal (R90) above Rf for parallel feedback
    # At (560, 160): pinA=(544,176), pinB=(464,176)
    # Actually cap R90: pinA offset (16,0)->(0,16), pinB offset (16,64)->(-64,16)
    # At (560, 160): pinA=(560,176), pinB=(496,176)
    cf_a = pin_position("cap", 0, 560, 160, 90)  # (560, 176) near out
    cf_b = pin_position("cap", 1, 560, 160, 90)  # (496, 176) near In-

    # Supply: Vpos at (688, 176), Vneg at (688, 416)
    vpos_p = pin_position("voltage", 0, 688, 176)
    vpos_n = pin_position("voltage", 1, 688, 176)
    vneg_p = pin_position("voltage", 0, 688, 416)
    vneg_n = pin_position("voltage", 1, 688, 416)

    # === WIRING ===
    # I1 pin+ (320,240) to In- node (480,320)
    # Route: (320,240) right to (480,240), down to (480,320)
    sch.add_wire(i1p[0], i1p[1], inn[0], i1p[1])
    sch.add_wire(inn[0], i1p[1], inn[0], inn[1])

    # Rf feedback: out up to Rf pinA, Rf pinB left and down to In- junction
    sch.add_wire(out[0], out[1], rf_a[0], rf_a[1])  # (544,336)->(544,224)
    sch.add_wire(rf_b[0], rf_b[1], rf_b[0], inn[1])  # (464,224)->(464,320)
    sch.add_wire(rf_b[0], inn[1], inn[0], inn[1])     # (464,320)->(480,320)

    # Cf feedback: parallel path
    # Connect Cf pinA to Rf pinA column, Cf pinB to Rf pinB column
    sch.add_wire(cf_a[0], cf_a[1], rf_a[0], cf_a[1])  # (560,176)->(544,176)
    sch.add_wire(rf_a[0], cf_a[1], rf_a[0], rf_a[1])  # (544,176)->(544,224) vertical
    sch.add_wire(cf_b[0], cf_b[1], rf_b[0], cf_b[1])  # (496,176)->(464,176)
    sch.add_wire(rf_b[0], cf_b[1], rf_b[0], rf_b[1])  # (464,176)->(464,224) vertical

    # Supply wiring
    sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])
    sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1])
    sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])
    sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1])

    # === COMPONENTS ===
    sch.add_component("voltage", "I1", f"AC {i_source}", 320, 224)
    sch.add_component(oa_sym, "U1", "", 512, 336)
    sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
    sch.add_component("cap", "Cf", cf, 560, 160, rotation=90)
    sch.add_component("voltage", "Vpos", "15", 688, 176)
    sch.add_component("voltage", "Vneg", "15", 688, 416)

    # === FLAGS ===
    sch.add_ground(*i1n)
    sch.add_ground(*inp)  # In+ to GND
    sch.add_ground(*vpos_n)
    sch.add_ground(*vneg_p)
    sch.add_net_label("out", *out)

    sch.add_directive(".ac dec 100 1 1meg", 80, 560)

    return sch
