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

Sources for supported analyses:
- Spectre: /org/ees/tools/vendor/cadence/spectre/spectre-23.10.802/doc/spectreref/
- ngspice: https://ngspice.sourceforge.io/docs/ngspice-html-manual/manual.xhtml
"""

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.

    Args:
        write: Save DC solution to file for reuse as initial conditions via readns.
               Only emitted if set. (e.g. "spectre.dc")
        maxiters: Max Newton-Raphson iterations per step (Spectre default: 150).
                  Only emitted if explicitly set.
        maxsteps: Max homotopy steps for convergence (Spectre default: 10000).
                  Only emitted if explicitly set.
        annotate: Log verbosity level (Spectre default: "sweep").
                  Values: no, title, sweep, status, estimated, steps, iters,
                  detailed, rejects, alliters. Only emitted if explicitly set.

    Example:
        dc = DC()                          # minimal — uses all Spectre defaults
        dc = DC(maxiters=300)              # override one parameter
        dc = DC(write="spectre.dc", annotate="status")  # save & verbose log
    """
    name: str = "dcOp"
    write: Optional[str] = None
    maxiters: Optional[int] = None
    maxsteps: Optional[int] = None
    annotate: Optional[str] = None
    save_op: bool = True

    def to_spectre(self) -> str:
        parts = [f'{self.name} dc']
        if self.write:
            parts.append(f'write="{self.write}"')
        if self.maxiters is not None:
            parts.append(f'maxiters={self.maxiters}')
        if self.maxsteps is not None:
            parts.append(f'maxsteps={self.maxsteps}')
        if self.annotate:
            parts.append(f'annotate={self.annotate}')
        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"
    annotate: Optional[str] = None

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

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


@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"
    annotate: Optional[str] = None

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

    def to_ngspice(self) -> str:
        # ngspice noise syntax: .noise <output> <ref> <variation> <points> <start> <stop>
        # output format: v(node) or v(node,ref)
        output_str = f"v({self.output},{self.ref})" if self.ref != "0" else f"v({self.output})"
        return f".noise {output_str} {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"
    annotate: Optional[str] = None

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

    def to_ngspice(self) -> str:
        # STB is Spectre-only (stability analysis)
        # ngspice does not have equivalent built-in analysis
        return f"// STB analysis not supported in ngspice"


