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

"""
Spectre (.scs) netlist parser.

This module provides a full parser for Spectre netlist format,
building an AST (Abstract Syntax Tree) that can be used to generate
equivalent Python code using analogpy.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import List, Dict, Optional


class TokenType(Enum):
    """Token types for SCS lexer."""
    IDENTIFIER = auto()
    NUMBER = auto()
    STRING = auto()
    EQUALS = auto()
    LPAREN = auto()
    RPAREN = auto()
    LBRACKET = auto()
    RBRACKET = auto()
    COMMA = auto()
    NEWLINE = auto()
    EOF = auto()
    COMMENT = auto()
    PLUS = auto()
    MINUS = auto()
    TIMES = auto()
    DIVIDE = auto()
    COLON = auto()  # For bus notation: net<3:0>
    DOT = auto()    # For signal paths: net.signal
    AT = auto()    # For @ parameter overrides


@dataclass
class Token:
    """A token from the SCS lexer."""
    type: TokenType
    value: str
    line: int
    column: int


@dataclass
class SCSParameter:
    """A parameter definition in SCS."""
    name: str
    value: str  # Keep as string for expressions


@dataclass
class SCSInstance:
    """An instance (device) in SCS."""
    name: str
    model: str  # e.g., "resistor", "vsource", or subckt name
    connections: List[str]  # net names
    params: Dict[str, str] = field(default_factory=dict)


@dataclass
class SCSSubckt:
    """A subcircuit definition in SCS."""
    name: str
    ports: List[str]
    parameters: List[str]  # Subckt parameters
    instances: List[SCSInstance] = field(default_factory=list)
    line_number: int = 0


@dataclass
class SCSAnalysis:
    """An analysis block in SCS."""
    name: str  # "tran", "dc", "ac", etc.
    params: Dict[str, str] = field(default_factory=dict)


@dataclass
class SCSNetlist:
    """The complete parsed SCS netlist."""
    parameters: Dict[str, str] = field(default_factory=dict)
    includes: List[str] = field(default_factory=list)
    subcircuits: List[SCSSubckt] = field(default_factory=list)
    instances: List[SCSInstance] = field(default_factory=list)
    analyses: List[SCSAnalysis] = field(default_factory=list)
    options: Dict[str, str] = field(default_factory=dict)
    save_signals: List[str] = field(default_factory=list)
    ahdl_includes: List[str] = field(default_factory=list)
    title: Optional[str] = None
    comments: List[str] = field(default_factory=list)


class Lexer:
    """Lexer for SCS netlist format."""

    def __init__(self, content: str):
        self.content = content
        self.pos = 0
        self.line = 1
        self.column = 1
        self.tokens: List[Token] = []
        self._current_idx = 0
        self._tokenize()

    def _tokenize(self) -> None:
        """Tokenize the entire content."""
        while self.pos < len(self.content):
            # Skip whitespace (but preserve newlines for debugging)
            if self.content[self.pos].isspace():
                if self.content[self.pos] == '\n':
                    self.tokens.append(Token(TokenType.NEWLINE, '\n', self.line, self.column))
                    self.line += 1
                    self.column = 1
                else:
                    self.column += 1
                self.pos += 1
                continue

            # Comments (// ...)
            if self.content[self.pos:self.pos + 2] == '//':
                comment = ''
                while self.pos < len(self.content) and self.content[self.pos] != '\n':
                    comment += self.content[self.pos]
                    self.pos += 1
                self.tokens.append(Token(TokenType.COMMENT, comment, self.line, self.column))
                continue

            # String literals (quoted)
            if self.content[self.pos] in ('"', "'"):
                quote = self.content[self.pos]
                self.pos += 1
                string_val = ''
                while self.pos < len(self.content) and self.content[self.pos] != quote:
                    if self.content[self.pos] == '\\' and self.pos + 1 < len(self.content):
                        string_val += self.content[self.pos + 1]
                        self.pos += 2
                    else:
                        string_val += self.content[self.pos]
                        self.pos += 1
                self.pos += 1  # Skip closing quote
                self.tokens.append(Token(TokenType.STRING, string_val, self.line, self.column))
                self.column += len(string_val) + 2
                continue

            # Numbers (including scientific notation)
            if self.content[self.pos].isdigit() or (
                self.content[self.pos] == '.' and self.pos + 1 < len(self.content)
                and self.content[self.pos + 1].isdigit()
            ):
                num = ''
                while self.pos < len(self.content) and (
                    self.content[self.pos].isdigit()
                    or self.content[self.pos] in '.-+eEfFgG'
                    or (self.content[self.pos] == '.' and '.' not in num)
                ):
                    num += self.content[self.pos]
                    self.pos += 1
                self.tokens.append(Token(TokenType.NUMBER, num, self.line, self.column))
                self.column += len(num)
                continue

            # Identifiers and keywords (including bus notation like net<3:0>)
            if self.content[self.pos].isalpha() or self.content[self.pos] == '_':
                ident = ''
                while self.pos < len(self.content) and (
                    self.content[self.pos].isalnum()
                    or self.content[self.pos] in '_<>-'
                ):
                    ident += self.content[self.pos]
                    self.pos += 1
                self.tokens.append(Token(TokenType.IDENTIFIER, ident, self.line, self.column))
                self.column += len(ident)
                continue

            # Single character tokens
            char = self.content[self.pos]
            token_type = {
                '=': TokenType.EQUALS,
                '(': TokenType.LPAREN,
                ')': TokenType.RPAREN,
                '[': TokenType.LBRACKET,
                ']': TokenType.RBRACKET,
                ',': TokenType.COMMA,
                '+': TokenType.PLUS,
                '-': TokenType.MINUS,
                '*': TokenType.TIMES,
                '/': TokenType.DIVIDE,
                ':': TokenType.COLON,
                '.': TokenType.DOT,
                '@': TokenType.AT,
            }.get(char)

            if token_type:
                self.tokens.append(Token(token_type, char, self.line, self.column))
                self.pos += 1
                self.column += 1
                continue

            # Unknown character - skip
            self.pos += 1
            self.column += 1

        self.tokens.append(Token(TokenType.EOF, '', self.line, self.column))

    def peek(self, offset: int = 0) -> Token:
        """Peek at a token without consuming it."""
        idx = self._current_idx + offset
        if idx < len(self.tokens):
            return self.tokens[idx]
        return self.tokens[-1]

    def consume(self) -> Token:
        """Consume and return the next token."""
        token = self.peek()
        self._current_idx += 1
        return token


class Parser:
    """Recursive descent parser for SCS netlist format."""

    def __init__(self, content: str):
        self.lexer = Lexer(self._preprocess(content))
        self.lexer._current_idx = 0

    def _preprocess(self, content: str) -> str:
        """Handle line continuations - replace backslash-newline with nothing."""
        result = []
        i = 0
        while i < len(content):
            if content[i] == chr(92) and i + 1 < len(content) and content[i + 1] == chr(10):
                # Skip backslash and newline (line continuation)
                i += 2
                # Skip any leading whitespace on next line
                while i < len(content) and content[i] in " \t":
                    i += 1
            else:
                result.append(content[i])
                i += 1
        return "".join(result)
    def parse(self) -> SCSNetlist:
        """Parse the SCS content and return the netlist AST."""
        netlist = SCSNetlist()

        while not self._is_at_end():
            # Skip newlines
            if self._check(TokenType.NEWLINE):
                self._advance()
                continue

            # Skip comments
            if self._check(TokenType.COMMENT):
                comment_token = self._advance()
                netlist.comments.append(comment_token.value)
                continue

            # Check for end of file
            if self._check(TokenType.EOF):
                break

            # Parse top-level statements
            token = self.peek()
            token_value = token.value.lower() if token.type == TokenType.IDENTIFIER else token.value

            if token_value == 'parameters':
                self._parse_parameters(netlist)
            elif token_value == 'include':
                self._parse_include(netlist)
            elif token_value == 'ahdl_include':
                self._parse_ahdl_include(netlist)
            elif token_value == 'subckt' or token_value == 'subcircuit':
                subckt = self._parse_subckt()
                netlist.subcircuits.append(subckt)
            elif token_value == 'simulator':
                self._parse_simulator(netlist)
            elif token_value == 'global':
                self._parse_global()
            elif token_value == 'simulatoroptions':
                self._parse_simulator_options(netlist)
            elif token_value == 'save':
                self._parse_save(netlist)
            elif token_value == 'saveoptions':
                self._parse_save_options(netlist)
            elif token_value in ('tran', 'dc', 'ac', 'noise', 'stb', 'pss', 'pac', 'pnoise', 'pxf', 'sp', 'xf', 'pz', 'hb', 'dcmatch', 'acmatch', 'traninfo', 'modelparameter', 'element', 'outputparameter', 'designparamvals', 'primitives', 'subckts'):
                # Analysis blocks and info statements
                if token_value in ('tran', 'dc', 'ac', 'noise', 'stb', 'pss', 'pac', 'pnoise', 'pxf', 'sp', 'xf', 'pz', 'hb', 'dcmatch', 'acmatch'):
                    analysis = self._parse_analysis()
                    netlist.analyses.append(analysis)
                else:
                    # Info statements - skip
                    self._skip_to_newline()
            elif token.type == TokenType.IDENTIFIER:
                # This could be an instance
                instance = self._parse_instance()
                if instance:
                    netlist.instances.append(instance)
                else:
                    # Skip unknown statements
                    self._skip_to_newline()
            else:
                # Skip unknown tokens
                self._advance()

        return netlist

    def _is_at_end(self) -> bool:
        """Check if we're at the end of tokens."""
        return self.peek().type == TokenType.EOF

    def peek(self, offset: int = 0) -> Token:
        """Peek at current or offset token."""
        return self.lexer.peek(offset)

    def _advance(self) -> Token:
        """Advance and return current token."""
        return self.lexer.consume()

    def _check(self, token_type: TokenType) -> bool:
        """Check if current token matches type."""
        if self._is_at_end():
            return False
        return self.peek().type == token_type

    def _match(self, *token_types: TokenType) -> bool:
        """Check if current token matches any of the given types."""
        for token_type in token_types:
            if self._check(token_type):
                self._advance()
                return True
        return False

    def _expect(self, token_type: TokenType, message: str = "Unexpected token") -> Token:
        """Expect a specific token type or raise an error."""
        if self._check(token_type):
            return self._advance()
        raise SyntaxError(f"{message}: {self.peek()}")

    def _skip_to_newline(self) -> None:
        """Skip tokens until newline or EOF."""
        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):
            self._advance()
        if self._check(TokenType.NEWLINE):
            self._advance()

    def _parse_parameters(self, netlist: SCSNetlist) -> None:
        """Parse parameters statement."""
        self._advance()  # consume 'parameters'

        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):
            # Get parameter name
            if not self._check(TokenType.IDENTIFIER):
                break
            param_name = self._advance().value

            # Check for = value
            if self._check(TokenType.EQUALS):
                self._advance()  # consume '='
                param_value = self._parse_value()
                netlist.parameters[param_name] = param_value

            # Skip comma if present
            if self._check(TokenType.COMMA):
                self._advance()
            # Also stop if next token is IDENTIFIER (new param name) without '='
            elif self._check(TokenType.IDENTIFIER):
                # Look ahead to see if this is a new parameter (followed by =)
                if self.peek(1).type != TokenType.EQUALS:
                    break

    def _parse_value(self) -> str:
        """Parse a parameter value (can include expressions)."""
        value_parts = []
        paren_depth = 0  # Track parentheses depth for expressions
        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF) and not self._check(TokenType.COMMA):
            token = self.peek()
            if token.type == TokenType.LPAREN:
                paren_depth += 1
                value_parts.append(self._advance().value)
            elif token.type == TokenType.RPAREN:
                paren_depth -= 1
                value_parts.append(self._advance().value)
            elif token.type in (TokenType.NUMBER, TokenType.STRING):
                value_parts.append(self._advance().value)
            elif token.type == TokenType.IDENTIFIER:
                # Look ahead: if next token is '=', this is a new parameter
                if self.peek(1).type == TokenType.EQUALS:
                    break
                value_parts.append(self._advance().value)
            elif token.type in (TokenType.PLUS, TokenType.MINUS, TokenType.TIMES, TokenType.DIVIDE):
                value_parts.append(self._advance().value)
            elif token.type == TokenType.COLON:
                value_parts.append(self._advance().value)
            else:
                break
        return ''.join(value_parts)

    def _parse_include(self, netlist: SCSNetlist) -> None:
        """Parse include statement."""
        self._advance()  # consume 'include'

        # Get the file path
        if self._check(TokenType.STRING):
            path = self._advance().value
            netlist.includes.append(path)
        elif self._check(TokenType.IDENTIFIER):
            # Could be a variable like $CADENCE/...
            path = self._advance().value
            netlist.includes.append(path)

        self._skip_to_newline()

    def _parse_ahdl_include(self, netlist: SCSNetlist) -> None:
        """Parse ahdl_include statement."""
        self._advance()  # consume 'ahdl_include'

        # Get the file path
        if self._check(TokenType.STRING):
            path = self._advance().value
            netlist.ahdl_includes.append(path)

        self._skip_to_newline()

    def _parse_simulator(self, netlist: SCSNetlist) -> None:
        """Parse simulator lang=spectre statement."""
        # Skip 'simulator lang=spectre' line
        self._skip_to_newline()

    def _parse_global(self) -> None:
        """Parse global 0 statement."""
        self._skip_to_newline()

    def _parse_simulator_options(self, netlist: SCSNetlist) -> None:
        """Parse simulatorOptions statement."""
        self._advance()  # consume 'simulatorOptions'

        # Skip 'options' keyword if present
        if self._check(TokenType.IDENTIFIER):
            self._advance()

        # Parse options
        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):
            if self._check(TokenType.IDENTIFIER):
                opt_name = self._advance().value
                if self._check(TokenType.EQUALS):
                    self._advance()
                    opt_value = self._parse_value()
                    netlist.options[opt_name] = opt_value
            elif self._check(TokenType.COMMA):
                self._advance()
            else:
                break

    def _parse_save(self, netlist: SCSNetlist) -> None:
        """Parse save statement."""
        self._advance()  # consume 'save'

        # Track current signal being built
        current_signal = []
        just_added_continuation = False  # Track if last token was part of signal continuation

        while not self._check(TokenType.EOF):
            if self._check(TokenType.IDENTIFIER) or self._check(TokenType.NUMBER):
                # Check if we should save previous signal first
                # Only save if we have a current signal AND we didn't just add a continuation token
                if current_signal and not just_added_continuation:
                    netlist.save_signals.append(''.join(current_signal))
                    current_signal = []

                # Add to current signal
                current_signal.append(self._advance().value)
                just_added_continuation = False

            elif self._check(TokenType.COLON):
                # Colon within signal (like T1:d), add it - this is a continuation
                current_signal.append(self._advance().value)
                just_added_continuation = True

            elif self._check(TokenType.LBRACKET):
                # Handle array indices like [1][4] - this is a continuation
                current_signal.append(self._advance().value)
                just_added_continuation = True

            elif self._check(TokenType.RBRACKET):
                current_signal.append(self._advance().value)
                just_added_continuation = True

            elif self._check(TokenType.DOT):
                # Dot within a signal (like net.signal), add it - this is a continuation
                current_signal.append(self._advance().value)
                just_added_continuation = True

            elif self._check(TokenType.COMMA):
                # Comma separates signals
                if current_signal:
                    netlist.save_signals.append(''.join(current_signal))
                    current_signal = []
                self._advance()
                just_added_continuation = False

            elif self._check(TokenType.NEWLINE):
                # End of save statement - save any remaining signal
                if current_signal:
                    netlist.save_signals.append(''.join(current_signal))
                    current_signal = []
                self._advance()
                break

            else:
                # Unknown token - save current signal and break
                if current_signal:
                    netlist.save_signals.append(''.join(current_signal))
                break

    def _parse_save_options(self, netlist: SCSNetlist) -> None:
        """Parse saveOptions statement."""
        self._skip_to_newline()

    def _parse_analysis(self) -> SCSAnalysis:
        """Parse analysis block (tran, dc, ac, etc.)."""
        token = self._advance()
        name = token.value.lower()

        analysis = SCSAnalysis(name=name)

        # Parse analysis parameters
        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):
            if self._check(TokenType.IDENTIFIER):
                param_name = self._advance().value
                if self._check(TokenType.EQUALS):
                    self._advance()
                    param_value = self._parse_value()
                    analysis.params[param_name] = param_value
                else:
                    # It's a keyword like 'info'
                    pass
            elif self._check(TokenType.COMMA):
                self._advance()
            else:
                break

        return analysis

    def _parse_subckt(self) -> SCSSubckt:
        """Parse subcircuit definition."""
        self._advance()  # consume 'subckt' or 'subcircuit'

        # Get subckt name
        if not self._check(TokenType.IDENTIFIER):
            raise SyntaxError(f"Expected subckt name, got {self.peek()}")
        name = self._advance().value

        # Get ports
        ports = []
        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):
            if self._check(TokenType.IDENTIFIER):
                ports.append(self._advance().value)
            elif self._check(TokenType.COMMA):
                self._advance()
            else:
                break

        # Check for subckt parameters after ports
        parameters = []
        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):
            if self._check(TokenType.IDENTIFIER):
                param_name = self.peek().value
                self._advance()
                # If followed by =, it's a parameter with default value
                if self._check(TokenType.EQUALS):
                    self._advance()
                    self._parse_value()  # consume the value
                    parameters.append(param_name)
                else:
                    # Could be a parameter without default or a port
                    if param_name not in ports:
                        parameters.append(param_name)
            elif self._check(TokenType.COMMA):
                self._advance()
            else:
                break

        subckt = SCSSubckt(name=name, ports=ports, parameters=parameters, line_number=self.peek().line)

        # Parse subckt body (instances)
        while not self._is_at_end():
            # Check for ends (end of subckt)
            if self._check(TokenType.IDENTIFIER) and self.peek().value.lower() == 'ends':
                self._advance()  # consume 'ends'
                break

            # Skip newlines
            if self._check(TokenType.NEWLINE):
                self._advance()
                continue

            # Skip comments
            if self._check(TokenType.COMMENT):
                self._advance()
                continue

            # Parse instance
            instance = self._parse_instance()
            if instance:
                subckt.instances.append(instance)
            else:
                self._advance()

        return subckt

    def _parse_instance(self) -> Optional[SCSInstance]:
        """Parse an instance (device)."""
        if not self._check(TokenType.IDENTIFIER):
            return None

        # Get instance name
        name = self._advance().value

        # Get connections (net names) - between parentheses
        connections = []
        in_parens = False
        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):
            if self._check(TokenType.LPAREN):
                self._advance()  # consume '('
                in_parens = True
            elif self._check(TokenType.RPAREN):
                self._advance()  # consume ')'
                in_parens = False
            elif self._check(TokenType.IDENTIFIER):
                # If we're inside parens, it's a connection; otherwise it's the model
                if in_parens:
                    connections.append(self._advance().value)
                else:
                    break  # End of connections, this is the model
            elif self._check(TokenType.NUMBER):
                # Numbers can appear in connections (like subckt indices)
                if in_parens:
                    connections.append(self._advance().value)
                else:
                    break
            elif self._check(TokenType.COMMA):
                self._advance()
            else:
                break

        if not connections:
            return None

        # Get model/type
        if not self._check(TokenType.IDENTIFIER):
            return None
        model = self._advance().value

        # Get parameters
        params = {}
        while not self._check(TokenType.NEWLINE) and not self._check(TokenType.EOF):
            if self._check(TokenType.IDENTIFIER):
                param_name = self._advance().value
                if self._check(TokenType.EQUALS):
                    self._advance()
                    param_value = self._parse_value()
                    params[param_name] = param_value
                else:
                    # Could be a keyword like 'model=' without value
                    pass
            elif self._check(TokenType.COMMA):
                self._advance()
            else:
                break

        return SCSInstance(name=name, model=model, connections=connections, params=params)


