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

"""Analysis classes for circuit simulation."""

from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any


@dataclass
class Analysis:
    """Base class for all analysis types."""
    name: str = ""
    extras: Dict[str, Any] = field(default_factory=dict)

    def _extras_str(self) -> str:
        """Format extras dict as key=value pairs for Spectre output."""
        if not self.extras:
            return ""
        return " ".join(f"{k}={v}" for k, v in self.extras.items())

    def to_spectre(self) -> str:
        raise NotImplementedError

    def to_ngspice(self) -> str:
        raise NotImplementedError


@dataclass
class DC(Analysis):
    """
    DC operating point analysis.

    Example:
        dc = DC()
        dc = DC(name="dcOp", save_op=True)
    """
    name: str = "dcOp"
    write: str = "spectre.dc"
    maxiters: int = 150
    maxsteps: int = 10000
    save_op: bool = True

    def to_spectre(self) -> str:
        parts = [f'{self.name} dc']
        if self.write:
            parts.append(f'write="{self.write}"')
        parts.append(f'maxiters={self.maxiters}')
        parts.append(f'maxsteps={self.maxsteps}')
        parts.append('annotate=status')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        line = " ".join(parts)

        # Add operating point info if requested
        if self.save_op:
            line += f"\n{self.name}Info info what=oppoint where=rawfile"

        return line

    def to_ngspice(self) -> str:
        return ".op"


@dataclass
class AC(Analysis):
    """
    AC small-signal analysis.

    Example:
        ac = AC(start=1, stop=1e9, points=100)
        ac = AC(start=1, stop=1e9, points=100, variation="dec")
    """
    name: str = "ac"
    start: float = 1.0
    stop: float = 1e9
    points: int = 100
    variation: str = "dec"  # "dec", "oct", "lin"

    def to_spectre(self) -> str:
        parts = [f'{self.name} ac start={self.start} stop={self.stop} {self.variation}={self.points} annotate=status']
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        return f".ac {self.variation} {self.points} {self.start} {self.stop}"


@dataclass
class Transient(Analysis):
    """
    Transient time-domain analysis.

    Example:
        tran = Transient(stop=1e-6)
        tran = Transient(stop=1e-3, start=0, step=1e-9)
        tran = Transient(stop=1e-6, cmin=1e-18)  # Add min capacitance for convergence
        tran = Transient(stop=1e-6, extras={"errpreset": "conservative"})
    """
    name: str = "tran"
    stop: float = 1e-6
    start: float = 0.0
    step: Optional[float] = None
    maxstep: Optional[float] = None
    cmin: Optional[float] = None

    def __post_init__(self):
        if self.step is None:
            self.step = self.stop / 1000

    def to_spectre(self) -> str:
        parts = [f'{self.name} tran']
        if self.start > 0:
            parts.append(f'start={self.start}')
        parts.append(f'stop={self.stop}')
        if self.maxstep:
            parts.append(f'maxstep={self.maxstep}')
        if self.cmin is not None:
            parts.append(f'cmin={self.cmin}')
        parts.append('annotate=status')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        return f".tran {self.step} {self.stop} {self.start}"


@dataclass
class Noise(Analysis):
    """
    Noise analysis.

    Example:
        noise = Noise(output="vout", ref="gnd", input_src="VIN", start=1, stop=1e9)
    """
    name: str = "noise"
    output: str = ""
    ref: str = "0"
    input_src: str = ""
    start: float = 1.0
    stop: float = 1e9
    points: int = 100
    variation: str = "dec"

    def to_spectre(self) -> str:
        return (f'{self.name} noise start={self.start} stop={self.stop} '
                f'{self.variation}={self.points} '
                f'oprobe={self.output} iprobe={self.input_src} annotate=status')

    def to_ngspice(self) -> str:
        return f".noise v({self.output},{self.ref}) {self.input_src} {self.variation} {self.points} {self.start} {self.stop}"


@dataclass
class STB(Analysis):
    """
    Stability analysis (loop gain, phase margin).

    Example:
        stb = STB(probe="X1.out", start=1, stop=1e9)
    """
    name: str = "stb"
    probe: str = ""
    start: float = 1.0
    stop: float = 1e9
    points: int = 100
    variation: str = "dec"

    def to_spectre(self) -> str:
        return (f'{self.name} stb start={self.start} stop={self.stop} '
                f'{self.variation}={self.points} probe={self.probe} annotate=status')

    def to_ngspice(self) -> str:
        # ngspice doesn't have built-in STB, would need workaround
        return f"// STB analysis not directly supported in ngspice"
