# Copyright (c) 2017-2026 Juancarlo Añez (apalala@gmail.com)
# SPDX-License-Identifier: BSD-4-Clause
from __future__ import annotations

from typing import Any, Protocol

from ..exceptions import FailedLeftRecursion
from .infos import RuleInfo
from ..parserconfig import ParserConfig
from ..tokenizing import Cursor
from ..util import color, info
from ..util.unicode_characters import C_CUT, C_ENTRY, C_FAILURE, C_RECURSION, C_SUCCESS


class EventColor(color.Color):
    def __init__(self):
        self.ENTRY = self.YELLOW + self.BRIGHT + C_ENTRY
        self.SUCCESS = self.GREEN + self.BRIGHT + C_SUCCESS
        self.FAILURE = self.RED + self.BRIGHT + C_FAILURE
        self.RECURSION = self.RED + self.BRIGHT + C_RECURSION
        self.CUT = self.MAGENTA + self.BRIGHT + C_CUT


C = EventColor()


class EventTracer(Protocol):
    def trace(self, msg: str, *args: Any, **kwargs: Any) -> None: ...

    def trace_event(self, cursor: Cursor, event: str) -> None: ...

    def trace_entry(self, cursor: Cursor) -> None: ...

    def trace_success(self, cursor: Cursor) -> None: ...

    def trace_failure(self, cursor: Cursor, ex: Exception | None = None) -> None: ...

    def trace_recursion(self, cursor: Cursor) -> None: ...

    def trace_cut(self, cursor: Cursor) -> None: ...

    def trace_match(
        self,
        cursor: Cursor,
        token: Any,
        name: str | None = None,
        failed: bool = False,
    ) -> None: ...

    def rulestack(self) -> str: ...


class InfoEventTracer(EventTracer):
    def __init__(
        self,
        ruleinfos: list[RuleInfo],
        *,
        config: ParserConfig | None = None,
        **settings: Any,
    ) -> None:
        self.config = ParserConfig.new(config, **settings)

        # NOTE: not copying / sharing same list with caller
        self.ruleinfos = ruleinfos

        if self.config.colorize:
            color.init()
        global C  # noqa: PLW0603
        C = EventColor()

    def trace(self, msg: str, *args: Any, **kwargs: Any) -> None:
        if not self.config.trace:
            return

        info(msg, *args, **kwargs)

    def trace_event(
        self,
        cursor: Cursor,
        event: str,
    ) -> None:
        if not self.config.trace:
            return

        fname = ''
        if self.config.trace_filename:
            fname = cursor.lineinfo().filename
        if fname:
            fname += '\n'

        lookahead = cursor.lookahead().rstrip()
        lookahead = '\n' + lookahead if lookahead else ''

        message = (
            f'{event}{self.rulestack()}'
            f' {C.DIM}{fname}'
            f'{cursor.lookahead_pos()}{C.RESET}'
            f'{C.RESET_ALL}{lookahead}{C.RESET_ALL}'
        )
        self.trace(message)

    def trace_entry(self, cursor: Cursor) -> None:
        self.trace_event(cursor, f'{C.ENTRY}')

    def trace_success(self, cursor: Cursor) -> None:
        self.trace_event(cursor, f'{C.SUCCESS}')

    def trace_failure(self, cursor: Cursor, ex: Exception | None = None) -> None:
        if isinstance(ex, FailedLeftRecursion):
            self.trace_recursion(cursor)
        else:
            self.trace_event(cursor, f'{C.FAILURE}')

    def trace_recursion(self, cursor: Cursor) -> None:
        self.trace_event(cursor, f'{C.RECURSION}')

    def trace_cut(self, cursor: Cursor) -> None:
        self.trace_event(cursor, f'{C.CUT}')

    def trace_match(
        self,
        cursor: Cursor,
        token: Any,
        name: str | None = None,
        failed: bool = False,
    ) -> None:
        if not self.config.trace:
            return

        name_str = f'/{name}/' if name else ''
        if self.config.trace_filename:
            fname = cursor.lineinfo().filename + '\n'
        else:
            fname = ''

        mark = f'{C.FAILURE}' if failed else f'{C.SUCCESS}'

        lookahead = cursor.lookahead().rstrip()
        lookahead = '\n' + lookahead if lookahead else lookahead

        message = (
            f'{mark}'
            f"'{token}{name_str}"
            f'{C.DIM}{fname}'
            f'{C.RESET_ALL}{lookahead}{C.RESET_ALL}'
        )
        self.trace(message)

    def rulestack(self) -> str:
        rulestack = [r.name for r in reversed(self.ruleinfos)]
        stack = self.config.trace_separator.join(rulestack)
        if (
            max((len(s) for s in stack.splitlines()), default=0)
            > self.config.trace_length
        ):
            stack = stack[: self.config.trace_length]
            stack = stack.rsplit(self.config.trace_separator, 1)[0]
            stack += self.config.trace_separator
        return stack


class NullEventTracer(EventTracer):
    def trace(self, msg: str, *args: Any, **kwargs: Any) -> None:
        pass

    def trace_event(self, cursor: Cursor, event: str) -> None:
        pass

    def trace_entry(self, cursor: Cursor) -> None:
        pass

    def trace_success(self, cursor: Cursor) -> None:
        pass

    def trace_failure(self, cursor: Cursor, ex: Exception | None = None) -> None:
        pass

    def trace_recursion(self, cursor: Cursor) -> None:
        pass

    def trace_cut(self, cursor: Cursor) -> None:
        pass

    def trace_match(
        self,
        cursor: Cursor,
        token: Any,
        name: str | None = None,
        failed: bool = False,
    ) -> None:
        pass

    def rulestack(self) -> str:
        return ""
