import ast as python_ast
import contextlib
import copy
import decimal
import operator
import sys
from typing import Any, Optional, Union

from vyper.ast.metadata import NodeMetadata
from vyper.compiler.settings import VYPER_ERROR_CONTEXT_LINES, VYPER_ERROR_LINE_NUMBERS
from vyper.exceptions import (
    ArgumentException,
    CompilerPanic,
    InvalidLiteral,
    InvalidOperation,
    OverflowException,
    StructureException,
    SyntaxException,
    TypeMismatch,
    UnfoldableNode,
    ZeroDivisionException,
)
from vyper.utils import MAX_DECIMAL_PLACES, SizeLimits, annotate_source_code

NODE_BASE_ATTRIBUTES = ("_children", "_depth", "_parent", "ast_type", "node_id", "_metadata")
NODE_SRC_ATTRIBUTES = (
    "col_offset",
    "end_col_offset",
    "end_lineno",
    "full_source_code",
    "lineno",
    "node_source_code",
    "src",
)

DICT_AST_SKIPLIST = ("full_source_code", "node_source_code")


def get_node(
    ast_struct: Union[dict, python_ast.AST], parent: Optional["VyperNode"] = None
) -> "VyperNode":
    """
    Convert an AST structure to a vyper AST node.

    This is a recursive call, all child nodes of the input value are also
    converted to Vyper nodes.

    Parameters
    ----------
    ast_struct: dict | AST
        Annotated python AST node or vyper AST dict to generate the node from.
    parent: VyperNode, optional
        Parent node of the node being created.

    Returns
    -------
    VyperNode
        The generated AST object.
    """
    if not isinstance(ast_struct, dict):
        ast_struct = ast_struct.__dict__

        # workaround: some third party module (ex. ipython) might insert
        # a "parent" member into the node, creating a duplicate kwarg
        # error below when calling vy_class()
        if "parent" in ast_struct:
            ast_struct = copy.copy(ast_struct)
            del ast_struct["parent"]

    if ast_struct["ast_type"] == "AnnAssign" and isinstance(parent, Module):
        # Replace `implements` interface declarations `AnnAssign` with `ImplementsDecl`
        if getattr(ast_struct["target"], "id", None) == "implements":
            if ast_struct["value"] is not None:
                _raise_syntax_exc("`implements` cannot have a value assigned", ast_struct)
            ast_struct["ast_type"] = "ImplementsDecl"
        # Replace state and local variable declarations `AnnAssign` with `VariableDecl`
        # Parent node is required for context to determine whether replacement should happen.
        else:
            ast_struct["ast_type"] = "VariableDecl"

    vy_class = getattr(sys.modules[__name__], ast_struct["ast_type"], None)
    if not vy_class:
        if ast_struct["ast_type"] == "Delete":
            _raise_syntax_exc("Deleting is not supported", ast_struct)
        elif ast_struct["ast_type"] in ("ExtSlice", "Slice"):
            _raise_syntax_exc("Vyper does not support slicing", ast_struct)
        elif ast_struct["ast_type"] == "UAdd":
            _raise_syntax_exc("Vyper does not support + as a unary operator", parent)
        else:
            _raise_syntax_exc(
                f"Invalid syntax (unsupported '{ast_struct['ast_type']}' Python AST node)",
                ast_struct,
            )

    return vy_class(parent=parent, **ast_struct)


def compare_nodes(left_node: "VyperNode", right_node: "VyperNode") -> bool:
    """
    Compare the represented value(s) of two vyper nodes.

    This method evaluates a sort of "loose equality". It recursively compares the
    values of each field within two different nodes but does not compare the
    node_id or any members related to source offsets.

    Arguments
    ---------
    left_node : VyperNode
        First node object to compare.
    right_node : VyperNode
        Second node object to compare.

    Returns
    -------
    bool
        True if the given nodes represent the same value(s), False otherwise.
    """
    if not isinstance(left_node, type(right_node)):
        return False

    for field_name in (i for i in left_node.get_fields() if i not in VyperNode.__slots__):
        left_value = getattr(left_node, field_name, None)
        right_value = getattr(right_node, field_name, None)

        # compare types instead of isinstance() in case one node class inherits the other
        if type(left_value) is not type(right_value):
            return False

        if isinstance(left_value, list):
            if next((i for i in zip(left_value, right_value) if not compare_nodes(*i)), None):
                return False
        elif isinstance(left_value, VyperNode):
            if not compare_nodes(left_value, right_value):
                return False
        elif left_value != right_value:
            return False

    return True


def _to_node(obj, parent):
    # if object is a Python node or dict representing a node, convert to a Vyper node
    if isinstance(obj, (dict, python_ast.AST)):
        return get_node(obj, parent)
    if isinstance(obj, VyperNode):
        # if object is already a vyper node, make sure the parent is set correctly
        # and fix any missing source offsets
        obj._parent = parent
        for field_name in NODE_SRC_ATTRIBUTES:
            if getattr(obj, field_name) is None:
                setattr(obj, field_name, getattr(parent, field_name, None))
    return obj


