"""State-machine lexer with O(n) guaranteed performance.

Implements a window-based approach: scan entire lines, classify, then commit.
This eliminates position rewinds and guarantees forward progress.

No regex in the hot path. Zero ReDoS vulnerability by construction.

Thread Safety:
Lexer instances are single-use. Create one per source string.
All state is instance-local; no shared mutable state.

"""

from collections.abc import Callable, Iterator

from patitas.lexer.classifiers import (
    DirectiveClassifierMixin,
    FenceClassifierMixin,
    FootnoteClassifierMixin,
    HeadingClassifierMixin,
    HtmlClassifierMixin,
    LinkRefClassifierMixin,
    ListClassifierMixin,
    QuoteClassifierMixin,
    ThematicClassifierMixin,
)
from patitas.lexer.modes import LexerMode
from patitas.lexer.scanners import (
    BlockScannerMixin,
    DirectiveScannerMixin,
    FenceScannerMixin,
    HtmlScannerMixin,
)
from patitas.location import SourceLocation
from patitas.tokens import Token, TokenType


class Lexer(
    # Classifiers (pure logic, no position mutation)
    HeadingClassifierMixin,
    FenceClassifierMixin,
    ThematicClassifierMixin,
    QuoteClassifierMixin,
    ListClassifierMixin,
    LinkRefClassifierMixin,
    FootnoteClassifierMixin,
    HtmlClassifierMixin,
    DirectiveClassifierMixin,
    # Scanners (mode-specific scanning logic)
    BlockScannerMixin,
    FenceScannerMixin,
    DirectiveScannerMixin,
    HtmlScannerMixin,
):
    """State-machine lexer with O(n) guaranteed performance.

    Uses a window-based approach for block scanning:
    1. Scan to end of line (find window)
    2. Classify the line (pure logic, no position changes)
    3. Commit position (always advances)

    This eliminates rewinds and guarantees forward progress.

    Usage:
            >>> lexer = Lexer("# Hello\n\nWorld")
            >>> for token in lexer.tokenize():
            ...     print(token)
        Token(ATX_HEADING, '# Hello', 1:1)
        Token(BLANK_LINE, '', 2:1)
        Token(PARAGRAPH_LINE, 'World', 3:1)
        Token(EOF, '', 3:6)

    Thread Safety:
        Lexer instances are single-use. Create one per source string.
        All state is instance-local; no shared mutable state.

    """

    __slots__ = (
        "_col",
        "_consumed_newline",
        # Directive state
        "_directive_stack",  # Stack of (colon_count, name) for nested directives
        "_fence_char",
        "_fence_count",
        "_fence_indent",  # Leading spaces on opening fence for CommonMark stripping
        "_fence_info",  # Language hint from fence start
        "_html_block_content",  # Accumulated HTML content
        "_html_block_indent",  # Indent of opening line for line_indent
        "_html_block_start",  # Start position for location
        # HTML block state
        "_html_block_type",  # 1-7 per CommonMark spec
        "_lineno",
        "_mode",
        "_pos",
        "_previous_line_blank",  # Track blank lines for inline HTML block decisions
        "_saved_col",
        "_saved_lineno",
        "_source",
        "_source_file",
        "_source_len",  # Cached len(source) to avoid repeated calls
        # Transformation
        "_text_transformer",
    )

    def __init__(
        self,
        source: str,
        source_file: str | None = None,
        text_transformer: Callable[[str], str] | None = None,
    ) -> None:
        """Initialize lexer with source text.

        Args:
            source: Markdown source text
            source_file: Optional source file path for error messages
            text_transformer: Optional callback to transform plain text lines
        """
        self._source = source
        self._source_len = len(source)  # Cache length
        self._pos = 0
        self._lineno = 1
        self._col = 1
        self._mode = LexerMode.BLOCK
        self._source_file = source_file
        self._text_transformer = text_transformer

        # Fenced code state
        self._fence_char: str = ""
        self._fence_count: int = 0
        self._fence_info: str = ""
        self._fence_indent: int = 0

        # HTML block state
        self._html_block_type: int = 0
        self._html_block_content: list[str] = []
        self._html_block_start: int = 0
        self._html_block_indent: int = 0
        self._previous_line_blank: bool = True

        # Directive state: stack of (colon_count, name) for nested directives
        self._directive_stack: list[tuple[int, str]] = []

        # Line consumption tracking
        self._consumed_newline: bool = False

        # Saved location for efficient location tracking
        self._saved_lineno: int = 1
        self._saved_col: int = 1

    def tokenize(self) -> Iterator[Token]:
        """Tokenize source into token stream.

        Yields:
            Token objects one at a time

        Complexity: O(n) where n = len(source)
        Memory: O(1) iterator (tokens yielded, not accumulated)
        """
        source_len = self._source_len  # Local var for faster access
        while self._pos < source_len:
            yield from self._dispatch_mode()

        # Emit any accumulated HTML block content at EOF (types 1-5 may not
        # have found their closing delimiter before source ended)
        if self._mode == LexerMode.HTML_BLOCK and self._html_block_content:
            yield from self._emit_html_block()

        yield self._make_token_at_current(TokenType.EOF, "", line_indent=0)

    def _dispatch_mode(self) -> Iterator[Token]:
        """Dispatch to appropriate scanner based on current mode.

        Yields:
            Token objects from the mode-specific scanner.
        """
        if self._mode == LexerMode.BLOCK:
            yield from self._scan_block()
        elif self._mode == LexerMode.CODE_FENCE:
            yield from self._scan_code_fence_content()
        elif self._mode == LexerMode.DIRECTIVE:
            yield from self._scan_directive_content()
        elif self._mode == LexerMode.HTML_BLOCK:
            yield from self._scan_html_block_content()

    # =========================================================================
    # Window navigation helpers
    # =========================================================================

    def _find_line_end(self) -> int:
        """Find the end of the current line (position of \\n or EOF).

        Uses str.find for O(n) with low constant factor (C implementation).

        Returns:
            Position of newline or end of source.
        """
        source = self._source
        idx = source.find("\n", self._pos)
        return idx if idx != -1 else self._source_len

    def _calc_indent(self, line: str) -> tuple[int, int]:
        """Calculate indent level and content start position.

        Spaces count as 1, tabs expand to next multiple of 4.

        Args:
            line: Line content

        Returns:
            (indent_spaces, content_start_index)
        """
        indent = 0
        pos = 0
        line_len = len(line)  # Cache length
        while pos < line_len:
            char = line[pos]
            if char == " ":
                indent += 1
                pos += 1
            elif char == "\t":
                indent += 4 - (indent % 4)
                pos += 1
            else:
                break
        return indent, pos

    def _expand_tabs(self, text: str, start_col: int = 1) -> str:
        """Expand tabs in text to spaces based on start_col (1-indexed)."""
        result = []
        col = start_col
        for char in text:
            if char == "\t":
                expansion = 4 - ((col - 1) % 4)
                result.append(" " * expansion)
                col += expansion
            else:
                result.append(char)
                col += 1
        return "".join(result)

    def _commit_to(self, line_end: int) -> None:
        """Commit position to line_end, consuming newline if present.

        Sets self._consumed_newline to indicate if a newline was consumed.
        Uses optimized string operations instead of character-by-character loop.

        Args:
            line_end: Position to commit to.
        """
        source = self._source
        source_len = self._source_len
        pos = self._pos

        # Fast path: if no position change, skip
        if line_end == pos:
            self._consumed_newline = False
            if pos < source_len and source[pos] == "\n":
                self._pos += 1
                self._lineno += 1
                self._col = 1
                self._consumed_newline = True
            return

        # Count newlines in skipped segment using C-optimized str.count
        segment = source[pos:line_end]
        newline_count = segment.count("\n")

        if newline_count > 0:
            # Find position of last newline to calculate column
            last_nl = segment.rfind("\n")
            self._lineno += newline_count
            self._col = len(segment) - last_nl  # chars after last newline + 1
        else:
            self._col += len(segment)

        self._pos = line_end
        self._consumed_newline = False

        # Consume the newline if present
        if line_end < source_len and source[line_end] == "\n":
            self._pos += 1
            self._lineno += 1
            self._col = 1
            self._consumed_newline = True

    # =========================================================================
    # Location tracking
    # =========================================================================

    def _save_location(self) -> None:
        """Save current location for O(1) token location creation.

        Call this at the START of scanning a line, before any position changes.
        """
        self._saved_lineno = self._lineno
        self._saved_col = self._col

    def _make_token(
        self,
        token_type: TokenType,
        value: str,
        start_pos: int,
        *,
        start_col: int | None = None,
        end_pos: int | None = None,
        line_indent: int = -1,
    ) -> Token:
        """Create a Token with raw coordinates (lazy SourceLocation).

        O(1) - uses pre-saved location from _save_location() call.
        Avoids SourceLocation allocation until token.location is accessed.

        Args:
            token_type: The token type.
            value: The raw string value.
            start_pos: Start position in source.
            start_col: Optional column override (1-indexed).
            end_pos: Optional end position override.
            line_indent: Pre-computed indent level.

        Returns:
            Token with pre-created SourceLocation (avoids property overhead).
        """
        loc = SourceLocation(
            lineno=self._saved_lineno,
            col_offset=start_col if start_col is not None else self._saved_col,
            offset=start_pos,
            end_offset=end_pos if end_pos is not None else self._pos,
            end_lineno=self._lineno,
            end_col_offset=self._col,
            source_file=self._source_file,
        )
        return Token(
            type=token_type,
            value=value,
            _lineno=self._saved_lineno,
            _col=start_col if start_col is not None else self._saved_col,
            _start_offset=start_pos,
            _end_offset=end_pos if end_pos is not None else self._pos,
            line_indent=line_indent,
            _end_lineno=self._lineno,
            _end_col=self._col,
            _source_file=self._source_file,
            _location_cache=loc,
        )

    def _make_token_at_current(
        self,
        token_type: TokenType,
        value: str,
        *,
        line_indent: int = 0,
    ) -> Token:
        """Create a Token at current position (for EOF and similar).

        Args:
            token_type: The token type.
            value: The raw string value.
            line_indent: Pre-computed indent level.

        Returns:
            Token at current position with pre-created SourceLocation.
        """
        loc = SourceLocation(
            lineno=self._lineno,
            col_offset=self._col,
            offset=self._pos,
            end_offset=self._pos,
            end_lineno=self._lineno,
            end_col_offset=self._col,
            source_file=self._source_file,
        )
        return Token(
            type=token_type,
            value=value,
            _lineno=self._lineno,
            _col=self._col,
            _start_offset=self._pos,
            _end_offset=self._pos,
            line_indent=line_indent,
            _end_lineno=self._lineno,
            _end_col=self._col,
            _source_file=self._source_file,
            _location_cache=loc,
        )
