import random
from collections.abc import Iterator, Sequence
from typing import TYPE_CHECKING, Any, Optional

import fandango.language.grammar.nodes as nodes
from fandango.errors import FandangoValueError
from fandango.language.grammar.has_settings import HasSettings
from fandango.language.grammar.nodes.alternative import Alternative
from fandango.language.grammar.nodes.concatenation import Concatenation
from fandango.language.grammar.nodes.node import Node, NodeType
from fandango.language.grammar.nodes.terminal import TerminalNode
from fandango.language.symbols.terminal import Terminal
from fandango.language.tree import DerivationTree
from fandango.language.symbols.symbol import Symbol
from fandango.language.symbols.non_terminal import NonTerminal

if TYPE_CHECKING:
    import fandango.language.grammar.node_visitors


class Repetition(Node):
    def __init__(
        self,
        node: Node,
        grammar_settings: Sequence[HasSettings],
        id: str = "",
        min_: int = 0,
        max_: Optional[int] = None,
        distance_to_completion: float = float("inf"),
    ):
        self.id = id
        self.min = min_
        self._max = max_
        self.node = node
        self.bounds_constraint: Optional[
            "fandango.constraints.repetition_bounds.RepetitionBoundsConstraint"
        ] = None
        self.iteration = 0
        if min_ < 0:
            raise FandangoValueError(
                f"Minimum repetitions {min_} must be greater than or equal to 0"
            )
        if self.max <= 0 or self.max < min_:
            raise FandangoValueError(
                f"Maximum repetitions {self.max} must be greater than 0 or greater than min {min_}"
            )
        super().__init__(
            NodeType.REPETITION,
            grammar_settings,
            distance_to_completion=distance_to_completion,
        )

    def to_symbol(self) -> Symbol:
        return NonTerminal(f"<__{self.id}>")

    @property
    def internal_max(self) -> Optional[int]:
        return self._max

    @property
    def max(self) -> int:
        if self._max is None:
            return nodes.MAX_REPETITIONS
        return self._max

    def accept(
        self,
        visitor: "fandango.language.grammar.node_visitors.node_visitor.NodeVisitor[fandango.language.grammar.node_visitors.node_visitor.AggregateType, fandango.language.grammar.node_visitors.node_visitor.ResultType]",
    ) -> Any:  # should be ResultType, beartype falls on its face
        return visitor.visitRepetition(self)

    def fuzz(
        self,
        parent: DerivationTree,
        grammar: "fandango.language.grammar.grammar.Grammar",
        max_nodes: int = 100,
        in_message: bool = False,
        override_current_iteration: Optional[int] = None,
        override_starting_repetition: int = 0,
        override_iterations_to_perform: Optional[int] = None,
    ) -> None:
        prev_parent_size = parent.size()
        prev_children_len = len(parent.children)
        if override_current_iteration is None:
            self.iteration += 1
            current_iteration = self.iteration
        else:
            current_iteration = override_current_iteration

        rep_goal = random.randint(self.min, self.max)
        if override_iterations_to_perform is not None:
            rep_goal = override_iterations_to_perform - override_starting_repetition

        reserved_max_nodes = self.distance_to_completion

        for rep in range(rep_goal):
            current_rep = rep + override_starting_repetition
            if self.node.distance_to_completion >= max_nodes:
                if rep >= self.min and override_iterations_to_perform is None:
                    break
                self.node.fuzz(parent, grammar, 0, in_message)
            else:
                reserved_max_nodes -= self.node.distance_to_completion
                self.node.fuzz(
                    parent, grammar, int(max_nodes - reserved_max_nodes), in_message
                )
            for child in parent.children[prev_children_len:]:
                child.origin_repetitions.insert(
                    0, (self.id, current_iteration, current_rep)
                )
            max_nodes -= parent.size() - prev_parent_size
            prev_parent_size = parent.size()
            prev_children_len = len(parent.children)

    def format_as_spec(self) -> str:
        if self.min == self.max:
            return f"{self.node.format_as_spec()}{{{self.min}}}"
        return f"{self.node.format_as_spec()}{{{self.min},{self.max}}}"

    def descendents(
        self,
        grammar: "fandango.language.grammar.grammar.Grammar",
        filter_controlflow: bool = False,
    ) -> Iterator["Node"]:
        base: list[Node] = []
        # if self.min == 0:
        # base.append(TerminalNode(Terminal(""), self._grammar_settings))
        if 0 < self.max:
            if filter_controlflow and self.node.is_controlflow:
                base.extend(self.node.descendents(grammar, filter_controlflow=True))
            else:
                base.append(self.node)
        if filter_controlflow:
            yield from base
            return

        yield Alternative(
            base
            + [
                Concatenation([self.node] * r, self._grammar_settings)
                for r in range(max(2, self.min), self.max + 1)
            ],
            self._grammar_settings,
        )

    def children(self) -> list[Node]:
        return [self.node]

    def in_parties(self, parties: list[str]) -> bool:
        return self.node.in_parties(parties)