def _to_dict(value):
    # if value is a Vyper node, convert to a dict
    if isinstance(value, VyperNode):
        return value.to_dict()
    return value


def _node_filter(node, filters):
    # recursive equality check for VyperNode.get_children filters
    if not filters:
        return True
    for key, value in filters.items():
        if isinstance(value, set):
            if node.get(key) not in value:
                return False
        elif node.get(key) != value:
            return False
    return True


def _sort_nodes(node_iterable):
    # sorting function for VyperNode.get_children

    def sortkey(key):
        return float("inf") if key is None else key

    return sorted(
        node_iterable, key=lambda k: (sortkey(k.lineno), sortkey(k.col_offset), k.node_id)
    )


def _raise_syntax_exc(error_msg: str, ast_struct: dict) -> None:
    # helper function to raise a SyntaxException from a dict representing a node
    raise SyntaxException(
        error_msg,
        ast_struct.get("full_source_code"),
        ast_struct.get("lineno"),
        ast_struct.get("col_offset"),
    )


def _validate_numeric_bounds(
    node: Union["BinOp", "UnaryOp"], value: Union[decimal.Decimal, int]
) -> None:
    if isinstance(value, decimal.Decimal):
        # this will change if/when we add more decimal types
        lower, upper = SizeLimits.MIN_AST_DECIMAL, SizeLimits.MAX_AST_DECIMAL
    elif isinstance(value, int):
        lower, upper = SizeLimits.MIN_INT256, SizeLimits.MAX_UINT256
    else:
        raise CompilerPanic(f"Unexpected return type from {node._op}: {type(value)}")
    if not lower <= value <= upper:
        raise OverflowException(
            f"Result of {node.op.description} ({value}) is outside bounds of all numeric types",
            node,
        )


