# 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 re
from dataclasses import dataclass, field
import schemdraw
import schemdraw.elements as elm
from schemdraw import Drawing
import importlib
import warnings
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


# =============================================================================
# Port-mismatch warning support
# =============================================================================

# Tracks cell types already warned about, so we emit one warning per type.
_warned_port_mismatches: set = set()


def _warn_port_mismatch(type_name: str, unresolved: list, inst_name: str) -> None:
    """Emit a one-time Python warning when cell port names don't match symbol ports."""
    if type_name in _warned_port_mismatches:
        return
    _warned_port_mismatches.add(type_name)
    pairs = ', '.join(f"'{p}'→'{n}'" for p, n in unresolved)
    warnings.warn(
        f"[analogpy schematic] Cell '{type_name}' has port name(s) that don't match "
        f"its symbol template: {pairs}. "
        f"Net labels will be missing for these ports (example instance: '{inst_name}'). "
        f"Add 'port_names' to SYMBOL_CONFIGS to fix, e.g. "
        f"{{'port_names': {{{', '.join(repr(p)+': <symbol_port>' for p, _ in unresolved)}}}}}.  "
        f"This does NOT affect circuit connectivity or simulation.",
        stacklevel=3,
    )


def _draw_port_mismatch_warning(d, x: float, y: float, scale: float,
                                 unresolved: list) -> None:
    """Draw unresolved port→net pairs in red near the symbol body."""
    WC = "#CC2222"  # warning red
    line_h = 0.35 * scale
    # Place below symbol center; each unresolved port gets one line
    for i, (port_name, net_name) in enumerate(unresolved):
        ty = y - 0.8 * scale - i * line_h
        _label(d, (x, ty), f"!{port_name}={net_name}", WC,
               halign='center', valign='top')


# =============================================================================
# Image-background schemdraw element
# =============================================================================

class _ImageBackground(elm.Element):
    """Schemdraw element that draws a PNG/JPG image inside a box symbol.

    The image is rendered at draw time via the schemdraw backend's native
    ``fig.image()`` call, so it respects the drawing coordinate system.
    It is given a low z-order so box outline and labels render on top.
    """
    def __init__(self, image_path: str, left: float, bottom: float,
                 width: float, height: float, **kwargs):
        super().__init__(**kwargs)
        self._img_path = image_path
        self._img_left = left
        self._img_bottom = bottom
        self._img_width = width
        self._img_height = height

    def _draw(self, fig) -> None:
        try:
            fig.image(self._img_path,
                      xy=(self._img_left, self._img_bottom),
                      width=self._img_width,
                      height=self._img_height,
                      zorder=0)
        except Exception as e:
            warnings.warn(
                f"[analogpy schematic] Could not draw image '{self._img_path}': {e}",
                stacklevel=2,
            )


# =============================================================================
# Bus Instance Grouping Utilities
# =============================================================================

_BUS_RE = re.compile(r'^(.+)<(\d+)>$')


def parse_bus_name(name: str) -> Tuple[str, Optional[int]]:
    """Parse a bus-patterned name like 'R<3>' into ('R', 3).
    Returns (name, None) for non-bus names."""
    m = _BUS_RE.match(name)
    if m:
        return m.group(1), int(m.group(2))
    return name, None


def compress_net_names(net_names: List) -> str:
    """Compress a list of net names into bus notation.

    Examples:
        ["vdd", "vdd", "vdd"] -> "vdd"
        ["net_r<6>", "net_r<5>", ..., "net_r<0>"] -> "net_r<6:0>"
        ["vdd", "net_r<6>", "net_r<5>"] -> "vdd, net_r<6:5>"
    """
    names = [n.name if hasattr(n, 'name') else str(n) for n in net_names]

    # All identical
    unique = list(dict.fromkeys(names))
    if len(unique) == 1:
        return unique[0]

    # Group into runs of contiguous bus indices with same base, and non-bus names
    segments = []
    i = 0
    while i < len(names):
        base, idx = parse_bus_name(names[i])
        if idx is not None:
            # Start a bus run
            run_base = base
            run_indices = [idx]
            j = i + 1
            while j < len(names):
                b2, idx2 = parse_bus_name(names[j])
                if b2 == run_base and idx2 is not None and abs(idx2 - run_indices[-1]) == 1:
                    run_indices.append(idx2)
                    j += 1
                else:
                    break
            if len(run_indices) > 1:
                segments.append(f"{run_base}<{run_indices[0]}:{run_indices[-1]}>")
            else:
                segments.append(names[i])
            i = j
        else:
            segments.append(names[i])
            i += 1

    # Deduplicate consecutive identical segments
    deduped = [segments[0]]
    for s in segments[1:]:
        if s != deduped[-1]:
            deduped.append(s)

    return ', '.join(deduped)


@dataclass
class BusGroup:
    """A group of instances that share a bus-patterned name."""
    base_name: str
    indices: List[int]
    instances: List  # List of CircuitInstance, sorted by index
    subcircuit: object  # shared Circuit
    params: Dict  # shared params
    port_nets: Dict[str, List]  # port_name -> [Net for each index, in order]

    @property
    def display_name(self) -> str:
        if len(self.indices) >= 2:
            return f"{self.base_name}<{self.indices[0]}:{self.indices[-1]}>"
        return f"{self.base_name}<{self.indices[0]}>"

    @property
    def count(self) -> int:
        return len(self.indices)


def group_bus_instances(instances: list) -> list:
    """Group bus-patterned instances into BusGroup objects.

    Grouping criteria: same base name, same subcircuit, same params (value equality).
    Groups with fewer than BUS_MIN_GROUP_SIZE members stay as individual instances.

    Returns a mixed list of CircuitInstance and BusGroup.
    """
    min_group = LAYOUT.BUS_MIN_GROUP_SIZE

    # Collect potential bus members
    bus_candidates = {}  # (base_name, subcircuit_name, params_key) -> [(index, instance)]
    non_bus = []

    for inst in instances:
        base, idx = parse_bus_name(inst.name)
        if idx is not None:
            params = getattr(inst, 'params', {})
            subckt_name = inst.subcircuit.name if hasattr(inst, 'subcircuit') else ''
            params_key = tuple(sorted(params.items())) if params else ()
            key = (base, subckt_name, params_key)
            bus_candidates.setdefault(key, []).append((idx, inst))
        else:
            non_bus.append(inst)

    result = []
    used_instances = set()

    for (base, subckt_name, params_key), members in bus_candidates.items():
        if len(members) < min_group:
            for _, inst in members:
                non_bus.append(inst)
            continue

        # Sort by index (descending by default to match common bus ordering)
        members.sort(key=lambda m: m[0], reverse=True)
        indices = [m[0] for m in members]
        insts = [m[1] for m in members]
        for inst in insts:
            used_instances.add(id(inst))

        # Build port_nets: for each port, collect the net from each instance (in order)
        port_nets = {}
        representative = insts[0]
        for port_name in representative.connections:
            port_nets[port_name] = [inst.connections.get(port_name) for inst in insts]

        group = BusGroup(
            base_name=base,
            indices=indices,
            instances=insts,
            subcircuit=representative.subcircuit,
            params=dict(getattr(representative, 'params', {})),
            port_nets=port_nets,
        )
        result.append(group)

    # Non-bus instances maintain their original relative order
    for inst in non_bus:
        if id(inst) not in used_instances:
            result.append(inst)

    return result


def _draw_bus_slash(d, x, y, scale, count, rotation=0):
    """Draw a diagonal slash with count annotation on a bus-collapsed symbol."""
    sz = LAYOUT.BUS_SLASH_SIZE * scale
    SC = COLORS.SYMBOL_OUTLINE
    IC = COLORS.INSTANCE_NAME

    # Diagonal slash through symbol center
    x1, y1 = x - sz, y - sz
    x2, y2 = x + sz, y + sz
    if rotation:
        x1, y1 = _rotate_pt(x1, y1, x, y, rotation)
        x2, y2 = _rotate_pt(x2, y2, x, y, rotation)
    d += elm.Line().at((x1, y1)).to((x2, y2)).color(SC).linewidth(2)

    # Count annotation
    tx, ty = x + sz * 0.7, y + sz * 1.5
    if rotation:
        tx, ty = _rotate_pt(tx, ty, x, y, rotation)
    _label(d, (tx, ty), str(count), IC, halign='left')


def draw_bus_symbol(d, x, y, bus_group, scale=1.0, rotation=0, style=None):
    """Draw a collapsed bus symbol: base symbol + slash + count + compressed net labels."""
    draw_func, func_args, _ = _get_symbol_draw_func_and_args(
        bus_group.subcircuit, bus_group.display_name,
        bus_group.params, rotation=rotation
    )

    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': scale,
        'rotation': rotation,
    })
    port_positions = draw_func(**func_args)
    _draw_bus_slash(d, x, y, scale, bus_group.count, rotation)

    # Draw compressed net labels at each port
    NLC = COLORS.NET_LABEL
    for port_name, nets in bus_group.port_nets.items():
        if port_name in port_positions:
            compressed = compress_net_names(nets)
            px, py = port_positions[port_name]
            # Place label near port, offset from symbol center
            if py > y:  # port above center
                _label(d, (px + LAYOUT.NET_LABEL_OFFSET * scale, py + LAYOUT.NET_LABEL_OFFSET_Y * scale),
                       compressed, NLC, halign='left')
            else:  # port below center
                _label(d, (px + LAYOUT.NET_LABEL_OFFSET * scale, py + LAYOUT.NET_LABEL_OFFSET_Y * scale),
                       compressed, NLC, halign='left')

    return port_positions


# =============================================================================
# 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 _resolve_relative_positions(
    relative_positions: dict,
    instance_positions: dict,
) -> None:
    """Resolve relative_to position specs iteratively.

    Handles chains like m1→vin→vdd regardless of dict insertion order by
    repeating the resolution pass until no more progress can be made.
    Emits a warning for any specs that remain unresolvable (self-references,
    missing anchors, or circular dependencies).
    """
    remaining = dict(relative_positions)
    while remaining:
        resolved_this_pass = []
        for inst_name, rel_spec in remaining.items():
            ref = rel_spec.get('relative_to')
            # Accept a CircuitInstance object as well as a string name
            if ref is not None and not isinstance(ref, str):
                ref = getattr(ref, 'name', None)
                if ref:
                    rel_spec = dict(rel_spec, relative_to=ref)
            ref_name = ref
            if ref_name and ref_name in instance_positions:
                ref_x, ref_y = instance_positions[ref_name]
                instance_positions[inst_name] = (
                    ref_x + rel_spec.get('x_shift', 0),
                    ref_y + rel_spec.get('y_shift', 0),
                )
                resolved_this_pass.append(inst_name)
        if not resolved_this_pass:
            # No progress — remaining entries are unresolvable
            for inst_name, rel_spec in remaining.items():
                ref_name = rel_spec.get('relative_to', '?')
                warnings.warn(
                    f"[analogpy schematic] schematic_position relative_to='{ref_name}' "
                    f"for instance '{inst_name}' could not be resolved "
                    f"('{ref_name}' has no position — check for self-references, "
                    f"missing instances, or circular dependencies). "
                    f"Instance will be placed at default position.",
                    stacklevel=4,
                )
            break
        for name in resolved_this_pass:
            del remaining[name]


def _rotate_pt(px: float, py: float, cx: float, cy: float, angle_deg: float) -> Tuple[float, float]:
    """Rotate a point around a center by angle in degrees."""
    if angle_deg == 0:
        return px, py
    rad = math.radians(angle_deg)
    s, c = math.sin(rad), math.cos(rad)
    dx, dy = px - cx, py - cy
    nx = cx + dx * c - dy * s
    ny = cy + dx * s + dy * c
    return nx, ny


# Module-level state for show_bboxes debug mode.
# When set to a Drawing instance, _label() records text positions into
# _bbox_records for later rendering as matplotlib patches.
_show_bboxes_drawing = None
_bbox_records = []  # list of (pos, text, fontsize, halign, category)
_bbox_tracker = None  # BBoxTracker instance for collision detection

# Module-level state for per-instance label tracking.
# Set by create_cell_schematic before each symbol draw call.
_current_layout = None       # SchematicLayout being built
_current_instance = None     # Instance name currently being drawn (or None for header labels)
_pending_pin_labels = []     # Buffer for pin labels drawn before ports are populated

