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

"""ngspice netlist generator.

Generates ngspice-compatible SPICE netlists from the analogpy Circuit AST.
Mirrors the structure of spectre.py but outputs ngspice syntax.
"""

from .circuit import Circuit, CircuitInstance, _CircuitBase, ParamRef
from .spectre import is_builtin, collect_subcircuits


# Mapping from analogpy primitive spectre_type to ngspice instance name prefix
_NGSPICE_PREFIX = {
    "resistor": "r",
    "capacitor": "c",
    "inductor": "l",
    "nmos": "m",
    "pmos": "m",
    "vsource": "v",
    "isource": "i",
    "diode": "d",
    "bjt": "q",
    "jfet": "j",
    "vcvs": "e",
    "vccs": "g",
    "ccvs": "h",
    "cccs": "f",
    "iprobe": "v",  # zero-volt source in ngspice
    "port": "v",    # treat as voltage source
}


def _format_value(value) -> str:
    """Format a numeric or string value for ngspice output.

    Numbers are formatted with :g to drop trailing zeros.
    Strings are returned as-is (no quoting in ngspice).
    """
    if isinstance(value, ParamRef):
        return value.name
    if isinstance(value, float):
        return f"{value:g}"
    if isinstance(value, int):
        return str(value)
    return str(value)


def _ensure_prefix(name: str, spectre_type: str) -> str:
    """Ensure instance name has the correct ngspice prefix.

    ngspice requires instance names to start with a specific letter
    based on device type (R for resistor, C for capacitor, etc.).
    If the name already starts with the correct letter (case-insensitive),
    it is returned unchanged. Otherwise, any existing I_ prefix is stripped
    and the correct prefix is prepended.
    """
    prefix = _NGSPICE_PREFIX.get(spectre_type, "x")
    if name.lower().startswith(prefix):
        return name

    # Strip the analogpy auto-generated I_ prefix
    if name.startswith("I_"):
        name = name[2:]

    return f"{prefix}{name}"


def _emit_vsource(inst: CircuitInstance) -> str:
    """Emit an ngspice voltage source line.

    Handles DC, pulse, sine, and pwl source types.
    Format: Vname n+ n- [dc] [transient_spec]
    """
    p_net = inst.connections["p"].name
    n_net = inst.connections["n"].name
    name = _ensure_prefix(inst.name, "vsource")

    dc = inst.params.get("dc", 0)
    # Check both the explicit type param and the device's _source_type
    src_type = inst.params.get("type")
    if src_type is None:
        src_type = getattr(inst.subcircuit, '_source_type', None)

    if src_type == "pulse":
        val0 = _format_value(inst.params.get("val0", 0))
        val1 = _format_value(inst.params.get("val1", 0))
        delay = _format_value(inst.params.get("delay", 0))
        rise = _format_value(inst.params.get("rise", 0))
        fall = _format_value(inst.params.get("fall", 0))
        width = _format_value(inst.params.get("width", 0))
        period = _format_value(inst.params.get("period", 0))
        return f"{name} {p_net} {n_net} {_format_value(dc)} pulse ({val0} {val1} {delay} {rise} {fall} {width} {period})"

    if src_type == "sine" or src_type == "sin":
        vo = _format_value(inst.params.get("vo", inst.params.get("sinedc", 0)))
        va = _format_value(inst.params.get("va", inst.params.get("ampl", 0)))
        freq = _format_value(inst.params.get("freq", 0))
        td = _format_value(inst.params.get("td", 0))
        theta = _format_value(inst.params.get("theta", 0))
        return f"{name} {p_net} {n_net} sin({vo} {va} {freq} {td} {theta})"

    if src_type == "pwl":
        wave = inst.params.get("wave", [])
        wave_str = " ".join(_format_value(v) for v in wave)
        return f"{name} {p_net} {n_net} pwl({wave_str})"

    # DC only
    return f"{name} {p_net} {n_net} {_format_value(dc)}"


def _emit_isource(inst: CircuitInstance) -> str:
    """Emit an ngspice current source line."""
    p_net = inst.connections["p"].name
    n_net = inst.connections["n"].name
    name = _ensure_prefix(inst.name, "isource")
    dc = inst.params.get("dc", 0)
    return f"{name} {p_net} {n_net} {_format_value(dc)}"


def _emit_passive(inst: CircuitInstance, spectre_type: str) -> str:
    """Emit an ngspice passive component line (R, L, C).

    Format: Rname n1 n2 value
    """
    circuit = inst.subcircuit
    connections = [inst.connections[port].name for port in circuit.ports]
    name = _ensure_prefix(inst.name, spectre_type)

    # Get the primary parameter value (r, c, or l)
    param_key = {"resistor": "r", "capacitor": "c", "inductor": "l"}[spectre_type]
    value = inst.params.get(param_key, 0)

    return f"{name} {' '.join(connections)} {_format_value(value)}"


