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

"""
Convert Spectre (.scs) netlists to analogpy Python code.

This module provides functionality to:
1. Parse SCS netlist files into an AST
2. Parse Spectre run scripts (tempRun, runSimulation) for command parameters
3. Generate equivalent Python testbench code using analogpy
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path
import re
from typing import Dict, List, Optional, Tuple

from .scs_parser import (
    SCSNetlist, SCSInstance, SCSSubckt, SCSAnalysis,
    parse_scs, parse_scs_file, parse_run_script
)


# Mapping from Spectre device types to analogpy devices
DEVICE_MAP = {
    'resistor': 'resistor',
    'capacitor': 'capacitor',
    'inductor': 'inductor',
    'vsource': 'vsource',
    'isource': 'isource',
    'vpulse': 'vpulse',
    'vsin': 'vsin',
    'vpwl': 'vpwl',
    'vcvs': 'vcvs',
    'vccs': 'vccs',
    'ccvs': 'ccvs',
    'cccs': 'cccs',
    'nmos': 'nmos',
    'pmos': 'pmos',
    'diode': 'diode',
    'bjt': 'bjt',
    'jfet': 'jfet',
    'iprobe': 'iprobe',
    'mutual_inductor': 'mutual_inductor',
    'port': 'port',
}


def _clean_param_name(name: str) -> str:
    """Clean a parameter name for use as a Python variable."""
    # Replace special characters with underscores
    name = name.replace('<', '_').replace('>', '_')
    name = name.replace('-', '_').replace(':', '_')
    name = name.replace('=', '').replace('+', '')
    # Remove leading/trailing underscores
    name = name.strip('_')
    # Add prefix if starts with a number
    if name and name[0].isdigit():
        name = 'p_' + name
    return name


def _format_value(value: str) -> str:
    """Format a parameter value for Python."""
    # Handle common Spectre notations
    value = value.strip()

    # Handle parameter references
    if value.isidentifier():
        return value

    # Convert Spectre suffix notation to Python scientific notation
    # Spectre uses: m=milli, u=micro, n=nano, p=pico, k=kilo, meg=mega, g=giga
    suffixes = {
        'm': 'e-3',   # milli
        'u': 'e-6',   # micro
        'n': 'e-9',   # nano
        'p': 'e-12',  # pico
        'f': 'e-15',  # femto
        'k': 'e3',    # kilo
        'meg': 'e6',  # mega
        'g': 'e9',    # giga
    }

    # Match patterns like: 0.2m, 1.5u, 100n, 3.3k, etc.
    # Also handles: (1.25e-13*2552) - preserve expressions

    # If it's already a number or expression, return as-is
    try:
        float(value)
        return value
    except ValueError:
        pass

    # Check for suffix pattern: number followed by suffix
    for suffix, replacement in suffixes.items():
        # Match number followed by suffix at end or before operators
        pattern = r'(\d+\.?\d*)(' + suffix + r')($|[+\-*/)])'
        def replacer(match):
            num = match.group(1)
            suffix_char = match.group(2)
            end = match.group(3)
            return num + replacement + end

        new_value = re.sub(pattern, replacer, value)
        if new_value != value:
            value = new_value
            break

    return value


def _generate_parameters_section(netlist: SCSNetlist) -> str:
    """Generate the parameters section of the Python code."""
    lines = []
    lines.append("# =============================================================================")
    lines.append("# Parameters from SCS netlist")
    lines.append("# =============================================================================")
    lines.append("")

    # Group parameters by category (optional, just list them alphabetically)
    if netlist.parameters:
        for name, value in sorted(netlist.parameters.items()):
            clean_name = _clean_param_name(name)
            # Try to convert to a reasonable Python value
            # Format the value (convert Spectre notation like 0.2m to 0.2e-3)
            formatted_value = _format_value(value)

            try:
                # Check if it's a simple number
                float(formatted_value)
                # It's a number, use as-is
                lines.append(f"{clean_name} = {formatted_value}")
            except ValueError:
                # It's an expression
                lines.append(f"{clean_name} = {formatted_value}  # Expression")

    if not netlist.parameters:
        lines.append("# No parameters defined")

    lines.append("")
    return '\n'.join(lines)


def _generate_includes_section(netlist: SCSNetlist) -> str:
    """Generate the includes section."""
    lines = []
    lines.append("# =============================================================================")
    lines.append("# Include files (from SCS netlist)")
    lines.append("# =============================================================================")
    lines.append("")

    if netlist.includes:
        for include_path in netlist.includes:
            # Convert to Python-friendly path format
            lines.append(f'# tb.include("{include_path}")')
    else:
        lines.append("# No includes")

    lines.append("")
    return '\n'.join(lines)


def _generate_ahdl_section(netlist: SCSNetlist) -> str:
    """Generate the Verilog-A includes section."""
    lines = []
    lines.append("# =============================================================================")
    lines.append("# Verilog-A behavioral models (ahdl_include in SCS)")
    lines.append("# =============================================================================")
    lines.append("")

    if netlist.ahdl_includes:
        for va_path in netlist.ahdl_includes:
            lines.append(f'# tb.behavioral_model("{va_path}")')
    else:
        lines.append("# No AHDL includes")

    lines.append("")
    return '\n'.join(lines)


def _generate_testbench_function(netlist: SCSNetlist) -> str:
    """Generate the testbench creation function."""
    lines = []
    lines.append("# =============================================================================")
    lines.append("# Testbench")
    lines.append("# =============================================================================")
    lines.append("")
    lines.append("def create_testbench() -> Testbench:")
    lines.append('    """Create testbench from converted SCS netlist."""')
    lines.append("")
    lines.append("    tb = Testbench(\"converted\")")
    lines.append("")
    lines.append("    gnd = tb.gnd()")
    lines.append("")

    # Generate nets from instances
    all_nets = set()
    for instance in netlist.instances:
        for conn in instance.connections:
            if conn not in ('0', 'gnd') and conn not in all_nets:
                all_nets.add(conn)

    # Generate net declarations
    for net in sorted(all_nets):
        clean_name = _clean_param_name(net).replace('[', '_').replace(']', '_')
        lines.append(f'    {clean_name} = tb.net("{net}")')

    if all_nets:
        lines.append("")

    # Generate instances
    lines.append("    # Instances")
    for instance in netlist.instances:
        device = instance.model.lower()

        # Get the analogpy device function
        if device in DEVICE_MAP:
            device_func = DEVICE_MAP[device]
        else:
            # It's a subcircuit reference - we'll handle this
            device_func = '# SUBSTRING: ' + device

        # Generate connections
        conn_names = []
        for conn in instance.connections:
            if conn in ('0', 'gnd'):
                conn_names.append('gnd')
            else:
                clean_name = _clean_param_name(conn).replace('[', '_').replace(']', '_')
                conn_names.append(clean_name)

        # Generate parameters
        if instance.params:
            param_str = ', '.join([f'{k}={_format_value(v)}' for k, v in instance.params.items()])
            lines.append(f'    # {instance.name}: {device} ({", ".join(conn_names)})')
            if device.startswith('# '):
                lines.append(f'    # tb.add_instance({device_func}, "{instance.name}",')
                lines.append(f'    #             {param_str})')
            else:
                lines.append(f'    tb.add_instance({device_func}, "{instance.name}",')
                lines.append(f'    #             {param_str})  # TODO: uncomment and fix connections')
        else:
            lines.append(f'    # tb.add_instance({device_func}, "{instance.name}", ...)')

    lines.append("")
    return '\n'.join(lines)


def _generate_analyses_section(netlist: SCSNetlist) -> str:
    """Generate the analyses section."""
    lines = []
    lines.append("# =============================================================================")
    lines.append("# Analyses")
    lines.append("# =============================================================================")
    lines.append("")

    for analysis in netlist.analyses:
        name = analysis.name.lower()

        if name == 'tran':
            stop = analysis.params.get('stop', '1')
            step = analysis.params.get('step', 'stop/100')
            lines.append(f"    tran = Transient(")
            lines.append(f"        stop={stop},")
            lines.append(f"        step={step},")

            # Optional parameters
            if 'maxstep' in analysis.params:
                lines.append(f"        maxstep={analysis.params['maxstep']},")
            if 'cmin' in analysis.params:
                lines.append(f"        cmin={analysis.params['cmin']},")

            lines.append("    )")
            lines.append("    tb.add_analysis(tran)")
            lines.append("")

        elif name == 'dc':
            lines.append("    tb.add_analysis(DC(name='dcOp'))")
            lines.append("")

        elif name == 'ac':
            lines.append(f"    # AC analysis: {analysis.params}")
            lines.append("    # tb.add_analysis(AC(...))")
            lines.append("")

        else:
            lines.append(f"    # Unsupported analysis: {name} - {analysis.params}")
            lines.append("")

    if not netlist.analyses:
        lines.append("    # No analyses defined")

    lines.append("")
    return '\n'.join(lines)


def _generate_save_section(netlist: SCSNetlist) -> str:
    """Generate the save/signals section."""
    lines = []
    lines.append("# =============================================================================")
    lines.append("# Save signals")
    lines.append("# =============================================================================")
    lines.append("")

    if netlist.save_signals:
        for signal in netlist.save_signals:
            lines.append(f'    tb.save("{signal}")')
    else:
        lines.append("    # No save signals")

    lines.append("")
    lines.append("    return tb")
    lines.append("")

    return '\n'.join(lines)


def _generate_run_script_section(run_parser) -> str:
    """Generate the build_spectre_command function."""
    lines = []
    lines.append("# =============================================================================")
    lines.append("# Spectre run script parameters (from tempRun/runSimulation)")
    lines.append("# =============================================================================")
    lines.append("")
    lines.append("# NOTE: The following parameters were extracted from the original")
    lines.append("# tempRun/runSimulation script. Some parameters are commented out")
    lines.append("# because they are ADE-specific and will cause errors if included")
    lines.append("# in a direct CLI run of Spectre:")
    lines.append("#")
    lines.append("# IGNORED (will cause errors if included):")
    lines.append("#   -env ade          : ADE environment setup (broken for CLI)")
    lines.append("#   +adespetkn=...    : ADE security token (useless outside ADE)")
    lines.append("#   -ahdllibdir <path>: ADE AHDL library directory")
    lines.append("#")
    lines.append("# COMMENTED OUT (user should verify/edit):")
    lines.append("#   -format psfxl     : Use psfascii instead for ASCII output")
    lines.append("#   -raw <dir>        : Use current folder instead")
    lines.append("#   -log <file>       : Use current folder instead")
    lines.append("#")
    lines.append("")

    # Include paths
    if run_parser and run_parser.include_paths:
        lines.append("# Include paths extracted from run script:")
        for path in run_parser.include_paths:
            lines.append(f'# CADENCE_BASE = "{path}"  # TODO: set correct path')
        lines.append("")

    # Options
    if run_parser and run_parser.options:
        lines.append("# Spectre options (from run script):")
        for key, value in sorted(run_parser.options.items()):
            if key == 'mt':
                lines.append(f"THREADS = {value}  # Multi-threading")
            elif key == 'aps':
                lines.append(f"APS = '+aps={value}'  # APS setting")
            elif key == 'escchars':
                lines.append(f"ESCS = '+escchars'  # Escape characters")
            elif key == 'maxw':
                lines.append(f"MAXW = {value}  # Max warnings")
            elif key == 'maxn':
                lines.append(f"MAXN = {value}  # Max notes")
            else:
                lines.append(f"# {key} = {value}")
        lines.append("")

    # Ignored parameters
    if run_parser and run_parser.ignored:
        lines.append("# Ignored ADE-specific parameters:")
        for key, value in sorted(run_parser.ignored.items()):
            if value:
                lines.append(f"# -ahdllibdir {value}  # (ignored)")
            else:
                lines.append(f"# -{key}  # (ignored)")
        lines.append("")

    lines.append("def build_spectre_command(netlist_path: str) -> str:")
    lines.append('    """Build the Spectre command line from extracted parameters."""')
    lines.append("    # TODO: Uncomment and modify the following based on your setup:")
    lines.append("")
    lines.append("    # include_paths = [")
    if run_parser and run_parser.include_paths:
        for path in run_parser.include_paths[:2]:  # Show first 2 as examples
            lines.append(f'    #     "{path}",')
        lines.append('    #     # ... add more paths as needed')
    else:
        lines.append('    #     "path/to/models",')
    lines.append("    # ]")
    lines.append("")
    lines.append("    # cmd = (SpectreCommand(netlist_path)")
    lines.append("    #         .output_format(\"psfascii\")")
    lines.append("    #         .include_path(*include_paths)")
    lines.append("    #         .accuracy(\"liberal\", \"+aps\")")
    lines.append("    #         .threads(16))")
    lines.append("    # return cmd.build()")
    lines.append("")
    lines.append('    # Placeholder - replace with actual command:')
    lines.append('    return f"spectre {netlist_path} +escchars +mt=16 ++aps=liberal"')
    lines.append("")

    return '\n'.join(lines)


def generate_python(netlist: SCSNetlist, run_script_path: Optional[str] = None,
                    output_path: Optional[str] = None,
                    netlist_code: Optional[str] = None,
                    netlist_filename: Optional[str] = None) -> str:
    """Generate Python code from a parsed SCS netlist.

    Args:
        netlist: The parsed SCS netlist AST.
        run_script_path: Optional path to tempRun or runSimulation script.
        output_path: Optional path to write the generated Python file.

    Returns:
        The generated Python code as a string.
    """
    # Parse run script if provided
    run_parser = None
    if run_script_path and os.path.exists(run_script_path):
        run_parser = parse_run_script(run_script_path)

    lines = []

    # Header
    lines.append("#!/usr/bin/env python3")
    lines.append('"""')
    lines.append("Converted Spectre Netlist")
    lines.append("")
    lines.append("This file was automatically generated from an SCS netlist using analogpy.")
    lines.append("Edit this file to customize the testbench as needed.")
    lines.append('"""')
    lines.append("")
    lines.append("import argparse")
    lines.append("from pathlib import Path")
    lines.append("")
    lines.append("from analogpy import (")
    lines.append("    Testbench, generate_spectre, SpectreCommand,")
    lines.append("    vsource, vpulse, resistor, capacitor, inductor,")
    lines.append("    nmos, pmos, DC, AC, Transient,")
    lines.append(")")
    lines.append("")

    # Add netlist import if present
    if netlist_code and netlist_filename:
        lines.append(f"# Import converted netlist (subcircuits)")
        lines.append(f"import {netlist_filename[:-3]}  # Subcircuit definitions")
        lines.append("")

    # Parameters section
    lines.append(_generate_parameters_section(netlist))

    # Includes section
    lines.append(_generate_includes_section(netlist))

    # AHDL section
    lines.append(_generate_ahdl_section(netlist))

    # Testbench function
    lines.append(_generate_testbench_function(netlist))

    # Analyses section
    lines.append(_generate_analyses_section(netlist))

    # Save section
    lines.append(_generate_save_section(netlist))

    # Run script section
    lines.append(_generate_run_script_section(run_parser))

    # Main function
    lines.append("# =============================================================================")
    lines.append("# Main")
    lines.append("# =============================================================================")
    lines.append("")
    lines.append("def main():")
    lines.append("    parser = argparse.ArgumentParser(description=\"Converted testbench\")")
    lines.append("    parser.add_argument(\"--netlist\", action=\"store_true\",")
    lines.append('                        help="Print Spectre netlist to stdout and exit")')
    lines.append("    parser.add_argument(\"--output\", \"-o\", type=Path, default=Path(\".\"),")
    lines.append('                        help="Output directory (default: current directory)")')
    lines.append("    args = parser.parse_args()")
    lines.append("")
    lines.append("    args.output.mkdir(parents=True, exist_ok=True)")
    lines.append("")
    lines.append("    tb = create_testbench()")
    lines.append("")
    lines.append("    # --- netlist-only mode ---")
    lines.append("    if args.netlist:")
    lines.append("        print(generate_spectre(tb))")
    lines.append("        return")
    lines.append("")
    lines.append("    # --- default: write netlist + run script ---")
    lines.append('    netlist_file = args.output / "converted.scs"')
    lines.append('    runscript_file = args.output / "runSimulation.sh"')
    lines.append("")
    lines.append("    netlist = generate_spectre(tb)")
    lines.append("    netlist_file.write_text(netlist)")
    lines.append('    print(f"Netlist written to:   {netlist_file}")')
    lines.append("")
    lines.append("    cmd = build_spectre_command(str(netlist_file))")
    lines.append("    import os")
    lines.append('    runscript_file.write_text(f"#!/bin/bash\\n{cmd}\\n")')
    lines.append("    os.chmod(runscript_file, 0o755)")
    lines.append('    print(f"Run script written to: {runscript_file}")')
    lines.append("")
    lines.append("")
    lines.append("if __name__ == \"__main__\":")
    lines.append("    main()")

    code = '\n'.join(lines)

    if output_path:
        Path(output_path).write_text(code)

    return code


