"""Common parse tree nodes."""

from abc import ABC
from abc import abstractmethod
from typing import Collection
from typing import Dict
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import TextIO

from typing_extensions import Literal

from liquid.context import Context
from liquid.exceptions import DisabledTagError
from liquid.exceptions import Error
from liquid.expression import Expression
from liquid.token import TOKEN_ILLEGAL
from liquid.token import TOKEN_TAG
from liquid.token import Token

# ruff: noqa: D102

IllegalToken = Token(-1, TOKEN_ILLEGAL, "")


class Node(ABC):
    """Base class for all nodes in a parse tree."""

    __slots__ = ()

    # Indicates that nodes that do automatic whitespace suppression should output this
    # node regardless of its contents.
    force_output = False

    def token(self) -> Token:
        """The token that started this node."""
        token: Token = getattr(self, "tok", IllegalToken)
        return token

    def raise_for_disabled(self, disabled_tags: Collection[str]) -> None:
        """Raise a DisabledTagError if this node's type is in the given list."""
        tok = self.token()
        if tok.type == TOKEN_TAG and tok.value in disabled_tags:
            raise DisabledTagError(
                f"{tok.value} usage is not allowed in this context",
                linenum=tok.linenum,
            )

    def render(self, context: Context, buffer: TextIO) -> Optional[bool]:
        """Check disabled tags before delegating to `render_to_output`."""
        if context.disabled_tags:
            self.raise_for_disabled(context.disabled_tags)
        return self.render_to_output(context, buffer)

    async def render_async(self, context: Context, buffer: TextIO) -> Optional[bool]:
        """An async version of `liquid.ast.Node.render`."""
        if context.disabled_tags:
            self.raise_for_disabled(context.disabled_tags)
        if hasattr(self, "render_to_output_async"):
            return await self.render_to_output_async(context, buffer)
        return self.render_to_output(context, buffer)

    @abstractmethod
    def render_to_output(
        self,
        context: Context,
        buffer: TextIO,
    ) -> Optional[bool]:
        """Render this node to the output buffer."""

    async def render_to_output_async(
        self,
        context: Context,
        buffer: TextIO,
    ) -> Optional[bool]:
        """An async version of `liquid.ast.Node.render_to_output`."""
        return self.render_to_output(context, buffer)

    def children(self) -> List["ChildNode"]:
        """Return a list of child nodes and/or expressions associated with this node."""
        raise NotImplementedError(f"{self.__class__.__name__}.children")


class ChildNode(NamedTuple):
    """An AST node and expression pair with optional scope and load data.

    Args:
        linenum: The line number of the child's first token in the template source text.
        expression: An `liquid.expression.Expression`. If not `None`, this expression is
            expected to be related to the given `liquid.ast.Node`.
        node: A `liquid.ast.Node`. Typically a `BlockNode` or `ConditionalBlockNode`.
        template_scope: A list of names the parent node adds to the template "local"
            scope. For example, the built-in `assign`, `capture`, `increment` and
            `decrement` tags all add names to the template scope. This helps us
            identify, through static analysis, names that are assumed to be "global".
        block_scope: A list of names available to the given child node. For example,
            the `for` tag adds the name "forloop" for the duration of its block.
        load_mode: If not `None`, indicates that the given expression should be used to
            load a partial template. In "render" mode, the partial will be analyzed in
            an isolated namespace, without access to the parent's template local scope.
            In "include" mode, the partial will have access to the parents template
            local scope and the parent's scope can be updated by the partial template
            too.
        load_context: Meta data a template `Loader` might need to find the source
            of a partial template.
    """

    linenum: int
    expression: Optional[Expression] = None
    node: Optional[Node] = None
    template_scope: Optional[List[str]] = None
    block_scope: Optional[List[str]] = None
    load_mode: Optional[Literal["render", "include", "extends"]] = None
    load_context: Optional[Dict[str, str]] = None


class ParseTree(Node):
    """The root node of all syntax trees."""

    __slots__ = ("statements", "version")

    def __init__(self) -> None:
        self.statements: List[Node] = []

    def __str__(self) -> str:  # pragma: no cover
        return "".join(str(s) for s in self.statements)

    def __repr__(self) -> str:
        return f"ParseTree({self.statements})"

    def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
        for stmt in self.statements:
            stmt.render(context, buffer)
        return None


class IllegalNode(Node):
    """Parse tree node representing an illegal or unregistered tag.

    Illegal nodes are necessary when running in "warn" or "lax" mode. In
    strict mode, an exception should be raised as soon as an illegal token
    is found.
    """

    __slots__ = ("tok",)

    def __init__(self, tok: Token):
        self.tok = tok

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

    def __repr__(self) -> str:  # pragma: no cover
        return f"IllegalNode(tok={self.tok})"

    def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
        pass


class BlockNode(Node):
    """A parse tree node representing a sequence of statements."""

    __slots__ = ("tok", "statements", "forced_output")

    def __init__(self, tok: Token, statements: Optional[List[Node]] = None):
        self.tok = tok
        self.statements = statements or []
        self.forced_output = False

    def __str__(self) -> str:
        return "".join(str(s) for s in self.statements)

    def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
        for stmt in self.statements:
            try:
                stmt.render(context, buffer)
            except Error as err:
                # Maybe resume rendering the block after an error.
                context.error(err)
        return True

    async def render_to_output_async(
        self, context: Context, buffer: TextIO
    ) -> Optional[bool]:
        for stmt in self.statements:
            try:
                await stmt.render_async(context, buffer)
            except Error as err:
                # Maybe resume rendering the block after an error.
                context.error(err)
        return True

    def children(self) -> List["ChildNode"]:
        return [
            ChildNode(
                linenum=self.tok.linenum,
                node=stmt,
            )
            for stmt in self.statements
        ]


class ConditionalBlockNode(Node):
    """A node containing a sequence of statements and a conditional expression."""

    __slots__ = ("tok", "condition", "block", "forced_output")

    def __init__(self, tok: Token, condition: Expression, block: BlockNode):
        self.tok = tok
        self.condition = condition
        self.block = block
        self.forced_output = block.forced_output

    def __str__(self) -> str:
        return f"{self.condition} {{ {self.block} }}"

    def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
        if self.condition.evaluate(context):
            self.block.render(context, buffer)
            return True
        return False

    async def render_to_output_async(
        self, context: Context, buffer: TextIO
    ) -> Optional[bool]:
        if await self.condition.evaluate_async(context):
            await self.block.render_async(context, buffer)
            return True
        return False

    def children(self) -> List["ChildNode"]:
        return [
            ChildNode(
                linenum=self.tok.linenum,
                node=self.block,
                expression=self.condition,
            )
        ]
