# Copyright 2026 Gaofeng Fan
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Symbol rendering using schemdraw."""

import matplotlib
matplotlib.use('Agg')  # Use non-interactive backend BEFORE importing pyplot
matplotlib.rcParams['svg.fonttype'] = 'none'  # Ensure searchable text in SVG

from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
import math
import schemdraw
import schemdraw.elements as elm
from schemdraw import Drawing
import importlib
import yaml
from pathlib import Path
from pathlib import Path

from .config import (
    SymbolStyle, SymbolConfig, PortType, group_ports_by_type,
    COLORS, LAYOUT, extract_veriloga_filename
)

if TYPE_CHECKING:
    from ..circuit import Circuit, Instance



# =============================================================================
# Auto-detection of user-defined parameters on subcircuit objects
# =============================================================================

# Attributes that belong to Circuit/_CircuitBase internals, not user parameters.
_CIRCUIT_BASE_ATTRS = frozenset({
    'name', 'nets', 'instances', 'subcircuit_instances', '_instance_counters',
    'ports', 'port_directions', 'parameters', 'comments',
    '_wired_nets', '_wired_net_ports', 'param_blacklist', '_module_name',
    '_is_primitive', '_primitive_spectre_type',
})


def _get_instance_params(inst) -> dict:
    """
    Get display parameters for a subcircuit instance.

    Resolution order:
    1. If inst.params is non-empty, use it (primitives with dc=, r=, etc.)
    2. Otherwise, auto-detect user-defined attributes on inst.subcircuit
       by scanning vars(subcircuit) and excluding Circuit base class internals.
    3. Apply param_blacklist from both the instance and the subcircuit.
    """
    params = getattr(inst, 'params', {})
    if params:
        display_params = dict(params)
    else:
        subcircuit = getattr(inst, 'subcircuit', None)
        if subcircuit is None:
            return {}
        display_params = {}
        for attr_name, attr_value in vars(subcircuit).items():
            if attr_name.startswith('_'):
                continue
            if attr_name in _CIRCUIT_BASE_ATTRS:
                continue
            if callable(attr_value):
                continue
            if attr_value is not None:
                display_params[attr_name] = attr_value

    # Apply param_blacklist from instance and subcircuit
    blacklist = set()
    inst_bl = getattr(inst, 'param_blacklist', [])
    if inst_bl:
        blacklist.update(inst_bl)
    sc = getattr(inst, 'subcircuit', None)
    if sc:
        sc_bl = getattr(sc, 'param_blacklist', [])
        if sc_bl:
            blacklist.update(sc_bl)
    if blacklist:
        display_params = {k: v for k, v in display_params.items() if k not in blacklist}

    return display_params


def _label(d, pos, text, color, halign=None, valign=None, zorder=None):
    """Add a text label with correct alignment.

    schemdraw ignores halign/valign on the Label() constructor;
    they must be passed to the .label() method for correct positioning.
    """
    lbl_kwargs = {}
    if halign: lbl_kwargs['halign'] = halign
    if valign: lbl_kwargs['valign'] = valign
    el = elm.Label().at(pos).label(text, **lbl_kwargs).color(color)
    if zorder is not None:
        el = el.zorder(zorder)
    d += el

# =============================================================================
# 1. Basic Helpers and Primitives
# =============================================================================

def _draw_coordinate_axes(d, x_min: float, x_max: float, y_min: float, y_max: float, scale: float = 1.0):
    """Draw frame-style coordinate axes (rectangular box around plot area) with optional grid."""
    if not LAYOUT.SHOW_AXES: return
    if not all(math.isfinite(v) for v in (x_min, x_max, y_min, y_max)):
        return

    ac = LAYOUT.AXIS_COLOR
    ts = LAYOUT.AXIS_TICK_SPACING
    extend = LAYOUT.AXIS_EXTEND * scale
    lc = LAYOUT.AXIS_LABEL_COLOR
    gc = LAYOUT.GRID_COLOR
    show_grid = LAYOUT.SHOW_GRID
    tick_size = LAYOUT.AXIS_TICK_SIZE * scale
    mirror = LAYOUT.AXIS_MIRROR

    # Frame bounds (with extension for padding)
    x0, x1 = x_min - extend, x_max + extend
    y0, y1 = y_min - extend, y_max + extend

    # Draw grid lines first (behind everything)
    if show_grid and ts > 0:
        dl, gl = 0.05 * scale, 0.35 * scale
        x = math.floor(x0 / ts) * ts
        while x <= x1:
            cy = y0
            while cy < y1:
                d += elm.Line().at((x, cy)).to((x, min(cy + dl, y1))).color(gc).zorder(0)
                cy += dl + gl
            x += ts
        y = math.floor(y0 / ts) * ts
        while y <= y1:
            cx = x0
            while cx < x1:
                d += elm.Line().at((cx, y)).to((min(cx + dl, x1), y)).color(gc).zorder(0)
                cx += dl + gl
            y += ts

    # Draw rectangular frame
    d += elm.Line().at((x0, y0)).to((x1, y0)).color(ac).zorder(1)  # Bottom
    d += elm.Line().at((x0, y0)).to((x0, y1)).color(ac).zorder(1)  # Left
    if mirror:
        d += elm.Line().at((x0, y1)).to((x1, y1)).color(ac).zorder(1)  # Top
        d += elm.Line().at((x1, y0)).to((x1, y1)).color(ac).zorder(1)  # Right

    # Draw tick marks and labels
    if ts > 0:
        x = math.floor(x0 / ts) * ts
        while x <= x1:
            d += elm.Line().at((x, y0)).to((x, y0 - tick_size)).color(ac).zorder(1)
            if mirror:
                d += elm.Line().at((x, y1)).to((x, y1 + tick_size)).color(ac).zorder(1)
            label_val = f"{int(x)}" if x == int(x) else f"{x:.1f}"
            _label(d, (x, y0 - tick_size * 2.5), label_val, lc, zorder=2)
            x += ts

        y = math.floor(y0 / ts) * ts
        while y <= y1:
            d += elm.Line().at((x0, y)).to((x0 - tick_size, y)).color(ac).zorder(1)
            if mirror:
                d += elm.Line().at((x1, y)).to((x1 + tick_size, y)).color(ac).zorder(1)
            label_val = f"{int(y)}" if y == int(y) else f"{y:.1f}"
            _label(d, (x0 - tick_size * 3.5, y), label_val, lc, zorder=2)
            y += ts


def _draw_circle_base(d, x: float, y: float, r: float, color: str):
    """Draw a circle at (x, y) with radius r."""
    steps = 32
    for i in range(steps):
        a1, a2 = 2 * math.pi * i / steps, 2 * math.pi * (i + 1) / steps
        d += elm.Line().at((x + r * math.cos(a1), y + r * math.sin(a1))).to((x + r * math.cos(a2), y + r * math.sin(a2))).color(color)


def _draw_pin_marker_helper(d, px: float, py: float, pin_size: float, color: str):
    """Draw a filled pin marker as elongated diamond."""
    xs, sy = 1.5, pin_size; sx = pin_size * xs
    d += elm.Line().at((px, py - sy)).to((px + sx, py)).color(color)
    d += elm.Line().at((px + sx, py)).to((px, py + sy)).color(color)
    d += elm.Line().at((px, py + sy)).to((px - sx, py)).color(color)
    d += elm.Line().at((px - sx, py)).to((px, py - sy)).color(color)
    step = sy / 4
    for yo in range(-3, 4):
        off = yo * step; hw = sx * (1 - abs(off) / sy) if sy > 0 else 0
        if hw > 0: d += elm.Line().at((px - hw, py + off)).to((px + hw, py + off)).color(color)


def _draw_input_marker(d, px, py, sz, color, side='left'):
    if side == 'left':
        d += elm.Line().at((px-sz, py-sz)).to((px+sz, py)).color(color); d += elm.Line().at((px+sz, py)).to((px-sz, py+sz)).color(color); d += elm.Line().at((px-sz, py+sz)).to((px-sz, py-sz)).color(color)
        step = sz/3
        for i in range(-2, 3):
            off = i*step; hw = sz*(1-abs(off)/sz) if abs(off)<sz else 0
            if hw > 0: d += elm.Line().at((px-sz, py+off)).to((px-sz+hw*2, py+off)).color(color)
    elif side == 'right':
        d += elm.Line().at((px+sz, py-sz)).to((px-sz, py)).color(color); d += elm.Line().at((px-sz, py)).to((px+sz, py+sz)).color(color); d += elm.Line().at((px+sz, py+sz)).to((px+sz, py-sz)).color(color)
    elif side == 'top':
        d += elm.Line().at((px-sz, py+sz)).to((px, py-sz)).color(color); d += elm.Line().at((px, py-sz)).to((px+sz, py+sz)).color(color); d += elm.Line().at((px+sz, py+sz)).to((px-sz, py+sz)).color(color)
    elif side == 'bottom':
        d += elm.Line().at((px-sz, py-sz)).to((px, py+sz)).color(color); d += elm.Line().at((px, py+sz)).to((px+sz, py-sz)).color(color); d += elm.Line().at((px+sz, py-sz)).to((px-sz, py-sz)).color(color)


def _draw_output_marker(d, px, py, sz, color, side='left'):
    if side == 'left':
        d += elm.Line().at((px+sz, py-sz)).to((px-sz, py)).color(color); d += elm.Line().at((px-sz, py)).to((px+sz, py+sz)).color(color); d += elm.Line().at((px+sz, py+sz)).to((px+sz, py-sz)).color(color)
    elif side == 'right':
        d += elm.Line().at((px-sz, py-sz)).to((px+sz, py)).color(color); d += elm.Line().at((px+sz, py)).to((px-sz, py+sz)).color(color); d += elm.Line().at((px-sz, py+sz)).to((px-sz, py-sz)).color(color)
    elif side == 'top':
        d += elm.Line().at((px-sz, py-sz)).to((px, py+sz)).color(color); d += elm.Line().at((px, py+sz)).to((px+sz, py-sz)).color(color); d += elm.Line().at((px+sz, py-sz)).to((px-sz, py-sz)).color(color)
    elif side == 'bottom':
        d += elm.Line().at((px-sz, py+sz)).to((px, py-sz)).color(color); d += elm.Line().at((px, py-sz)).to((px+sz, py+sz)).color(color); d += elm.Line().at((px+sz, py+sz)).to((px-sz, py+sz)).color(color)


def _draw_polarity_markers(d, x: float, y_plus: float, y_minus: float, size: float, color: str):
    """Draw graphical + and - polarity markers using lines.

    Args:
        d: schemdraw Drawing
        x: x-coordinate center
        y_plus: y-coordinate for + marker
        y_minus: y-coordinate for - marker
        size: half-length of the marker lines
        color: line color
    """
    # Plus sign: horizontal + vertical cross
    d += elm.Line().at((x - size, y_plus)).to((x + size, y_plus)).color(color)
    d += elm.Line().at((x, y_plus - size)).to((x, y_plus + size)).color(color)
    # Minus sign: horizontal line only
    d += elm.Line().at((x - size, y_minus)).to((x + size, y_minus)).color(color)


def _draw_inout_marker(d, px, py, sz, color, side='left'):
    xs, sy = 1.5, sz; sx = sz * xs
    d += elm.Line().at((px, py-sy)).to((px+sx, py)).color(color); d += elm.Line().at((px+sx, py)).to((px, py+sy)).color(color); d += elm.Line().at((px, py+sy)).to((px-sx, py)).color(color); d += elm.Line().at((px-sx, py)).to((px, py-sy)).color(color)
    step = sy/3
    for i in range(-2, 3):
        off = i*step; hw = sx*(1-abs(off)/sy)
        if hw > 0: d += elm.Line().at((px-hw, py+off)).to((px+hw, py+off)).color(color)


def _draw_direction_marker(d, px, py, sz, color, direction='inout', side='left'):
    if direction == 'input': _draw_input_marker(d, px, py, sz, color, side)
    elif direction == 'output': _draw_output_marker(d, px, py, sz, color, side)
    else: _draw_inout_marker(d, px, py, sz, color, side)


def _draw_parameter_labels(d, x, y, params, scale=1.0, start_offset=0.0, **kwargs):
    """Draw parameters as a left-aligned text block (one label per line)."""
    if not params:
        return
    TC, lys = COLORS.CELL_TYPE, LAYOUT.LABEL_Y_SPACING * scale
    valign = kwargs.pop('valign', 'center')

    for i, (k, v) in enumerate(params.items()):
        py = y - (start_offset + i) * lys
        if not math.isfinite(py):
            continue
        param_label = f"{k}={v}"
        _label(d, (x, py), param_label, TC, halign='left', valign=valign)


# =============================================================================
# 2. Individual Symbol Drawing Functions
# =============================================================================

def draw_resistor_symbol(d, x, y, name, scale=1.0, model="resistor", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    ps, ll, lxo, lys = LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    bl, za = 0.8 * scale, 0.15 * scale; tb, ty = y+bl/2, y+bl/2+ll
    d += elm.Line().at((x, tb)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    cy, nz, zh = tb, 4, bl/4
    for i in range(nz):
        ny = cy-zh/2; amp = za if i%2==0 else -za; d += elm.Line().at((x, cy)).to((x+amp, ny)).color(SC)
        cy = ny; ny = cy-zh/2; d += elm.Line().at((x+amp, cy)).to((x, ny)).color(SC); cy = ny
    bb, by = y-bl/2, y-bl/2-ll; d += elm.Line().at((x, bb)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+za+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_capacitor_symbol(d, x, y, name, scale=1.0, model="capacitor", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    ps, ll, lxo, lys = LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    pw, pg = 0.4 * scale, 0.15 * scale; ty, by = y+pg/2+ll, y-pg/2-ll
    d += elm.Line().at((x, y+pg/2)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x-pw/2, y+pg/2)).to((x+pw/2, y+pg/2)).color(SC); d += elm.Line().at((x-pw/2, y-pg/2)).to((x+pw/2, y-pg/2)).color(SC)
    d += elm.Line().at((x, y-pg/2)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+pw/2+lxo+LAYOUT.CAPACITOR_LABEL_EXTRA_X*scale, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_inductor_symbol(d, x, y, name, scale=1.0, model="inductor", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    ps, ll, lxo, lys = LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    bl, nl = 0.6 * scale, 4; lr = bl/(nl*2); tb, ty = y+bl/2, y+bl/2+ll
    d += elm.Line().at((x, tb)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    for i in range(nl):
        cy = tb-(i+0.5)*(bl/nl)
        for j in range(10):
            a1, a2 = math.pi/2-j*math.pi/10, math.pi/2-(j+1)*math.pi/10
            d += elm.Line().at((x+lr*math.cos(a1), cy+lr*math.sin(a1))).to((x+lr*math.cos(a2), cy+lr*math.sin(a2))).color(SC)
    bb, by = y-bl/2, y-bl/2-ll; d += elm.Line().at((x, bb)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+lr+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_diode_symbol(d, x, y, name, scale=1.0, model="diode", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    ps, ll, lxo, lys = LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    bl, tw = 0.4 * scale, 0.3 * scale; tb, ty = y+bl/2, y+bl/2+ll
    d += elm.Line().at((x, tb)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    bb, by = y-bl/2, y-bl/2-ll; d += elm.Line().at((x, bb)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    d += elm.Line().at((x-tw/2, tb)).to((x+tw/2, tb)).color(SC); d += elm.Line().at((x-tw/2, tb)).to((x, bb)).color(SC); d += elm.Line().at((x+tw/2, tb)).to((x, bb)).color(SC); d += elm.Line().at((x-tw/2, bb)).to((x+tw/2, bb)).color(SC)
    lx, ly = x+tw/2+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_vsource_symbol(d, x, y, name, scale=1.0, model="vsource", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    _draw_circle_base(d, x, y, r, SC); _draw_polarity_markers(d, x, y+r*0.4, y-r*0.4, r*0.15, SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_isource_symbol(d, x, y, name, scale=1.0, model="isource", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    _draw_circle_base(d, x, y, r, SC); d += elm.Line().at((x, y+r*0.5)).to((x, y-r*0.5)).color(SC); hs = 0.15*scale
    d += elm.Line().at((x-hs/2, y-r*0.5+hs)).to((x, y-r*0.5)).color(SC); d += elm.Line().at((x+hs/2, y-r*0.5+hs)).to((x, y-r*0.5)).color(SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_vpulse_symbol(d, x, y, name, scale=1.0, model="vpulse", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    _draw_circle_base(d, x, y, r, SC); sw = r*0.4
    d += elm.Line().at((x-sw, y-sw/2)).to((x-sw/2, y-sw/2)).color(SC); d += elm.Line().at((x-sw/2, y-sw/2)).to((x-sw/2, y+sw/2)).color(SC); d += elm.Line().at((x-sw/2, y+sw/2)).to((x+sw/2, y+sw/2)).color(SC); d += elm.Line().at((x+sw/2, y+sw/2)).to((x+sw/2, y-sw/2)).color(SC); d += elm.Line().at((x+sw/2, y-sw/2)).to((x+sw, y-sw/2)).color(SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_vsin_symbol(d, x, y, name, scale=1.0, model="vsin", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    _draw_circle_base(d, x, y, r, SC)
    for i in range(20):
        a1, a2 = i*2*math.pi/20, (i+1)*2*math.pi/20
        d += elm.Line().at((x-r*0.5+r*a1/(2*math.pi), y+r*0.4*math.sin(a1))).to((x-r*0.5+r*a2/(2*math.pi), y+r*0.4*math.sin(a2))).color(SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_vpwl_symbol(d, x, y, name, scale=1.0, model="vpwl", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    _draw_circle_base(d, x, y, r, SC)
    pts = [(-0.5, -0.3), (-0.2, 0.4), (0.1, -0.1), (0.5, 0.3)]
    for i in range(len(pts)-1):
        d += elm.Line().at((x+pts[i][0]*r, y+pts[i][1]*r)).to((x+pts[i+1][0]*r, y+pts[i+1][1]*r)).color(SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def _draw_iprobe_symbol(d, x, y, name, scale=1.0, model="iprobe", label=None, params=None, show_params=True, show_port_labels=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    _draw_circle_base(d, x, y, r, SC); ax, at, ab, ahs = x-r*0.2, y+r*0.5, y-r*0.5, r*0.25
    d += elm.Line().at((ax, at)).to((ax, ab)).color(SC); d += elm.Line().at((ax-ahs*0.6, ab+ahs*0.8)).to((ax, ab)).color(SC); d += elm.Line().at((ax+ahs*0.6, ab+ahs*0.8)).to((ax, ab)).color(SC)
    arcr, arcx, arcy = r*0.4, x+r*0.2, y
    for i in range(16):
        a1, a2 = -math.pi/2+i*math.pi/16, -math.pi/2+(i+1)*math.pi/16
        d += elm.Line().at((arcx+arcr*math.cos(a1), arcy+arcr*math.sin(a1))).to((arcx+arcr*math.cos(a2), arcy+arcr*math.sin(a2))).color(SC)
    na, nl = math.pi/6, arcr*0.9; d += elm.Line().at((arcx, arcy)).to((arcx+nl*math.sin(na), arcy+nl*math.cos(na))).color(SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC)
    if show_port_labels: _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC)
    if show_port_labels: _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_vcvs_symbol(d, x, y, name, scale=1.0, model="vcvs", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    d += elm.Line().at((x, y+r)).to((x+r, y)).color(SC); d += elm.Line().at((x+r, y)).to((x, y-r)).color(SC); d += elm.Line().at((x, y-r)).to((x-r, y)).color(SC); d += elm.Line().at((x-r, y)).to((x, y+r)).color(SC)
    _draw_polarity_markers(d, x, y+r*0.3, y-r*0.3, r*0.15, SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    cyo, cpx = r*0.4, x-r-ll; d += elm.Line().at((x-r/2, y+cyo)).to((cpx, y+cyo)).color(SC); _draw_pin_marker_helper(d, cpx, y+cyo, ps, PMC); _label(d, (cpx-0.2*scale, y+cyo), 'nc+', PLC, halign='right')
    d += elm.Line().at((x-r/2, y-cyo)).to((cpx, y-cyo)).color(SC); _draw_pin_marker_helper(d, cpx, y-cyo, ps, PMC); _label(d, (cpx-0.2*scale, y-cyo), 'nc-', PLC, halign='right')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by), 'nc+': (cpx, y + cyo), 'nc-': (cpx, y - cyo)}


def draw_vccs_symbol(d, x, y, name, scale=1.0, model="vccs", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    d += elm.Line().at((x, y+r)).to((x+r, y)).color(SC); d += elm.Line().at((x+r, y)).to((x, y-r)).color(SC); d += elm.Line().at((x, y-r)).to((x-r, y)).color(SC); d += elm.Line().at((x-r, y)).to((x, y+r)).color(SC)
    d += elm.Line().at((x, y+r*0.4)).to((x, y-r*0.4)).color(SC); hs = 0.12*scale; d += elm.Line().at((x-hs/2, y-r*0.4+hs)).to((x, y-r*0.4)).color(SC); d += elm.Line().at((x+hs/2, y-r*0.4+hs)).to((x, y-r*0.4)).color(SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    cyo, cpx = r*0.4, x-r-ll; d += elm.Line().at((x-r/2, y+cyo)).to((cpx, y+cyo)).color(SC); _draw_pin_marker_helper(d, cpx, y+cyo, ps, PMC); _label(d, (cpx-0.2*scale, y+cyo), 'nc+', PLC, halign='right')
    d += elm.Line().at((x-r/2, y-cyo)).to((cpx, y-cyo)).color(SC); _draw_pin_marker_helper(d, cpx, y-cyo, ps, PMC); _label(d, (cpx-0.2*scale, y-cyo), 'nc-', PLC, halign='right')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by), 'nc+': (cpx, y + cyo), 'nc-': (cpx, y - cyo)}


def draw_ccvs_symbol(d, x, y, name, probe="I_SENSE", scale=1.0, model="ccvs", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    d += elm.Line().at((x, y+r)).to((x+r, y)).color(SC); d += elm.Line().at((x+r, y)).to((x, y-r)).color(SC); d += elm.Line().at((x, y-r)).to((x-r, y)).color(SC); d += elm.Line().at((x-r, y)).to((x, y+r)).color(SC)
    _draw_polarity_markers(d, x, y+r*0.3, y-r*0.3, r*0.15, SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    ep = params.copy() if params else {}; ep.setdefault('probe', probe)
    if show_params and ep:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, ep, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_cccs_symbol(d, x, y, name, probe="I_SENSE", scale=1.0, model="cccs", label=None, params=None, show_params=True, **kwargs):
    SC, IC, CC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    r, ps, ll, lxo, lys = LAYOUT.SOURCE_RADIUS*scale, LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    d += elm.Line().at((x, y+r)).to((x+r, y)).color(SC); d += elm.Line().at((x+r, y)).to((x, y-r)).color(SC); d += elm.Line().at((x, y-r)).to((x-r, y)).color(SC); d += elm.Line().at((x-r, y)).to((x, y+r)).color(SC)
    d += elm.Line().at((x, y+r*0.4)).to((x, y-r*0.4)).color(SC); hs = 0.12*scale; d += elm.Line().at((x-hs/2, y-r*0.4+hs)).to((x, y-r*0.4)).color(SC); d += elm.Line().at((x+hs/2, y-r*0.4+hs)).to((x, y-r*0.4)).color(SC)
    ty, by = y+r+ll, y-r-ll; d += elm.Line().at((x, y+r)).to((x, ty)).color(SC); _draw_pin_marker_helper(d, x, ty, ps, PMC); _label(d, (x+0.2*scale, ty), 'p', PLC, halign='left')
    d += elm.Line().at((x, y-r)).to((x, by)).color(SC); _draw_pin_marker_helper(d, x, by, ps, PMC); _label(d, (x+0.2*scale, by), 'n', PLC, halign='left')
    lx, ly = x+r+lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')
    ep = params.copy() if params else {}; ep.setdefault('probe', probe)
    if show_params and ep:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, ep, scale=scale, start_offset=st)
    return {'p': (x, ty), 'n': (x, by)}


def draw_nmos_symbol(d, x, y, name, scale=1.0, model="nmos", label=None, params=None, show_params=True, style='source', **kwargs):
    """Draw NMOS transistor symbol (Cadence-style 4-terminal).

    Args:
        style: Symbol style variant:
            - 'source' (default): Arrow on source horizontal stub pointing INTO channel
            - 'body': Arrow on body pointing INTO device
            - 'classic': Original compact style

    Cadence-style NMOS with:
    - Gate: vertical bar on left (with gap to channel)
    - Channel: 3 horizontal stubs from vertical backbone
    - Drain at top, Source at bottom, Body in middle
    """
    SC = COLORS.SYMBOL_OUTLINE
    IC = COLORS.INSTANCE_NAME
    CC = COLORS.CELL_NAME
    TC = COLORS.CELL_TYPE
    PMC = COLORS.PIN_MARKER
    PLC = COLORS.PIN_LABEL

    ps = LAYOUT.PIN_SIZE * scale
    ll = LAYOUT.LEAD_LENGTH * scale
    lxo = LAYOUT.LABEL_X_OFFSET * scale
    lys = LAYOUT.LABEL_Y_SPACING * scale

    # Classic style: original compact implementation
    if style == 'classic':
        gw, gh, bg = 0.1*scale, 0.6*scale, 0.1*scale
        gx = x - gw - bg
        d += elm.Line().at((gx-ll, y)).to((gx, y)).color(SC)
        _draw_pin_marker_helper(d, gx-ll, y, ps, PMC)
        _label(d, (gx-ll, y+0.2*scale), 'g', PLC, halign='center')
        d += elm.Line().at((gx, y-gh/2)).to((gx, y+gh/2)).color(SC)
        bx = x + bg
        d += elm.Line().at((bx, y+gh/2)).to((bx, y+0.1*scale)).color(SC)
        d += elm.Line().at((bx, y+gh/2)).to((bx+ll, y+gh/2)).color(SC)
        _draw_pin_marker_helper(d, bx+ll, y+gh/2, ps, PMC)
        _label(d, (bx+ll+0.2*scale, y+gh/2), 'd', PLC, halign='left')
        d += elm.Line().at((bx, y)).to((bx+ll, y)).color(SC)
        _draw_pin_marker_helper(d, bx+ll, y, ps, PMC)
        _label(d, (bx+ll+0.2*scale, y), 'b', PLC, halign='left')
        d += elm.Line().at((bx, y-gh/2)).to((bx, y-0.1*scale)).color(SC)
        d += elm.Line().at((bx, y-gh/2)).to((bx+ll, y-gh/2)).color(SC)
        _draw_pin_marker_helper(d, bx+ll, y-gh/2, ps, PMC)
        _label(d, (bx+ll+0.2*scale, y-gh/2), 's', PLC, halign='left')
        # Arrow on source pointing into channel
        ax, ay, hs = bx+ll/2, y-gh/2, 0.1*scale
        d += elm.Line().at((ax, ay)).to((ax+hs, ay+hs/2)).color(SC)
        d += elm.Line().at((ax, ay)).to((ax+hs, ay-hs/2)).color(SC)
        lx, ly = bx+ll+lxo, y
        _label(d, (lx, ly + lys), name, IC, halign='left')
        if kwargs.get('show_type_label', True):
            _label(d, (lx, ly), label or model, CC, halign='left')
        if show_params and params:
            st = 1.0 if kwargs.get('show_type_label', True) else 0.0
            _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
        return {'d': (bx+ll, y+gh/2), 'g': (gx-ll, y), 's': (bx+ll, y-gh/2), 'b': (bx+ll, y)}

    # Cadence-style geometry (shared by 'body' and 'source')
    ch_height = 0.5 * scale
    stub_len = 0.25 * scale
    gate_gap = 0.08 * scale

    gate_x = x
    ch_x = x + gate_gap
    stub_end_x = ch_x + stub_len

    top_y = y + ch_height / 2
    mid_y = y
    bot_y = y - ch_height / 2

    # Gate: horizontal lead + vertical bar
    d += elm.Line().at((gate_x - ll, y)).to((gate_x, y)).color(SC)
    _draw_pin_marker_helper(d, gate_x - ll, y, ps, PMC)
    _label(d, (gate_x - ll - 0.1*scale, y), 'g', PLC, halign='right')
    d += elm.Line().at((gate_x, bot_y)).to((gate_x, top_y)).color(SC)

    # Channel backbone
    d += elm.Line().at((ch_x, bot_y)).to((ch_x, top_y)).color(SC)

    # Three horizontal stubs
    d += elm.Line().at((ch_x, top_y)).to((stub_end_x, top_y)).color(SC)
    d += elm.Line().at((ch_x, mid_y)).to((stub_end_x, mid_y)).color(SC)
    d += elm.Line().at((ch_x, bot_y)).to((stub_end_x, bot_y)).color(SC)

    # Drain: vertical lead up from top stub
    drain_y = top_y + ll
    d += elm.Line().at((stub_end_x, top_y)).to((stub_end_x, drain_y)).color(SC)
    _draw_pin_marker_helper(d, stub_end_x, drain_y, ps, PMC)
    _label(d, (stub_end_x + 0.15*scale, drain_y), 'd', PLC, halign='left')

    # Source: vertical lead down from bottom stub
    source_y = bot_y - ll
    d += elm.Line().at((stub_end_x, bot_y)).to((stub_end_x, source_y)).color(SC)
    _draw_pin_marker_helper(d, stub_end_x, source_y, ps, PMC)
    _label(d, (stub_end_x + 0.15*scale, source_y), 's', PLC, halign='left')

    # Body: horizontal lead right from middle stub
    body_x = stub_end_x + ll
    d += elm.Line().at((stub_end_x, mid_y)).to((body_x, mid_y)).color(SC)
    _draw_pin_marker_helper(d, body_x, y, ps, PMC)
    _label(d, (body_x + 0.15*scale, y), 'b', PLC, halign='left')

    # Arrow placement depends on style
    arrow_size = 0.06 * scale
    if style == 'source':
        # Arrow on SOURCE horizontal stub pointing RIGHT (out of channel)
        arrow_x = ch_x + stub_len / 2
        d += elm.Line().at((arrow_x - arrow_size, bot_y - arrow_size)).to((arrow_x, bot_y)).color(SC)
        d += elm.Line().at((arrow_x - arrow_size, bot_y + arrow_size)).to((arrow_x, bot_y)).color(SC)
    else:  # 'body'
        # Arrow on BODY pointing IN (left, into device)
        arrow_x = stub_end_x + ll/2
        d += elm.Line().at((arrow_x + arrow_size, mid_y - arrow_size)).to((arrow_x, mid_y)).color(SC)
        d += elm.Line().at((arrow_x + arrow_size, mid_y + arrow_size)).to((arrow_x, mid_y)).color(SC)

    # Labels
    lx, ly = body_x + lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')

    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)

    return {'d': (stub_end_x, drain_y), 'g': (gate_x - ll, y), 's': (stub_end_x, source_y), 'b': (body_x, y)}


def draw_pmos_symbol(d, x, y, name, scale=1.0, model="pmos", label=None, params=None, show_params=True, style='source', **kwargs):
    """Draw PMOS transistor symbol (Cadence-style 4-terminal).

    Args:
        style: Symbol style variant:
            - 'source' (default): Arrow on source horizontal stub pointing OUT of channel
            - 'body': Arrow on body pointing OUT of device
            - 'classic': Original compact style with bubble

    Cadence-style PMOS with:
    - Gate: vertical bar on left with bubble (inversion indicator)
    - Channel: 3 horizontal stubs from vertical backbone
    - Drain at bottom, Source at top, Body in middle
    """
    SC = COLORS.SYMBOL_OUTLINE
    IC = COLORS.INSTANCE_NAME
    CC = COLORS.CELL_NAME
    TC = COLORS.CELL_TYPE
    PMC = COLORS.PIN_MARKER
    PLC = COLORS.PIN_LABEL

    ps = LAYOUT.PIN_SIZE * scale
    ll = LAYOUT.LEAD_LENGTH * scale
    lxo = LAYOUT.LABEL_X_OFFSET * scale
    lys = LAYOUT.LABEL_Y_SPACING * scale

    # Classic style: original compact implementation with bubble
    if style == 'classic':
        gw, gh, bg, br = 0.1*scale, 0.6*scale, 0.1*scale, 0.06*scale
        gx = x - gw - bg
        d += elm.Line().at((gx-ll, y)).to((gx-br*2, y)).color(SC)
        _draw_pin_marker_helper(d, gx-ll, y, ps, PMC)
        _label(d, (gx-ll, y+0.2*scale), 'g', PLC, halign='center')
        _draw_circle_base(d, gx-br, y, br, SC)
        d += elm.Line().at((gx, y-gh/2)).to((gx, y+gh/2)).color(SC)
        bx = x + bg
        d += elm.Line().at((bx, y+gh/2)).to((bx, y+0.1*scale)).color(SC)
        d += elm.Line().at((bx, y+gh/2)).to((bx+ll, y+gh/2)).color(SC)
        _draw_pin_marker_helper(d, bx+ll, y+gh/2, ps, PMC)
        _label(d, (bx+ll+0.2*scale, y+gh/2), 's', PLC, halign='left')
        # Arrow on source pointing out
        ax, ay, hs = bx+ll/2, y+gh/2, 0.1*scale
        d += elm.Line().at((ax+hs, ay)).to((ax, ay+hs/2)).color(SC)
        d += elm.Line().at((ax+hs, ay)).to((ax, ay-hs/2)).color(SC)
        d += elm.Line().at((bx, y)).to((bx+ll, y)).color(SC)
        _draw_pin_marker_helper(d, bx+ll, y, ps, PMC)
        _label(d, (bx+ll+0.2*scale, y), 'b', PLC, halign='left')
        d += elm.Line().at((bx, y-gh/2)).to((bx, y-0.1*scale)).color(SC)
        d += elm.Line().at((bx, y-gh/2)).to((bx+ll, y-gh/2)).color(SC)
        _draw_pin_marker_helper(d, bx+ll, y-gh/2, ps, PMC)
        _label(d, (bx+ll+0.2*scale, y-gh/2), 'd', PLC, halign='left')
        lx, ly = bx+ll+lxo, y
        _label(d, (lx, ly + lys), name, IC, halign='left')
        if kwargs.get('show_type_label', True):
            _label(d, (lx, ly), label or model, CC, halign='left')
        if show_params and params:
            st = 1.0 if kwargs.get('show_type_label', True) else 0.0
            _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)
        return {'d': (bx+ll, y-gh/2), 'g': (gx-ll, y), 's': (bx+ll, y+gh/2), 'b': (bx+ll, y)}

    # Cadence-style geometry (shared by 'body' and 'source')
    ch_height = 0.5 * scale
    stub_len = 0.25 * scale
    gate_gap = 0.08 * scale
    bubble_r = 0.04 * scale

    gate_x = x
    ch_x = x + gate_gap
    stub_end_x = ch_x + stub_len

    top_y = y + ch_height / 2   # Source (PMOS)
    mid_y = y                   # Body
    bot_y = y - ch_height / 2   # Drain (PMOS)

    # Gate: horizontal lead + bubble + vertical bar
    bubble_x = gate_x - bubble_r
    d += elm.Line().at((gate_x - ll, y)).to((bubble_x - bubble_r, y)).color(SC)
    _draw_pin_marker_helper(d, gate_x - ll, y, ps, PMC)
    _label(d, (gate_x - ll - 0.1*scale, y), 'g', PLC, halign='right')
    _draw_circle_base(d, bubble_x, y, bubble_r, SC)
    d += elm.Line().at((gate_x, bot_y)).to((gate_x, top_y)).color(SC)

    # Channel backbone
    d += elm.Line().at((ch_x, bot_y)).to((ch_x, top_y)).color(SC)

    # Three horizontal stubs
    d += elm.Line().at((ch_x, top_y)).to((stub_end_x, top_y)).color(SC)
    d += elm.Line().at((ch_x, mid_y)).to((stub_end_x, mid_y)).color(SC)
    d += elm.Line().at((ch_x, bot_y)).to((stub_end_x, bot_y)).color(SC)

    # Source: vertical lead up from top stub
    source_y = top_y + ll
    d += elm.Line().at((stub_end_x, top_y)).to((stub_end_x, source_y)).color(SC)
    _draw_pin_marker_helper(d, stub_end_x, source_y, ps, PMC)
    _label(d, (stub_end_x + 0.15*scale, source_y), 's', PLC, halign='left')

    # Drain: vertical lead down from bottom stub
    drain_y = bot_y - ll
    d += elm.Line().at((stub_end_x, bot_y)).to((stub_end_x, drain_y)).color(SC)
    _draw_pin_marker_helper(d, stub_end_x, drain_y, ps, PMC)
    _label(d, (stub_end_x + 0.15*scale, drain_y), 'd', PLC, halign='left')

    # Body: horizontal lead right from middle stub
    body_x = stub_end_x + ll
    d += elm.Line().at((stub_end_x, mid_y)).to((body_x, mid_y)).color(SC)
    _draw_pin_marker_helper(d, body_x, y, ps, PMC)
    _label(d, (body_x + 0.15*scale, y), 'b', PLC, halign='left')

    # Arrow placement depends on style
    arrow_size = 0.06 * scale
    if style == 'source':
        # Arrow on SOURCE horizontal stub pointing LEFT (into channel)
        arrow_x = ch_x + stub_len / 2
        d += elm.Line().at((arrow_x + arrow_size, top_y - arrow_size)).to((arrow_x, top_y)).color(SC)
        d += elm.Line().at((arrow_x + arrow_size, top_y + arrow_size)).to((arrow_x, top_y)).color(SC)
    else:  # 'body'
        # Arrow on BODY pointing OUT (right, out of device)
        arrow_x = stub_end_x + ll/2
        d += elm.Line().at((arrow_x - arrow_size, mid_y - arrow_size)).to((arrow_x, mid_y)).color(SC)
        d += elm.Line().at((arrow_x - arrow_size, mid_y + arrow_size)).to((arrow_x, mid_y)).color(SC)

    # Labels
    lx, ly = body_x + lxo, y
    _label(d, (lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, (lx, ly), label or model, CC, halign='left')

    if show_params and params:
        st = 1.0 if kwargs.get('show_type_label', True) else 0.0
        _draw_parameter_labels(d, lx, ly, params, scale=scale, start_offset=st)

    return {'d': (stub_end_x, drain_y), 'g': (gate_x - ll, y), 's': (stub_end_x, source_y), 'b': (body_x, y)}


def _draw_box_symbol(d, x, y, name, model, ports, scale=1.0, color='#FFFFFF', params=None, label=None, show_port_labels=True, show_type_label=True, show_params=True, fill_color=None):
    SC, IC, MC, TC, PMC, PLC = COLORS.SYMBOL_OUTLINE, COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE, COLORS.PIN_MARKER, COLORS.PIN_LABEL
    ps, ll, cw, psp, lxo, lys = LAYOUT.PIN_SIZE*scale, LAYOUT.LEAD_LENGTH*scale, LAYOUT.CHAR_WIDTH*scale, LAYOUT.BOX_PORT_SPACING*scale, LAYOUT.LABEL_X_OFFSET*scale, LAYOUT.LABEL_Y_SPACING*scale
    gp = group_ports_by_type(ports); tp, bp, lp, rp = gp[PortType.POWER], gp[PortType.GROUND], gp[PortType.INPUT]+gp[PortType.INOUT]+gp[PortType.UNKNOWN], gp[PortType.OUTPUT]
    pl = [f"{k} = {v}" for k, v in params.items()] if show_params and params else []
    tpad = LAYOUT.BOX_TEXT_PADDING * scale; mw, nw = len(label or model)*cw + tpad*2, len(name)*cw + tpad
    vf = extract_veriloga_filename(params); tl = f'({vf})' if vf else '(LUT)'; tw = len(tl)*cw + tpad*2 if show_type_label and not pl else 0
    pw = max((len(l) for l in pl), default=0)*cw + tpad*2; twp, bwp = (len(tp)+1)*psp if tp else 0, (len(bp)+1)*psp if bp else 0
    width = max(LAYOUT.BOX_MIN_WIDTH*scale, mw, nw, tw, pw, twp, bwp)
    lh, rh = (len(lp)+1)*psp if lp else 0, (len(rp)+1)*psp if rp else 0
    height = max(LAYOUT.BOX_MIN_HEIGHT*scale, lh, rh, (1 + (len(pl) if pl else (1 if show_type_label else 0)))*lys + tpad*2)
    l, r, t, b = x-width/2, x+width/2, y+height/2, y-height/2
    d += elm.Line().at((l, t)).to((r, t)).color(SC); d += elm.Line().at((r, t)).to((r, b)).color(SC); d += elm.Line().at((r, b)).to((l, b)).color(SC); d += elm.Line().at((l, b)).to((l, t)).color(SC)
    _label(d, (r+lxo, t+lys/2), name, IC, halign='left')
    _label(d, (l+tpad, t-tpad), label or model, MC, halign='left', valign='top')
    if show_params and params:
        _draw_parameter_labels(d, l+tpad, t-tpad, params, scale=scale, start_offset=1.0, valign='top')
    elif show_type_label: _label(d, (l+tpad, t-tpad-lys), tl, TC, halign='left', valign='top')
    pos = {}
    if tp:
        stx = x-(len(tp)-1)*psp/2
        for i, pn in enumerate(tp): px, py = stx+i*psp, t+ll; d += elm.Line().at((px, t)).to((px, py)).color(SC); _draw_pin_marker_helper(d, px, py, ps, PMC); pos[pn]=(px, py+ps); _label(d, (px, py+0.2*scale), pn, PLC, valign='bottom')
    if bp:
        stx = x-(len(bp)-1)*psp/2
        for i, pn in enumerate(bp): px, py = stx+i*psp, b-ll; d += elm.Line().at((px, b)).to((px, py)).color(SC); _draw_pin_marker_helper(d, px, py, ps, PMC); pos[pn]=(px, py-ps); _label(d, (px, py-0.2*scale), pn, PLC, valign='top')
    if lp:
        pspl = height/(len(lp)+1)
        for i, pn in enumerate(lp): px, py = l-ll, t-(i+1)*pspl; d += elm.Line().at((l, py)).to((px, py)).color(SC); _draw_pin_marker_helper(d, px, py, ps, PMC); pos[pn]=(px-ps, py); _label(d, (px-ps-0.4*scale, py), pn, PLC, halign='left')
    if rp:
        pspr = height/(len(rp)+1)
        for i, pn in enumerate(rp): px, py = r+ll, t-(i+1)*pspr; d += elm.Line().at((r, py)).to((px, py)).color(SC); _draw_pin_marker_helper(d, px, py, ps, PMC); pos[pn]=(px+ps, py); _label(d, (px+ps+0.1*scale, py), pn, PLC, halign='left')
    return pos


def draw_cell_symbol(d: Drawing, cell_name: str, ports: List[str], x: float = 0, y: float = 0, instance_name: str = None, port_overrides: Dict[str, PortType] = None, port_directions: Dict[str, str] = None, min_width: float = 2.0, min_height: float = 1.5, port_spacing: float = 0.4, separator_gap: float = 0.3, instance_label_pos: str = 'top-right') -> Dict[str, Tuple[float, float]]:
    PC, PMC, CC, IC, BC = COLORS.PIN_LABEL, COLORS.PIN_MARKER, COLORS.CELL_NAME, COLORS.INSTANCE_NAME, COLORS.SYMBOL_OUTLINE
    pd = port_directions or {}; gp = group_ports_by_type(ports, port_overrides); lp, rp, tp, bp = gp[PortType.INPUT]+gp[PortType.INOUT]+gp[PortType.UNKNOWN], gp[PortType.OUTPUT], gp[PortType.POWER], gp[PortType.GROUND]
    lg = [g for g in [gp[PortType.INPUT], gp[PortType.INOUT], gp[PortType.UNKNOWN]] if g]; ls = max(0, len(lg)-1)
    lh, rh, tw, bw = len(lp)*port_spacing+ls*separator_gap, len(rp)*port_spacing, len(tp)*port_spacing, len(bp)*port_spacing
    cw = 0.12; ltw, rtw, ctw = max((len(p) for p in lp), default=0)*cw+0.5, max((len(p) for p in rp), default=0)*cw+0.5, len(cell_name)*cw+0.4
    height, width = max(min_height, lh+0.6, rh+0.6), max(min_width, tw+0.6, bw+0.6, max(ltw+rtw, ctw))
    l, r, t, b = x-width/2, x+width/2, y+height/2, y-height/2
    d += elm.Line().at((l,t)).to((r,t)).color(BC); d += elm.Line().at((r,t)).to((r,b)).color(BC); d += elm.Line().at((r,b)).to((l,b)).color(BC); d += elm.Line().at((l,b)).to((l,t)).color(BC)
    if instance_name:
        if instance_label_pos == 'top-right': ix, iy = r+0.8, t+0.2
        elif instance_label_pos == 'top-left': ix, iy = l-0.8-len(instance_name)*cw, t+0.2
        else: ix, iy = r+0.8, t+0.2
        _label(d, (ix, iy), instance_name, IC, halign='left')
    _label(d, (x, y), cell_name, CC); psz, pos = 0.10, {}
    def dpmark(drawing, px, py, pn, side='left'): _draw_direction_marker(drawing, px, py, psz, PMC, pd.get(pn, 'inout'), side)
    if lp:
        cy = t-0.3
        for idx, g in enumerate(lg):
            if idx>0: d += elm.Line().at((l+0.1, cy-separator_gap/2)).to((l+0.4, cy-separator_gap/2)).color('#AAAAAA'); cy-=separator_gap
            for pn in g: dpmark(d, l, cy, pn, 'left'); _label(d, (l+0.25, cy), pn, PC, halign='left'); pos[pn]=(l-psz, cy); cy-=port_spacing
    if rp:
        sy = t-0.3-(height-0.6-len(rp)*port_spacing)/2
        for i, pn in enumerate(rp): py = sy-i*port_spacing; dpmark(d, r, py, pn, 'right'); _label(d, (r-0.25, py), pn, PC, halign='right'); pos[pn]=(r+psz, py)
    if tp:
        sx = x-(len(tp)-1)*port_spacing/2
        for i, pn in enumerate(tp): px = sx+i*port_spacing; dpmark(d, px, t, pn, 'top'); _label(d, (px, t-0.45), pn, PC, valign='top'); pos[pn]=(px, t+psz)
    if bp:
        sx = x-(len(bp)-1)*port_spacing/2
        for i, pn in enumerate(bp): px = sx+i*port_spacing; dpmark(d, px, b, pn, 'bottom'); _label(d, (px, b+0.20), pn, PC, valign='bottom'); pos[pn]=(px, b-psz)
    return pos


# =============================================================================
# 3. Registry and Dispatcher
# =============================================================================

SYMBOL_REGISTRY = {
    'resistor': {'func': draw_resistor_symbol, 'category': 'passive', 'example_args': {'name': 'R1'}, 'description': 'Resistor (zigzag)'},
    'capacitor': {'func': draw_capacitor_symbol, 'category': 'passive', 'example_args': {'name': 'C1'}, 'description': 'Capacitor (parallel plates)'},
    'inductor': {'func': draw_inductor_symbol, 'category': 'passive', 'example_args': {'name': 'L1'}, 'description': 'Inductor (loops)'},
    'vsource': {'func': draw_vsource_symbol, 'category': 'source', 'example_args': {'name': 'V1', 'dc': 5.0}, 'description': 'Voltage source'},
    'isource': {'func': draw_isource_symbol, 'category': 'source', 'example_args': {'name': 'I1', 'dc': 0.001}, 'description': 'Current source'},
    'vpulse': {'func': draw_vpulse_symbol, 'category': 'source', 'example_args': {'name': 'V1', 'v1': 0, 'v2': 5}, 'description': 'Pulse voltage source'},
    'vpwl': {'func': draw_vpwl_symbol, 'category': 'source', 'example_args': {'name': 'V1'}, 'description': 'PWL voltage source'},
    'vsin': {'func': draw_vsin_symbol, 'category': 'source', 'example_args': {'name': 'V1'}, 'description': 'Sine voltage source'},
    'iprobe': {'func': _draw_iprobe_symbol, 'category': 'source', 'example_args': {'name': 'I_SENSE'}, 'description': 'Current probe'},
    'vcvs': {'func': draw_vcvs_symbol, 'category': 'controlled_source', 'example_args': {'name': 'E1'}, 'description': 'Voltage-controlled voltage source'},
    'vccs': {'func': draw_vccs_symbol, 'category': 'controlled_source', 'example_args': {'name': 'G1'}, 'description': 'Voltage-controlled current source'},
    'ccvs': {'func': draw_ccvs_symbol, 'category': 'controlled_source', 'example_args': {'name': 'H1', 'probe': 'I_SENSE'}, 'description': 'Current-controlled voltage source'},
    'cccs': {'func': draw_cccs_symbol, 'category': 'controlled_source', 'example_args': {'name': 'F1', 'probe': 'I_OLED'}, 'description': 'Current-controlled current source'},
    'diode': {'func': draw_diode_symbol, 'category': 'semiconductor', 'example_args': {'name': 'D1'}, 'description': 'Diode (triangle with bar)'},
    'nmos': {'func': draw_nmos_symbol, 'category': 'semiconductor', 'example_args': {'name': 'M1'}, 'description': 'NMOS transistor (arrow on source)'},
    'pmos': {'func': draw_pmos_symbol, 'category': 'semiconductor', 'example_args': {'name': 'M2'}, 'description': 'PMOS transistor (arrow on source)'},
    'box': {'func': _draw_box_symbol, 'category': 'generic', 'example_args': {'name': 'X1', 'model': 'subckt', 'ports': ['A', 'B']}, 'description': 'Generic box symbol'},
}


def _get_symbol_draw_func_and_args(cell, iname, params=None) -> Tuple[callable, dict, str]:
    renderer = get_default_renderer(); sk = cell.name if cell else 'box'
    if getattr(cell, "_is_primitive", False): sk = getattr(cell, '_primitive_spectre_type', sk)
    cfg = renderer.get_config(sk, cell); ak = cfg.template if cfg.template else sk
    if ak not in SYMBOL_REGISTRY: ak = 'box'
    entry = SYMBOL_REGISTRY[ak]; df, fa = entry['func'], {'name': iname}
    if ak == 'box': fa['model'] = cell.name; fa['ports'] = cell.ports if hasattr(cell, 'ports') else []; fa['params'] = params
    else: fa['params'] = params; fa['model'] = cell.name
    if cfg.style == SymbolStyle.MINIMAL: fa['show_params'] = fa['show_type_label'] = fa['show_port_labels'] = False
    elif cfg.style == SymbolStyle.STANDARD: fa['show_params'] = False
    if cfg.label: fa['label'] = cfg.label
    return df, fa, ak


# =============================================================================
# 4. Smart Routing Helpers
# =============================================================================

def _norm_seg(x1, y1, x2, y2): return (x1, y1, x2, y2) if (x1, y1) <= (x2, y2) else (x2, y2, x1, y1)
def _segments_intersect(seg1, seg2, tol=1e-3):
    x1, y1, x2, y2 = seg1; x3, y3, x4, y4 = seg2; s1v, s2v = abs(x1-x2)<tol, abs(x3-x4)<tol; ep = {(x1, y1), (x2, y2)}
    if (x3, y3) in ep or (x4, y4) in ep: return False
    if s1v and s2v: return abs(x1-x3)<tol and max(min(y1, y2), min(y3, y4)) <= min(max(y1, y2), max(y3, y4))
    if (not s1v) and (not s2v): return abs(y1-y3)<tol and max(min(x1, x2), min(x3, x4)) <= min(max(x1, x2), max(x3, x4))
    if s1v and not s2v: return (min(x3, x4) <= x1 <= max(x3, x4)) and (min(y1, y2) <= y3 <= max(y1, y2))
    if not s1v and s2v: return (min(x1, x2) <= x3 <= max(x1, x2)) and (min(y3, y4) <= y1 <= max(y3, y4))
    return False
def _segment_intersects_box(seg, box, margin):
    x1, y1, x2, y2 = seg; l, r, b, t = box; l-=margin; r+=margin; b-=margin; t+=margin
    if abs(x1-x2)<1e-6: return l<=x1<=r and not (max(y1, y2)<b or min(y1, y2)>t)
    if abs(y1-y2)<1e-6: return b<=y1<=t and not (max(x1, x2)<l or min(x1, x2)>r)
    return False
def _segments_hit_boxes(segments, bboxes, scale):
    m = LAYOUT.WIRE_SYMBOL_MARGIN * scale
    for s in segments:
        for b in bboxes:
            if _segment_intersects_box(s, b, m): return True
    return False
def _would_cross(segments, wire_segments):
    for s in segments:
        for e in wire_segments:
            if _segments_intersect(s, e): return True
    return False
def _find_routing_channel(p1, p2, bboxes, wire_segments, scale, cs, rs):
    (x1, y1), (x2, y2) = p1, p2
    if abs(x1-x2)<0.2*cs:
        s = [(x1, y1, x1, y2)];
        if not _would_cross(s, wire_segments) and not _segments_hit_boxes(s, bboxes, scale): return s
    if abs(y1-y2)<0.2*rs:
        s = [(x1, y1, x2, y1)]
        if not _would_cross(s, wire_segments) and not _segments_hit_boxes(s, bboxes, scale): return s
    s1, s2 = [(x1, y1, x1, y2), (x1, y2, x2, y2)], [(x1, y1, x2, y1), (x2, y1, x2, y2)]
    if not _would_cross(s1, wire_segments) and not _segments_hit_boxes(s1, bboxes, scale): return s1
    if not _would_cross(s2, wire_segments) and not _segments_hit_boxes(s2, bboxes, scale): return s2
    if bboxes:
        ml, mr, dm = min(b[0] for b in bboxes), max(b[1] for b in bboxes), LAYOUT.WIRE_SYMBOL_MARGIN*scale + 0.6*scale
        for detx in [ml-dm, mr+dm]:
            s = [(x1, y1, detx, y1), (detx, y1, detx, y2), (detx, y2, x2, y2)]
            if not _would_cross(s, wire_segments) and not _segments_hit_boxes(s, bboxes, scale): return s
    return None
def _draw_smart_multipoint_wire(d, points, net_name, bboxes, wire_segments, scale):
    if len(points) < 2: return False
    WC, NLC = COLORS.WIRE, COLORS.NET_LABEL; sp = sorted(points, key=lambda p: -p[1]); xcoords = [p[0] for p in sp]
    same_col = (max(xcoords)-min(xcoords)) < 0.5*scale; avgx = sum(xcoords)/len(xcoords); ty, by = sp[0][1], sp[-1][1]
    if same_col:
        s = (avgx, ty, avgx, by)
        if _segments_hit_boxes([s], bboxes, scale) and bboxes:
            minl = min(b[0] for b in bboxes); dm = LAYOUT.WIRE_SYMBOL_MARGIN*scale + 0.6*scale; spx = minl-dm
        else: spx = avgx
    else:
        if bboxes: minl = min(b[0] for b in bboxes); dm = LAYOUT.WIRE_SYMBOL_MARGIN*scale + 0.6*scale; spx = minl-dm
        else: spx = sp[0][0]
    d += elm.Line().at((spx, ty)).to((spx, by)).color(WC); wire_segments.append(_norm_seg(spx, ty, spx, by))
    for (px, py) in sp:
        d += elm.Dot(radius=0.05*scale).at((spx, py)).color(WC).fill(WC)
        if abs(px-spx)>0.1*scale: d += elm.Line().at((px, py)).to((spx, py)).color(WC); d += elm.Dot(radius=0.05*scale).at((px, py)).color(WC).fill(WC); wire_segments.append(_norm_seg(px, py, spx, py))
    _label(d, (spx + LAYOUT.NET_LABEL_OFFSET * scale, (ty+by)/2 + LAYOUT.NET_LABEL_OFFSET_Y * scale), net_name, NLC, halign='left')
    return True


# =============================================================================
# 5. Main Schematic Functions
# =============================================================================

def _arrange_ports_in_grid(ports: List[str], pins_per_row: int, x_start: float, y_start: float,
                           x_spacing: float, y_spacing: float, grow_direction: str = 'down') -> List[Tuple[str, float, float]]:
    """
    Arrange ports in a grid layout with specified pins per row.

    Args:
        ports: List of port names to arrange
        pins_per_row: Number of pins per row before wrapping
        x_start: Starting x coordinate (leftmost pin)
        y_start: Starting y coordinate (first row)
        x_spacing: Horizontal spacing between pins
        y_spacing: Vertical spacing between rows
        grow_direction: 'down' for ground pins, 'up' for power pins

    Returns:
        List of (port_name, x, y) tuples
    """
    result = []
    for i, port in enumerate(ports):
        col = i % pins_per_row
        row = i // pins_per_row
        x = x_start + col * x_spacing
        if grow_direction == 'down':
            y = y_start - row * y_spacing
        else:
            y = y_start + row * y_spacing
        result.append((port, x, y))
    return result


def create_cell_schematic(circuit, title=None, scale=1.0, compact=False) -> Drawing:
    """
    Create a stable, label-based schematic view for a circuit.
    """
    renderer = get_default_renderer()
    with schemdraw.Drawing() as d:
        d.config(unit=1.5, fontsize=9)

        WIRE_COLOR = COLORS.WIRE
        PORT_COLOR = COLORS.PORT_MARKER
        TITLE_COLOR = COLORS.CELL_NAME
        NET_LABEL_COLOR = COLORS.NET_LABEL

        title = title or circuit.name
        instances = circuit.subcircuit_instances if hasattr(circuit, 'subcircuit_instances') else []
        ports = circuit.ports if hasattr(circuit, 'ports') else []
        port_directions = circuit.port_directions if hasattr(circuit, 'port_directions') else {}
        net_connections = {}
        net_connections_by_port = {}

        # Import PortType early so nested functions can access it
        from .config import infer_port_type, PortType

        # Primitive: just draw symbol and return
        if getattr(circuit, '_is_primitive', False):
            draw_func, func_args, _ = _get_symbol_draw_func_and_args(circuit, circuit.name.upper())
            func_args.update({'d': d, 'x': 0, 'y': 0, 'scale': scale * 1.5})
            draw_func(**func_args)
            return d

        # Layout configuration
        compact_factor = 0.6 if compact else 1.0
        col_spacing = LAYOUT.SCHEMATIC_COL_SPACING * scale * compact_factor
        row_spacing = LAYOUT.SCHEMATIC_ROW_SPACING * scale * compact_factor
        marker_size = 0.12 * scale

        # Adaptive spacing based on symbol sizes
        def estimate_symbol_size(inst, sc=1.0):
            kind = inst.subcircuit.name if hasattr(inst, 'subcircuit') else 'unknown'
            conn_ports = list(inst.connections.keys()) if hasattr(inst, 'connections') else []

            char_width = LAYOUT.CHAR_WIDTH * sc
            text_padding = LAYOUT.BOX_TEXT_PADDING * sc

            # Collect all lines of text that will be drawn beside the symbol
            text_lines = [inst.name]
            # Some symbols show model name too
            text_lines.append(kind)
            
            inst_params = _get_instance_params(inst)
            if inst_params:
                for k, v in inst_params.items():
                    text_lines.append(f"{k} = {v}")
            
            # Find the longest line to determine required horizontal space
            max_line_len = max((len(line) for line in text_lines), default=0)
            text_width = max_line_len * char_width
            
            # Base symbol width (simplified estimate)
            base_width = 2.0 * sc
            
            # extra_right is the space needed for labels to the right of the symbol center
            extra_right = (LAYOUT.LABEL_X_OFFSET * sc) + text_width + (text_padding / 2)

            # Cap the extra_right but make it generous (prevent extreme outliers)
            max_contribution = LAYOUT.MAX_LABEL_SPACING_CONTRIBUTION * sc
            extra_right = min(extra_right, max_contribution)

            # Height estimation based on line count
            line_count = len(text_lines)
            text_height = line_count * LAYOUT.LABEL_Y_SPACING * sc + text_padding
            height = max(1.5 * sc, text_height)

            return base_width, height, extra_right

        max_w, max_h, max_extra = 0.0, 0.0, 0.0
        for inst in instances:
            w, h, extra = estimate_symbol_size(inst, scale)
            if math.isfinite(w): max_w = max(max_w, w)
            if math.isfinite(h): max_h = max(max_h, h)
            if math.isfinite(extra): max_extra = max(max_extra, extra)

        # Cap max_w for spacing calculation to prevent long model names from causing excessive gaps
        max_w_for_spacing = min(max_w, LAYOUT.MAX_SYMBOL_WIDTH_FOR_SPACING * scale)

        if max_w_for_spacing > 0:
            col_spacing = max(col_spacing, max_w_for_spacing + max_extra + 1.0 * scale)
        if max_h > 0:
            row_spacing = max(row_spacing, max_h + 1.0 * scale)

        # Final safety check for spacings
        if not math.isfinite(col_spacing): col_spacing = 5.5 * scale
        if not math.isfinite(row_spacing): row_spacing = 3.5 * scale

        num_cols = 2
        num_rows = (len(instances) + num_cols - 1) // num_cols
        start_x = 1.5 * scale

        # Safety check for starting points
        if not math.isfinite(start_x): start_x = 1.5 * scale

        # === Horizontal Header Layout ===
        # [Power Supplies | Title | Ground Supplies] in columns
        # Circuit components below

        section_gap = 0.2 * scale  # Vertical gap between header and circuit
        pin_y_spacing = 0.6 * scale  # Vertical spacing between port rows
        
        # Use larger offset for header port labels to avoid overlap
        # (marker extent + character width buffer + padding)
        port_label_offset = marker_size + LAYOUT.CHAR_WIDTH * 2 * scale + LAYOUT.LABEL_PADDING * scale

        # Separate ports by type
        power_ports = [p for p in ports if infer_port_type(p) == PortType.POWER] if ports else []
        ground_ports = [p for p in ports if infer_port_type(p) == PortType.GROUND] if ports else []

        # Calculate header row count (max of power/ground rows, min 1 for title)
        power_row_count = len(power_ports)
        ground_row_count = len(ground_ports)
        header_rows = max(power_row_count, ground_row_count, 1)
        header_height = header_rows * pin_y_spacing

        # Calculate circuit extent
        circuit_height = num_rows * row_spacing
        circuit_bottom = 1.0 * scale
        circuit_top = circuit_bottom + circuit_height

        # Header starts above circuit
        header_bottom_y = circuit_top + section_gap
        header_top_y = header_bottom_y + header_height

        # Place instances
        instance_positions = {}
        relative_positions = {}
        for i, inst in enumerate(instances):
            pos = getattr(inst, 'schematic_position', None)
            if pos is not None:
                if isinstance(pos, dict):
                    relative_positions[inst.name] = pos
                else:
                    instance_positions[inst.name] = pos
            else:
                col = i % num_cols
                row = i // num_cols
                cx = start_x + col * col_spacing
                cy = circuit_top - row * row_spacing - row_spacing
                instance_positions[inst.name] = (cx, cy)

        for inst_name, rel_spec in relative_positions.items():
            ref_name = rel_spec.get('relative_to')
            x_shift = rel_spec.get('x_shift', 0)
            y_shift = rel_spec.get('y_shift', 0)
            if ref_name and ref_name in instance_positions:
                ref_x, ref_y = instance_positions[ref_name]
                instance_positions[inst_name] = (ref_x + x_shift, ref_y + y_shift)

        # Calculate actual bounds for title centering (including manually placed instances)
        all_x_coords = [start_x, start_x + (col_spacing * (num_cols - 1) if num_cols > 1 else 0)]
        for inst_name, pos in instance_positions.items():
            all_x_coords.append(pos[0])
            # Add some buffer for labels
            all_x_coords.append(pos[0] + 2.0 * scale)
        
        min_x_bound = min(all_x_coords)
        max_x_bound = max(all_x_coords)
        center_x = (min_x_bound + max_x_bound) / 2

        # Column positions (left, middle, right)
        # Use circuit width to determine column centers
        circuit_width = col_spacing * (num_cols - 1) if num_cols > 1 else col_spacing
        left_col_x = start_x - marker_size  # Power supplies (offset to align connection point)
        middle_col_x = center_x  # Title centered over entire circuit
        right_col_x = start_x + circuit_width + marker_size  # Ground supplies (offset to align connection point)

        # Cell name title removed — already shown in section heading

        # Draw power port markers in left column (stacked vertically, top to bottom)
        if power_ports:
            for i, port in enumerate(power_ports):
                py = header_top_y - (i + 0.5) * pin_y_spacing
                px = left_col_x
                if not math.isfinite(px) or not math.isfinite(py): continue
                direction = port_directions.get(port, 'inout')
                _draw_direction_marker(d, px, py, marker_size, PORT_COLOR, direction, 'left')
                _label(d, (px + port_label_offset, py), port, PORT_COLOR, halign='left')
                # Connection point is to the right of the marker
                net_connections.setdefault(port, []).append((px + marker_size, py))

        # Draw ground port markers in right column (stacked vertically, top to bottom)
        if ground_ports:
            for i, port in enumerate(ground_ports):
                py = header_top_y - (i + 0.5) * pin_y_spacing
                px = right_col_x
                if not math.isfinite(px) or not math.isfinite(py): continue
                direction = port_directions.get(port, 'inout')
                _draw_direction_marker(d, px, py, marker_size, PORT_COLOR, direction, 'right')
                _label(d, (px - port_label_offset, py), port, PORT_COLOR, halign='right')
                # Connection point is to the left of the marker
                net_connections.setdefault(port, []).append((px - marker_size, py))

        component_positions = {}
        for inst in instances:
            cx, cy = instance_positions.get(inst.name, (start_x, circuit_top))
            draw_func, func_args, _ = _get_symbol_draw_func_and_args(inst.subcircuit, inst.name, _get_instance_params(inst))
            func_args.update({'d': d, 'x': cx, 'y': cy, 'scale': scale})
            component_positions[inst.name] = draw_func(**func_args)

            if hasattr(inst, 'connections'):
                for port_name, net in inst.connections.items():
                    net_name = net.name if hasattr(net, 'name') else str(net)
                    if inst.name in component_positions and port_name in component_positions[inst.name]:
                        point = component_positions[inst.name][port_name]
                        if math.isfinite(point[0]) and math.isfinite(point[1]):
                            net_connections.setdefault(net_name, []).append(point)
                            net_connections_by_port.setdefault(net_name, []).append((inst.name, port_name, point))

        connection_style = LAYOUT.SCHEMATIC_CONNECTION_STYLE
        wired_nets = circuit.get_wired_nets() if hasattr(circuit, 'get_wired_nets') else []
        wired_net_ports = circuit.get_wired_net_ports() if hasattr(circuit, 'get_wired_net_ports') else {}

        def draw_wire_for_points(points, drawing):
            if len(points) < 2:
                return
            sorted_points = sorted(points, key=lambda p: -p[1])
            x_coords = [p[0] for p in sorted_points]
            x_min_wire, x_max_wire = min(x_coords), max(x_coords)
            same_column = (x_max_wire - x_min_wire) < 0.5 * scale
            stub_tolerance = 0.1 * scale  # Only draw horizontal stub if offset exceeds this

            if same_column:
                avg_x = sum(x_coords) / len(x_coords)
                top_y = sorted_points[0][1]
                bottom_y = sorted_points[-1][1]
                drawing += elm.Line().at((avg_x, top_y)).to((avg_x, bottom_y)).color(WIRE_COLOR)
                for (px, py) in sorted_points:
                    drawing += elm.Dot(radius=0.05 * scale).at((avg_x, py)).color(WIRE_COLOR).fill(WIRE_COLOR)
                    if abs(px - avg_x) > stub_tolerance:
                        drawing += elm.Line().at((px, py)).to((avg_x, py)).color(WIRE_COLOR)
            else:
                # Top-down smart routing: only draw horizontal stubs when needed
                top_point = sorted_points[0]
                trunk_x, top_y = top_point
                bottom_y = sorted_points[-1][1]

                # Draw main vertical trunk
                drawing += elm.Line().at((trunk_x, top_y)).to((trunk_x, bottom_y)).color(WIRE_COLOR)
                drawing += elm.Dot(radius=0.05 * scale).at((trunk_x, top_y)).color(WIRE_COLOR).fill(WIRE_COLOR)

                for (px, py) in sorted_points[1:]:
                    # Only draw horizontal stub if point is not aligned with trunk
                    if abs(px - trunk_x) > stub_tolerance:
                        drawing += elm.Line().at((trunk_x, py)).to((px, py)).color(WIRE_COLOR)
                        drawing += elm.Dot(radius=0.05 * scale).at((trunk_x, py)).color(WIRE_COLOR).fill(WIRE_COLOR)
                        drawing += elm.Dot(radius=0.05 * scale).at((px, py)).color(WIRE_COLOR).fill(WIRE_COLOR)
                    else:
                        # Point is aligned - just draw dot on trunk
                        drawing += elm.Dot(radius=0.05 * scale).at((trunk_x, py)).color(WIRE_COLOR).fill(WIRE_COLOR)

        if connection_style == 'wires':
            for net_name, points in net_connections.items():
                draw_wire_for_points(points, d)
        else:
            for net_name, points in net_connections.items():
                if net_name in wired_nets and len(points) >= 2:
                    selected = []
                    port_specs = wired_net_ports.get(net_name, [])
                    if port_specs:
                        for inst_name, port_name in port_specs:
                            point = component_positions.get(inst_name, {}).get(port_name)
                            if point:
                                selected.append(point)
                    else:
                        selected = list(points)

                    if len(selected) >= 2:
                        draw_wire_for_points(selected, d)
                        # Add net label on the wire (at midpoint of first segment)
                        sorted_sel = sorted(selected, key=lambda p: -p[1])
                        mid_y = (sorted_sel[0][1] + sorted_sel[-1][1]) / 2
                        wire_x = sorted_sel[0][0]  # Use x of top point (where vertical wire is)
                        _label(d, (wire_x + LAYOUT.NET_LABEL_OFFSET * scale, mid_y + LAYOUT.NET_LABEL_OFFSET_Y * scale), net_name, NET_LABEL_COLOR, halign='left')
                        # Also label non-wired points (like port markers)
                        selected_set = set(selected)
                        for (px, py) in points:
                            if (px, py) in selected_set:
                                continue
                            _label(d, (px + LAYOUT.NET_LABEL_OFFSET * scale, py + LAYOUT.NET_LABEL_OFFSET_Y * scale), net_name, NET_LABEL_COLOR, halign='left')
                        continue
                for (px, py) in points:
                    _label(d, (px + LAYOUT.NET_LABEL_OFFSET * scale, py + LAYOUT.NET_LABEL_OFFSET_Y * scale), net_name, NET_LABEL_COLOR, halign='left')

        # Collect all drawn element positions (instances + ports) for axes bounds
        all_x = [pos[0] for pos in instance_positions.values()]
        all_y = [pos[1] for pos in instance_positions.values()]
        for net_name, points in net_connections.items():
            for (px, py) in points:
                if math.isfinite(px) and math.isfinite(py):
                    all_x.append(px)
                    all_y.append(py)
        if all_x and all_y:
            margin = 0.5 * scale
            _draw_coordinate_axes(d, min(all_x) - margin, max(all_x) + margin,
                                  min(all_y) - margin, max(all_y) + margin, scale)

    return d


# =============================================================================
# 6. Renderer and Management
# =============================================================================

class SymbolRenderer:
    """Handles symbol configuration and lookup."""
    def __init__(self, default_style: SymbolStyle = SymbolStyle.DETAILED):
        self.default_style = default_style
        self.model_configs: Dict[str, SymbolConfig] = {}

    def register_model(self, model_name: str, config: SymbolConfig):
        self.model_configs[model_name] = config

    def get_config(self, model_name: str, cell=None) -> SymbolConfig:
        module_name = getattr(cell, "_module_name", None) if cell is not None else None
        if module_name:
            _load_symbol_configs_from_module(module_name, self)
        if model_name in self.model_configs:
            return self.model_configs[model_name]
        return SymbolConfig(style=self.default_style)

_default_renderer = None
_loaded_symbol_modules = set()
_loaded_default_symbol_configs = False


def _parse_symbol_style(value):
    if isinstance(value, SymbolStyle):
        return value
    if isinstance(value, str):
        key = value.strip().upper()
        if key in SymbolStyle.__members__:
            return SymbolStyle[key]
        for style in SymbolStyle:
            if style.value.upper() == key:
                return style
    return None


def _register_symbol_configs(renderer: "SymbolRenderer", configs: Dict):
    if not isinstance(configs, dict):
        return
    for model_name, cfg in configs.items():
        if isinstance(cfg, SymbolConfig):
            renderer.register_model(model_name, cfg)
            continue
        if not isinstance(cfg, dict):
            continue
        style = _parse_symbol_style(cfg.get("style"))
        template = cfg.get("template") or cfg.get("type")
        config = SymbolConfig(
            style=style or SymbolStyle.STANDARD,
            label=cfg.get("label"),
            color=cfg.get("color", "#E8E8E8"),
            border_color=cfg.get("border_color", "#000000"),
            port_names=cfg.get("port_names"),
            template=template,
            width=cfg.get("width", 2.0),
            height=cfg.get("height", 1.0),
            is_custom=bool(cfg.get("is_custom", False)),
        )
        renderer.register_model(model_name, config)


def _load_symbol_configs_from_module(module_name: str, renderer: "SymbolRenderer"):
    if not module_name or module_name in _loaded_symbol_modules:
        return
    try:
        module = importlib.import_module(module_name)
    except Exception:
        _loaded_symbol_modules.add(module_name)
        return
    configs = getattr(module, "SYMBOL_CONFIGS", None)
    if configs:
        _register_symbol_configs(renderer, configs)
    _loaded_symbol_modules.add(module_name)

def get_default_renderer():
    global _default_renderer
    global _loaded_default_symbol_configs
    if _default_renderer is None:
        _default_renderer = SymbolRenderer()
    if not _loaded_default_symbol_configs:
        default_path = Path(__file__).with_name("default_symbol_configs.yaml")
        if default_path.exists():
            try:
                with default_path.open("r", encoding="utf-8") as f:
                    data = yaml.safe_load(f)
                _register_symbol_configs(_default_renderer, data or {})
            except Exception:
                pass
        _loaded_default_symbol_configs = True
    return _default_renderer

def get_all_categories() -> List[str]:
    """Get all unique categories from the registry."""
    return sorted(list(set(e.get('category', 'unknown') for e in SYMBOL_REGISTRY.values())))

def generate_symbol_image(symbol_key: str, output_path: str, dpi: int = 150):
    """Generate a single symbol image for the registry/review."""
    with schemdraw.Drawing() as d:
        d.config(unit=1.2, fontsize=10)
        entry = SYMBOL_REGISTRY.get(symbol_key)
        if entry:
            func = entry['func']
            args = entry.get('example_args', {}).copy()
            args.update({'d': d, 'x': 5, 'y': 5, 'scale': 1.0})
            func(**args)
            _draw_coordinate_axes(d, 0, 10, 0, 10)
        d.save(output_path, dpi=dpi)

def create_testbench_block_diagram(testbench, scale=1.0, style: Optional[SymbolStyle] = None) -> Drawing:
    """
    Create a block diagram for a testbench showing subcircuit instances.
    Uses proper cell symbols for each instance.
    """
    with schemdraw.Drawing() as d:
        d.config(unit=1.2, fontsize=8)

        instances = testbench.subcircuit_instances if hasattr(testbench, 'subcircuit_instances') else []

        def estimate_symbol_size(inst, sc=1.0):
            kind = inst.subcircuit.name if hasattr(inst, 'subcircuit') else 'unknown'
            conn_ports = list(inst.connections.keys()) if hasattr(inst, 'connections') else []

            char_width = LAYOUT.CHAR_WIDTH * sc
            text_padding = LAYOUT.BOX_TEXT_PADDING * sc

            # Collect all lines of text
            text_lines = [inst.name, kind]
            inst_params = _get_instance_params(inst)
            if inst_params:
                for k, v in inst_params.items():
                    text_lines.append(f"{k} = {v}")
            
            max_line_len = max((len(line) for line in text_lines), default=0)
            text_width = max_line_len * char_width
            
            base_width = 2.0 * sc
            extra_right = (LAYOUT.LABEL_X_OFFSET * sc) + text_width + (text_padding / 2)
            
            # Use the global contribution cap
            max_contribution = LAYOUT.MAX_LABEL_SPACING_CONTRIBUTION * sc
            extra_right = min(extra_right, max_contribution)

            line_count = len(text_lines)
            text_height = line_count * LAYOUT.LABEL_Y_SPACING * sc + text_padding
            height = max(1.5 * sc, text_height)

            return base_width, height, extra_right

        per_inst_sizes = []
        max_h = 0.0
        for inst in instances:
            w, h, extra = estimate_symbol_size(inst, scale)
            per_inst_sizes.append((w, h, extra))
            if math.isfinite(h):
                max_h = max(max_h, h)

        spacing_y = max(2.5 * scale, max_h + 0.6 * scale)
        if not math.isfinite(spacing_y):
            spacing_y = 2.5 * scale

        instances_per_row = 3

        num_rows = max(1, (len(instances) - 1) // instances_per_row + 1)
        start_x = 2.0 * scale
        y_base = 2.0 * scale
        y_inst_start = y_base + num_rows * spacing_y

        if not math.isfinite(start_x): start_x = 2.0 * scale
        if not math.isfinite(y_inst_start): y_inst_start = 5.0 * scale

        # Precompute per-row spacing and row widths based on actual symbol sizes
        row_spacings = []
        row_widths = []
        for row in range(num_rows):
            row_instances = []
            for col in range(instances_per_row):
                idx = row * instances_per_row + col
                if idx >= len(instances):
                    break
                row_instances.append(idx)
            if not row_instances:
                row_spacings.append(3.0 * scale)
                row_widths.append(0.0)
                continue
            row_max_w = max(per_inst_sizes[idx][0] for idx in row_instances)
            row_max_extra = max(per_inst_sizes[idx][2] for idx in row_instances)
            row_spacing_x = max(3.0 * scale, row_max_w + row_max_extra + 0.8 * scale)
            if not math.isfinite(row_spacing_x):
                row_spacing_x = 3.0 * scale
            row_spacings.append(row_spacing_x)
            row_widths.append(row_spacing_x * (len(row_instances) - 1))

        for i, inst in enumerate(instances):
            row = i // instances_per_row
            col = i % instances_per_row
            spacing_x = row_spacings[row] if row < len(row_spacings) else 3.0 * scale
            x = col * spacing_x + start_x
            y = y_inst_start - row * spacing_y
            
            if not math.isfinite(x) or not math.isfinite(y):
                continue

            # Pass style if provided
            draw_func, func_args, _ = _get_symbol_draw_func_and_args(inst.subcircuit, inst.name, _get_instance_params(inst))
            if style:
                if style == SymbolStyle.MINIMAL: func_args['show_params'] = func_args['show_type_label'] = func_args['show_port_labels'] = False
                elif style == SymbolStyle.STANDARD: func_args['show_params'] = False
                elif style == SymbolStyle.DETAILED: func_args['show_params'] = True
            
            func_args.update({'d': d, 'x': x, 'y': y, 'scale': 0.8 * scale})
            port_positions = draw_func(**func_args)

            if hasattr(inst, 'connections'):
                for port_name, net in inst.connections.items():
                    if port_name not in port_positions:
                        continue
                    net_name = net.name if hasattr(net, 'name') else str(net)
                    px, py = port_positions[port_name]
                    if math.isfinite(px) and math.isfinite(py):
                        _label(d, (px + LAYOUT.NET_LABEL_OFFSET * scale, py + LAYOUT.NET_LABEL_OFFSET_Y * scale), net_name, COLORS.NET_LABEL, halign='left')

        # Draw ground symbol at bottom center
        max_row_width = max(row_widths) if row_widths else 0.0
        gnd_x = (max_row_width / 2) + start_x
        if math.isfinite(gnd_x) and math.isfinite(y_base):
            d += elm.Ground().at((gnd_x, y_base))
            _label(d, (gnd_x + 0.5 * scale, y_base), "0", COLORS.NET_LABEL, halign='left')

        x_min = start_x - 0.8 * scale
        x_max = start_x + max_row_width + 1.4 * scale
        y_min = y_base - 0.6 * scale
        y_max = y_inst_start + 1.4 * scale
        
        if all(math.isfinite(v) for v in (x_min, x_max, y_min, y_max)):
            _draw_coordinate_axes(d, x_min, x_max, y_min, y_max, scale=1.0 * scale)

    return d


# =============================================================================
# 7. Standalone and Legend Generators
# =============================================================================

def create_cell_symbol_standalone_v2(circuit, instance_name=None, scale=1.0):
    with schemdraw.Drawing() as d:
        d.config(unit=1, fontsize=10); df, fa, sk = _get_symbol_draw_func_and_args(circuit, instance_name or circuit.name)
        fa.update({'d': d, 'x': 5, 'y': 5, 'scale': scale}); df(**fa); _draw_coordinate_axes(d, 0, 10, 0, 10)
    return d


def create_source_symbol_standalone(
    source_type: str,
    name: str = None,
) -> Drawing:
    """
    Create a standalone symbol drawing for a source device.

    Args:
        source_type: One of 'vpulse', 'vpwl', 'vsin', 'vcvs', 'vccs', 'ccvs', 'cccs', 'vsource', 'isource', 'iprobe'
        name: Optional name label

    Returns:
        schemdraw Drawing object
    """
    name = name or source_type.upper()

    draw_funcs = {
        'vpulse': draw_vpulse_symbol,
        'vpwl': draw_vpwl_symbol,
        'vsin': draw_vsin_symbol,
        'vcvs': draw_vcvs_symbol,
        'vccs': draw_vccs_symbol,
        'ccvs': draw_ccvs_symbol,
        'cccs': draw_cccs_symbol,
        'vsource': draw_vsource_symbol,
        'isource': draw_isource_symbol,
        'iprobe': _draw_iprobe_symbol,
    }

    if source_type not in draw_funcs:
        # Fallback to box if it's a known generic type, or raise if unknown
        if source_type == 'box':
            with schemdraw.Drawing() as d:
                d.config(unit=1.5, fontsize=10)
                _draw_box_symbol(d, 0, 0, name, "subckt", ports=['p', 'n'], scale=1.0)
                _draw_coordinate_axes(d, -2, 2, -2, 2)
            return d
        raise ValueError(f"Unknown source type: {source_type}. Available: {list(draw_funcs.keys())}")

    with schemdraw.Drawing() as d:
        d.config(unit=1.5, fontsize=10)
        # All draw funcs should accept d, x, y, name, scale and handle additional args as kwargs
        draw_funcs[source_type](d, 0, 0, name, 1.0)
        _draw_coordinate_axes(d, -2, 2, -2, 2)

    return d


def draw_iprobe_symbol_standalone(name="I1", params=None):
    with schemdraw.Drawing() as d:
        d.config(unit=1, fontsize=10); _draw_iprobe_symbol(d, 5, 5, name, params=params); _draw_coordinate_axes(d, 0, 10, 0, 10)
    return d


def generate_legend(output_path: str = None, dpi: int = 150) -> Drawing:
    """Generate a legend explaining schematic colors and markers."""
    with schemdraw.Drawing() as d:
        d.config(unit=1.2, fontsize=10)

        # Colors
        WC, PC, NLC = COLORS.WIRE, COLORS.PORT_MARKER, COLORS.NET_LABEL
        IC, CC, PAC = COLORS.INSTANCE_NAME, COLORS.CELL_NAME, COLORS.CELL_TYPE

        # Layout parameters - increased spacing
        yb = 1.0       # y base
        sx = 1.0       # left column x
        rh = 1.2       # row height (was 1.0)
        cw = 7.5       # column width (was 6.0)
        marker_x = 0.3  # marker x offset from column start
        label_x = 1.5   # label x offset from column start (was 1.0)
        marker_sz = 0.10  # marker size

        # Title (closer to content - was 9 * rh)
        _label(d, (sx + cw / 2, yb + 7.8 * rh), "Schematic Legend", CC)

        # Left column: Color Coding
        y = yb + 7 * rh
        _label(d, (sx, y), "Color Coding:", '#555555')

        y -= rh
        d += elm.Line().at((sx, y)).to((sx + 0.5, y)).color(IC)
        _label(d, (sx + label_x, y), "Instance Name", IC, halign='left')

        y -= rh
        d += elm.Line().at((sx, y)).to((sx + 0.5, y)).color(CC)
        _label(d, (sx + label_x, y), "Cell Type / Model", CC, halign='left')

        y -= rh
        d += elm.Line().at((sx, y)).to((sx + 0.5, y)).color(PAC)
        _label(d, (sx + label_x, y), "Parameters / Files", PAC, halign='left')

        y -= rh
        d += elm.Line().at((sx, y)).to((sx + 0.5, y)).color(WC)
        _label(d, (sx + label_x, y), "Nets / Wires", WC, halign='left')

        # Right column: Port Markers
        rx = sx + cw
        y = yb + 7 * rh
        _label(d, (rx, y), "Port Markers:", '#555555')

        y -= rh
        _draw_input_marker(d, rx + marker_x, y, marker_sz, PC, 'left')
        _label(d, (rx + label_x, y), "Input Port", PC, halign='left')

        y -= rh
        _draw_output_marker(d, rx + marker_x, y, marker_sz, PC, 'left')
        _label(d, (rx + label_x, y), "Output Port", PC, halign='left')

        y -= rh
        _draw_inout_marker(d, rx + marker_x, y, marker_sz, PC, 'left')
        _label(d, (rx + label_x, y), "Inout Port / Pin", PC, halign='left')

        # Footer
        _label(d, (sx + cw / 2, yb - 0.5), "Generated by analogpy", '#AAAAAA')

        if output_path:
            d.save(output_path, dpi=dpi)
    return d


def create_cell_symbol_standalone(cell_name, ports, instance_name=None, port_overrides=None, port_directions=None, instance_label_pos='top-right') -> Drawing:
    with schemdraw.Drawing() as d:
        d.config(unit=1, fontsize=10); num_ports = len(ports)
        sx, sy = 3.0 + num_ports * 0.2, 3.0 + num_ports * 0.2
        draw_cell_symbol(d, cell_name, ports, x=sx, y=sy, instance_name=instance_name, port_overrides=port_overrides, port_directions=port_directions, instance_label_pos=instance_label_pos)
        extent = max(2.5, num_ports * 0.3)
        _draw_coordinate_axes(d, -1.0, sx + extent + 2.0, -1.0, sy + extent)
    return d


def get_symbols_by_category(category: str = None) -> Dict[str, Dict]:
    """
    Get symbols from the registry, optionally filtered by category.
    
    If category is None, returns a dict mapping category name to list of symbol info.
    If category is specified, returns a dict mapping symbol key to its registry entry.
    """
    if category:
        return {k: e for k, e in SYMBOL_REGISTRY.items() if e.get('category') == category}
    
    cats = {}
    for k, e in SYMBOL_REGISTRY.items():
        cats.setdefault(e.get('category', 'unknown'), []).append({
            'key': k, 
            'description': e.get('description', ''), 
            'example_args': e.get('example_args', {})
        })
    return cats