# Color map for bbox outlines by text category
_BBOX_COLORS = {
    'instance_name': '#FF6600',   # Orange - matches instance name color
    'cell_name': '#006600',       # Green - matches cell/model name color
    'parameter': '#666666',       # Gray - matches parameter text color
    'net_label': '#0066CC',       # Blue - matches net label color
    'pin_label': '#CC0000',       # Red - matches pin marker color
    'port_label': '#CC0000',      # Red - matches port marker color
    'default': '#999999',         # Gray fallback
}


def _infer_bbox_category(color):
    """Map a label color to a bbox category string."""
    if color == COLORS.INSTANCE_NAME:
        return 'instance_name'
    elif color == COLORS.CELL_NAME:
        return 'cell_name'
    elif color in (COLORS.CELL_TYPE, COLORS.PARAMETER_TEXT):
        return 'parameter'
    elif color == COLORS.NET_LABEL:
        return 'net_label'
    elif color == COLORS.PIN_LABEL:
        return 'pin_label'
    elif color == COLORS.PORT_MARKER:
        return 'port_label'
    return 'default'


def _label(d, pos, text, color, halign=None, valign=None, zorder=None, fontsize=None, rotation=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.

    Args:
        rotation: If 'vertical' or 90, rotate the entire text 90° counter-clockwise.
    """
    use_rotation = (rotation == 'vertical' or rotation == 90)

    lbl_kwargs = {}
    if halign: lbl_kwargs['halign'] = halign
    if valign: lbl_kwargs['valign'] = valign
    if fontsize is not None: lbl_kwargs['fontsize'] = fontsize
    if use_rotation: lbl_kwargs['rotate'] = 90
    el = elm.Label().at(pos).label(text, **lbl_kwargs).color(color)
    if zorder is not None:
        el = el.zorder(zorder)
    d += el

    # Record for bbox overlay and collision tracking
    # Skip axis labels (not relevant for collision avoidance)
    if color != LAYOUT.AXIS_LABEL_COLOR:
        category = _infer_bbox_category(color)

        # Visual bbox overlay records (only when show_bboxes is on)
        if _show_bboxes_drawing is not None and d is _show_bboxes_drawing:
            _bbox_records.append((pos, text, fontsize, halign, category))
            if _bbox_tracker is not None:
                _bbox_tracker.add_text_bbox(
                    pos[0], pos[1], text,
                    fontsize=fontsize or LAYOUT.SCHEMATIC_FONTSIZE,
                    halign=halign or 'center',
                    label=f'{category}:{text}',
                )

        # Per-instance SchematicLayout tracking (always, for collision avoidance)
        if _current_layout is not None and _current_instance is not None:
            from .config import LabelPlacement
            inst_layout = _current_layout.instances.get(_current_instance)
            if inst_layout is not None:
                lp = LabelPlacement(
                    text=text, pos=pos, anchor=inst_layout.center,
                    halign=halign or 'center',
                    fontsize=fontsize, category=category,
                )
                if category == 'instance_name' and inst_layout.inst_name_label is None:
                    inst_layout.inst_name_label = lp
                elif category == 'cell_name' and inst_layout.cell_name_label is None:
                    inst_layout.cell_name_label = lp
                elif category == 'parameter':
                    # Extract param key from "key=value" text
                    pkey = text.split('=')[0] if '=' in text else text
                    inst_layout.param_labels[pkey] = lp
                elif category == 'pin_label':
                    # Match pin label text (e.g., 's', 'g') to port by name.
                    # Ports may not exist yet (populated after draw_func returns),
                    # so buffer unmatched labels for deferred assignment.
                    if text in inst_layout.ports and inst_layout.ports[text].pin_label is None:
                        inst_layout.ports[text].pin_label = lp
                    else:
                        _pending_pin_labels.append((_current_instance, text, lp))


class BBoxTracker:
    """Track occupied bounding boxes for smart collision avoidance."""

    def __init__(self, scale=1.0):
        self.scale = scale
        self.bboxes = []  # list of (x_min, y_min, x_max, y_max)
        self.labels = []  # list of label strings, parallel to self.bboxes

    def add_bbox(self, x, y, width, height, label=''):
        """Add a bounding box."""
        self.bboxes.append((x - width/2, y - height/2, x + width/2, y + height/2))
        self.labels.append(label)

    def add_text_bbox(self, x, y, text, fontsize=10, halign='left', label=''):
        """Add a text bounding box based on estimated character size."""
        char_width = 0.12 * fontsize / 10 * self.scale
        char_height = 0.15 * fontsize / 10 * self.scale
        text_width = len(text) * char_width

        if halign == 'left':
            x_min, x_max = x, x + text_width
        elif halign == 'right':
            x_min, x_max = x - text_width, x
        else:  # center
            x_min, x_max = x - text_width/2, x + text_width/2

        y_min, y_max = y - char_height/2, y + char_height/2
        self.bboxes.append((x_min, y_min, x_max, y_max))
        self.labels.append(label or text)

    def check_collision(self, x, y, width, height, margin=0.1):
        """Check if a bbox at (x, y) with given size collides with any existing bbox."""
        x_min, y_min = x - width/2 - margin, y - height/2 - margin
        x_max, y_max = x + width/2 + margin, y + height/2 + margin

        for bx_min, by_min, bx_max, by_max in self.bboxes:
            if not (x_max < bx_min or x_min > bx_max or y_max < by_min or y_min > by_max):
                return True  # Collision detected
        return False

    def find_best_position(self, x, y, preferred_offset, offsets_to_try, text_width, text_height):
        """Try preferred position first, then try alternatives."""
        # Try preferred position
        px = x + preferred_offset[0]
        py = y + preferred_offset[1]
        if not self.check_collision(px, py, text_width, text_height):
            return px, py

        # Try alternative offsets
        for offset in offsets_to_try:
            px = x + offset[0]
            py = y + offset[1]
            if not self.check_collision(px, py, text_width, text_height):
                return px, py

        # Return preferred anyway as fallback
        return x + preferred_offset[0], y + preferred_offset[1]

    def find_all_collisions(self):
        """Find all pairs of overlapping bounding boxes.

        Returns:
            List of (label_a, label_b, overlap_area) tuples for each collision.
        """
        collisions = []
        n = len(self.bboxes)
        for i in range(n):
            ax0, ay0, ax1, ay1 = self.bboxes[i]
            for j in range(i + 1, n):
                bx0, by0, bx1, by1 = self.bboxes[j]
                # Check overlap
                ox0 = max(ax0, bx0)
                oy0 = max(ay0, by0)
                ox1 = min(ax1, bx1)
                oy1 = min(ay1, by1)
                if ox0 < ox1 and oy0 < oy1:
                    area = (ox1 - ox0) * (oy1 - oy0)
                    collisions.append((self.labels[i], self.labels[j], area))
        return collisions


def _net_label(d, pos, text, color, halign=None, valign=None, zorder=None):
    """Add a net name label using NET_LABEL_FONTSIZE."""
    _label(d, pos, text, color, halign=halign, valign=valign,
           zorder=zorder, fontsize=LAYOUT.NET_LABEL_FONTSIZE)

# =============================================================================
# 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, rotation=0):
    """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
        p1 = _rotate_pt(x + r * math.cos(a1), y + r * math.sin(a1), x, y, rotation)
        p2 = _rotate_pt(x + r * math.cos(a2), y + r * math.sin(a2), x, y, rotation)
        d += elm.Line().at(p1).to(p2).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'):
    """Draw input port marker: unfilled triangle pointing INTO the circuit."""
    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_output_marker(d, px, py, sz, color, side='left'):
    """Draw output port marker: solid filled triangle pointing OUT of the circuit."""
    # Draw outline
    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)
    # Solid fill with dense horizontal/vertical lines
    n = 8  # number of fill lines per half
    if side in ('left', 'right'):
        # Horizontal fill: at each y level, the triangle width tapers toward the tip
        for i in range(-n, n + 1):
            off = i * sz / n
            if abs(off) >= sz:
                continue
            hw = sz - abs(off)  # half-width at this y level
            if side == 'left':
                # Tip at left (px-sz), flat at right (px+sz)
                d += elm.Line().at((px + sz - 2 * hw, py + off)).to((px + sz, py + off)).color(color)
            else:
                # Tip at right (px+sz), flat at left (px-sz)
                d += elm.Line().at((px - sz, py + off)).to((px - sz + 2 * hw, py + off)).color(color)
    else:
        # Vertical fill for top/bottom
        for i in range(-n, n + 1):
            off = i * sz / n
            if abs(off) >= sz:
                continue
            hw = sz - abs(off)
            if side == 'top':
                d += elm.Line().at((px + off, py - sz)).to((px + off, py - sz + 2 * hw)).color(color)
            else:
                d += elm.Line().at((px + off, py + sz)).to((px + off, py + sz - 2 * hw)).color(color)


def _draw_polarity_markers(d, x: float, y_plus: float, y_minus: float, size: float, color: str, rotation=0, cx=None, cy=None):
    """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
        rotation: rotation in degrees
        cx, cy: rotation center (defaults to x, center of y_plus/y_minus)
    """
    rcx = cx if cx is not None else x
    rcy = cy if cy is not None else (y_plus + y_minus) / 2
    
    def _rp(px, py): return _rotate_pt(px, py, rcx, rcy, rotation)

    # Plus sign: horizontal + vertical cross
    d += elm.Line().at(_rp(x - size, y_plus)).to(_rp(x + size, y_plus)).color(color)
    d += elm.Line().at(_rp(x, y_plus - size)).to(_rp(x, y_plus + size)).color(color)
    # Minus sign: horizontal line only
    d += elm.Line().at(_rp(x - size, y_minus)).to(_rp(x + size, y_minus)).color(color)