def convert_scs_file(scs_path: str, run_script_path: Optional[str] = None,
                     output_path: Optional[str] = None,
                     netlist_path: Optional[str] = None) -> str:
    """Convert an SCS file to Python using analogpy.

    Args:
        scs_path: Path to the input SCS file.
        run_script_path: Optional path to tempRun or runSimulation script.
        output_path: Optional path for the output Python file.
                     If None, uses <scs_path>.py in the same directory.
        netlist_path: Optional path to a separate netlist file to convert.
                     If None, looks for "netlist" in the same directory.

    Returns:
        The generated Python code as a string.
    """
    # Parse the SCS file
    netlist = parse_scs_file(scs_path)

    # Determine output path
    if output_path is None:
        scs_path_obj = Path(scs_path)
        output_path = str(scs_path_obj.with_suffix('.py'))

    # Check for netlist file
    netlist_code = None
    netlist_filename = None
    if netlist_path and os.path.exists(netlist_path):
        # Convert netlist file
        netlist_code = _generate_netlist_file(netlist_path)
        netlist_filename = Path(netlist_path).stem + '_converted.py'
    elif 'netlist' in netlist.includes:
        # Look for "netlist" file in same directory as SCS
        scs_dir = Path(scs_path).parent
        potential_netlist = scs_dir / 'netlist'
        if potential_netlist.exists():
            netlist_code = _generate_netlist_file(str(potential_netlist))
            netlist_filename = 'netlist_converted.py'

    # Generate Python code
    return generate_python(netlist, run_script_path, output_path, netlist_code, netlist_filename)