@dataclass
class PSS(Analysis):
    """
    Periodic steady-state analysis (Spectre only).

    Example:
        pss = PSS(fund=1e9, harms=7)
        pss = PSS(fund=1e9, tstab=100e-9, errpreset="conservative")
    """
    name: str = "myPss"
    fund: Optional[float] = None
    period: Optional[float] = None
    harms: Optional[int] = None
    tstab: Optional[float] = None
    maxstep: Optional[float] = None
    errpreset: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} pss']
        if self.fund is not None:
            parts.append(f'fund={self.fund}')
        if self.period is not None:
            parts.append(f'period={self.period}')
        if self.harms is not None:
            parts.append(f'harms={self.harms}')
        if self.tstab is not None:
            parts.append(f'tstab={self.tstab}')
        if self.maxstep is not None:
            parts.append(f'maxstep={self.maxstep}')
        if self.errpreset is not None:
            parts.append(f'errpreset={self.errpreset}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        # ngspice supports PSS (experimental RF analysis)
        # ref: https://ngspice.sourceforge.io/docs/ngspice-html-manual/manual.xhtml
        parts = ["pss"]
        if self.fund is not None:
            parts.append(f"fund={self.fund}")
        if self.period is not None:
            parts.append(f"period={self.period}")
        if self.harms is not None:
            parts.append(f"harms={self.harms}")
        return " ".join(parts)


@dataclass
class PAC(Analysis):
    """
    Periodic AC analysis (Spectre only). Requires a prior PSS analysis.

    Example:
        pac = PAC(stop=100e6)
        pac = PAC(start=1, stop=100e6, points=200)
    """
    name: str = "myPac"
    start: Optional[float] = None
    stop: Optional[float] = None
    points: Optional[int] = None
    variation: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} pac']
        if self.start is not None:
            parts.append(f'start={self.start}')
        if self.stop is not None:
            parts.append(f'stop={self.stop}')
        if self.variation is not None and self.points is not None:
            parts.append(f'{self.variation}={self.points}')
        elif self.points is not None:
            parts.append(f'dec={self.points}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        # ngspice supports PAC (Periodic AC, experimental)
        # ref: https://ngspice.sourceforge.io/docs/ngspice-html-manual/manual.xhtml
        parts = ["pac"]
        if self.start is not None:
            parts.append(f"start={self.start}")
        if self.stop is not None:
            parts.append(f"stop={self.stop}")
        if self.points is not None:
            parts.append(f"dec={self.points}")
        return " ".join(parts)


@dataclass
class PNoise(Analysis):
    """
    Periodic noise analysis (Spectre only). Requires a prior PSS analysis.

    Example:
        pnoise = PNoise(stop=100e6)
        pnoise = PNoise(start=1, stop=100e6, points=200)
    """
    name: str = "myPnoise"
    start: Optional[float] = None
    stop: Optional[float] = None
    points: Optional[int] = None
    variation: Optional[str] = None
    oprobe: Optional[str] = None
    iprobe: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} pnoise']
        if self.start is not None:
            parts.append(f'start={self.start}')
        if self.stop is not None:
            parts.append(f'stop={self.stop}')
        if self.variation is not None and self.points is not None:
            parts.append(f'{self.variation}={self.points}')
        elif self.points is not None:
            parts.append(f'dec={self.points}')
        if self.oprobe is not None:
            parts.append(f'oprobe={self.oprobe}')
        if self.iprobe is not None:
            parts.append(f'iprobe={self.iprobe}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        # ngspice supports PNoise (Periodic Noise, experimental)
        # ref: https://ngspice.sourceforge.io/docs/ngspice-html-manual/manual.xhtml
        parts = ["pnoise"]
        if self.start is not None:
            parts.append(f"start={self.start}")
        if self.stop is not None:
            parts.append(f"stop={self.stop}")
        if self.points is not None:
            parts.append(f"dec={self.points}")
        return " ".join(parts)


@dataclass
class PXF(Analysis):
    """
    Periodic transfer function analysis (Spectre only). Requires a prior PSS analysis.

    Example:
        pxf = PXF(stop=100e6)
        pxf = PXF(start=1, stop=100e6, points=200)
    """
    name: str = "myPxf"
    start: Optional[float] = None
    stop: Optional[float] = None
    points: Optional[int] = None
    variation: Optional[str] = None
    oprobe: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} pxf']
        if self.start is not None:
            parts.append(f'start={self.start}')
        if self.stop is not None:
            parts.append(f'stop={self.stop}')
        if self.variation is not None and self.points is not None:
            parts.append(f'{self.variation}={self.points}')
        elif self.points is not None:
            parts.append(f'dec={self.points}')
        if self.oprobe is not None:
            parts.append(f'oprobe={self.oprobe}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        return "// PXF analysis not directly supported in ngspice"


@dataclass
class SP(Analysis):
    """
    S-parameter analysis (Spectre only).

    Example:
        sp = SP(start=1e6, stop=10e9, points=200)
    """
    name: str = "mySp"
    start: Optional[float] = None
    stop: Optional[float] = None
    points: Optional[int] = None
    variation: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} sp']
        if self.start is not None:
            parts.append(f'start={self.start}')
        if self.stop is not None:
            parts.append(f'stop={self.stop}')
        if self.variation is not None and self.points is not None:
            parts.append(f'{self.variation}={self.points}')
        elif self.points is not None:
            parts.append(f'dec={self.points}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        return "// SP analysis not directly supported in ngspice"


@dataclass
class XF(Analysis):
    """
    Transfer function analysis (Spectre only).

    Example:
        xf = XF(start=1, stop=10e9, points=200)
    """
    name: str = "myXf"
    start: Optional[float] = None
    stop: Optional[float] = None
    points: Optional[int] = None
    variation: Optional[str] = None
    oprobe: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} xf']
        if self.start is not None:
            parts.append(f'start={self.start}')
        if self.stop is not None:
            parts.append(f'stop={self.stop}')
        if self.variation is not None and self.points is not None:
            parts.append(f'{self.variation}={self.points}')
        elif self.points is not None:
            parts.append(f'dec={self.points}')
        if self.oprobe is not None:
            parts.append(f'oprobe={self.oprobe}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        # ngspice supports XF (.tf) - Transfer Function analysis
        # ref: https://ngspice.sourceforge.io/docs/ngspice-html-manual/manual.xhtml
        parts = ["tf"]
        if self.start is not None:
            parts.append(f"start={self.start}")
        if self.stop is not None:
            parts.append(f"stop={self.stop}")
        if self.points is not None:
            parts.append(f"dec={self.points}")
        return " ".join(parts)


@dataclass
class PZ(Analysis):
    """
    Pole-zero analysis (Spectre only).

    Example:
        pz = PZ()
        pz = PZ(iprobe="VIN", oprobe="vout")
    """
    name: str = "myPz"
    iprobe: Optional[str] = None
    oprobe: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} pz']
        if self.iprobe is not None:
            parts.append(f'iprobe={self.iprobe}')
        if self.oprobe is not None:
            parts.append(f'oprobe={self.oprobe}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        # ngspice supports PZ (.pz) - Pole-Zero analysis
        # ref: https://ngspice.sourceforge.io/docs/ngspice-html-manual/manual.xhtml
        parts = ["pz"]
        if self.iprobe is not None:
            parts.append(f"input={self.iprobe}")
        if self.oprobe is not None:
            parts.append(f"output={self.oprobe}")
        return " ".join(parts)


@dataclass
class Sweep(Analysis):
    """
    Parametric sweep analysis (Spectre only). Wraps child analyses.

    Example:
        sweep = Sweep(param="res", start=100, stop=10e3, step=100,
                      children=[DC()])
        sweep = Sweep(param="cap", values=[1e-12, 10e-12, 100e-12],
                      children=[AC(start=1, stop=1e9)])
    """
    name: str = "mySweep"
    param: str = ""
    start: Optional[float] = None
    stop: Optional[float] = None
    step: Optional[float] = None
    values: Optional[List[float]] = None
    children: List[Analysis] = field(default_factory=list)
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} sweep']
        if self.param:
            parts.append(f'param={self.param}')
        if self.start is not None:
            parts.append(f'start={self.start}')
        if self.stop is not None:
            parts.append(f'stop={self.stop}')
        if self.step is not None:
            parts.append(f'step={self.step}')
        if self.values is not None:
            val_str = " ".join(str(v) for v in self.values)
            parts.append(f'values=[{val_str}]')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        header = " ".join(parts)
        child_lines = "\n".join(f"    {c.to_spectre()}" for c in self.children)
        return f"{header} {{\n{child_lines}\n}}"

    def to_ngspice(self) -> str:
        return "// Sweep analysis not directly supported in ngspice"