class VyperNode:
    """
    Base class for all vyper AST nodes.

    Vyper nodes are generated from, and closely resemble, their Python counterparts.
    Divergences are always handled in a node's `__init__` method, and explained
    in the node docstring.

    Class Attributes
    ----------------
    __slots__ : Tuple
        Allowed field names for the node.
    _description : str, optional
        A human-readable description of the node. Used to give more verbose error
        messages.
    _only_empty_fields : Tuple, optional
        Field names that, if present, must be set to None or a `SyntaxException`
        is raised. This attribute is used to exclude syntax that is valid in Python
        but not in Vyper.
    _terminus : bool, optional
        If `True`, indicates that execution halts upon reaching this node.
    _translated_fields : Dict, optional
        Field names that are reassigned if encountered. Used to normalize fields
        across different Python versions.
    """

    __slots__ = NODE_BASE_ATTRIBUTES + NODE_SRC_ATTRIBUTES
    _only_empty_fields: tuple = ()
    _translated_fields: dict = {}

    def __init__(self, parent: Optional["VyperNode"] = None, **kwargs: dict):
        """
        AST node initializer method.

        Node objects are not typically instantiated directly, you should instead
        create them using the `get_node` method.

        Parameters
        ----------
        parent: VyperNode, optional
            Node which contains this node.
        **kwargs : dict
            Dictionary of fields to be included within the node.
        """
        self.set_parent(parent)
        self._children: set = set()
        self._metadata: NodeMetadata = NodeMetadata()

        for field_name in NODE_SRC_ATTRIBUTES:
            # when a source offset is not available, use the parent's source offset
            value = kwargs.get(field_name)
            if kwargs.get(field_name) is None:
                value = getattr(parent, field_name, None)
            setattr(self, field_name, value)

        for field_name, value in kwargs.items():
            if field_name in NODE_SRC_ATTRIBUTES:
                continue

            if field_name in self._translated_fields:
                field_name = self._translated_fields[field_name]

            if field_name in self.get_fields():
                if isinstance(value, list):
                    value = [_to_node(i, self) for i in value]
                else:
                    value = _to_node(value, self)
                setattr(self, field_name, value)

            elif value and field_name in self._only_empty_fields:
                _raise_syntax_exc(
                    f"Syntax is valid Python but not valid for Vyper\n"
                    f"class: {type(self).__name__}, field_name: {field_name}",
                    kwargs,
                )

        # add to children of parent last to ensure an accurate hash is generated
        if parent is not None:
            parent._children.add(self)

    # set parent, can be useful when inserting copied nodes into the AST
    def set_parent(self, parent: "VyperNode"):
        self._parent = parent
        self._depth = getattr(parent, "_depth", -1) + 1

    @classmethod
    def from_node(cls, node: "VyperNode", **kwargs) -> "VyperNode":
        """
        Return a new VyperNode based on an existing node.

        This method creates a new node with the same source offsets as an existing
        node. The new node can then replace the existing node within the AST.
        Preserving source offsets ensures accurate error reporting and source
        map generation from the modified AST.

        Arguments
        ---------
        node: VyperNode
            An existing Vyper node. The generated node will have the same source
            offsets and ID as this node.
        **kwargs : Any
            Fields and values for the new node.

        Returns
        -------
        Vyper node instance
        """
        ast_struct = {i: getattr(node, i) for i in VyperNode.__slots__ if not i.startswith("_")}
        ast_struct.update(ast_type=cls.__name__, **kwargs)
        return cls(**ast_struct)

    @classmethod
    def get_fields(cls) -> set:
        """
        Return a set of field names for this node.

        Attributes that are prepended with an underscore are considered private
        and are not included within this sequence.
        """
        slot_fields = [x for i in cls.__mro__ for x in getattr(i, "__slots__", [])]
        return set(i for i in slot_fields if not i.startswith("_"))

    def __hash__(self):
        values = [getattr(self, i, None) for i in VyperNode.__slots__ if not i.startswith("_")]
        return hash(tuple(values))

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return False
        if other.node_id != self.node_id:
            return False
        for field_name in (i for i in self.get_fields() if i not in VyperNode.__slots__):
            if getattr(self, field_name, None) != getattr(other, field_name, None):
                return False
        return True

    def __repr__(self):
        cls = type(self)
        class_repr = f"{cls.__module__}.{cls.__qualname__}"
        return f"{class_repr}:\n{self._annotated_source}"

    @property
    def _annotated_source(self):
        # return source with context / line/col info
        return annotate_source_code(
            self.full_source_code,
            self.lineno,
            self.col_offset,
            context_lines=VYPER_ERROR_CONTEXT_LINES,
            line_numbers=VYPER_ERROR_LINE_NUMBERS,
        )

    @property
    def description(self):
        """
        Property method providing a human-readable description of a node.

        Node-specific description strings are added via the `_descrption` class
        attribute. If this attribute is not found, the name of the class is
        returned instead.
        """
        return getattr(self, "_description", type(self).__name__)

    def evaluate(self) -> "VyperNode":
        """
        Attempt to evaluate the content of a node and generate a new node from it.

        If a node cannot be evaluated it should raise `UnfoldableNode`. This base
        method acts as a catch-all to raise on any inherited classes that do not
        implement the method.
        """
        raise UnfoldableNode(f"{type(self)} cannot be evaluated")

    def validate(self) -> None:
        """
        Validate the content of a node.

        Called by `ast.validation.validate_literal_nodes` to verify values within
        literal nodes.

        Returns `None` if the node is valid, raises `InvalidLiteral` or another
        more expressive exception if the value cannot be valid within a Vyper
        contract.
        """
        pass

    def to_dict(self) -> dict:
        """
        Return the node as a dict. Child nodes and their descendants are also converted.
        """
        ast_dict = {}
        for key in [i for i in self.get_fields() if i not in DICT_AST_SKIPLIST]:
            value = getattr(self, key, None)
            if isinstance(value, list):
                ast_dict[key] = [_to_dict(i) for i in value]
            else:
                ast_dict[key] = _to_dict(value)

        if "type" in self._metadata:
            ast_dict["type"] = str(self._metadata["type"])

        return ast_dict

    def get_ancestor(self, node_type: Union["VyperNode", tuple, None] = None) -> "VyperNode":
        """
        Return an ancestor node for this node.

        An ancestor is any node which exists within the AST above the given node.

        Arguments
        ---------
        node_type : VyperNode | tuple, optional
            A node type or tuple of types. If given, this method checks all
            ancestor nodes of this node starting with the parent, and returns
            the first node with a type matching the given value.

        Returns
        -------
        With no arguments given: the parent of this node.

        With `node_type`: the first matching ascendant node, or `None` if no node
        is found which matches the argument value.
        """
        if node_type is None or self._parent is None:
            return self._parent

        if isinstance(self._parent, node_type):
            return self._parent

        return self._parent.get_ancestor(node_type)

    def get_children(
        self,
        node_type: Union["VyperNode", tuple, None] = None,
        filters: Optional[dict] = None,
        reverse: bool = False,
    ) -> list:
        """
        Return a list of children of this node which match the given filter(s).

        Results are sorted by the starting source offset and node ID, ascending.

        Parameters
        ----------
        node_type : VyperNode | tuple, optional
            A node type or tuple of types. If given, only child nodes where the
            type matches this value are returned. This is functionally identical
            to calling `isinstance(child, node_type)`
        filters : dict, optional
            Dictionary of attribute names and expected values. Only nodes that
            contain the given attributes and match the given values are returned.
            * You can use dots within the name in order to check members of members.
              e.g. `{'annotation.func.id': "constant"}`
            * Expected values may be given as a set, in order to match a node must
              contain the given attribute and match any one value within the set.
              e.g. `{'id': {'public', 'constant'}}` will match nodes with an `id`
                    member that contains either "public" or "constant".
        reverse : bool, optional
            If `True`, the order of results is reversed prior to return.

        Returns
        -------
        list
            Child nodes matching the filter conditions.
        """
        children = _sort_nodes(self._children)
        if node_type is not None:
            children = [i for i in children if isinstance(i, node_type)]
        if reverse:
            children.reverse()
        if filters is None:
            return children
        return [i for i in children if _node_filter(i, filters)]

    def get_descendants(
        self,
        node_type: Union["VyperNode", tuple, None] = None,
        filters: Optional[dict] = None,
        include_self: bool = False,
        reverse: bool = False,
    ) -> list:
        """
        Return a list of descendant nodes of this node which match the given filter(s).

        A descendant is any node which exists within the AST beneath the given node.

        Results are sorted by the starting source offset and depth, ascending. You
        can rely on that the sequence will always contain a parent node prior to any
        of it's children. If the result is reversed, all children of a node will
        be in the sequence prior to their parent.

        Parameters
        ----------
        node_type : VyperNode | tuple, optional
            A node type or tuple of types. If given, only child nodes where the
            type matches this value are returned. This is functionally identical
            to calling `isinstance(child, node_type)`
        filters : dict, optional
            Dictionary of attribute names and expected values. Only nodes that
            contain the given attributes and match the given values are returned.
            * You can use dots within the name in order to check members of members.
              e.g. `{'annotation.func.id': "constant"}`
            * Expected values may be given as a set, in order to match a node must
              contain the given attribute and match any one value within the set.
              e.g. `{'id': {'public', 'constant'}}` will match nodes with an `id`
                    member that contains either "public" or "constant".
        include_self : bool, optional
            If True, this node is also included in the search results if it matches
            the given filter.
        reverse : bool, optional
            If `True`, the order of results is reversed prior to return.

        Returns
        -------
        list
            Descendant nodes matching the filter conditions.
        """
        children = self.get_children(node_type, filters)
        for node in self.get_children():
            children.extend(node.get_descendants(node_type, filters))
        if (
            include_self
            and (not node_type or isinstance(self, node_type))
            and _node_filter(self, filters)
        ):
            children.append(self)
        result = _sort_nodes(children)
        if reverse:
            result.reverse()
        return result

    def get(self, field_str: str) -> Any:
        """

        Recursive getter function for node attributes.

        Parameters
        ----------
        field_str : str
            Attribute string of the location of the node to return.

        Returns
        -------
        VyperNode : optional
            Value at the location of the given field string, if one
            exists. `None` if the field string is empty or invalid.
        """
        obj = self
        for key in field_str.split("."):
            obj = getattr(obj, key, None)
        return obj