def _draw_inout_marker(d, px, py, sz, color, side='left'):
    """Draw inout port marker: unfilled diamond shape.

    For left/right sides: horizontally elongated diamond.
    For top/bottom sides: vertically elongated diamond (rotated 90°).
    """
    if side in ('top', 'bottom'):
        # Vertically elongated: taller than wide
        sx, sy = sz, sz * 1.5
    else:
        # Horizontally elongated: wider than tall
        sx, sy = sz * 1.5, sz
    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)


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, cx, cy, params, scale=1.0, start_offset=0.0, rotation=0, base_lx=None, base_ly=None, **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')
    
    lx = base_lx if base_lx is not None else cx
    ly = base_ly if base_ly is not None else cy

    for i, (k, v) in enumerate(params.items()):
        py = ly - (start_offset + i) * lys
        if not math.isfinite(py):
            continue
        param_label = f"{k}={v}"
        pos = _rotate_pt(lx, py, cx, cy, rotation)
        _label(d, pos, 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, rotation=0, **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
    
    # Rotate helper
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    d += elm.Line().at(_rp(x, tb)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    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(_rp(x, cy)).to(_rp(x+amp, ny)).color(SC)
        cy = ny; ny = cy-zh/2
        d += elm.Line().at(_rp(x+amp, cy)).to(_rp(x, ny)).color(SC)
        cy = ny
        
    bb, by = y-bl/2, y-bl/2-ll
    d += elm.Line().at(_rp(x, bb)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+za+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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
        # For parameters, we might want to keep them horizontal, but let's rotate for now
        # Actually _draw_parameter_labels needs to be updated too
        _draw_parameter_labels(d, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_capacitor_symbol(d, x, y, name, scale=1.0, model="capacitor", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    d += elm.Line().at(_rp(x, y+pg/2)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x-pw/2, y+pg/2)).to(_rp(x+pw/2, y+pg/2)).color(SC)
    d += elm.Line().at(_rp(x-pw/2, y-pg/2)).to(_rp(x+pw/2, y-pg/2)).color(SC)
    
    d += elm.Line().at(_rp(x, y-pg/2)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+pw/2+lxo+LAYOUT.CAPACITOR_LABEL_EXTRA_X*scale, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_inductor_symbol(d, x, y, name, scale=1.0, model="inductor", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    d += elm.Line().at(_rp(x, tb)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    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(_rp(x+lr*math.cos(a1), cy+lr*math.sin(a1))).to(_rp(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(_rp(x, bb)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+lr+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_diode_symbol(d, x, y, name, scale=1.0, model="diode", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    d += elm.Line().at(_rp(x, tb)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    bb, by = y-bl/2, y-bl/2-ll
    d += elm.Line().at(_rp(x, bb)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    d += elm.Line().at(_rp(x-tw/2, tb)).to(_rp(x+tw/2, tb)).color(SC)
    d += elm.Line().at(_rp(x-tw/2, tb)).to(_rp(x, bb)).color(SC)
    d += elm.Line().at(_rp(x+tw/2, tb)).to(_rp(x, bb)).color(SC)
    d += elm.Line().at(_rp(x-tw/2, bb)).to(_rp(x+tw/2, bb)).color(SC)
    
    lx, ly = x+tw/2+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_vsource_symbol(d, x, y, name, scale=1.0, model="vsource", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    _draw_circle_base(d, x, y, r, SC, rotation=rotation)
    _draw_polarity_markers(d, x, y+r*0.4, y-r*0.4, r*0.15, SC, rotation=rotation, cx=x, cy=y)
    
    ty, by = y+r+ll, y-r-ll
    d += elm.Line().at(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_isource_symbol(d, x, y, name, scale=1.0, model="isource", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    _draw_circle_base(d, x, y, r, SC, rotation=rotation)
    d += elm.Line().at(_rp(x, y+r*0.5)).to(_rp(x, y-r*0.5)).color(SC)
    hs = 0.15*scale
    d += elm.Line().at(_rp(x-hs/2, y-r*0.5+hs)).to(_rp(x, y-r*0.5)).color(SC)
    d += elm.Line().at(_rp(x+hs/2, y-r*0.5+hs)).to(_rp(x, y-r*0.5)).color(SC)
    
    ty, by = y+r+ll, y-r-ll
    d += elm.Line().at(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_vpulse_symbol(d, x, y, name, scale=1.0, model="vpulse", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    _draw_circle_base(d, x, y, r, SC, rotation=rotation)
    sw = r*0.4
    d += elm.Line().at(_rp(x-sw, y-sw/2)).to(_rp(x-sw/2, y-sw/2)).color(SC)
    d += elm.Line().at(_rp(x-sw/2, y-sw/2)).to(_rp(x-sw/2, y+sw/2)).color(SC)
    d += elm.Line().at(_rp(x-sw/2, y+sw/2)).to(_rp(x+sw/2, y+sw/2)).color(SC)
    d += elm.Line().at(_rp(x+sw/2, y+sw/2)).to(_rp(x+sw/2, y-sw/2)).color(SC)
    d += elm.Line().at(_rp(x+sw/2, y-sw/2)).to(_rp(x+sw, y-sw/2)).color(SC)
    
    ty, by = y+r+ll, y-r-ll
    d += elm.Line().at(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_vsin_symbol(d, x, y, name, scale=1.0, model="vsin", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    _draw_circle_base(d, x, y, r, SC, rotation=rotation)
    for i in range(20):
        a1, a2 = i*2*math.pi/20, (i+1)*2*math.pi/20
        d += elm.Line().at(_rp(x-r*0.5+r*a1/(2*math.pi), y+r*0.4*math.sin(a1))).to(_rp(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(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_vpwl_symbol(d, x, y, name, scale=1.0, model="vpwl", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    _draw_circle_base(d, x, y, r, SC, rotation=rotation)
    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(_rp(x+pts[i][0]*r, y+pts[i][1]*r)).to(_rp(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(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(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, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    _draw_circle_base(d, x, y, r, SC, rotation=rotation)
    ax, at, ab, ahs = x-r*0.2, y+r*0.5, y-r*0.5, r*0.25
    d += elm.Line().at(_rp(ax, at)).to(_rp(ax, ab)).color(SC)
    d += elm.Line().at(_rp(ax-ahs*0.6, ab+ahs*0.8)).to(_rp(ax, ab)).color(SC)
    d += elm.Line().at(_rp(ax+ahs*0.6, ab+ahs*0.8)).to(_rp(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(_rp(arcx+arcr*math.cos(a1), arcy+arcr*math.sin(a1))).to(_rp(arcx+arcr*math.cos(a2), arcy+arcr*math.sin(a2))).color(SC)
        
    na, nl = math.pi/6, arcr*0.9
    d += elm.Line().at(_rp(arcx, arcy)).to(_rp(arcx+nl*math.sin(na), arcy+nl*math.cos(na))).color(SC)
    
    ty, by = y+r+ll, y-r-ll
    d += elm.Line().at(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    if show_port_labels: _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    if show_port_labels: _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_vcvs_symbol(d, x, y, name, scale=1.0, model="vcvs", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    d += elm.Line().at(_rp(x, y+r)).to(_rp(x+r, y)).color(SC)
    d += elm.Line().at(_rp(x+r, y)).to(_rp(x, y-r)).color(SC)
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x-r, y)).color(SC)
    d += elm.Line().at(_rp(x-r, y)).to(_rp(x, y+r)).color(SC)
    
    _draw_polarity_markers(d, x, y+r*0.3, y-r*0.3, r*0.15, SC, rotation=rotation, cx=x, cy=y)
    
    ty, by = y+r+ll, y-r-ll
    d += elm.Line().at(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    cyo, cpx = r*0.4, x-r-ll
    d += elm.Line().at(_rp(x-r/2, y+cyo)).to(_rp(cpx, y+cyo)).color(SC)
    _draw_pin_marker_helper(d, *_rp(cpx, y+cyo), ps, PMC)
    _label(d, _rp(cpx-0.2*scale, y+cyo), 'nc+', PLC, halign='right')
    
    d += elm.Line().at(_rp(x-r/2, y-cyo)).to(_rp(cpx, y-cyo)).color(SC)
    _draw_pin_marker_helper(d, *_rp(cpx, y-cyo), ps, PMC)
    _label(d, _rp(cpx-0.2*scale, y-cyo), 'nc-', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by), 'nc+': _rp(cpx, y + cyo), 'nc-': _rp(cpx, y - cyo)}


def draw_vccs_symbol(d, x, y, name, scale=1.0, model="vccs", label=None, params=None, show_params=True, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    d += elm.Line().at(_rp(x, y+r)).to(_rp(x+r, y)).color(SC)
    d += elm.Line().at(_rp(x+r, y)).to(_rp(x, y-r)).color(SC)
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x-r, y)).color(SC)
    d += elm.Line().at(_rp(x-r, y)).to(_rp(x, y+r)).color(SC)
    
    d += elm.Line().at(_rp(x, y+r*0.4)).to(_rp(x, y-r*0.4)).color(SC)
    hs = 0.12*scale
    d += elm.Line().at(_rp(x-hs/2, y-r*0.4+hs)).to(_rp(x, y-r*0.4)).color(SC)
    d += elm.Line().at(_rp(x+hs/2, y-r*0.4+hs)).to(_rp(x, y-r*0.4)).color(SC)
    
    ty, by = y+r+ll, y-r-ll
    d += elm.Line().at(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    cyo, cpx = r*0.4, x-r-ll
    d += elm.Line().at(_rp(x-r/2, y+cyo)).to(_rp(cpx, y+cyo)).color(SC)
    _draw_pin_marker_helper(d, *_rp(cpx, y+cyo), ps, PMC)
    _label(d, _rp(cpx-0.2*scale, y+cyo), 'nc+', PLC, halign='right')
    
    d += elm.Line().at(_rp(x-r/2, y-cyo)).to(_rp(cpx, y-cyo)).color(SC)
    _draw_pin_marker_helper(d, *_rp(cpx, y-cyo), ps, PMC)
    _label(d, _rp(cpx-0.2*scale, y-cyo), 'nc-', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by), 'nc+': _rp(cpx, y + cyo), 'nc-': _rp(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, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    d += elm.Line().at(_rp(x, y+r)).to(_rp(x+r, y)).color(SC)
    d += elm.Line().at(_rp(x+r, y)).to(_rp(x, y-r)).color(SC)
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x-r, y)).color(SC)
    d += elm.Line().at(_rp(x-r, y)).to(_rp(x, y+r)).color(SC)
    
    _draw_polarity_markers(d, x, y+r*0.3, y-r*0.3, r*0.15, SC, rotation=rotation, cx=x, cy=y)
    
    ty, by = y+r+ll, y-r-ll
    d += elm.Line().at(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, ep, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(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, rotation=0, **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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    d += elm.Line().at(_rp(x, y+r)).to(_rp(x+r, y)).color(SC)
    d += elm.Line().at(_rp(x+r, y)).to(_rp(x, y-r)).color(SC)
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x-r, y)).color(SC)
    d += elm.Line().at(_rp(x-r, y)).to(_rp(x, y+r)).color(SC)
    
    d += elm.Line().at(_rp(x, y+r*0.4)).to(_rp(x, y-r*0.4)).color(SC)
    hs = 0.12*scale
    d += elm.Line().at(_rp(x-hs/2, y-r*0.4+hs)).to(_rp(x, y-r*0.4)).color(SC)
    d += elm.Line().at(_rp(x+hs/2, y-r*0.4+hs)).to(_rp(x, y-r*0.4)).color(SC)
    
    ty, by = y+r+ll, y-r-ll
    d += elm.Line().at(_rp(x, y+r)).to(_rp(x, ty)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, ty), ps, PMC)
    _label(d, _rp(x-0.3*scale, ty), 'p', PLC, halign='right')
    
    d += elm.Line().at(_rp(x, y-r)).to(_rp(x, by)).color(SC)
    _draw_pin_marker_helper(d, *_rp(x, by), ps, PMC)
    _label(d, _rp(x-0.3*scale, by), 'n', PLC, halign='right')
    
    lx, ly = x+r+lxo, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, ep, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        
    return {'p': _rp(x, ty), 'n': _rp(x, by)}


def draw_nmos_symbol(d, x, y, name, scale=1.0, model="nmos", label=None, params=None, show_params=True, style='source', rotation=0, **kwargs):
    """Draw NMOS transistor symbol (Cadence-style 4-terminal)."""
    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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    # 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(_rp(gx-ll, y)).to(_rp(gx, y)).color(SC)
        _draw_pin_marker_helper(d, *_rp(gx-ll, y), ps, PMC)
        _label(d, _rp(gx-ll, y+0.2*scale), 'g', PLC, halign='center')
        d += elm.Line().at(_rp(gx, y-gh/2)).to(_rp(gx, y+gh/2)).color(SC)
        bx = x + bg
        d += elm.Line().at(_rp(bx, y+gh/2)).to(_rp(bx, y+0.1*scale)).color(SC)
        d += elm.Line().at(_rp(bx, y+gh/2)).to(_rp(bx+ll, y+gh/2)).color(SC)
        _draw_pin_marker_helper(d, *_rp(bx+ll, y+gh/2), ps, PMC)
        _label(d, _rp(bx+ll+0.2*scale, y+gh/2), 'd', PLC, halign='left')
        d += elm.Line().at(_rp(bx, y)).to(_rp(bx+ll, y)).color(SC)
        _draw_pin_marker_helper(d, *_rp(bx+ll, y), ps, PMC)
        _label(d, _rp(bx+ll+0.2*scale, y), 'b', PLC, halign='left')
        d += elm.Line().at(_rp(bx, y-gh/2)).to(_rp(bx, y-0.1*scale)).color(SC)
        d += elm.Line().at(_rp(bx, y-gh/2)).to(_rp(bx+ll, y-gh/2)).color(SC)
        _draw_pin_marker_helper(d, *_rp(bx+ll, y-gh/2), ps, PMC)
        _label(d, _rp(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(_rp(ax, ay)).to(_rp(ax+hs, ay+hs/2)).color(SC)
        d += elm.Line().at(_rp(ax, ay)).to(_rp(ax+hs, ay-hs/2)).color(SC)
        lx, ly = bx+ll+lxo, y
        _label(d, _rp(lx, ly + lys), name, IC, halign='left')
        if kwargs.get('show_type_label', True):
            _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        return {'d': _rp(bx+ll, y+gh/2), 'g': _rp(gx-ll, y), 's': _rp(bx+ll, y-gh/2), 'b': _rp(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, mid_y, bot_y = y + ch_height / 2, y, y - ch_height / 2

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

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

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

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

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

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

    # Arrow placement
    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(_rp(arrow_x - arrow_size, bot_y - arrow_size)).to(_rp(arrow_x, bot_y)).color(SC)
        d += elm.Line().at(_rp(arrow_x - arrow_size, bot_y + arrow_size)).to(_rp(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(_rp(arrow_x + arrow_size, mid_y - arrow_size)).to(_rp(arrow_x, mid_y)).color(SC)
        d += elm.Line().at(_rp(arrow_x + arrow_size, mid_y + arrow_size)).to(_rp(arrow_x, mid_y)).color(SC)

    # Labels
    lx, ly = body_x + lxo+0.5*scale, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)

    return {'d': _rp(stub_end_x, drain_y), 'g': _rp(gate_x - ll, y), 's': _rp(stub_end_x, source_y), 'b': _rp(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', rotation=0, **kwargs):
    """Draw PMOS transistor symbol (Cadence-style 4-terminal)."""
    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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    # 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(_rp(gx-ll, y)).to(_rp(gx-br*2, y)).color(SC)
        _draw_pin_marker_helper(d, *_rp(gx-ll, y), ps, PMC)
        _label(d, _rp(gx-ll, y+0.2*scale), 'g', PLC, halign='center')
        _draw_circle_base(d, *_rp(gx-br, y), br, SC, rotation=rotation)
        d += elm.Line().at(_rp(gx, y-gh/2)).to(_rp(gx, y+gh/2)).color(SC)
        bx = x + bg
        d += elm.Line().at(_rp(bx, y+gh/2)).to(_rp(bx, y+0.1*scale)).color(SC)
        d += elm.Line().at(_rp(bx, y+gh/2)).to(_rp(bx+ll, y+gh/2)).color(SC)
        _draw_pin_marker_helper(d, *_rp(bx+ll, y+gh/2), ps, PMC)
        _label(d, _rp(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(_rp(ax+hs, ay)).to(_rp(ax, ay+hs/2)).color(SC)
        d += elm.Line().at(_rp(ax+hs, ay)).to(_rp(ax, ay-hs/2)).color(SC)
        d += elm.Line().at(_rp(bx, y)).to(_rp(bx+ll, y)).color(SC)
        _draw_pin_marker_helper(d, *_rp(bx+ll, y), ps, PMC)
        _label(d, _rp(bx+ll+0.2*scale, y), 'b', PLC, halign='left')
        d += elm.Line().at(_rp(bx, y-gh/2)).to(_rp(bx, y-0.1*scale)).color(SC)
        d += elm.Line().at(_rp(bx, y-gh/2)).to(_rp(bx+ll, y-gh/2)).color(SC)
        _draw_pin_marker_helper(d, *_rp(bx+ll, y-gh/2), ps, PMC)
        _label(d, _rp(bx+ll+0.2*scale, y-gh/2), 'd', PLC, halign='left')
        lx, ly = bx+ll+lxo, y
        _label(d, _rp(lx, ly + lys), name, IC, halign='left')
        if kwargs.get('show_type_label', True):
            _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)
        return {'d': _rp(bx+ll, y-gh/2), 'g': _rp(gx-ll, y), 's': _rp(bx+ll, y+gh/2), 'b': _rp(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, mid_y, bot_y = y + ch_height / 2, y, y - ch_height / 2

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

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

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

    # Source: vertical lead up from top stub
    source_y = top_y + ll
    d += elm.Line().at(_rp(stub_end_x, top_y)).to(_rp(stub_end_x, source_y)).color(SC)
    _draw_pin_marker_helper(d, *_rp(stub_end_x, source_y), ps, PMC)
    _label(d, _rp(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(_rp(stub_end_x, bot_y)).to(_rp(stub_end_x, drain_y)).color(SC)
    _draw_pin_marker_helper(d, *_rp(stub_end_x, drain_y), ps, PMC)
    _label(d, _rp(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(_rp(stub_end_x, mid_y)).to(_rp(body_x, mid_y)).color(SC)
    _draw_pin_marker_helper(d, *_rp(body_x, y), ps, PMC)
    _label(d, _rp(body_x - 0.15*scale, y+0.15*scale), 'b', PLC, halign='left')

    # Arrow placement
    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(_rp(arrow_x + arrow_size, top_y - arrow_size)).to(_rp(arrow_x, top_y)).color(SC)
        d += elm.Line().at(_rp(arrow_x + arrow_size, top_y + arrow_size)).to(_rp(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(_rp(arrow_x - arrow_size, mid_y - arrow_size)).to(_rp(arrow_x, mid_y)).color(SC)
        d += elm.Line().at(_rp(arrow_x - arrow_size, mid_y + arrow_size)).to(_rp(arrow_x, mid_y)).color(SC)

    # Labels
    lx, ly = body_x + lxo+0.5*scale, y
    _label(d, _rp(lx, ly + lys), name, IC, halign='left')
    if kwargs.get('show_type_label', True):
        _label(d, _rp(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, x, y, params, scale=scale, start_offset=st, rotation=rotation, base_lx=lx, base_ly=ly)

    return {'d': _rp(stub_end_x, drain_y), 'g': _rp(gate_x - ll, y), 's': _rp(stub_end_x, source_y), 'b': _rp(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, rotation=0, image=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

    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    gp = group_ports_by_type(ports); tp, bp, lp, rp = gp[PortType.POWER], gp[PortType.GROUND], gp[PortType.INPUT]+gp[PortType.INOUT], gp[PortType.OUTPUT]+gp[PortType.UNKNOWN]
    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

    # Image background — rendered behind all other elements (zorder=0)
    if image:
        margin = 0.05 * scale
        d += _ImageBackground(image, l + margin, b + margin, (r - l) - 2*margin, (t - b) - 2*margin)

    d += elm.Line().at(_rp(l, t)).to(_rp(r, t)).color(SC)
    d += elm.Line().at(_rp(r, t)).to(_rp(r, b)).color(SC)
    d += elm.Line().at(_rp(r, b)).to(_rp(l, b)).color(SC)
    d += elm.Line().at(_rp(l, b)).to(_rp(l, t)).color(SC)
    
    _label(d, _rp(r-1.0*scale+lxo, t+lys/2), name, IC, halign='left')
    _label(d, _rp(l+tpad, t-tpad), label or model, MC, halign='left', valign='top')
    if show_params and params:
        _draw_parameter_labels(d, x, y, params, scale=scale, start_offset=1.0, rotation=rotation, base_lx=l+tpad, base_ly=t-tpad, valign='top')
    elif show_type_label: 
        _label(d, _rp(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(_rp(px, t)).to(_rp(px, py)).color(SC)
            _draw_pin_marker_helper(d, *_rp(px, py), ps, PMC)
            pos[pn]=_rp(px, py+ps)
            _label(d, _rp(px-0.5*scale, py+0.2*scale), pn, PLC, halign='right')
    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(_rp(px, b)).to(_rp(px, py)).color(SC)
            _draw_pin_marker_helper(d, *_rp(px, py), ps, PMC)
            pos[pn]=_rp(px, py-ps)
            _label(d, _rp(px-0.5*scale, py-0.2*scale), pn, PLC, halign='right')
    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(_rp(l, py)).to(_rp(px, py)).color(SC)
            _draw_pin_marker_helper(d, *_rp(px, py), ps, PMC)
            pos[pn]=_rp(px-ps, py)
            _label(d, _rp(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(_rp(r, py)).to(_rp(px, py)).color(SC)
            _draw_pin_marker_helper(d, *_rp(px, py), ps, PMC)
            pos[pn]=_rp(px+ps, py)
            _label(d, _rp(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', rotation: float = 0) -> 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
    
    def _rp(px, py): return _rotate_pt(px, py, x, y, rotation)

    pd = port_directions or {}
    # Convert explicit port_directions (e.g., {'inp': 'input'}) to PortType overrides
    # so group_ports_by_type places ports on the correct side.
    # Skip 'inout' (the default) — let pattern matching handle those (vdd→POWER, gnd→GROUND)
    direction_to_porttype = {
        'input': PortType.INPUT, 'output': PortType.OUTPUT,
        'power': PortType.POWER, 'ground': PortType.GROUND,
    }
    combined_overrides = {}
    for pn, direction in pd.items():
        pt = direction_to_porttype.get(direction)
        if pt is not None:
            combined_overrides[pn] = pt
    if port_overrides:
        combined_overrides.update(port_overrides)  # explicit overrides win
    gp = group_ports_by_type(ports, combined_overrides)
    lp, rp, tp, bp = gp[PortType.INPUT]+gp[PortType.INOUT], gp[PortType.OUTPUT]+gp[PortType.UNKNOWN], gp[PortType.POWER], gp[PortType.GROUND]
    lg = [g for g in [gp[PortType.INPUT], gp[PortType.INOUT]] if g]; ls = max(0, len(lg)-1)
    rg = [g for g in [gp[PortType.OUTPUT], gp[PortType.UNKNOWN]] if g]; rs = max(0, len(rg)-1)
    lh, rh, tw, bw = len(lp)*port_spacing+ls*separator_gap, len(rp)*port_spacing+rs*separator_gap, len(tp)*port_spacing, len(bp)*port_spacing
    cw = 0.14; psz = 0.10  # cw=char width estimate, psz=port marker half-size
    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))

    char_h_vert = 0.13  # approximate height per character in vertical text
    label_gap = 0.20    # minimum gap between adjacent text regions
    tb_label_inset = psz + 0.08  # top/bottom label anchor: just past the marker

    # --- Width: left_text + gap + cell_name + gap + right_text must fit ---
    left_text_w = (max(len(p) for p in lp) * cw + 0.25) if lp else 0
    right_text_w = (max(len(p) for p in rp) * cw + 0.25) if rp else 0
    cell_name_w = len(cell_name) * cw + 0.3
    min_width_from_text = left_text_w + label_gap + cell_name_w + label_gap + right_text_w
    width = max(width, min_width_from_text)

    # --- Height: top_vert_text + gap + cell_name + gap + bottom_vert_text must fit ---
    top_text_h = (max(len(p) for p in tp) * char_h_vert + tb_label_inset) if tp else 0
    bot_text_h = (max(len(p) for p in bp) * char_h_vert + tb_label_inset) if bp else 0
    cell_name_h = 0.40  # approximate cell name height
    min_height_from_text = top_text_h + label_gap + cell_name_h + label_gap + bot_text_h
    height = max(height, min_height_from_text)

    # --- Corner: side ports block must not vertically overlap with top/bottom vert text ---
    if (tp or bp) and (lp or rp):
        side_ports_n = max(len(lp), len(rp))
        side_block_half_h = side_ports_n * port_spacing / 2
        max_vert_reach = max(top_text_h, bot_text_h)
        min_half_h = side_block_half_h + max_vert_reach + label_gap
        height = max(height, min_half_h * 2)

    # --- Iterative collision-based box resizing ---
    # Simulate label bounding boxes, detect overlaps, grow box proportionally.
    # Growing box by dw moves each edge by dw/2, so scale_factor=2.2 (2x + margin).
    # Use conservative estimates for collision detection (larger than analytical sizing)
    # so real overlaps are caught even if analytical estimates are tight.
    _cw_est, _ch_est = 0.14, 0.18  # bbox estimation: wider chars, taller lines
    _ch_vert_est = max(char_h_vert, 0.16)  # vertical text: at least 0.16 per char
    for _resize_iter in range(5):
        _l, _r, _t, _b = x - width/2, x + width/2, y + height/2, y - height/2
        _sim = []  # (x0, y0, x1, y1, side)
        # Cell name at center
        _cn_w = len(cell_name) * _cw_est
        _sim.append((x - _cn_w/2, y - _ch_est/2, x + _cn_w/2, y + _ch_est/2, 'center'))
        # Left port labels
        if lp:
            _cy = y + (len(lp) * port_spacing + ls * separator_gap) / 2 - port_spacing / 2
            for _gi, _g in enumerate(lg):
                if _gi > 0: _cy -= separator_gap
                for _pn in _g:
                    _pw = len(_pn) * _cw_est
                    _sim.append((_l + 0.05, _cy - _ch_est/2, _l + 0.05 + _pw, _cy + _ch_est/2, 'left'))
                    _cy -= port_spacing
        # Right port labels
        if rp:
            _sy = y + (len(rp) * port_spacing + rs * separator_gap) / 2 - port_spacing / 2
            for _ri, _pn in enumerate(rp):
                _py = _sy - _ri * port_spacing
                _pw = len(_pn) * _cw_est
                _sim.append((_r - 0.25 - _pw, _py - _ch_est/2, _r - 0.25, _py + _ch_est/2, 'right'))
        # Top port labels (vertical text)
        if tp:
            _sx = x - (len(tp) - 1) * port_spacing / 2
            for _ti, _pn in enumerate(tp):
                _px = _sx + _ti * port_spacing
                _vw, _vh = _cw_est + 0.05, len(_pn) * _ch_vert_est
                _sim.append((_px - _vw/2, _t - tb_label_inset - _vh, _px + _vw/2, _t - tb_label_inset, 'top'))
        # Bottom port labels (vertical text)
        if bp:
            _sx = x - (len(bp) - 1) * port_spacing / 2
            for _bi, _pn in enumerate(bp):
                _px = _sx + _bi * port_spacing
                _vw, _vh = _cw_est + 0.05, len(_pn) * _ch_vert_est
                _sim.append((_px - _vw/2, _b + tb_label_inset, _px + _vw/2, _b + tb_label_inset + _vh, 'bottom'))
        # Detect overlaps and compute needed growth per axis
        _dw, _dh = 0.0, 0.0
        for _i in range(len(_sim)):
            for _j in range(_i + 1, len(_sim)):
                _ax0, _ay0, _ax1, _ay1, _as = _sim[_i]
                _bx0, _by0, _bx1, _by1, _bs = _sim[_j]
                _ox = max(0, min(_ax1, _bx1) - max(_ax0, _bx0))
                _oy = max(0, min(_ay1, _by1) - max(_ay0, _by0))
                if _ox > 0 and _oy > 0:
                    # Top/bottom port involved → grow height; otherwise → grow width
                    if _as in ('top', 'bottom') or _bs in ('top', 'bottom'):
                        _dh = max(_dh, _oy)
                    else:
                        _dw = max(_dw, _ox)
        if _dw == 0 and _dh == 0:
            break
        width += _dw * 2.2
        height += _dh * 2.2
    l, r, t, b = x-width/2, x+width/2, y+height/2, y-height/2
    
    d += elm.Line().at(_rp(l,t)).to(_rp(r,t)).color(BC)
    d += elm.Line().at(_rp(r,t)).to(_rp(r,b)).color(BC)
    d += elm.Line().at(_rp(r,b)).to(_rp(l,b)).color(BC)
    d += elm.Line().at(_rp(l,b)).to(_rp(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, _rp(ix, iy), instance_name, IC, halign='left')
        
    _label(d, _rp(x, y), cell_name, CC); pos = {}
    def dpmark(drawing, px, py, pn, side='left'):
        _draw_direction_marker(drawing, *_rp(px, py), psz, PMC, pd.get(pn, 'inout'), side)

    if lp:
        # Center left ports vertically, accounting for separator gaps between groups
        total_left_h = len(lp) * port_spacing + ls * separator_gap
        cy = y + total_left_h / 2 - port_spacing / 2
        for idx, g in enumerate(lg):
            if idx>0: d += elm.Line().at(_rp(l+0.1, cy-separator_gap/2)).to(_rp(l+0.4, cy-separator_gap/2)).color('#AAAAAA'); cy-=separator_gap
            for pn in g:
                dpmark(d, l, cy, pn, 'left')
                _label(d, _rp(l+0.05, cy+0.09), pn, PC, halign='left', valign='center')
                pos[pn]=_rp(l-psz, cy)
                cy-=port_spacing
    if rp:
        # Center right ports vertically
        total_right_h = len(rp) * port_spacing + rs * separator_gap
        sy = y + total_right_h / 2 - port_spacing / 2
        for i, pn in enumerate(rp):
            py = sy-i*port_spacing
            dpmark(d, r, py, pn, 'right')
            _label(d, _rp(r-0.25, py-0.1), pn, PC, halign='right', valign='center')
            pos[pn]=_rp(r+psz, py)
    # Top/bottom port label inset: place anchor just inside the edge, text grows inward
    tb_label_inset = psz + 0.08  # just past the marker
    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')
            # halign='right' in rotated frame → text extends downward from anchor
            _label(d, _rp(px-0.12, t-tb_label_inset), pn, PC, halign='right', rotation='vertical')
            pos[pn]=_rp(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')
            # halign='left' in rotated frame → text extends upward from anchor
            _label(d, _rp(px-0.12, b+tb_label_inset+0.1), pn, PC, halign='left', rotation='vertical')
            pos[pn]=_rp(px, b-psz)

    # Debug collision validation (should be resolved by iterative resizing above)
    if True:  # always validate; only emits warnings
        import warnings as _w
        _dbg_sim = []
        _cn_w = len(cell_name) * 0.14
        _dbg_sim.append((x - _cn_w/2, y - 0.09, x + _cn_w/2, y + 0.09, f'cell:{cell_name}'))
        if lp:
            _cy = y + (len(lp)*port_spacing+ls*separator_gap)/2 - port_spacing/2
            for _gi, _g in enumerate(lg):
                if _gi > 0: _cy -= separator_gap
                for _pn in _g:
                    _pw = len(_pn)*0.14
                    _dbg_sim.append((l+0.05, _cy-0.09, l+0.05+_pw, _cy+0.09, f'port:{_pn}'))
                    _cy -= port_spacing
        if rp:
            _sy = y + (len(rp)*port_spacing+rs*separator_gap)/2 - port_spacing/2
            for _ri, _pn in enumerate(rp):
                _py = _sy - _ri*port_spacing
                _pw = len(_pn)*0.14
                _dbg_sim.append((r-0.25-_pw, _py-0.09, r-0.25, _py+0.09, f'port:{_pn}'))
        if tp:
            _sx = x-(len(tp)-1)*port_spacing/2
            for _ti, _pn in enumerate(tp):
                _px = _sx+_ti*port_spacing
                _vw, _vh = 0.19, len(_pn)*0.16
                _dbg_sim.append((_px-_vw/2, t-tb_label_inset-_vh, _px+_vw/2, t-tb_label_inset, f'port:{_pn}'))
        if bp:
            _sx = x-(len(bp)-1)*port_spacing/2
            for _bi, _pn in enumerate(bp):
                _px = _sx+_bi*port_spacing
                _vw, _vh = 0.19, len(_pn)*0.16
                _dbg_sim.append((_px-_vw/2, b+tb_label_inset, _px+_vw/2, b+tb_label_inset+_vh, f'port:{_pn}'))
        for _i in range(len(_dbg_sim)):
            for _j in range(_i+1, len(_dbg_sim)):
                _ax0, _ay0, _ax1, _ay1, _al = _dbg_sim[_i]
                _bx0, _by0, _bx1, _by1, _bl = _dbg_sim[_j]
                _ox = max(0, min(_ax1, _bx1) - max(_ax0, _bx0))
                _oy = max(0, min(_ay1, _by1) - max(_ay0, _by0))
                if _ox > 0 and _oy > 0:
                    _w.warn(f"[symbol] residual collision: {_al!r} <-> {_bl!r} (overlap={_ox*_oy:.4f})")

    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, rotation=0) -> Tuple[callable, dict, str]:
    renderer = get_default_renderer()
    # Use _type_name (canonical constructor arg) for SYMBOL_CONFIGS lookup and display labels
    # so that auto-renamed subcircuits (collision avoidance) still resolve correctly.
    type_name = getattr(cell, '_type_name', None) if cell else None
    sk = type_name or (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'] = sk; fa['ports'] = cell.ports if hasattr(cell, 'ports') else []; fa['params'] = params
        if cfg.image: fa['image'] = cfg.image  # image background for box only
    else:
        fa['params'] = params; fa['model'] = sk
    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
    fa['rotation'] = rotation
    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(cell, title=None, scale=1.0, compact=False, collapse_bus: bool = True,
                         selection_outline: str = None, show_bboxes: bool = False) -> Drawing:
    """
    Create a stable, label-based schematic view for a cell.

    Args:
        cell: A Circuit object representing the cell definition.
        collapse_bus: If True, group bus-patterned instances into collapsed symbols.
        selection_outline: Draw selection outline around each instance.
            - 'bbox': Simple dotted rectangle bounding box
            - 'irregular': Dotted outline following actual symbol shape
            - None: No outline (for comparison)
        show_bboxes: If True, draw dotted bounding boxes around all text elements
            for debugging layout collisions.
    """
    circuit = cell  # internal alias for brevity
    global _show_bboxes_drawing, _bbox_tracker, _current_layout, _current_instance
    renderer = get_default_renderer()
    with schemdraw.Drawing() as d:
        if show_bboxes:
            _show_bboxes_drawing = d
            _bbox_tracker = BBoxTracker(scale=scale)

        # Build per-instance SchematicLayout
        from .config import SchematicLayout, InstanceLayout, PortPlacement, LabelPlacement
        layout = SchematicLayout(circuit_name=circuit.name if hasattr(circuit, 'name') else '')
        _current_layout = layout
        _current_instance = None

        d.config(unit=LAYOUT.DRAWING_UNIT, fontsize=LAYOUT.SCHEMATIC_FONTSIZE)

        # Smart collision detection for text elements
        bbox_tracker = BBoxTracker(scale=scale)

        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 = {}

        # Collapse bus-patterned instances
        if collapse_bus:
            instances = group_bus_instances(list(instances))

        # 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):
            # Handle BusGroup items
            if isinstance(inst, BusGroup):
                kind = inst.subcircuit.name if inst.subcircuit else 'unknown'
                char_width = LAYOUT.CHAR_WIDTH * sc
                text_padding = LAYOUT.BOX_TEXT_PADDING * sc
                text_lines = [inst.display_name, kind]
                if inst.params:
                    for k, v in inst.params.items():
                        text_lines.append(f"{k} = {v}")
                for port_name, nets in inst.port_nets.items():
                    compressed = compress_net_names(nets)
                    text_lines.append(compressed)
                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)
                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

            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 = 4
        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 []
        input_ports = [p for p in ports if infer_port_type(p) == PortType.INPUT] if ports else []
        output_ports = [p for p in ports if infer_port_type(p) == PortType.OUTPUT] if ports else []
        inout_ports = [p for p in ports if infer_port_type(p) in (PortType.INOUT, PortType.UNKNOWN)] if ports else []

        # Calculate header row count (left column: power + input + inout, right column: ground + output)
        left_ports = power_ports + input_ports + inout_ports
        right_ports = ground_ports + output_ports
        left_row_count = len(left_ports)
        right_row_count = len(right_ports)
        header_rows = max(left_row_count, right_row_count, 1)
        header_height = header_rows * pin_y_spacing

        # Header starts above circuit
        # Use fixed top-down layout for consistency across all schematic views
        circuit_top = 10.0 * scale
        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):
            if isinstance(inst, BusGroup):
                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.display_name] = (cx, cy)
            else:
                pos = getattr(inst, 'schematic_position', None)
                if pos is not None:
                    if isinstance(pos, dict):
                        if 'relative_to' in pos:
                            relative_positions[inst.name] = pos
                        else:
                            # Self-offset: nudge from auto grid position
                            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 + pos.get('x_shift', 0),
                                cy + pos.get('y_shift', 0),
                            )
                    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)

        _resolve_relative_positions(relative_positions, instance_positions)

        # Recompute header position based on actual topmost instance (not fixed circuit_top)
        if instance_positions:
            actual_circuit_top = -float('inf')
            for inst in instances:
                key = inst.display_name if isinstance(inst, BusGroup) else inst.name
                if key in instance_positions:
                    _, cy = instance_positions[key]
                    _, bh, _ = estimate_symbol_size(inst, scale)
                    actual_circuit_top = max(actual_circuit_top, cy + bh / 2)
            actual_circuit_top += 0.5 * scale  # padding above topmost symbol edge
            header_bottom_y = actual_circuit_top + section_gap
            header_top_y = header_bottom_y + header_height

        # Calculate actual bounds from placed instances (not theoretical grid)
        all_x_coords = []
        for inst in instances:
            key = inst.display_name if isinstance(inst, BusGroup) else inst.name
            if key in instance_positions:
                cx_i, _ = instance_positions[key]
                bw_i, _, extra_i = estimate_symbol_size(inst, scale)
                all_x_coords.append(cx_i - bw_i / 2)
                all_x_coords.append(cx_i + bw_i / 2 + extra_i)
        if not all_x_coords:
            all_x_coords = [start_x, start_x + col_spacing]

        min_x_bound = min(all_x_coords)
        max_x_bound = max(all_x_coords)
        center_x = (min_x_bound + max_x_bound) / 2

        # Port column positions: outside actual circuit bounds with padding
        port_col_padding = 0.8 * scale
        left_col_x = min_x_bound - port_col_padding - marker_size
        middle_col_x = center_x
        right_col_x = max_x_bound + port_col_padding + marker_size

        # Cell name title removed — already shown in section heading

        # Draw left-column port markers (power, input, inout) stacked vertically
        header_port_points = set()  # Track header connection points to skip duplicate labels
        if left_ports:
            for i, port in enumerate(left_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_pos = (px + port_label_offset, py)
                _label(d, label_pos, port, PORT_COLOR, halign='left')
                layout.header_labels[port] = LabelPlacement(
                    text=port, pos=label_pos, anchor=(px, py),
                    halign='left', category='port_label')
                # Connection point is to the right of the marker
                conn_pt = (px + marker_size, py)
                net_connections.setdefault(port, []).append(conn_pt)
                header_port_points.add(conn_pt)

        # Draw right-column port markers (ground, output) stacked vertically
        if right_ports:
            for i, port in enumerate(right_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_pos = (px - port_label_offset, py)
                _label(d, label_pos, port, PORT_COLOR, halign='right')
                layout.header_labels[port] = LabelPlacement(
                    text=port, pos=label_pos, anchor=(px, py),
                    halign='right', category='port_label')
                # Connection point is to the left of the marker
                conn_pt = (px - marker_size, py)
                net_connections.setdefault(port, []).append(conn_pt)
                header_port_points.add(conn_pt)

        component_positions = {}
        bboxes = []
        for inst in instances:
            # Determine item key and position
            if isinstance(inst, BusGroup):
                item_key = inst.display_name
            else:
                item_key = inst.name
            cx, cy = instance_positions.get(item_key, (start_x, circuit_top))
            # Calculate bounding box for collision detection
            bw, bh, _ = estimate_symbol_size(inst, scale)
            bboxes.append((cx - bw/2, cx + bw/2, cy - bh/2, cy + bh/2))

            # Create InstanceLayout and set drawing context
            if not isinstance(inst, BusGroup):
                cell_name_str = inst.subcircuit.name if hasattr(inst, 'subcircuit') and inst.subcircuit else 'unknown'
                inst_layout = InstanceLayout(
                    instance_name=inst.name,
                    cell_name=cell_name_str,
                    center=(cx, cy),
                    symbol_bbox=(cx - bw/2, cy - bh/2, cx + bw/2, cy + bh/2),
                )
                layout.instances[inst.name] = inst_layout
                _current_instance = inst.name

            if isinstance(inst, BusGroup):
                _current_instance = None
                port_positions = draw_bus_symbol(
                    d, cx, cy, inst, scale=scale, rotation=0
                )
                component_positions[item_key] = port_positions
                # Map all individual nets to the shared port positions
                for port_name, nets in inst.port_nets.items():
                    if port_name not in port_positions:
                        continue
                    point = port_positions[port_name]
                    if math.isfinite(point[0]) and math.isfinite(point[1]):
                        for net in nets:
                            net_name = net.name if hasattr(net, 'name') else str(net)
                            net_connections.setdefault(net_name, []).append(point)
                            net_connections_by_port.setdefault(net_name, []).append(
                                (item_key, port_name, point))
            else:
                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,
                    'rotation': getattr(inst, 'rotation', 0)
                })
                port_pos_dict = draw_func(**func_args)
                component_positions[inst.name] = port_pos_dict

                # Record port pin positions into InstanceLayout
                for sym_port, pin_pos in port_pos_dict.items():
                    inst_layout.ports[sym_port] = PortPlacement(
                        port_name=sym_port, pin_pos=pin_pos)

                # Flush buffered pin labels now that ports are populated
                remaining = []
                for buf_inst, buf_text, buf_lp in _pending_pin_labels:
                    if buf_inst == inst.name and buf_text in inst_layout.ports:
                        if inst_layout.ports[buf_text].pin_label is None:
                            inst_layout.ports[buf_text].pin_label = buf_lp
                            continue
                    remaining.append((buf_inst, buf_text, buf_lp))
                _pending_pin_labels.clear()
                _pending_pin_labels.extend(remaining)

                if hasattr(inst, 'connections'):
                    type_name = getattr(inst.subcircuit, '_type_name', None) or (inst.subcircuit.name if inst.subcircuit else '')
                    port_map = get_default_renderer().get_config(type_name, inst.subcircuit).port_names or {}
                    unresolved = []
                    for port_name, net in inst.connections.items():
                        net_name = net.name if hasattr(net, 'name') else str(net)
                        symbol_port = port_map.get(port_name, port_name)
                        if inst.name in component_positions and symbol_port in component_positions[inst.name]:
                            point = component_positions[inst.name][symbol_port]
                            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))
                                # Record net name on port placement
                                if symbol_port in inst_layout.ports:
                                    inst_layout.ports[symbol_port].net_name = net_name
                        else:
                            unresolved.append((port_name, net_name))
                    if unresolved:
                        _warn_port_mismatch(type_name, unresolved, inst.name)
                        _draw_port_mismatch_warning(d, cx, cy, scale, unresolved)

        _current_instance = None  # Done drawing instances

        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 {}
        wired_net_vias = circuit.get_wired_net_vias() if hasattr(circuit, 'get_wired_net_vias') else {}
        wired_net_relative_vias = circuit.get_wired_net_relative_vias() if hasattr(circuit, 'get_wired_net_relative_vias') else {}

        # Resolve relative vias
        resolved_relative_vias = {}  # net -> list of (x, y)
        for net_name, rel_vias in wired_net_relative_vias.items():
            for rv in rel_vias:
                ref = rv.get('relative_to')
                if not ref: continue
                
                base_x, base_y = None, None
                if isinstance(ref, (list, tuple)) and len(ref) == 2:
                    inst_name, port_name = ref
                    if inst_name in component_positions and port_name in component_positions[inst_name]:
                        base_x, base_y = component_positions[inst_name][port_name]
                elif isinstance(ref, str):
                    if ref in instance_positions:
                        base_x, base_y = instance_positions[ref]
                
                if base_x is not None and base_y is not None:
                    final_x = base_x + rv.get('x_shift', 0)
                    final_y = base_y + rv.get('y_shift', 0)
                    resolved_relative_vias.setdefault(net_name, []).append((final_x, final_y))

        def draw_wire_for_points(points, drawing):
            """Draw wire connecting multiple points. Returns list of (x1,y1,x2,y2) segments drawn."""
            drawn_segments = []
            if len(points) < 2:
                return drawn_segments
            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)
            stub_tolerance = 0.1 * scale  # Only draw horizontal stub if offset exceeds this

            # Special case: exactly 2 points → direct L-shaped or vertical connection
            if len(sorted_points) == 2:
                (x1, y1), (x2, y2) = sorted_points  # top first (higher y)
                x_diff = abs(x1 - x2)
                if x_diff <= stub_tolerance:
                    # Nearly same x → straight vertical line through both pins
                    avg_x = (x1 + x2) / 2
                    drawing += elm.Line().at((avg_x, y1)).to((avg_x, y2)).color(WIRE_COLOR)
                    drawing += elm.Dot(radius=0.05 * scale).at((x1, y1)).color(WIRE_COLOR).fill(WIRE_COLOR)
                    drawing += elm.Dot(radius=0.05 * scale).at((x2, y2)).color(WIRE_COLOR).fill(WIRE_COLOR)
                    drawn_segments.append((avg_x, y1, avg_x, y2))
                else:
                    # Different x → L-shaped wire with pin-label clearance
                    # Route through outer x (rightmost) with offsets to avoid
                    # overlapping pin labels near the endpoints
                    pin_clearance = 0.25 * scale
                    outer_x = max(x1, x2)
                    trunk_x = outer_x + pin_clearance  # offset trunk right of pin labels

                    # Determine trunk y-extent
                    trunk_y_top = y1
                    trunk_y_bot = y2
                    for (px, py) in sorted_points:
                        if abs(px - outer_x) > stub_tolerance:
                            # This point needs a horizontal stub; offset its y
                            if py >= (y1 + y2) / 2:  # top half → offset upward
                                trunk_y_top = max(trunk_y_top, py + pin_clearance)
                            else:  # bottom half → offset downward
                                trunk_y_bot = min(trunk_y_bot, py - pin_clearance)

                    # Draw vertical trunk
                    drawing += elm.Line().at((trunk_x, trunk_y_top)).to((trunk_x, trunk_y_bot)).color(WIRE_COLOR)
                    drawn_segments.append((trunk_x, trunk_y_top, trunk_x, trunk_y_bot))

                    # Connect each pin to the trunk
                    for (px, py) in sorted_points:
                        drawing += elm.Dot(radius=0.05 * scale).at((px, py)).color(WIRE_COLOR).fill(WIRE_COLOR)
                        if abs(px - outer_x) > stub_tolerance:
                            # Non-aligned point: vertical stub + horizontal to trunk
                            if py >= (y1 + y2) / 2:
                                offset_y = py + pin_clearance
                            else:
                                offset_y = py - pin_clearance
                            drawing += elm.Line().at((px, py)).to((px, offset_y)).color(WIRE_COLOR)
                            drawing += elm.Line().at((px, offset_y)).to((trunk_x, offset_y)).color(WIRE_COLOR)
                            drawn_segments.append((px, py, px, offset_y))
                            drawn_segments.append((px, offset_y, trunk_x, offset_y))
                        else:
                            # At outer_x: short horizontal stub to reach offset trunk
                            drawing += elm.Line().at((px, py)).to((trunk_x, py)).color(WIRE_COLOR)
                            drawn_segments.append((px, py, trunk_x, py))
                return drawn_segments

            # 3+ points: trunk-based routing
            same_column = (x_max_wire - x_min_wire) < 0.5 * scale

            if same_column:
                avg_x = sum(x_coords) / len(x_coords)
                top_y = sorted_points[0][1]
                bottom_y = sorted_points[-1][1]

                # Check for symbol collisions (only if enabled in config)
                trunk_segment = (avg_x, top_y, avg_x, bottom_y)
                if LAYOUT.SCHEMATIC_SMART_ROUTING and _segments_hit_boxes([trunk_segment], bboxes, scale):
                    # Collision detected! Route through side channel
                    min_l = min(b[0] for b in bboxes)
                    dm = LAYOUT.WIRE_SYMBOL_MARGIN * scale + 0.6 * scale
                    spx = min_l - dm

                    drawing += elm.Line().at((spx, top_y)).to((spx, bottom_y)).color(WIRE_COLOR)
                    drawn_segments.append((spx, top_y, spx, bottom_y))
                    for (px, py) in sorted_points:
                        drawing += elm.Dot(radius=0.05 * scale).at((spx, py)).color(WIRE_COLOR).fill(WIRE_COLOR)
                        # Connect each point to the side channel trunk
                        drawing += elm.Line().at((px, py)).to((spx, py)).color(WIRE_COLOR)
                        drawing += elm.Dot(radius=0.05 * scale).at((px, py)).color(WIRE_COLOR).fill(WIRE_COLOR)
                        drawn_segments.append((px, py, spx, py))
                else:
                    # Default: No collision check or disabled, draw direct vertical trunk
                    drawing += elm.Line().at((avg_x, top_y)).to((avg_x, bottom_y)).color(WIRE_COLOR)
                    drawn_segments.append((avg_x, top_y, avg_x, bottom_y))
                    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)
                            drawn_segments.append((px, py, avg_x, py))
            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)
                drawn_segments.append((trunk_x, top_y, trunk_x, bottom_y))

                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)
                        drawn_segments.append((trunk_x, py, px, py))
                    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)

            return drawn_segments

        def draw_context_label(point, name, inst_name=None, port_name=None):
            px, py = point
            # Try to find instance center for context
            cx = None
            if inst_name and inst_name in instance_positions:
                cx, _ = instance_positions[inst_name]

            # Simple logic: if pin is left of instance center, place label on left
            if cx is not None and px < cx - 0.05 * scale:
                lx = px - LAYOUT.NET_LABEL_OFFSET * scale
                halign = 'right'
            else:
                lx = px + LAYOUT.NET_LABEL_OFFSET * scale
                halign = 'left'
            label_pos = (lx, py + LAYOUT.NET_LABEL_OFFSET_Y * scale)
            _net_label(d, label_pos, name, NET_LABEL_COLOR, halign=halign)

            # Record net label into the InstanceLayout
            if inst_name and inst_name in layout.instances:
                inst_lo = layout.instances[inst_name]
                net_lp = LabelPlacement(
                    text=name, pos=label_pos, anchor=point,
                    halign=halign, fontsize=LAYOUT.NET_LABEL_FONTSIZE,
                    category='net_label',
                )
                # Find the port on this instance that connects to this net
                if port_name and port_name in inst_lo.ports:
                    inst_lo.ports[port_name].net_label = net_lp
                else:
                    # Try to find port by net name match
                    for pn, pp in inst_lo.ports.items():
                        if pp.net_name == name and pp.net_label is None:
                            pp.net_label = net_lp
                            break

        if connection_style == 'wires':
            for net_name, points in net_connections.items():
                # Add manual via points to the point set
                all_points = list(points)
                if net_name in wired_net_vias:
                    all_points.extend(wired_net_vias[net_name])
                if net_name in resolved_relative_vias:
                    all_points.extend(resolved_relative_vias[net_name])
                segs = draw_wire_for_points(all_points, d)
                layout.wire_segments.extend(segs)
        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:
                        # Use only instance pin points (exclude header port connection points)
                        inst_points = set()
                        for iname, pname, pcoord in net_connections_by_port.get(net_name, []):
                            inst_points.add(pcoord)
                        selected = [p for p in points if p in inst_points] or list(points)

                    # Add manual via points to the point set
                    if net_name in wired_net_vias:
                        selected.extend(wired_net_vias[net_name])
                    if net_name in resolved_relative_vias:
                        selected.extend(resolved_relative_vias[net_name])

                    if len(selected) >= 2:
                        segs = draw_wire_for_points(selected, d)
                        layout.wire_segments.extend(segs)
                        # Add net label on the wire (at trunk position)
                        sorted_sel = sorted(selected, key=lambda p: -p[1])
                        mid_y = (sorted_sel[0][1] + sorted_sel[-1][1]) / 2
                        # Use the actual trunk x: for L-shape with clearance,
                        # the trunk is offset right of max pin x
                        sel_x = [p[0] for p in sorted_sel]
                        x_diff = max(sel_x) - min(sel_x)
                        if x_diff > 0.1 * scale:
                            wire_x = max(sel_x) + 0.25 * scale  # trunk with clearance
                        else:
                            wire_x = sum(sel_x) / len(sel_x)  # vertical wire at avg x
                        wire_label_pos = (wire_x + LAYOUT.NET_LABEL_OFFSET * scale, mid_y + LAYOUT.NET_LABEL_OFFSET_Y * scale)
                        _net_label(d, wire_label_pos, net_name, NET_LABEL_COLOR, halign='left')

                        # Record wire label into SchematicLayout
                        from .config import WireLabelPlacement
                        connected_ports = [(iname, pname)
                                           for iname, pname, _ in net_connections_by_port.get(net_name, [])]
                        layout.wire_labels.append(WireLabelPlacement(
                            label=LabelPlacement(
                                text=net_name, pos=wire_label_pos,
                                anchor=(wire_x, mid_y),
                                halign='left', fontsize=LAYOUT.NET_LABEL_FONTSIZE,
                                category='net_label',
                            ),
                            net_name=net_name,
                            connected_ports=connected_ports,
                        ))
                        # Also label non-wired points (like port markers)
                        selected_set = set(selected)
                        # We need to map points back to instances for context labels
                        points_to_inst = {}
                        points_to_port = {}
                        for iname, pname, pcoord in net_connections_by_port.get(net_name, []):
                            points_to_inst[pcoord] = iname
                            points_to_port[pcoord] = pname

                        for p in points:
                            if p in selected_set or p in header_port_points:
                                continue
                            draw_context_label(p, net_name, points_to_inst.get(p), points_to_port.get(p))
                        continue

                # Default: label all points for this net
                points_to_inst = {}
                points_to_port = {}
                for iname, pname, pcoord in net_connections_by_port.get(net_name, []):
                    points_to_inst[pcoord] = iname
                    points_to_port[pcoord] = pname
                for p in points:
                    if p in header_port_points:
                        continue
                    draw_context_label(p, net_name, points_to_inst.get(p), points_to_port.get(p))

        # Collect all drawn element positions (instances + ports + vias) for axes bounds
        all_x = []
        all_y = []
        
        # Include full extent of all instance bounding boxes
        for l, r, b, t in bboxes:
            all_x.extend([l, r])
            all_y.extend([b, t])

        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)
        # Include via points in bounds
        for points in wired_net_vias.values():
            for (vx, vy) in points:
                if math.isfinite(vx) and math.isfinite(vy):
                    all_x.append(vx)
                    all_y.append(vy)
        for points in resolved_relative_vias.values():
            for (vx, vy) in points:
                if math.isfinite(vx) and math.isfinite(vy):
                    all_x.append(vx)
                    all_y.append(vy)

        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)

        # Store click-target metadata for SVG interactivity
        inst_targets = {}
        for inst in instances:
            if isinstance(inst, BusGroup):
                continue
            cx, cy = instance_positions.get(inst.name, (0, 0))
            bw, bh, _ = estimate_symbol_size(inst, scale)
            inst_targets[inst.name] = (cx, cy, bw, bh)
        d._analogpy_inst_targets = inst_targets
        d._analogpy_net_names = set(net_connections.keys())

        # Store bbox records for the SVG renderer to draw overlays
        if show_bboxes and _bbox_records:
            d._analogpy_bbox_records = list(_bbox_records)

        # Store the per-instance SchematicLayout on the drawing
        d._analogpy_schematic_layout = layout

        # Draw selection outlines around each instance if requested
        if selection_outline:
            for inst in instances:
                if isinstance(inst, BusGroup):
                    continue
                cx, cy = instance_positions.get(inst.name, (0, 0))
                bw, bh, _ = estimate_symbol_size(inst, scale)

                if selection_outline == 'bbox':
                    # Simple dotted rectangle bbox
                    l, r = cx - bw/2, cx + bw/2
                    t, b = cy + bh/2, cy - bh/2
                    # Draw 4 lines to form rectangle
                    d += elm.Line().at((l, b)).to((r, b)).color(COLORS.SYMBOL_OUTLINE).linestyle('dotted')
                    d += elm.Line().at((r, b)).to((r, t)).color(COLORS.SYMBOL_OUTLINE).linestyle('dotted')
                    d += elm.Line().at((r, t)).to((l, t)).color(COLORS.SYMBOL_OUTLINE).linestyle('dotted')
                    d += elm.Line().at((l, t)).to((l, b)).color(COLORS.SYMBOL_OUTLINE).linestyle('dotted')
                elif selection_outline == 'irregular':
                    # Draw dotted outline around symbol shape
                    # Use instance bbox with padding for now (simpler than hull)
                    padding = 0.15 * scale
                    l, r = cx - bw/2 - padding, cx + bw/2 + padding
                    t, b = cy + bh/2 + padding, cy - bh/2 - padding
                    # Draw irregular-ish outline with slight variations
                    d += elm.Line().at((l, b)).to((r, b)).color(COLORS.SYMBOL_OUTLINE).linestyle('dotted')
                    d += elm.Line().at((r, b)).to((r, t)).color(COLORS.SYMBOL_OUTLINE).linestyle('dotted')
                    d += elm.Line().at((r, t)).to((l, t)).color(COLORS.SYMBOL_OUTLINE).linestyle('dotted')
                    d += elm.Line().at((l, t)).to((l, b)).color(COLORS.SYMBOL_OUTLINE).linestyle('dotted')

    # Report collisions if tracking was enabled
    if _bbox_tracker is not None:
        collisions = _bbox_tracker.find_all_collisions()
        if collisions:
            d._analogpy_collisions = collisions

    _show_bboxes_drawing = None
    _bbox_tracker = None
    _current_layout = None
    _current_instance = None
    _bbox_records.clear()
    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)),
            image=cfg.get("image"),
        )
        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=LAYOUT.SCHEMATIC_FONTSIZE)
        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, collapse_bus: bool = True) -> Drawing:
    """
    Create a block diagram for a testbench showing subcircuit instances.
    Uses proper cell symbols for each instance.

    Args:
        collapse_bus: If True, group bus-patterned instances into collapsed symbols.
    """
    with schemdraw.Drawing() as d:
        d.config(unit=LAYOUT.DRAWING_UNIT, fontsize=LAYOUT.SCHEMATIC_FONTSIZE)

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

        # Group primitives together at the start, followed by ground, then the rest
        primitives = [inst for inst in instances_raw if getattr(inst.subcircuit, '_is_primitive', False)]
        cells = [inst for inst in instances_raw if not getattr(inst.subcircuit, '_is_primitive', False)]

        # Collapse bus-patterned instances into BusGroup objects
        if collapse_bus:
            primitives = group_bus_instances(primitives)
            cells = group_bus_instances(cells)

        # Ordered items to display (None represents the ground symbol)
        ordered_items = primitives + [None] + cells
        total_items = len(ordered_items)

        def estimate_symbol_size(inst, sc=1.0):
            if inst is None:  # Ground symbol
                return 1.0 * sc, 1.0 * sc, 0.5 * sc

            # Handle BusGroup items
            if isinstance(inst, BusGroup):
                kind = inst.subcircuit.name if inst.subcircuit else 'unknown'
                char_width = LAYOUT.CHAR_WIDTH * sc
                text_padding = LAYOUT.BOX_TEXT_PADDING * sc
                text_lines = [inst.display_name, kind]
                if inst.params:
                    for k, v in inst.params.items():
                        text_lines.append(f"{k} = {v}")
                # Add compressed net label lengths
                for port_name, nets in inst.port_nets.items():
                    compressed = compress_net_names(nets)
                    text_lines.append(compressed)
                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)
                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

            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 item in ordered_items:
            w, h, extra = estimate_symbol_size(item, 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 = 4

        # Calculate rows
        num_rows = max(1, (total_items - 1) // instances_per_row + 1)
        start_x = 2.0 * scale
        
        # Use fixed top-down layout to ensure consistent Y-coordinates across formats
        # regardless of row count or symbol style
        y_inst_start = 10.0 * scale

        if not math.isfinite(start_x): start_x = 2.0 * scale
        if not math.isfinite(y_inst_start): y_inst_start = 10.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 >= total_items:
                    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))

        net_connections = {}
        net_connections_by_port = {}  # net -> list of (inst_name, port_name, (x, y))
        component_positions = {}

        # Two-pass layout logic to support relative positioning
        instance_positions = {}
        relative_positions = {}

        for i, item in enumerate(ordered_items):
            row = i // instances_per_row
            col = i % instances_per_row
            spacing_x = row_spacings[row] if row < len(row_spacings) else 3.0 * scale

            # Default grid position
            default_x = col * spacing_x + start_x
            default_y = y_inst_start - row * spacing_y

            if item is None:
                instance_positions["__ground__"] = (default_x, default_y)
                continue

            # Get item key: display_name for BusGroup, name for regular instances
            if isinstance(item, BusGroup):
                item_key = item.display_name
                instance_positions[item_key] = (default_x, default_y)
            else:
                item_key = item.name
                # Check if user provided explicit position
                pos = getattr(item, 'schematic_position', None)
                if pos is not None:
                    if isinstance(pos, dict):
                        if 'relative_to' in pos:
                            relative_positions[item_key] = pos
                        else:
                            # Self-offset: nudge from auto grid position
                            instance_positions[item_key] = (
                                default_x + pos.get('x_shift', 0),
                                default_y + pos.get('y_shift', 0),
                            )
                    else:
                        instance_positions[item_key] = pos
                else:
                    instance_positions[item_key] = (default_x, default_y)

        _resolve_relative_positions(relative_positions, instance_positions)

        for i, item in enumerate(ordered_items):
            # Resolve position — use .get() with row/col fallback so unresolved
            # relative_to references degrade to default grid position, not KeyError.
            row = i // instances_per_row
            col = i % instances_per_row
            spacing_x = row_spacings[row] if row < len(row_spacings) else 3.0 * scale
            fallback = (col * spacing_x + start_x, y_inst_start - row * spacing_y)
            if item is None:
                x, y = instance_positions.get("__ground__", fallback)
            elif isinstance(item, BusGroup):
                x, y = instance_positions.get(item.display_name, fallback)
            else:
                x, y = instance_positions.get(item.name, fallback)

            if not math.isfinite(x) or not math.isfinite(y):
                continue

            # Check if this is the ground symbol marker
            if item is None:
                d += elm.Ground().at((x, y))
                _net_label(d, (x + 0.5 * scale, y), "0", COLORS.NET_LABEL, halign='left')
                continue

            # Handle BusGroup: draw collapsed symbol
            if isinstance(item, BusGroup):
                port_positions = draw_bus_symbol(
                    d, x, y, item, scale=0.8 * scale,
                    rotation=0, style=style
                )
                component_positions[item.display_name] = port_positions

                # Map ALL individual nets from port_nets to the shared port positions
                for port_name, nets in item.port_nets.items():
                    if port_name not in port_positions:
                        continue
                    px, py = port_positions[port_name]
                    if math.isfinite(px) and math.isfinite(py):
                        for net in nets:
                            net_name = net.name if hasattr(net, 'name') else str(net)
                            net_connections.setdefault(net_name, []).append((px, py))
                            net_connections_by_port.setdefault(net_name, []).append(
                                (item.display_name, port_name, (px, py)))
                continue

            inst = item

            # 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,
                'rotation': getattr(inst, 'rotation', 0)
            })
            port_positions = draw_func(**func_args)
            component_positions[inst.name] = port_positions

            if hasattr(inst, 'connections'):
                type_name = getattr(inst.subcircuit, '_type_name', None) or (inst.subcircuit.name if inst.subcircuit else '')
                port_map = get_default_renderer().get_config(type_name, inst.subcircuit).port_names or {}
                unresolved = []
                for port_name, net in inst.connections.items():
                    net_name = net.name if hasattr(net, 'name') else str(net)
                    symbol_port = port_map.get(port_name, port_name)
                    if symbol_port not in port_positions:
                        unresolved.append((port_name, net_name))
                        continue
                    px, py = port_positions[symbol_port]
                    if math.isfinite(px) and math.isfinite(py):
                        net_connections.setdefault(net_name, []).append((px, py))
                        net_connections_by_port.setdefault(net_name, []).append((inst.name, port_name, (px, py)))
                if unresolved:
                    _warn_port_mismatch(type_name, unresolved, inst.name)
                    _draw_port_mismatch_warning(d, x, y, 0.8 * scale, unresolved)

        # Draw explicit wires if requested
        WIRE_COLOR = COLORS.WIRE
        NET_LABEL_COLOR = COLORS.NET_LABEL
        wired_nets = testbench.get_wired_nets() if hasattr(testbench, 'get_wired_nets') else []
        wired_net_ports = testbench.get_wired_net_ports() if hasattr(testbench, 'get_wired_net_ports') else {}
        wired_net_vias = testbench.get_wired_net_vias() if hasattr(testbench, 'get_wired_net_vias') else {}
        wired_net_relative_vias = testbench.get_wired_net_relative_vias() if hasattr(testbench, 'get_wired_net_relative_vias') else {}

        # Resolve relative vias
        resolved_relative_vias = {}  # net -> list of (x, y)
        for net_name, rel_vias in wired_net_relative_vias.items():
            for rv in rel_vias:
                ref = rv.get('relative_to')
                if not ref: continue
                
                base_x, base_y = None, None
                if isinstance(ref, (list, tuple)) and len(ref) == 2:
                    inst_name, port_name = ref
                    if inst_name in component_positions and port_name in component_positions[inst_name]:
                        base_x, base_y = component_positions[inst_name][port_name]
                elif isinstance(ref, str):
                    if ref in instance_positions:
                        base_x, base_y = instance_positions[ref]
                
                if base_x is not None and base_y is not None:
                    final_x = base_x + rv.get('x_shift', 0)
                    final_y = base_y + rv.get('y_shift', 0)
                    resolved_relative_vias.setdefault(net_name, []).append((final_x, final_y))

        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]
            y_coords = [p[1] for p in sorted_points]
            xmin, xmax = min(x_coords), max(x_coords)
            ymin, ymax = min(y_coords), max(y_coords)
            
            width = xmax - xmin
            height = ymax - ymin
            stub_tolerance = 0.1 * scale

            # Use Horizontal Bus if points are more spread out horizontally than vertically
            if width > height:
                # Find the Y-level with the most points (the "main" row)
                y_counts = {}
                for py in y_coords:
                    ry = round(py / (0.5 * scale)) * (0.5 * scale)
                    y_counts[ry] = y_counts.get(ry, 0) + 1
                bus_y = max(y_counts.keys(), key=lambda y: y_counts[y])
                
                # Draw the horizontal bus line
                drawing += elm.Line().at((xmin, bus_y)).to((xmax, bus_y)).color(WIRE_COLOR)
                
                for px, py in points:
                    # Vertical stub to bus
                    if abs(py - bus_y) > stub_tolerance:
                        drawing += elm.Line().at((px, py)).to((px, bus_y)).color(WIRE_COLOR)
                        drawing += elm.Dot(radius=0.05 * scale).at((px, bus_y)).color(WIRE_COLOR).fill(WIRE_COLOR)
                    drawing += elm.Dot(radius=0.05 * scale).at((px, py)).color(WIRE_COLOR).fill(WIRE_COLOR)
            else:
                # Vertical Bus (Standard Trunk)
                # Find the X-level with the most points (the "main" column)
                x_counts = {}
                for px in x_coords:
                    rx = round(px / (0.5 * scale)) * (0.5 * scale)
                    x_counts[rx] = x_counts.get(rx, 0) + 1
                bus_x = max(x_counts.keys(), key=lambda x: x_counts[x])
                
                # Draw the vertical bus line
                drawing += elm.Line().at((bus_x, ymax)).to((bus_x, ymin)).color(WIRE_COLOR)
                
                for px, py in points:
                    # Horizontal stub to bus
                    if abs(px - bus_x) > stub_tolerance:
                        drawing += elm.Line().at((px, py)).to((bus_x, py)).color(WIRE_COLOR)
                        drawing += elm.Dot(radius=0.05 * scale).at((bus_x, py)).color(WIRE_COLOR).fill(WIRE_COLOR)
                    drawing += elm.Dot(radius=0.05 * scale).at((px, py)).color(WIRE_COLOR).fill(WIRE_COLOR)

        def draw_context_label(point, name, inst_name=None):
            px, py = point
            # Try to find instance center for context
            cx, cy = None, None
            if inst_name and inst_name in instance_positions:
                cx, cy = instance_positions[inst_name]
            
            if cx is not None and px < cx - 0.05 * scale:
                lx = px - LAYOUT.NET_LABEL_OFFSET * scale*2
                halign = 'right'
            else:
                lx = px + LAYOUT.NET_LABEL_OFFSET * scale
                halign = 'left'
            _net_label(d, (lx, py + LAYOUT.NET_LABEL_OFFSET_Y * scale),
                       name, NET_LABEL_COLOR, halign=halign)

        # Draw wires for wired nets
        for net_name in wired_nets:
            points = []
            # Get points from 'only' or from all instances
            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:
                        points.append(point)
            else:
                points = list(net_connections.get(net_name, []))
            
            # Add vias
            if net_name in wired_net_vias:
                points.extend(wired_net_vias[net_name])
            if net_name in resolved_relative_vias:
                points.extend(resolved_relative_vias[net_name])

            if len(points) >= 2:
                draw_wire_for_points(points, d)
                # Add net label on the wire (at midpoint)
                sorted_sel = sorted(points, key=lambda p: -p[1])
                mid_y = (sorted_sel[0][1] + sorted_sel[-1][1]) / 2
                
                # Find bus/trunk coordinate
                x_coords = [p[0] for p in points]
                y_coords = [p[1] for p in points]
                width = max(x_coords) - min(x_coords)
                height = max(y_coords) - min(y_coords)
                
                if width > height: # Horizontal bus
                    y_counts = {}
                    for py in y_coords:
                        ry = round(py / (0.5 * scale)) * (0.5 * scale)
                        y_counts[ry] = y_counts.get(ry, 0) + 1
                    bus_y = max(y_counts.keys(), key=lambda y: y_counts[y])
                    _net_label(d, ((max(x_coords)+min(x_coords))/2, bus_y + LAYOUT.NET_LABEL_OFFSET_Y * scale), net_name, NET_LABEL_COLOR, halign='center', valign='bottom')
                else: # Vertical trunk
                    x_counts = {}
                    for px in x_coords:
                        rx = round(px / (0.5 * scale)) * (0.5 * scale)
                        x_counts[rx] = x_counts.get(rx, 0) + 1
                    bus_x = max(x_counts.keys(), key=lambda x: x_counts[x])
                    _net_label(d, (bus_x + LAYOUT.NET_LABEL_OFFSET * scale, mid_y + LAYOUT.NET_LABEL_OFFSET_Y * scale), net_name, NET_LABEL_COLOR, halign='left')

        # Draw labels for non-wired nets and non-wired ports of wired nets
        wired_nets_set = set(wired_nets)
        for net_name, points in net_connections.items():
            if net_name in wired_nets_set:
                # For wired nets, only label ports that were NOT part of the wire
                # (if selective 'only' was used)
                port_specs = wired_net_ports.get(net_name, [])
                if port_specs:
                    wired_points = set()
                    for inst_name, port_name in port_specs:
                        p = component_positions.get(inst_name, {}).get(port_name)
                        if p: wired_points.add(p)
                    
                    for inst_name, port_name, pcoord in net_connections_by_port.get(net_name, []):
                        if pcoord not in wired_points:
                            draw_context_label(pcoord, net_name, inst_name)
                continue
            
            # Default: label all ports for this net
            for inst_name, port_name, pcoord in net_connections_by_port.get(net_name, []):
                draw_context_label(pcoord, net_name, inst_name)

        max_row_width = max(row_widths) if row_widths else 0.0

        x_min = start_x - 2.5 * scale
        x_max = start_x + max_row_width + 4 * scale
        # Calculate y_min based on y_inst_start and number of rows
        y_min = y_inst_start - (num_rows - 1) * spacing_y - 2.5 * scale
        y_max = y_inst_start + 0.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)

        # Store click-target metadata for SVG interactivity
        inst_targets = {}
        for item in ordered_items:
            if item is None or isinstance(item, BusGroup):
                continue
            cx, cy = instance_positions.get(item.name, (0, 0))
            bw, bh, _ = estimate_symbol_size(item, scale)
            inst_targets[item.name] = (cx, cy, bw, bh)
        d._analogpy_inst_targets = inst_targets
        d._analogpy_net_names = set(net_connections.keys())

    return d


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


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=LAYOUT.DRAWING_UNIT, fontsize=LAYOUT.SCHEMATIC_FONTSIZE)
                _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=LAYOUT.DRAWING_UNIT, fontsize=LAYOUT.SCHEMATIC_FONTSIZE)
        # 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=LAYOUT.DRAWING_UNIT, fontsize=LAYOUT.SCHEMATIC_FONTSIZE); _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=LAYOUT.DRAWING_UNIT, fontsize=LAYOUT.SCHEMATIC_FONTSIZE)

        # 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=None, *, cell_name=None, ports=None, instance_name=None, port_overrides=None, port_directions=None, instance_label_pos='top-right', show_bboxes=False) -> Drawing:
    """Create a standalone cell symbol Drawing.

    Usage:
        create_cell_symbol_standalone(my_circuit)
        create_cell_symbol_standalone(my_circuit, show_bboxes=True)
        create_cell_symbol_standalone(cell_name="inv", ports=["inp", "out", "vdd", "vss"])

    When a cell (Circuit object) is provided, cell_name, ports, and
    port_directions are extracted automatically.
    """
    if cell is not None:
        if cell_name is None:
            cell_name = cell.name
        if ports is None:
            ports = cell.ports if hasattr(cell, 'ports') else []
        if port_directions is None:
            port_directions = cell.port_directions if hasattr(cell, 'port_directions') else {}
    if cell_name is None or ports is None:
        raise ValueError("Either 'cell' or both 'cell_name' and 'ports' must be provided")

    global _show_bboxes_drawing, _bbox_records, _bbox_tracker
    with schemdraw.Drawing() as d:
        d.config(unit=1, fontsize=LAYOUT.SCHEMATIC_FONTSIZE); num_ports = len(ports)
        sx, sy = 2.0 + num_ports * 0.15, 2.0 + num_ports * 0.15
        if show_bboxes:
            _show_bboxes_drawing = d
            _bbox_records = []
            if not hasattr(d, '_analogpy_bbox_records'):
                d._analogpy_bbox_records = _bbox_records
        port_pos = 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)
        if show_bboxes:
            # Compute axes from actual port positions to cover the whole symbol
            all_x = [sx] + [p[0] for p in port_pos.values()]
            all_y = [sy] + [p[1] for p in port_pos.values()]
            margin = 1.0
            _draw_coordinate_axes(d, min(all_x) - margin, max(all_x) + margin,
                                  min(all_y) - margin, max(all_y) + margin)
            d._analogpy_bbox_records = _bbox_records
            _show_bboxes_drawing = None
    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