@dataclass
class HB(Analysis):
    """
    Harmonic balance analysis (Spectre only).

    Example:
        hb = HB(funds=[1e9], maxharms=[7])
        hb = HB(funds=[1e9, 1.1e9], maxharms=[7, 5])
    """
    name: str = "myHb"
    funds: Optional[List[float]] = None
    maxharms: Optional[List[int]] = None
    errpreset: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} hb']
        if self.funds is not None:
            val_str = " ".join(str(f) for f in self.funds)
            parts.append(f'funds=[{val_str}]')
        if self.maxharms is not None:
            val_str = " ".join(str(h) for h in self.maxharms)
            parts.append(f'maxharms=[{val_str}]')
        if self.errpreset is not None:
            parts.append(f'errpreset={self.errpreset}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        return "// HB analysis not directly supported in ngspice"


@dataclass
class DCMatch(Analysis):
    """
    DC mismatch analysis (Spectre only).

    Example:
        dcmatch = DCMatch()
        dcmatch = DCMatch(oprobe="vout")
    """
    name: str = "myDcmatch"
    oprobe: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} dcmatch']
        if self.oprobe is not None:
            parts.append(f'oprobe={self.oprobe}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        return "// DCMatch analysis not directly supported in ngspice"


@dataclass
class ACMatch(Analysis):
    """
    AC mismatch analysis (Spectre only).

    Example:
        acmatch = ACMatch(start=1, stop=1e9)
        acmatch = ACMatch(start=1, stop=1e9, points=200)
    """
    name: str = "myAcmatch"
    start: Optional[float] = None
    stop: Optional[float] = None
    points: Optional[int] = None
    variation: Optional[str] = None
    oprobe: Optional[str] = None
    annotate: Optional[str] = None

    def to_spectre(self) -> str:
        parts = [f'{self.name} acmatch']
        if self.start is not None:
            parts.append(f'start={self.start}')
        if self.stop is not None:
            parts.append(f'stop={self.stop}')
        if self.variation is not None and self.points is not None:
            parts.append(f'{self.variation}={self.points}')
        elif self.points is not None:
            parts.append(f'dec={self.points}')
        if self.oprobe is not None:
            parts.append(f'oprobe={self.oprobe}')
        if self.annotate is not None:
            parts.append(f'annotate={self.annotate}')
        extra = self._extras_str()
        if extra:
            parts.append(extra)
        return " ".join(parts)

    def to_ngspice(self) -> str:
        return "// ACMatch analysis not directly supported in ngspice"


@dataclass
class FFT(Analysis):
    """
    FFT analysis (ngspice-only).

    ngspice performs FFT on transient simulation results. Requires a priortran analysis.

    Example:
        fft = FFT(start=0, stop=1e-3, np=1024)
        fft = FFT(start=5e-6, stop=1e-3)  # DFT from 5us to 1ms
    """
    name: str = "fft"
    start: float = 0.0          # Start time for FFT window
    stop: float = None           # Stop time for FFT window
    np: int = 1024               # Number of points for FFT
    window: str = "hanning"     # Window function: rectangular, hanning, hamming, blackman, Kaiser
    shift: bool = False          # Shift DC component to center

    def to_spectre(self) -> str:
        # Spectre doesn't have native FFT - user should run tran and post-process
        return f"// FFT not supported in Spectre - run tran and use scipy.fft"

    def to_ngspice(self) -> str:
        parts = [f".fft"]
        if self.start > 0:
            parts.append(f"start={self.start}")
        if self.stop is not None:
            parts.append(f"stop={self.stop}")
        parts.append(f"np={self.np}")
        if self.window != "hanning":
            parts.append(f"window={self.window}")
        if self.shift:
            parts.append("shift=yes")
        return " ".join(parts)
