# 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.

"""
SVG export for analogpy schematics.

Generates SVG strings from Circuit and Testbench objects without
requiring reportlab or pypdf. Only needs schemdraw and matplotlib.

Usage:
    from analogpy.visualization.svg import render_schematic_svg

    svg_string = render_schematic_svg(my_circuit)

    # For testbench with all subcircuits:
    svg_dict = render_all_schematics_svg(my_testbench)
    # Returns: {"tb_name": "..svg..", "cell1": "..svg..", ...}
"""

import re
from typing import Dict, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from ..circuit import Circuit
    from ..testbench import Testbench


def _make_svg_responsive(svg: str) -> str:
    """Post-process SVG for responsive web embedding.

    - Strips XML declaration and DOCTYPE (not needed for inline SVG)
    - Replaces fixed width/height with width="100%" for responsive scaling
    - Keeps viewBox for proper aspect ratio
    """
    # Strip XML declaration and DOCTYPE
    svg = re.sub(r'<\?xml[^?]*\?>\s*', '', svg)
    svg = re.sub(r'<!DOCTYPE[^>]*>\s*', '', svg)

    # Replace fixed width/height with responsive width, remove height
    # to let viewBox control aspect ratio
    svg = re.sub(
        r'(<svg[^>]*)\s+width="[^"]*"',
        r'\1 width="100%"',
        svg
    )
    svg = re.sub(
        r'(<svg[^>]*)\s+height="[^"]*"',
        r'\1',
        svg
    )

    return svg.strip()