class TopLevel(VyperNode):
    """
    Inherited class for Module and FunctionDef nodes.

    Class attributes
    ----------------
    doc_string : Expr
        Expression node representing the docstring within this node.
    """

    __slots__ = ("body", "name", "doc_string")

    def __getitem__(self, key):
        return self.body[key]

    def __iter__(self):
        return iter(self.body)

    def __len__(self):
        return len(self.body)

    def __contains__(self, obj):
        return obj in self.body


class Module(TopLevel):
    __slots__ = ()

    def replace_in_tree(self, old_node: VyperNode, new_node: VyperNode) -> None:
        """
        Perform an in-place substitution of a node within the tree.

        Parameters
        ----------
        old_node : VyperNode
            Node object to be replaced.
        new_node : VyperNode
            Node object to replace new_node.

        Returns
        -------
        None
        """
        parent = old_node._parent

        if old_node not in parent._children:
            raise CompilerPanic("Node to be replaced does not exist within parent children")

        is_replaced = False
        for key in parent.get_fields():
            obj = getattr(parent, key, None)
            if obj == old_node:
                if is_replaced:
                    raise CompilerPanic("Node to be replaced exists as multiple members in parent")
                setattr(parent, key, new_node)
                is_replaced = True
            elif isinstance(obj, list) and obj.count(old_node):
                if is_replaced or obj.count(old_node) > 1:
                    raise CompilerPanic("Node to be replaced exists as multiple members in parent")
                obj[obj.index(old_node)] = new_node
                is_replaced = True
        if not is_replaced:
            raise CompilerPanic("Node to be replaced does not exist within parent members")

        parent._children.remove(old_node)

        new_node._parent = parent
        new_node._depth = old_node._depth
        parent._children.add(new_node)

    def add_to_body(self, node: VyperNode) -> None:
        """
        Add a new node to the body of this node.

        This method should be used in favor of directly modifying `body`, as
        it also sets the parent/child relationships used in node traversal.

        Arguments
        ---------
        node: VyperNode
            Vyper node to be appended to the body of the this node.
        """
        self.body.append(node)
        node._depth = self._depth + 1
        node._parent = self
        self._children.add(node)

    def remove_from_body(self, node: VyperNode) -> None:
        """
        Remove a node from the body of this node.

        This method should be used in favor of directly modifying `body`, as
        it also removes the parent/child relationship used in node traversal.

        Arguments
        ---------
        node: VyperNode
            Vyper node to be appended to the body of the this node.
        """
        self.body.remove(node)
        self._children.remove(node)

    @contextlib.contextmanager
    def namespace(self):
        from vyper.semantics.namespace import get_namespace, override_global_namespace

        # kludge implementation for backwards compatibility.
        # TODO: replace with type_from_ast
        try:
            ns = self._metadata["namespace"]
        except AttributeError:
            ns = get_namespace()
        with override_global_namespace(ns):
            yield


