"""Lightweight S-expression tokenizer and tree builder for KiCad files.

KiCad uses S-expressions for all file formats (.kicad_sch, .kicad_pcb,
.kicad_sym, sym-lib-table, etc.).  This module provides a generic parser
that turns the text into nested Python lists, plus helpers for querying
the resulting tree.

Example:
    >>> tree = parse('(kicad_sch (version 20250114) (generator "eeschema"))')
    >>> tree
    ['kicad_sch', ['version', '20250114'], ['generator', 'eeschema']]
    >>> find_node(tree, 'version')
    ['version', '20250114']
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Union

# A parsed node is either a string (atom) or a list of nodes.
Node = Union[str, list]

# Regex that matches one token at a time:
#  - quoted string (captures content without quotes)
#  - open/close paren
#  - unquoted atom (anything that isn't whitespace or parens)
_TOKEN_RE = re.compile(r'"((?:[^"\\]|\\.)*)"|(\()|(\))|([^\s()]+)')


def tokenize(text: str):
    """Yield tokens from S-expression text.

    Tokens are: '(', ')', or string values (with quotes stripped).
    """
    for m in _TOKEN_RE.finditer(text):
        quoted, lparen, rparen, atom = m.groups()
        if lparen:
            yield "("
        elif rparen:
            yield ")"
        elif quoted is not None:
            # Unescape backslash-escaped characters inside quoted strings
            yield quoted.replace('\\"', '"').replace("\\\\", "\\")
        else:
            yield atom


def parse(text: str) -> Node:
    """Parse S-expression text into a nested list structure.

    Returns the first top-level expression found.
    Raises ValueError if the text is malformed.
    """
    tokens = list(tokenize(text))
    if not tokens:
        raise ValueError("Empty S-expression")

    result, _ = _parse_expr(tokens, 0)
    return result


def parse_all(text: str) -> list[Node]:
    """Parse all top-level S-expressions from text."""
    tokens = list(tokenize(text))
    results = []
    pos = 0
    while pos < len(tokens):
        if tokens[pos] == "(":
            node, pos = _parse_expr(tokens, pos)
            results.append(node)
        else:
            pos += 1
    return results


def _parse_expr(tokens: list[str], pos: int) -> tuple[Node, int]:
    """Parse one expression starting at `pos`. Returns (node, new_pos)."""
    if tokens[pos] == "(":
        pos += 1  # skip '('
        children = []
        while pos < len(tokens) and tokens[pos] != ")":
            child, pos = _parse_expr(tokens, pos)
            children.append(child)
        if pos < len(tokens):
            pos += 1  # skip ')'
        return children, pos
    else:
        return tokens[pos], pos + 1


def parse_file(filepath: str | Path) -> Node:
    """Parse a KiCad file and return the top-level S-expression tree."""
    text = Path(filepath).read_text(encoding="utf-8")
    return parse(text)


# ── Tree query helpers ──────────────────────────────────────────────


def find_node(tree: list, tag: str) -> list | None:
    """Find the first direct child node with the given tag name.

    A "node" is a list whose first element is a string matching `tag`.
    Returns None if not found.
    """
    if not isinstance(tree, list):
        return None
    for child in tree:
        if isinstance(child, list) and child and child[0] == tag:
            return child
    return None


def find_nodes(tree: list, tag: str) -> list[list]:
    """Find all direct child nodes with the given tag name."""
    if not isinstance(tree, list):
        return []
    return [c for c in tree if isinstance(c, list) and c and c[0] == tag]


def find_node_recursive(tree: list, tag: str) -> list | None:
    """Find the first node with the given tag anywhere in the tree (DFS)."""
    if not isinstance(tree, list):
        return None
    if tree and tree[0] == tag:
        return tree
    for child in tree:
        if isinstance(child, list):
            result = find_node_recursive(child, tag)
            if result is not None:
                return result
    return None


def find_nodes_recursive(tree: list, tag: str) -> list[list]:
    """Find all nodes with the given tag anywhere in the tree (DFS)."""
    results = []
    _find_nodes_recursive_impl(tree, tag, results)
    return results


def _find_nodes_recursive_impl(tree, tag, results):
    if not isinstance(tree, list):
        return
    if tree and tree[0] == tag:
        results.append(tree)
    for child in tree:
        if isinstance(child, list):
            _find_nodes_recursive_impl(child, tag, results)


def node_value(node: list | None) -> str | None:
    """Get the second element of a node as a string, or None."""
    if node and len(node) >= 2 and isinstance(node[1], str):
        return node[1]
    return None


def node_values(node: list | None) -> list[str]:
    """Get all string elements after the tag name."""
    if not node:
        return []
    return [v for v in node[1:] if isinstance(v, str)]