"""Parser and editor for LTspice .asc schematic files.

LTspice .asc format is a simple text format with components, wires, and directives.
"""

import re
from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class Component:
    """A component instance in the schematic."""

    name: str  # Instance name (e.g., R1, C1, M1)
    symbol: str  # Symbol name (e.g., res, cap, nmos)
    x: int
    y: int
    rotation: int  # 0, 90, 180, 270
    mirror: bool
    attributes: dict[str, str] = field(default_factory=dict)

    @property
    def value(self) -> str | None:
        """Get component value (e.g., '10k' for resistor)."""
        return self.attributes.get("Value") or self.attributes.get("Value2")

    @value.setter
    def value(self, val: str):
        """Set component value."""
        self.attributes["Value"] = val


@dataclass
class Wire:
    """A wire connection."""

    x1: int
    y1: int
    x2: int
    y2: int


@dataclass
class Text:
    """Text annotation or SPICE directive."""

    x: int
    y: int
    content: str
    type: str = "comment"  # "comment" or "spice"


@dataclass
class Flag:
    """A net flag/label."""

    x: int
    y: int
    name: str
    type: str = "0"  # Net type


@dataclass
class Schematic:
    """A parsed LTspice schematic."""

    version: int = 4
    sheet: tuple[int, int, int, int] = (1, 1, 0, 0)
    components: list[Component] = field(default_factory=list)
    wires: list[Wire] = field(default_factory=list)
    texts: list[Text] = field(default_factory=list)
    flags: list[Flag] = field(default_factory=list)

    def get_component(self, name: str) -> Component | None:
        """Get component by instance name (case-insensitive)."""
        name_lower = name.lower()
        for comp in self.components:
            if comp.name.lower() == name_lower:
                return comp
        return None

    def get_components_by_symbol(self, symbol: str) -> list[Component]:
        """Get all components using a specific symbol."""
        symbol_lower = symbol.lower()
        return [c for c in self.components if c.symbol.lower() == symbol_lower]

    def get_spice_directives(self) -> list[str]:
        """Get all SPICE directives from the schematic."""
        return [t.content for t in self.texts if t.type == "spice"]


def parse_schematic(path: Path | str) -> Schematic:
    """Parse an LTspice .asc schematic file.

    Args:
        path: Path to the .asc file

    Returns:
        Schematic object with parsed contents
    """
    path = Path(path)
    content = path.read_text(encoding="utf-8", errors="replace")
    lines = content.split("\n")

    schematic = Schematic()
    current_component: Component | None = None

    i = 0
    while i < len(lines):
        line = lines[i].strip()

        if line.startswith("Version"):
            parts = line.split()
            if len(parts) >= 2:
                schematic.version = int(parts[1])

        elif line.startswith("SHEET"):
            parts = line.split()
            if len(parts) >= 5:
                schematic.sheet = (int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]))

        elif line.startswith("WIRE"):
            parts = line.split()
            if len(parts) >= 5:
                schematic.wires.append(
                    Wire(int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]))
                )

        elif line.startswith("FLAG"):
            parts = line.split()
            if len(parts) >= 4:
                schematic.flags.append(
                    Flag(
                        int(parts[1]), int(parts[2]), parts[3], parts[4] if len(parts) > 4 else "0"
                    )
                )

        elif line.startswith("SYMBOL"):
            # Save previous component before starting a new one
            if current_component and current_component.name:
                schematic.components.append(current_component)

            # Start of a new component
            parts = line.split()
            if len(parts) >= 4:
                symbol = parts[1]
                x = int(parts[2])
                y = int(parts[3])

                # Parse rotation/mirror from remaining parts
                rotation = 0
                mirror = False
                if len(parts) > 4:
                    rot_str = parts[4]
                    if rot_str.startswith("M"):
                        mirror = True
                        rot_str = rot_str[1:]
                    if rot_str.startswith("R"):
                        rotation = int(rot_str[1:])

                current_component = Component(
                    name="", symbol=symbol, x=x, y=y, rotation=rotation, mirror=mirror
                )
            else:
                current_component = None

        elif line.startswith("SYMATTR") and current_component:
            parts = line.split(None, 2)
            if len(parts) >= 3:
                attr_name = parts[1]
                attr_value = parts[2]

                if attr_name == "InstName":
                    current_component.name = attr_value
                else:
                    current_component.attributes[attr_name] = attr_value

        elif line.startswith("TEXT"):
            # Parse text/directive
            match = re.match(r"TEXT\s+(-?\d+)\s+(-?\d+)\s+(\w+)\s+(\d+)\s*(.*)", line)
            if match:
                x, y = int(match.group(1)), int(match.group(2))
                # groups 3 (align) and 4 (size) are parsed but not stored
                content = match.group(5) if match.group(5) else ""

                # Check for multi-line text (continuation with \n or actual newlines)
                text_type = "comment"
                if content.startswith("!"):
                    text_type = "spice"
                    content = content[1:]
                elif content.startswith(";"):
                    content = content[1:]

                schematic.texts.append(Text(x, y, content, text_type))

        i += 1

    # Save the last component if not already saved
    if current_component and current_component.name:
        if current_component not in schematic.components:
            schematic.components.append(current_component)

    return schematic


def write_schematic(schematic: Schematic, path: Path | str) -> None:
    """Write a schematic to an .asc file.

    Args:
        schematic: Schematic to write
        path: Output path
    """
    path = Path(path)
    lines = []

    lines.append(f"Version {schematic.version}")
    lines.append(
        f"SHEET {schematic.sheet[0]} {schematic.sheet[1]} {schematic.sheet[2]} {schematic.sheet[3]}"
    )

    # Write wires
    for wire in schematic.wires:
        lines.append(f"WIRE {wire.x1} {wire.y1} {wire.x2} {wire.y2}")

    # Write flags
    for flag in schematic.flags:
        lines.append(f"FLAG {flag.x} {flag.y} {flag.name}")

    # Write components
    for comp in schematic.components:
        rot_str = f"R{comp.rotation}"
        if comp.mirror:
            rot_str = "M" + rot_str

        lines.append(f"SYMBOL {comp.symbol} {comp.x} {comp.y} {rot_str}")
        lines.append(f"SYMATTR InstName {comp.name}")

        for attr_name, attr_value in comp.attributes.items():
            if attr_name != "InstName":
                lines.append(f"SYMATTR {attr_name} {attr_value}")

    # Write text/directives
    for text in schematic.texts:
        prefix = "!" if text.type == "spice" else ""
        lines.append(f"TEXT {text.x} {text.y} Left 2 {prefix}{text.content}")

    path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def modify_component_value(
    path: Path | str, component_name: str, new_value: str, output_path: Path | str | None = None
) -> Schematic:
    """Modify a component's value in a schematic.

    Args:
        path: Input schematic path
        component_name: Name of component to modify (e.g., "R1")
        new_value: New value to set
        output_path: Output path (defaults to overwriting input)

    Returns:
        Modified schematic

    Raises:
        ValueError: If component not found
    """
    schematic = parse_schematic(path)

    comp = schematic.get_component(component_name)
    if not comp:
        available = [c.name for c in schematic.components]
        raise ValueError(
            f"Component '{component_name}' not found. Available components: {', '.join(available)}"
        )

    comp.value = new_value

    write_schematic(schematic, output_path or path)
    return schematic