class FunctionDef(TopLevel):
    __slots__ = ("args", "returns", "decorator_list", "pos")


class DocStr(VyperNode):
    """
    A docstring.

    Attributes
    ----------
    value : str
        Value of the node, represented as an string.
    """

    __slots__ = ("value",)
    _translated_fields = {"s": "value"}


class arguments(VyperNode):
    __slots__ = ("args", "defaults", "default")
    _only_empty_fields = ("posonlyargs", "vararg", "kwonlyargs", "kwarg", "kw_defaults")


class arg(VyperNode):
    __slots__ = ("arg", "annotation")


# base class for stmt nodes. doesn't do anything except classification
class Stmt(VyperNode):
    pass


class Return(Stmt):
    __slots__ = ("value",)
    _is_terminus = True


class Expr(Stmt):
    __slots__ = ("value",)


class Log(Stmt):
    __slots__ = ("value",)


class EnumDef(TopLevel):
    __slots__ = ("name", "body")


class EventDef(TopLevel):
    __slots__ = ("name", "body")


class InterfaceDef(TopLevel):
    __slots__ = ("name", "body")


class StructDef(TopLevel):
    __slots__ = ("name", "body")


# base class for expression nodes
# note that it is named ExprNode to avoid a conflict with
# the Expr type (which is a type of statement node, see python AST docs).
class ExprNode(VyperNode):
    __slots__ = ("_expr_info",)


class Constant(ExprNode):
    # inherited class for all simple constant node types
    __slots__ = ("value",)


class Num(Constant):
    # inherited class for all numeric constant node types
    __slots__ = ()
    _translated_fields = {"n": "value"}

    @property
    def n(self):
        # TODO phase out use of Num.n and remove this
        return self.value

    def validate(self):
        if self.value < SizeLimits.MIN_INT256:
            raise OverflowException("Value is below lower bound for all numeric types", self)
        if self.value > SizeLimits.MAX_UINT256:
            raise OverflowException("Value exceeds upper bound for all numeric types", self)


class Int(Num):
    """
    An integer.

    Attributes
    ----------
    value : int
        Value of the node, represented as an integer.
    """

    __slots__ = ()


class Decimal(Num):
    """
    A decimal.

    Attributes
    ----------
    value : decimal.Decimal
        Value of the node, represented as a Decimal object.
    """

    __slots__ = ()

    def __init__(self, parent: Optional["VyperNode"] = None, **kwargs: dict):
        super().__init__(parent, **kwargs)
        if not isinstance(self.value, decimal.Decimal):
            self.value = decimal.Decimal(self.value)

    def to_dict(self):
        ast_dict = super().to_dict()
        ast_dict["value"] = self.node_source_code
        return ast_dict

    def validate(self):
        if self.value.as_tuple().exponent < -MAX_DECIMAL_PLACES:
            raise InvalidLiteral("Vyper supports a maximum of ten decimal points", self)
        if self.value < SizeLimits.MIN_AST_DECIMAL:
            raise OverflowException("Value is below lower bound for decimal types", self)
        if self.value > SizeLimits.MAX_AST_DECIMAL:
            raise OverflowException("Value exceeds upper bound for decimal types", self)


class Hex(Constant):
    """
    A hexadecimal value, e.g. `0xFF`

    Attributes
    ----------
    value : str
        Value of the node, represented as a string taken directly from the contract source.
    """

    __slots__ = ()
    _translated_fields = {"n": "value"}

    def validate(self):
        if "_" in self.value:
            raise InvalidLiteral("Underscores not allowed in hex literals", self)
        if len(self.value) % 2:
            raise InvalidLiteral("Hex notation requires an even number of digits", self)

    @property
    def n_nibbles(self):
        """
        The number of nibbles this hex value represents
        """
        return len(self.value) - 2

    @property
    def n_bytes(self):
        """
        The number of bytes this hex value represents
        """
        return self.n_nibbles // 2


class Str(Constant):
    __slots__ = ()
    _translated_fields = {"s": "value"}

    def validate(self):
        for c in self.value:
            if ord(c) >= 256:
                raise InvalidLiteral(f"'{c}' is not an allowed string literal character", self)

    @property
    def s(self):
        # TODO phase out use of Str.s and remove this
        return self.value