class SpectreRunScriptParser:
    """Parser for Spectre run scripts (tempRun, runSimulation)."""

    # Parameters to extract from run scripts
    KEEP_PARAMS = [
        'I',          # Include paths
        'escchars',   # Escape characters
        'aps',        # APS setting
        'mt',         # Multi-threading
        'maxw',      # Max warnings
        'maxn',       # Max notes
        'logstatus',  # Log status
    ]

    # Parameters to ignore (ADE-specific, will cause errors in CLI)
    IGNORE_PARAMS = [
        'env',          # ADE environment (-env ade)
        'adespetkn',    # ADE security token
        'ahdllibdir',   # ADE AHDL library directory
        'format',       # Use psfascii instead
        'raw',          # Use current folder
        'log',          # Use current folder
    ]

    def __init__(self, content: str):
        self.content = content.strip()
        self.include_paths: List[str] = []
        self.options: Dict[str, str] = {}
        self.ignored: Dict[str, str] = {}
        self._parse()

    def _parse(self) -> None:
        """Parse the run script content."""
        # Split into tokens while preserving -I paths
        tokens = self._tokenize()

        i = 0
        while i < len(tokens):
            token = tokens[i]

            # Handle -I include paths (with or without space)
            if token == '-I':
                # -I <path> (with space)
                if i + 1 < len(tokens):
                    self.include_paths.append(tokens[i + 1])
                    i += 2
                    continue
            elif token.startswith('-I') and len(token) > 2:
                # -I<path> (no space)
                self.include_paths.append(token[2:])
                i += 1
                continue

            # Handle flags like +escchars, ++aps=liberal, +mt=16
            if token.startswith('+') or token.startswith('++'):
                key = token.lstrip('+')
                if '=' in key:
                    k, v = key.split('=', 1)
                    self.options[k] = v
                else:
                    self.options[key] = 'yes'

            # Handle -flag value pairs
            elif token.startswith('-') and not token.startswith('-I'):
                flag = token.lstrip('-')
                # Check if next token is a value (not another flag)
                if i + 1 < len(tokens):
                    next_token = tokens[i + 1]
                    if not next_token.startswith('-') and not next_token.startswith('+'):
                        if flag in self.KEEP_PARAMS:
                            self.options[flag] = next_token
                        elif flag in self.IGNORE_PARAMS:
                            self.ignored[flag] = next_token
                        i += 2
                        continue
                    else:
                        # Flag without value
                        if flag in self.KEEP_PARAMS:
                            self.options[flag] = 'yes'
                        elif flag in self.IGNORE_PARAMS:
                            self.ignored[flag] = ''
                else:
                    # Flag at end
                    if flag in self.KEEP_PARAMS:
                        self.options[flag] = 'yes'
                    elif flag in self.IGNORE_PARAMS:
                        self.ignored[flag] = ''

            i += 1

    def _tokenize(self) -> List[str]:
        """Simple tokenization of the run script."""
        # Remove 'spectre' at the beginning
        content = self.content
        if content.startswith('spectre'):
            content = content[7:].strip()

        # Split on whitespace but preserve quoted strings
        tokens = []
        current = ''
        in_quote = False
        quote_char = None

        for char in content:
            if char in ('"', "'") and not in_quote:
                in_quote = True
                quote_char = char
                current += char
            elif char == quote_char and in_quote:
                in_quote = False
                quote_char = None
                current += char
            elif char.isspace() and not in_quote:
                if current:
                    tokens.append(current)
                    current = ''
            else:
                current += char

        if current:
            tokens.append(current)

        return tokens


def parse_scs(content: str) -> SCSNetlist:
    """Parse SCS content into an AST.

    Args:
        content: The SCS netlist content as a string.

    Returns:
        An SCSNetlist object containing the parsed netlist.
    """
    parser = Parser(content)
    return parser.parse()


def parse_scs_file(file_path: str) -> SCSNetlist:
    """Parse an SCS file into an AST.

    Args:
        file_path: Path to the SCS file.

    Returns:
        An SCSNetlist object containing the parsed netlist.
    """
    with open(file_path, 'r') as f:
        content = f.read()
    return parse_scs(content)


def parse_run_script(file_path: str) -> SpectreRunScriptParser:
    """Parse a Spectre run script (tempRun, runSimulation).

    Args:
        file_path: Path to the run script file.

    Returns:
        A SpectreRunScriptParser object with extracted parameters.
    """
    with open(file_path, 'r') as f:
        content = f.read()
    return SpectreRunScriptParser(content)