def drawing_to_svg(drawing, responsive: bool = True) -> str:
    """Convert a schemdraw Drawing object to an SVG string.

    Uses schemdraw's get_imagedata() for direct SVG export,
    falling back to saving to a temp file if needed.

    If the drawing has a ``_analogpy_inst_targets`` attribute (set by
    ``create_testbench_block_diagram`` / ``create_cell_schematic``), a
    second render pass is performed to inject transparent click-target
    rectangles with ``id="analogpy-inst-{name}"`` for each component.
    This enables bidirectional schematic↔editor selection in the browser.

    Args:
        drawing: A schemdraw.Drawing object.
        responsive: If True, make SVG responsive for web embedding
                    (width="100%", no fixed height).

    Returns:
        SVG content as a string.
    """
    import matplotlib
    matplotlib.rcParams['svg.fonttype'] = 'none'  # Text as text, not paths

    def _render(d):
        svg = None
        try:
            data = d.get_imagedata('svg')
            if isinstance(data, bytes):
                svg = data.decode('utf-8')
            else:
                svg = str(data)
        except Exception:
            pass

        # Fallback: save to temp file
        if svg is None:
            import tempfile, os
            fd, temp_path = tempfile.mkstemp(suffix='.svg')
            os.close(fd)
            try:
                d.save(temp_path)
                with open(temp_path, 'r') as f:
                    svg = f.read()
            finally:
                if os.path.exists(temp_path):
                    os.unlink(temp_path)
        return svg

    svg = _render(drawing)

    # Collision avoidance pass: resolve label overlaps by moving text artists
    schematic_layout = getattr(drawing, '_analogpy_schematic_layout', None)
    if schematic_layout and svg:
        try:
            moves = schematic_layout.resolve_collisions()
            if moves:
                drawing._analogpy_moves = moves  # Store for reporting
                ax = drawing.fig.ax
                # Build lookup: match layout labels to matplotlib text artists by text + approximate position
                # We need to find the text artist for each moved label
                for owner_key, label, old_pos, new_pos in moves:
                    # Find the matplotlib text artist matching this label
                    best_artist = None
                    best_dist = float('inf')
                    for text_artist in ax.texts:
                        if text_artist.get_text() != label.text:
                            continue
                        tx, ty = text_artist.get_position()
                        # Match by proximity to old position
                        dist = (tx - old_pos[0]) ** 2 + (ty - old_pos[1]) ** 2
                        if dist < best_dist:
                            best_dist = dist
                            best_artist = text_artist

                    if best_artist is not None:
                        best_artist.set_position(new_pos)
                        # Update halign if changed
                        best_artist.set_ha(label.halign)

                svg = _render(drawing)
        except Exception:
            pass

    # Second pass: inject text bounding box overlays if show_bboxes is enabled
    bbox_records = getattr(drawing, '_analogpy_bbox_records', None)
    if bbox_records and svg:
        try:
            import matplotlib.patches as mpatches
            from .symbols import _BBOX_COLORS

            # Use schemdraw's own axes (not plt.get_fignums() which may differ)
            ax = drawing.fig.ax
            fig = ax.get_figure()
            fig.canvas.draw()
            renderer_mpl = fig.canvas.get_renderer()

            # Match recorded labels to rendered text artists by text content
            remaining = list(bbox_records)
            data_bboxes = []  # (label, x0, y0, x1, y1) in data coords
            for text_artist in ax.texts:
                txt = text_artist.get_text()
                matched_idx = None
                for i, (pos, rec_text, fontsize, halign, category) in enumerate(remaining):
                    if rec_text == txt:
                        matched_idx = i
                        break
                if matched_idx is None:
                    continue

                _, _, _, _, category = remaining.pop(matched_idx)
                bbox_color = _BBOX_COLORS.get(category, _BBOX_COLORS['default'])

                # Get actual rendered text bounding box and convert to data coords
                bbox = text_artist.get_window_extent(renderer_mpl)
                data_bbox = bbox.transformed(ax.transData.inverted())

                data_bboxes.append((f'{category}:{txt}',
                                    data_bbox.x0, data_bbox.y0,
                                    data_bbox.x1, data_bbox.y1))

                rect = mpatches.Rectangle(
                    (data_bbox.x0, data_bbox.y0),
                    data_bbox.width, data_bbox.height,
                    linewidth=0.8, facecolor='none', edgecolor=bbox_color,
                    linestyle='dotted', zorder=10,
                )
                ax.add_patch(rect)

            # Post-render collision detection using actual text extents
            render_collisions = []
            for i in range(len(data_bboxes)):
                la, ax0, ay0, ax1, ay1 = data_bboxes[i]
                for j in range(i + 1, len(data_bboxes)):
                    lb, bx0, by0, bx1, by1 = data_bboxes[j]
                    ox0, oy0 = max(ax0, bx0), max(ay0, by0)
                    ox1, oy1 = min(ax1, bx1), min(ay1, by1)
                    if ox0 < ox1 and oy0 < oy1:
                        area = (ox1 - ox0) * (oy1 - oy0)
                        render_collisions.append((la, lb, area))
            if render_collisions:
                drawing._analogpy_render_collisions = render_collisions

            svg = _render(drawing)
        except Exception:
            pass  # Fall back to SVG without bbox overlays

    # Third pass: inject transparent click-target rects with component IDs
    inst_targets = getattr(drawing, '_analogpy_inst_targets', None)
    if inst_targets and svg:
        try:
            import matplotlib.pyplot as plt
            import matplotlib.patches as mpatches
            figs = [plt.figure(n) for n in plt.get_fignums()]
            axes = figs[-1].get_axes() if figs else []
            if axes:
                ax = axes[0]
                for name, (cx, cy, bw, bh) in inst_targets.items():
                    rect = mpatches.Rectangle(
                        (cx - bw / 2, cy - bh / 2), bw, bh,
                        linewidth=1, facecolor=(0, 0, 0, 0), edgecolor='#999999',
                        linestyle='dashed',
                    )
                    rect.set_gid(f'analogpy-inst-{name}')
                    ax.add_patch(rect)
                svg = _render(drawing)
        except Exception:
            pass  # Fall back to SVG without click targets

    if responsive and svg:
        svg = _make_svg_responsive(svg)

    # Tag net-name text elements with class="analogpy-net-{name}" for browser highlighting
    net_names = getattr(drawing, '_analogpy_net_names', None)
    if net_names and svg:
        import re
        for net_name in net_names:
            # Match <g id="text_N"> blocks whose <text> content is exactly this net name
            svg = re.sub(
                r'(<g id="text_\d+">)(\s*<text [^>]*>' + re.escape(net_name) + r'</text>\s*</g>)',
                r'<g class="analogpy-net-' + net_name + r'">\1\2</g>',
                svg
            )

    return svg.strip() if svg else ""