def _emit_mosfet(inst: CircuitInstance) -> str:
    """Emit an ngspice MOSFET line.

    Format: Mname drain gate source bulk MODEL [W=val] [L=val] [NF=val]
    """
    circuit = inst.subcircuit
    connections = [inst.connections[port].name for port in circuit.ports]
    name = _ensure_prefix(inst.name, circuit._primitive_spectre_type)

    model = inst.params.get("model", circuit._primitive_spectre_type.upper())

    # Build parameter string (W, L, NF — uppercase in ngspice)
    param_parts = []
    for key in ("w", "l", "nf"):
        if key in inst.params:
            param_parts.append(f"{key.upper()}={_format_value(inst.params[key])}")

    parts = [name] + connections + [model]
    if param_parts:
        parts.extend(param_parts)

    return " ".join(parts)


def _emit_diode(inst: CircuitInstance) -> str:
    """Emit an ngspice diode line.

    Format: Dname anode cathode MODEL [params]
    """
    circuit = inst.subcircuit
    connections = [inst.connections[port].name for port in circuit.ports]
    name = _ensure_prefix(inst.name, "diode")
    model = inst.params.get("model", "D")

    param_parts = []
    for key, value in inst.params.items():
        if key != "model":
            param_parts.append(f"{key}={_format_value(value)}")

    parts = [name] + connections + [model]
    if param_parts:
        parts.extend(param_parts)

    return " ".join(parts)


def _emit_bjt(inst: CircuitInstance) -> str:
    """Emit an ngspice BJT line.

    Format: Qname collector base emitter [substrate] MODEL [params]
    """
    circuit = inst.subcircuit
    connections = [inst.connections[port].name for port in circuit.ports]
    name = _ensure_prefix(inst.name, "bjt")
    model = inst.params.get("model", "NPN")

    param_parts = []
    for key, value in inst.params.items():
        if key != "model":
            param_parts.append(f"{key}={_format_value(value)}")

    parts = [name] + connections + [model]
    if param_parts:
        parts.extend(param_parts)

    return " ".join(parts)


def _emit_jfet(inst: CircuitInstance) -> str:
    """Emit an ngspice JFET line.

    Format: Jname drain gate source MODEL [params]
    """
    circuit = inst.subcircuit
    connections = [inst.connections[port].name for port in circuit.ports]
    name = _ensure_prefix(inst.name, "jfet")
    model = inst.params.get("model", "NJF")

    param_parts = []
    for key, value in inst.params.items():
        if key != "model":
            param_parts.append(f"{key}={_format_value(value)}")

    parts = [name] + connections + [model]
    if param_parts:
        parts.extend(param_parts)

    return " ".join(parts)


def _emit_vcvs(inst: CircuitInstance) -> str:
    """Emit ngspice VCVS (E element).

    Format: Ename n+ n- nc+ nc- gain
    """
    circuit = inst.subcircuit
    connections = [inst.connections[port].name for port in circuit.ports]
    name = _ensure_prefix(inst.name, "vcvs")
    gain = _format_value(inst.params.get("gain", 1))
    return f"{name} {' '.join(connections)} {gain}"


def _emit_vccs(inst: CircuitInstance) -> str:
    """Emit ngspice VCCS (G element).

    Format: Gname n+ n- nc+ nc- gm
    """
    circuit = inst.subcircuit
    connections = [inst.connections[port].name for port in circuit.ports]
    name = _ensure_prefix(inst.name, "vccs")
    gm = _format_value(inst.params.get("gm", 1))
    return f"{name} {' '.join(connections)} {gm}"


def _emit_iprobe(inst: CircuitInstance) -> str:
    """Emit ngspice equivalent of iprobe (zero-volt voltage source).

    Format: Vname n+ n- 0
    """
    circuit = inst.subcircuit
    connections = [inst.connections[port].name for port in circuit.ports]
    name = _ensure_prefix(inst.name, "iprobe")
    return f"{name} {' '.join(connections)} 0"