class Star(Repetition):
    def __init__(
        self,
        node: Node,
        grammar_settings: Sequence[HasSettings],
        id: str = "",
    ):
        super().__init__(node, grammar_settings, id, min_=0, distance_to_completion=0.0)

    def to_symbol(self) -> Symbol:
        return NonTerminal(f"<__{self.id}>")

    def accept(
        self,
        visitor: "fandango.language.grammar.node_visitors.node_visitor.NodeVisitor[fandango.language.grammar.node_visitors.node_visitor.AggregateType, fandango.language.grammar.node_visitors.node_visitor.ResultType]",
    ) -> Any:  # should be ResultType, beartype falls on its face
        return visitor.visitStar(self)

    def format_as_spec(self) -> str:
        return self.node.format_as_spec() + "*"


class Plus(Repetition):
    def __init__(
        self,
        node: Node,
        grammar_settings: Sequence[HasSettings],
        id: str = "",
    ):
        super().__init__(node, grammar_settings, id, min_=1)

    def to_symbol(self) -> Symbol:
        return NonTerminal(f"<__{self.id}>")

    def fuzz(
        self,
        parent: DerivationTree,
        grammar: "fandango.language.grammar.grammar.Grammar",
        max_nodes: int = 100,
        in_message: bool = False,
        override_current_iteration: Optional[int] = None,
        override_starting_repetition: int = 0,
        override_iterations_to_perform: Optional[int] = None,
    ) -> None:
        # Gmutator mutation (1b)
        if random.random() < self.settings.get("plus_should_return_nothing"):
            return  # nop, don't add a node
        else:
            return super().fuzz(
                parent,
                grammar,
                max_nodes,
                in_message,
                override_current_iteration,
                override_starting_repetition,
                override_iterations_to_perform,
            )

    def accept(
        self,
        visitor: "fandango.language.grammar.node_visitors.node_visitor.NodeVisitor[fandango.language.grammar.node_visitors.node_visitor.AggregateType, fandango.language.grammar.node_visitors.node_visitor.ResultType]",
    ) -> Any:  # should be ResultType, beartype falls on its face
        return visitor.visitPlus(self)

    def format_as_spec(self) -> str:
        return self.node.format_as_spec() + "+"


class Option(Repetition):
    def __init__(
        self,
        node: Node,
        grammar_settings: Sequence[HasSettings],
        id: str = "",
    ):
        super().__init__(
            node, grammar_settings, id, min_=0, max_=1, distance_to_completion=0.0
        )

    def to_symbol(self) -> Symbol:
        return NonTerminal(f"<__{self.id}>")

    def fuzz(
        self,
        parent: DerivationTree,
        grammar: "fandango.language.grammar.grammar.Grammar",
        max_nodes: int = 100,
        in_message: bool = False,
        override_current_iteration: Optional[int] = None,
        override_starting_repetition: int = 0,
        override_iterations_to_perform: Optional[int] = None,
    ) -> None:
        # Gmutator mutation (1c)
        should_return_multiple = random.random() < self.settings.get(
            "option_should_return_multiple"
        )
        if should_return_multiple:
            repetition = Repetition(
                self.node, self._grammar_settings, id=self.id, min_=2
            )
            repetition.distance_to_completion = self.node.distance_to_completion * 2 + 1
            return repetition.fuzz(
                parent,
                grammar,
                max_nodes,
                in_message,
                override_current_iteration,
                override_starting_repetition,
                override_iterations_to_perform,
            )
        else:
            return super().fuzz(
                parent,
                grammar,
                max_nodes,
                in_message,
                override_current_iteration,
                override_starting_repetition,
                override_iterations_to_perform,
            )

    def accept(
        self,
        visitor: "fandango.language.grammar.node_visitors.node_visitor.NodeVisitor[fandango.language.grammar.node_visitors.node_visitor.AggregateType, fandango.language.grammar.node_visitors.node_visitor.ResultType]",
    ) -> Any:  # should be ResultType, beartype falls on its face
        return visitor.visitOption(self)

    def format_as_spec(self) -> str:
        return self.node.format_as_spec() + "?"