def render_schematic_svg(cell, title: Optional[str] = None,
                         scale: float = 1.0, compact: bool = False,
                         selection_outline: str = None,
                         show_bboxes: bool = False) -> str:
    """Render a single circuit's schematic as an SVG string.

    Args:
        cell: A Circuit or Testbench object.
        title: Optional title override.
        scale: Drawing scale factor (default 1.0).
        compact: If True, use tighter spacing (default False).
        selection_outline: Draw selection outline around each instance.
            - 'bbox': Simple dotted rectangle bounding box
            - 'irregular': Dotted outline following actual symbol shape (default)
            - None: No outline
        show_bboxes: If True, draw bounding boxes around all text elements
            for debugging layout (instance names, cell types, params, net labels).

    Returns:
        SVG content as a string.
    """
    from .symbols import create_cell_schematic
    drawing = create_cell_schematic(cell, title=title, scale=scale, compact=compact,
                                    selection_outline=selection_outline,
                                    show_bboxes=show_bboxes)

    # Print per-instance layout summary and collision report
    schematic_layout = getattr(drawing, '_analogpy_schematic_layout', None)
    if show_bboxes and schematic_layout:
        print(f"\n[SchematicLayout] circuit={schematic_layout.circuit_name!r}, "
              f"{len(schematic_layout.instances)} instance(s), "
              f"{len(schematic_layout.header_labels)} header label(s), "
              f"{len(schematic_layout.wire_labels)} wire label(s)")
        for inst_name, inst_lo in schematic_layout.instances.items():
            labels = inst_lo.all_labels()
            ports_with_net = [p for p in inst_lo.ports.values() if p.net_label]
            print(f"  {inst_name} ({inst_lo.cell_name}): "
                  f"{len(labels)} labels, "
                  f"{len(inst_lo.ports)} ports ({len(ports_with_net)} with net labels)")

        # Per-instance collision report using SchematicLayout
        layout_collisions = schematic_layout.find_collisions()
        if layout_collisions:
            print(f"\n[SchematicLayout] {len(layout_collisions)} collision(s) before avoidance:")
            for a, b, area in sorted(layout_collisions, key=lambda c: -c[2]):
                print(f"  OVERLAP: {a!r}  <->  {b!r}  (area={area:.4f})")

    svg = drawing_to_svg(drawing)

    # Report moves and remaining collisions
    if show_bboxes and schematic_layout:
        moves = getattr(drawing, '_analogpy_moves', [])
        if moves:
            print(f"\n[collision avoidance] {len(moves)} label(s) moved:")
            for owner_key, label, old_pos, new_pos in moves:
                print(f"  MOVED: {owner_key:30s}  ({old_pos[0]:.2f},{old_pos[1]:.2f}) -> ({new_pos[0]:.2f},{new_pos[1]:.2f})  halign={label.halign}")

        remaining = schematic_layout.find_collisions()
        print(f"\n[SchematicLayout] {len(remaining)} collision(s) after avoidance")
        if remaining:
            for a, b, area in sorted(remaining, key=lambda c: -c[2]):
                print(f"  REMAINING: {a!r}  <->  {b!r}  (area={area:.4f})")

    # Print render-accurate collisions (from matplotlib text extents)
    render_collisions = getattr(drawing, '_analogpy_render_collisions', None)
    if render_collisions:
        print(f"\n[post-render] {len(render_collisions)} collision(s) (pixel-accurate):")
        for a, b, area in sorted(render_collisions, key=lambda c: -c[2]):
            print(f"  OVERLAP: {a!r}  <->  {b!r}  (area={area:.4f})")
    print()

    return svg


def render_block_diagram_svg(testbench, scale: float = 1.0) -> str:
    """Render a testbench block diagram as an SVG string.

    Args:
        testbench: A Testbench object.
        scale: Drawing scale factor (default 1.0).

    Returns:
        SVG content as a string.
    """
    from .symbols import create_testbench_block_diagram
    drawing = create_testbench_block_diagram(testbench, scale=scale)
    return drawing_to_svg(drawing)


def render_all_schematics_svg(ckt, depth: int = 100) -> Dict[str, str]:
    """Render schematics for a circuit/testbench and all its subcircuits.

    Returns a dict mapping circuit name to SVG string.
    The top-level circuit is included first.

    For a Testbench, a block diagram is also included under
    the key "{name}_block_diagram".

    Args:
        ckt: A Circuit or Testbench object.
        depth: Maximum recursion depth for subcircuits.

    Returns:
        Dict of {name: svg_string} for each circuit in the hierarchy.
    """
    from ..spectre import collect_subcircuits, is_builtin

    result: Dict[str, str] = {}

    # If testbench, include block diagram
    is_testbench = hasattr(ckt, 'analyses')
    if is_testbench:
        try:
            result[f"{ckt.name}_block_diagram"] = render_block_diagram_svg(ckt)
        except Exception:
            pass

    # Top-level schematic
    try:
        result[ckt.name] = render_schematic_svg(ckt)
    except Exception:
        pass

    # Subcircuit schematics (recursively collected, non-primitive only)
    if depth > 0:
        subcircuits = collect_subcircuits(ckt)
        for name, subckt in subcircuits.items():
            if not is_builtin(subckt):
                try:
                    result[name] = render_schematic_svg(subckt)
                except Exception:
                    pass

    return result