def emit_ngspice_instance(inst: CircuitInstance) -> str:
    """Emit a single ngspice instance line.

    Dispatches to type-specific emitters based on the device type.
    For user-defined subcircuits, uses X-prefix format.
    """
    circuit = inst.subcircuit

    if hasattr(circuit, '_is_primitive') and circuit._is_primitive:
        spectre_type = circuit._primitive_spectre_type

        if spectre_type == "vsource":
            return _emit_vsource(inst)
        elif spectre_type == "isource":
            return _emit_isource(inst)
        elif spectre_type in ("resistor", "capacitor", "inductor"):
            return _emit_passive(inst, spectre_type)
        elif spectre_type in ("nmos", "pmos"):
            return _emit_mosfet(inst)
        elif spectre_type == "diode":
            return _emit_diode(inst)
        elif spectre_type == "bjt":
            return _emit_bjt(inst)
        elif spectre_type == "jfet":
            return _emit_jfet(inst)
        elif spectre_type == "vcvs":
            return _emit_vcvs(inst)
        elif spectre_type == "vccs":
            return _emit_vccs(inst)
        elif spectre_type == "iprobe":
            return _emit_iprobe(inst)
        else:
            # Generic primitive fallback
            connections = [inst.connections[port].name for port in circuit.ports]
            name = _ensure_prefix(inst.name, spectre_type)
            param_parts = [f"{k}={_format_value(v)}" for k, v in inst.params.items()]
            parts = [name] + connections
            if param_parts:
                parts.extend(param_parts)
            return " ".join(parts)
    else:
        # User-defined subcircuit instance: X prefix
        connections = [inst.connections[port].name for port in circuit.ports]
        name = inst.name
        if not name.lower().startswith("x"):
            if name.startswith("I_"):
                name = name[2:]
            name = f"x{name}"

        parts = [name] + connections + [circuit.name]
        if inst.params:
            param_parts = [f"{k}={_format_value(v)}" for k, v in inst.params.items()]
            parts.extend(param_parts)

        return " ".join(parts)


def emit_ngspice_subcircuit(subckt: Circuit) -> str:
    """Emit an ngspice subcircuit definition block.

    Format:
        .subckt name port1 port2 ...
        [instance lines]
        .ends name
    """
    lines = []

    # Comments
    if subckt.comments:
        for comment in subckt.comments:
            lines.append(f"* {comment}")

    # Header
    port_list = " ".join(subckt.ports)
    lines.append(f".subckt {subckt.name} {port_list}")

    # Parameters (as .param directives)
    if subckt.parameters:
        for param in subckt.parameters:
            lines.append(f".param {param}")

    # Body: all instances
    for sub_inst in subckt.subcircuit_instances:
        lines.append(f"  {emit_ngspice_instance(sub_inst)}")

    # Footer
    lines.append(f".ends {subckt.name}")

    return "\n".join(lines)


def generate_ngspice(ckt: _CircuitBase) -> str:
    """Generate an ngspice netlist from the analogpy Circuit AST.

    Handles:
    - Built-in device instances (resistor, MOSFET, vsource, etc.)
    - User-defined subcircuit definitions (recursive)
    - Subcircuit instances
    - Voltage/current sources with DC, pulse, sine
    - Testbench-specific: analyses, includes, global nets, saves

    The first line is always the circuit title (ngspice convention).
    The netlist ends with .end.
    """
    lines = []

    # Line 1: Title (ngspice treats the first line as a title/comment)
    lines.append(ckt.name)

    is_testbench = hasattr(ckt, 'analyses')

    # .include directives
    if is_testbench and hasattr(ckt, '_includes') and ckt._includes:
        for inc in ckt._includes:
            lines.append(f".include {inc}")

    # .global declarations (skip "0" since it's always ground in ngspice)
    if is_testbench and hasattr(ckt, 'global_nets') and ckt.global_nets:
        non_zero_globals = [g for g in ckt.global_nets if g != "0"]
        if non_zero_globals:
            lines.append(f".global {' '.join(non_zero_globals)}")

    # Behavioral models (Verilog-A) — not supported by ngspice
    if is_testbench and hasattr(ckt, 'behavioral_models') and ckt.behavioral_models:
        lines.append("* WARNING: Verilog-A behavioral models are not supported by ngspice.")
        lines.append("*   These models require Cadence Spectre or a compatible simulator.")
        lines.append("*   Equation-based VA models may be compiled via OpenVAF -> OSDI,")
        lines.append("*   but data-driven LUT models (e.g. $table_model) have no ngspice equivalent.")
        lines.append("*   The following models will NOT be loaded; simulation will likely fail:")
        for model_path in ckt.behavioral_models:
            lines.append(f"*     {model_path}")
        lines.append("")

    if lines[-1] != "":
        lines.append("")

    # Subcircuit definitions
    subcircuits = collect_subcircuits(ckt)
    if subcircuits:
        for subckt in subcircuits.values():
            lines.append(emit_ngspice_subcircuit(subckt))
            lines.append("")

    # Top-level instances
    for sub_inst in ckt.subcircuit_instances:
        lines.append(emit_ngspice_instance(sub_inst))

    # Testbench-specific sections
    if is_testbench:
        # Save statements
        if hasattr(ckt, 'get_all_saves'):
            all_saves = ckt.get_all_saves()
            if len(all_saves) > 0:
                lines.append(all_saves.to_ngspice())

        # Analysis commands
        if ckt.analyses:
            for analysis in ckt.analyses:
                lines.append(analysis.to_ngspice())

    # End
    lines.append("")
    lines.append(".end")

    return "\n".join(lines)