def _generate_netlist_file(netlist_path: str) -> str:
    """Generate Python code for a netlist file (subcircuits)."""
    netlist = parse_scs_file(netlist_path)

    lines = []
    lines.append("# Converted netlist (subcircuits)")
    lines.append("# This file contains subcircuit definitions from the original netlist")
    lines.append("")
    lines.append("from analogpy import Circuit, Subckt")
    lines.append("")

    # Generate subcircuits
    for subckt in netlist.subcircuits:
        lines.append(f"# Subcircuit: {subckt.name}")
        lines.append(f"# Ports: {', '.join(subckt.ports)}")
        if subckt.parameters:
            lines.append(f"# Parameters: {', '.join(subckt.parameters)}")
        lines.append(f"# TODO: Convert to analogpy Circuit definition")
        lines.append("")

    if not netlist.subcircuits:
        lines.append("# No subcircuits found in netlist file")

    lines.append("")
    lines.append("# TODO: Define subcircuit instances as Circuit objects")
    lines.append("# Example:")
    lines.append("# pixel_circuit = Circuit(")
    lines.append('#     "pixel_tft_oled_sdc_d970_poc1_vthShift",')
    lines.append("#     ports=['drain', 'gate', 'source', 'vdd', 'oled', 'gnd']")
    lines.append("# )")

    return '\n'.join(lines)