class Bytes(Constant):
    __slots__ = ()
    _translated_fields = {"s": "value"}

    def __init__(self, parent: Optional["VyperNode"] = None, **kwargs: dict):
        super().__init__(parent, **kwargs)
        if isinstance(self.value, str):
            # convert hex string to bytes
            length = len(self.value) // 2 - 1
            self.value = int(self.value, 16).to_bytes(length, "big")

    def to_dict(self):
        ast_dict = super().to_dict()
        ast_dict["value"] = f"0x{self.value.hex()}"
        return ast_dict

    @property
    def s(self):
        return self.value


class List(ExprNode):
    __slots__ = ("elements",)
    _translated_fields = {"elts": "elements"}


class Tuple(ExprNode):
    __slots__ = ("elements",)
    _translated_fields = {"elts": "elements"}

    def validate(self):
        if not self.elements:
            raise InvalidLiteral("Cannot have an empty tuple", self)


class Dict(ExprNode):
    __slots__ = ("keys", "values")


class NameConstant(Constant):
    __slots__ = ("value",)


class Name(ExprNode):
    __slots__ = ("id",)


class UnaryOp(ExprNode):
    __slots__ = ("op", "operand")

    def evaluate(self) -> ExprNode:
        """
        Attempt to evaluate the unary operation.

        Returns
        -------
        Int | Decimal
            Node representing the result of the evaluation.
        """
        if isinstance(self.op, Not) and not isinstance(self.operand, NameConstant):
            raise UnfoldableNode("Node contains invalid field(s) for evaluation")
        if isinstance(self.op, USub) and not isinstance(self.operand, (Int, Decimal)):
            raise UnfoldableNode("Node contains invalid field(s) for evaluation")
        if isinstance(self.op, Invert) and not isinstance(self.operand, Int):
            raise UnfoldableNode("Node contains invalid field(s) for evaluation")

        value = self.op._op(self.operand.value)
        _validate_numeric_bounds(self, value)
        return type(self.operand).from_node(self, value=value)


class Operator(VyperNode):
    pass


class USub(Operator):
    __slots__ = ()
    _description = "negation"
    _op = operator.neg


class Not(Operator):
    __slots__ = ()
    _op = operator.not_


class Invert(Operator):
    __slots__ = ()
    _description = "bitwise not"
    _pretty = "~"

    def _op(self, value):
        return (2**256 - 1) ^ value


class BinOp(ExprNode):
    __slots__ = ("left", "op", "right")

    def evaluate(self) -> ExprNode:
        """
        Attempt to evaluate the arithmetic operation.

        Returns
        -------
        Int | Decimal
            Node representing the result of the evaluation.
        """
        left, right = self.left, self.right
        if type(left) is not type(right):
            raise UnfoldableNode("Node contains invalid field(s) for evaluation")
        if not isinstance(left, (Int, Decimal)):
            raise UnfoldableNode("Node contains invalid field(s) for evaluation")

        # this validation is performed to prevent the compiler from hanging
        # on very large shifts and improve the error message for negative
        # values.
        if isinstance(self.op, (LShift, RShift)) and not (0 <= right.value <= 256):
            raise InvalidLiteral("Shift bits must be between 0 and 256", right)

        value = self.op._op(left.value, right.value)
        _validate_numeric_bounds(self, value)
        return type(left).from_node(self, value=value)


class Add(Operator):
    __slots__ = ()
    _description = "addition"
    _pretty = "+"
    _op = operator.add


class Sub(Operator):
    __slots__ = ()
    _description = "subtraction"
    _pretty = "-"
    _op = operator.sub


class Mult(Operator):
    __slots__ = ()
    _description = "multiplication"
    _pretty = "*"

    def _op(self, left, right):
        assert type(left) is type(right)
        value = left * right
        if isinstance(left, decimal.Decimal):
            # ensure that the result is truncated to MAX_DECIMAL_PLACES
            return value.quantize(
                decimal.Decimal(f"{1:0.{MAX_DECIMAL_PLACES}f}"), decimal.ROUND_DOWN
            )
        else:
            return value


class Div(Operator):
    __slots__ = ()
    _description = "division"
    _pretty = "/"

    def _op(self, left, right):
        # evaluate the operation using true division or floor division
        assert type(left) is type(right)
        if not right:
            raise ZeroDivisionException("Division by zero")

        if isinstance(left, decimal.Decimal):
            value = left / right
            if value < 0:
                # the EVM always truncates toward zero
                value = -(-left / right)
            # ensure that the result is truncated to MAX_DECIMAL_PLACES
            return value.quantize(
                decimal.Decimal(f"{1:0.{MAX_DECIMAL_PLACES}f}"), decimal.ROUND_DOWN
            )
        else:
            value = left // right
            if value < 0:
                return -(-left // right)
            return value


class Mod(Operator):
    __slots__ = ()
    _description = "modulus"
    _pretty = "%"

    def _op(self, left, right):
        if not right:
            raise ZeroDivisionException("Modulo by zero")

        value = abs(left) % abs(right)
        if left < 0:
            value = -value
        return value


class Pow(Operator):
    __slots__ = ()
    _description = "exponentiation"
    _pretty = "**"

    def _op(self, left, right):
        if isinstance(left, decimal.Decimal):
            raise TypeMismatch("Cannot perform exponentiation on decimal values.", self._parent)
        if right < 0:
            raise InvalidOperation("Cannot calculate a negative power", self._parent)
        return int(left**right)


class BitAnd(Operator):
    __slots__ = ()
    _description = "bitwise and"
    _pretty = "&"
    _op = operator.and_


class BitOr(Operator):
    __slots__ = ()
    _description = "bitwise or"
    _pretty = "|"
    _op = operator.or_


class BitXor(Operator):
    __slots__ = ()
    _description = "bitwise xor"
    _pretty = "^"
    _op = operator.xor


class LShift(Operator):
    __slots__ = ()
    _description = "bitwise left shift"
    _pretty = "<<"
    _op = operator.lshift


class RShift(Operator):
    __slots__ = ()
    _description = "bitwise right shift"
    _pretty = ">>"
    _op = operator.rshift


class BoolOp(ExprNode):
    __slots__ = ("op", "values")

    def evaluate(self) -> ExprNode:
        """
        Attempt to evaluate the boolean operation.

        Returns
        -------
        NameConstant
            Node representing the result of the evaluation.
        """
        if next((i for i in self.values if not isinstance(i, NameConstant)), None):
            raise UnfoldableNode("Node contains invalid field(s) for evaluation")

        values = [i.value for i in self.values]
        if None in values:
            raise UnfoldableNode("Node contains invalid field(s) for evaluation")

        value = self.op._op(values)
        return NameConstant.from_node(self, value=value)


class And(Operator):
    __slots__ = ()
    _description = "logical and"
    _op = all


class Or(Operator):
    __slots__ = ()
    _description = "logical or"
    _op = any


class Compare(ExprNode):
    """
    A comparison of two values.

    Attributes
    ----------
    left : ExprNode
        The left-hand value in the comparison.
    op : Operator
        The comparison operator.
    right : ExprNode
        The right-hand value in the comparison.
    """

    __slots__ = ("left", "op", "right")

    def __init__(self, *args, **kwargs):
        if len(kwargs["ops"]) > 1 or len(kwargs["comparators"]) > 1:
            _raise_syntax_exc("Cannot have a comparison with more than two elements", kwargs)

        kwargs["op"] = kwargs.pop("ops")[0]
        kwargs["right"] = kwargs.pop("comparators")[0]
        super().__init__(*args, **kwargs)

    def evaluate(self) -> ExprNode:
        """
        Attempt to evaluate the comparison.

        Returns
        -------
        NameConstant
            Node representing the result of the evaluation.
        """
        left, right = self.left, self.right
        if not isinstance(left, Constant):
            raise UnfoldableNode("Node contains invalid field(s) for evaluation")

        # CMC 2022-08-04 we could probably remove these evaluation rules as they
        # are taken care of in the IR optimizer now.
        if isinstance(self.op, (In, NotIn)):
            if not isinstance(right, List):
                raise UnfoldableNode("Node contains invalid field(s) for evaluation")
            if next((i for i in right.elements if not isinstance(i, Constant)), None):
                raise UnfoldableNode("Node contains invalid field(s) for evaluation")
            if len(set([type(i) for i in right.elements])) > 1:
                raise UnfoldableNode("List contains multiple literal types")
            value = self.op._op(left.value, [i.value for i in right.elements])
            return NameConstant.from_node(self, value=value)

        if not isinstance(left, type(right)):
            raise UnfoldableNode("Cannot compare different literal types")

        if not isinstance(self.op, (Eq, NotEq)) and not isinstance(left, (Int, Decimal)):
            raise TypeMismatch(f"Invalid literal types for {self.op.description} comparison", self)

        value = self.op._op(left.value, right.value)
        return NameConstant.from_node(self, value=value)


class Eq(Operator):
    __slots__ = ()
    _description = "equality"
    _op = operator.eq


class NotEq(Operator):
    __slots__ = ()
    _description = "non-equality"
    _op = operator.ne


class Lt(Operator):
    __slots__ = ()
    _description = "less than"
    _op = operator.lt


class LtE(Operator):
    __slots__ = ()
    _description = "less-or-equal"
    _op = operator.le


class Gt(Operator):
    __slots__ = ()
    _description = "greater than"
    _op = operator.gt


class GtE(Operator):
    __slots__ = ()
    _description = "greater-or-equal"
    _op = operator.ge


class In(Operator):
    __slots__ = ()
    _description = "membership"

    def _op(self, left, right):
        return left in right


class NotIn(Operator):
    __slots__ = ()
    _description = "exclusion"

    def _op(self, left, right):
        return left not in right


class Call(ExprNode):
    __slots__ = ("func", "args", "keywords", "keyword")


class keyword(VyperNode):
    __slots__ = ("arg", "value")


class Attribute(ExprNode):
    __slots__ = ("attr", "value")


class Subscript(ExprNode):
    __slots__ = ("slice", "value")

    def evaluate(self) -> ExprNode:
        """
        Attempt to evaluate the subscript.

        This method reduces an indexed reference to a literal array into the value
        within the array, e.g. `["foo", "bar"][1]` becomes `"bar"`

        Returns
        -------
        ExprNode
            Node representing the result of the evaluation.
        """
        if not isinstance(self.value, List):
            raise UnfoldableNode("Subscript object is not a literal list")
        elements = self.value.elements
        if len(set([type(i) for i in elements])) > 1:
            raise UnfoldableNode("List contains multiple node types")
        idx = self.slice.get("value.value")
        if not isinstance(idx, int) or idx < 0 or idx >= len(elements):
            raise UnfoldableNode("Invalid index value")

        return elements[idx]


class Index(VyperNode):
    __slots__ = ("value",)


class Assign(Stmt):
    """
    An assignment.

    Attributes
    ----------
    target : VyperNode
        Left-hand side of the assignment.
    value : ExprNode
        Right-hand side of the assignment.
    """

    __slots__ = ("target", "value")

    def __init__(self, *args, **kwargs):
        if len(kwargs["targets"]) > 1:
            _raise_syntax_exc("Assignment statement must have one target", kwargs)

        kwargs["target"] = kwargs.pop("targets")[0]
        super().__init__(*args, **kwargs)


class AnnAssign(VyperNode):
    __slots__ = ("target", "annotation", "value", "simple")


class VariableDecl(VyperNode):
    """
    A contract variable declaration.

    Excludes `simple` attribute from Python `AnnAssign` node.

    Attributes
    ----------
    target : VyperNode
        Left-hand side of the assignment.
    value : VyperNode
        Right-hand side of the assignment.
    annotation : VyperNode
        Type of variable.
    is_constant : bool, optional
        If true, indicates that the variable is a constant variable.
    is_public : bool, optional
        If true, indicates that the variable is a public state variable.
    is_immutable : bool, optional
        If true, indicates that the variable is an immutable variable.
    """

    __slots__ = (
        "target",
        "annotation",
        "value",
        "is_constant",
        "is_public",
        "is_immutable",
        "is_transient",
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.is_constant = False
        self.is_public = False
        self.is_immutable = False
        self.is_transient = False

        def _check_args(annotation, call_name):
            # do the same thing as `validate_call_args`
            # (can't be imported due to cyclic dependency)
            if len(annotation.args) != 1:
                raise ArgumentException("Invalid number of arguments to `{call_name}`:", self)

        # the annotation is a "function" call, e.g.
        # `foo: public(constant(uint256))`
        # pretend we were parsing actual Vyper AST. annotation would be
        # TYPE | PUBLIC "(" TYPE | ((IMMUTABLE | CONSTANT) "(" TYPE ")") ")"
        if self.annotation.get("func.id") == "public":
            _check_args(self.annotation, "public")
            self.is_public = True
            # unwrap one layer
            self.annotation = self.annotation.args[0]

        func_id = self.annotation.get("func.id")
        if func_id in ("immutable", "constant", "transient"):
            _check_args(self.annotation, func_id)
            setattr(self, f"is_{func_id}", True)
            # unwrap one layer
            self.annotation = self.annotation.args[0]

        if isinstance(self.annotation, Call):
            _raise_syntax_exc("Invalid scope for variable declaration", self.annotation)


class AugAssign(Stmt):
    __slots__ = ("op", "target", "value")


class Raise(Stmt):
    __slots__ = ("exc",)
    _only_empty_fields = ("cause",)
    _is_terminus = True


class Assert(Stmt):
    __slots__ = ("test", "msg")


class Pass(Stmt):
    __slots__ = ()


class _Import(Stmt):
    __slots__ = ("name", "alias")

    def __init__(self, *args, **kwargs):
        if len(kwargs["names"]) > 1:
            _raise_syntax_exc("Assignment statement must have one target", kwargs)
        names = kwargs.pop("names")[0]
        kwargs["name"] = names.name
        kwargs["alias"] = names.asname
        super().__init__(*args, **kwargs)


class Import(_Import):
    __slots__ = ()


class ImportFrom(_Import):
    __slots__ = ("level", "module")


class ImplementsDecl(Stmt):
    """
    An `implements` declaration.

    Excludes `simple` and `value` attributes from Python `AnnAssign` node.

    Attributes
    ----------
    target : Name
        Name node for the `implements` keyword
    annotation : Name
        Name node for the interface to be implemented
    """

    __slots__ = ("target", "annotation")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if not isinstance(self.annotation, Name):
            raise StructureException("not an identifier", self.annotation)


class If(Stmt):
    __slots__ = ("test", "body", "orelse")


class IfExp(ExprNode):
    __slots__ = ("test", "body", "orelse")


class For(Stmt):
    __slots__ = ("iter", "target", "body")
    _only_empty_fields = ("orelse",)


class Break(Stmt):
    __slots__ = ()


class Continue(Stmt):
    __slots__ = ()
