import abc
import decimal
import enum
import typing
from abc import ABC, abstractmethod
from collections.abc import Iterator, Mapping, Sequence, Set
from functools import cached_property

import attrs
from immutabledict import immutabledict

from puya.algo_constants import SUPPORTED_AVM_VERSIONS
from puya.avm import AVMType, OnCompletionAction
from puya.awst import wtypes
from puya.awst.txn_fields import TxnField
from puya.awst.visitors import (
    ContractMemberVisitor,
    ExpressionVisitor,
    RootNodeVisitor,
    StatementVisitor,
)
from puya.awst.wtypes import WType
from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puya.program_refs import ContractReference, LogicSigReference
from puya.utils import unique

T = typing.TypeVar("T")

@attrs.frozen
class Node:
    source_location: SourceLocation

@attrs.frozen
class Statement(Node, ABC):
    @abstractmethod
    def accept(self, visitor: StatementVisitor[T]) -> T: ...

@attrs.frozen
class Expression(Node, ABC):
    wtype: WType

    @abstractmethod
    def accept(self, visitor: ExpressionVisitor[T]) -> T: ...

@attrs.frozen
class ExpressionStatement(Statement):
    expr: Expression
    source_location: SourceLocation = attrs.field(init=False)

    @source_location.default
    def _source_location(self) -> SourceLocation:
        return self.expr.source_location

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_expression_statement(self)

@attrs.frozen(repr=False)
class _ExpressionHasWType:
    instances: tuple[WType, ...]
    types: tuple[type[WType], ...]

    def __call__(
        self,
        inst: Node,
        attr: attrs.Attribute,  # type: ignore[type-arg]
        value: Expression,
    ) -> None:
        """
        We use a callable class to be able to change the ``__repr__``.
        """
        wtype = value.wtype
        if wtype in self.instances:
            return
        for allowed_t in self.types:
            if isinstance(wtype, allowed_t):
                return
        raise InternalError(
            f"{type(inst).__name__}.{attr.name}: expression of WType {wtype} received,"
            f" expected {' or '.join(self._names)}"
        )

    def __repr__(self) -> str:
        return f"<expression_has_wtype validator for type {' | '.join(self._names)}>"

    @property
    def _names(self) -> Iterator[str]:
        for inst in self.instances:
            yield inst.name
        for typ in self.types:
            yield typ.__name__

def expression_has_wtype(*one_of_these: WType | type[WType]) -> _ExpressionHasWType:
    instances = []
    types = list[type[WType]]()
    for item in one_of_these:
        if isinstance(item, type):
            types.append(item)
        else:
            instances.append(item)
    return _ExpressionHasWType(instances=tuple(instances), types=tuple(types))

@attrs.frozen(repr=False)
class _WTypeIsOneOf:
    instances: tuple[WType, ...]
    types: tuple[type[WType], ...]

    def __call__(
        self,
        inst: Node,
        attr: attrs.Attribute,  # type: ignore[type-arg]
        value: WType,
    ) -> None:
        """
        We use a callable class to be able to change the ``__repr__``.
        """
        wtype = value
        if wtype in self.instances:
            return
        for allowed_t in self.types:
            if isinstance(wtype, allowed_t):
                return
        raise InternalError(
            f"{type(inst).__name__}.{attr.name}: set to {wtype},"
            f" expected {' or '.join(self._names)}"
        )

    def __repr__(self) -> str:
        return f"<expression_has_wtype validator for type {' | '.join(self._names)}>"

    @property
    def _names(self) -> Iterator[str]:
        for inst in self.instances:
            yield inst.name
        for typ in self.types:
            yield typ.__name__

def wtype_is_one_of(*one_of_these: WType | type[WType]) -> _WTypeIsOneOf:
    instances = []
    types = list[type[WType]]()
    for item in one_of_these:
        if isinstance(item, type):
            types.append(item)
        else:
            instances.append(item)
    return _WTypeIsOneOf(instances=tuple(instances), types=tuple(types))

wtype_is_uint64 = expression_has_wtype(wtypes.uint64_wtype)
wtype_is_biguint = expression_has_wtype(wtypes.biguint_wtype)
wtype_is_bool = expression_has_wtype(wtypes.bool_wtype)
wtype_is_bytes = expression_has_wtype(wtypes.bytes_wtype)

Label = typing.NewType("Label", str)

@attrs.frozen(kw_only=True)
class Block(Statement):
    """
    A (non-basic) block used to group statements. Can contain nested blocks, loops, and branching
    structures. No lexical scoping is offered or implied by this block.

    body: A sequence of statements which represent this block
    comment: An optional comment of what this block represents. Only influences
                 non-functional output
    label: An optional label for this block allowing goto statements to jump to this block.
           Must be unique per subroutine.
    """

    body: Sequence[Statement] = attrs.field(converter=tuple[Statement, ...])
    label: Label | None = None
    comment: str | None = None

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_block(self)

@attrs.frozen(kw_only=True)
class Goto(Statement):
    """
    Branch unconditionally to the block with the specified label.

    target: The label of a block within the same subroutine
    """

    target: Label

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_goto(self)

@attrs.frozen
class IfElse(Statement):
    condition: Expression = attrs.field(validator=[wtype_is_bool])
    if_branch: Block
    else_branch: Block | None

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_if_else(self)

@attrs.frozen
class Switch(Statement):
    value: Expression
    cases: Mapping[Expression, Block] = attrs.field(converter=immutabledict)
    default_case: Block | None

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_switch(self)

@attrs.frozen
class WhileLoop(Statement):
    condition: Expression = attrs.field(validator=[wtype_is_bool])
    loop_body: Block

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_while_loop(self)

@attrs.frozen
class LoopExit(Statement):
    """break out of the current innermost loop"""

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_loop_exit(self)

@attrs.frozen
class LoopContinue(Statement):
    """continue with the next iteration of the current innermost loop"""

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_loop_continue(self)

@attrs.frozen
class ReturnStatement(Statement):
    value: Expression | None

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_return_statement(self)

@attrs.frozen
class AssertExpression(Expression):
    condition: Expression | None
    error_message: str | None
    wtype: WType = attrs.field(default=wtypes.void_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_assert_expression(self)

@attrs.frozen(kw_only=True)
class IntegerConstant(Expression):
    wtype: WType = attrs.field(
        validator=[
            wtype_is_one_of(
                wtypes.uint64_wtype,
                wtypes.biguint_wtype,
                wtypes.ARC4UIntN,
            )
        ]
    )
    value: int = attrs.field()
    teal_alias: str | None = None

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_integer_constant(self)

@attrs.frozen
class DecimalConstant(Expression):
    wtype: wtypes.ARC4UFixedNxM
    value: decimal.Decimal = attrs.field()

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_decimal_constant(self)

def UInt64Constant(  # noqa: N802
    *, source_location: SourceLocation, value: int, teal_alias: str | None = None
) -> IntegerConstant:
    return IntegerConstant(
        source_location=source_location,
        value=value,
        wtype=wtypes.uint64_wtype,
        teal_alias=teal_alias,
    )

def BigUIntConstant(  # noqa: N802
    *, source_location: SourceLocation, value: int
) -> IntegerConstant:
    return IntegerConstant(
        source_location=source_location,
        value=value,
        wtype=wtypes.biguint_wtype,
    )

@attrs.frozen
class BoolConstant(Expression):
    value: bool
    wtype: WType = attrs.field(
        default=wtypes.bool_wtype,
        validator=wtype_is_one_of(wtypes.bool_wtype, wtypes.arc4_bool_wtype),
    )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_bool_constant(self)

@enum.unique
class BytesEncoding(enum.StrEnum):
    unknown = enum.auto()
    base16 = enum.auto()
    base32 = enum.auto()
    base64 = enum.auto()
    utf8 = enum.auto()

@attrs.frozen(repr=False)
class _WTypeIsBackedBy:
    backed_by: typing.Literal[AVMType.uint64, AVMType.bytes]

    def __call__(
        self,
        inst: Node,
        attr: attrs.Attribute,  # type: ignore[type-arg]
        value: WType,
    ) -> None:
        """
        We use a callable class to be able to change the ``__repr__``.
        """
        if not isinstance(inst, Node):
            raise InternalError(f"{self!r} used on type {type(inst).__name__}, expected Node")
        if value.scalar_type != self.backed_by:
            raise InternalError(
                f"{type(inst).__name__}.{attr.name}: set to {value},"
                f" which is not backed by {value.scalar_type}, not {self.backed_by.name}"
            )

    def __repr__(self) -> str:
        return f"<wtype_is_{self.backed_by.name}_backed validator>"

wtype_is_bytes_backed: typing.Final = _WTypeIsBackedBy(backed_by=AVMType.bytes)
wtype_is_uint64_backed: typing.Final = _WTypeIsBackedBy(backed_by=AVMType.uint64)

@attrs.frozen(kw_only=True)
class BytesConstant(Expression):
    wtype: WType = attrs.field(default=wtypes.bytes_wtype, validator=wtype_is_bytes_backed)
    value: bytes = attrs.field()
    encoding: BytesEncoding = attrs.field()

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_bytes_constant(self)

@attrs.frozen
class StringConstant(Expression):
    value: str = attrs.field()
    wtype: WType = attrs.field(
        default=wtypes.string_wtype,
        validator=[
            wtype_is_one_of(
                wtypes.string_wtype,
                wtypes.arc4_string_alias,
            )
        ],
    )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_string_constant(self)

@attrs.frozen
class VoidConstant(Expression):
    # useful as a "no-op"
    wtype: WType = attrs.field(default=wtypes.void_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_void_constant(self)

@attrs.frozen
class TemplateVar(Expression):
    wtype: WType
    name: str

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_template_var(self)

@attrs.frozen
class MethodConstant(Expression):
    wtype: WType = attrs.field(default=wtypes.bytes_wtype, init=False)
    value: str

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_method_constant(self)

@attrs.frozen(kw_only=True)
class AddressConstant(Expression):
    wtype: WType = attrs.field(
        default=wtypes.account_wtype,
        validator=wtype_is_one_of(wtypes.account_wtype, wtypes.arc4_address_alias),
    )
    value: str

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_address_constant(self)

@attrs.frozen
class ARC4Encode(Expression):
    value: Expression
    wtype: wtypes.ARC4Type = attrs.field()

    @wtype.validator
    def _wtype_validator(self, _attribute: object, wtype: wtypes.ARC4Type) -> None:
        if not wtype.can_encode_type(self.value.wtype):
            raise InternalError(
                f"cannot ARC4 encode {self.value.wtype} to {wtype}", self.source_location
            )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_arc4_encode(self)

@attrs.frozen
class Copy(Expression):
    """
    Create a new copy of 'value'
    """

    value: Expression
    wtype: WType = attrs.field(init=False)

    @wtype.default
    def _wtype(self) -> WType:
        return self.value.wtype

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_copy(self)

@attrs.frozen
class ArrayConcat(Expression):
    """
    Given 'left' or 'right' that is logically an array - concat it with the other value which is
    an iterable type with the same element type
    """

    left: Expression
    right: Expression

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_array_concat(self)

@attrs.frozen
class ArrayPop(Expression):
    base: Expression

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_array_pop(self)

@attrs.frozen
class ArrayExtend(Expression):
    """
    Given 'base' that is logically an array - extend it with 'other' which is an iterable type with
    the same element type
    """

    base: Expression
    other: Expression

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_array_extend(self)

@attrs.frozen
class ARC4Decode(Expression):
    value: Expression = attrs.field(
        validator=expression_has_wtype(
            wtypes.arc4_bool_wtype,
            wtypes.ARC4UIntN,
            wtypes.ARC4Tuple,
            wtypes.ARC4Struct,
            wtypes.ARC4DynamicArray,  # only if element type is bytes for now
        )
    )

    @value.validator
    def _value_wtype_validator(self, _attribute: object, value: Expression) -> None:
        assert isinstance(value.wtype, wtypes.ARC4Type)  # validated by `value`
        if not value.wtype.can_encode_type(self.wtype):
            raise InternalError(
                f"ARC4Decode from {value.wtype} should have non ARC4 target type {self.wtype}",
                self.source_location,
            )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_arc4_decode(self)

CompileTimeConstantExpression: typing.TypeAlias = (
    IntegerConstant
    | DecimalConstant
    | BoolConstant
    | BytesConstant
    | AddressConstant
    | MethodConstant
)

@attrs.define
class IntrinsicCall(Expression):
    op_code: str
    immediates: Sequence[str | int] = attrs.field(default=(), converter=tuple[str | int, ...])
    stack_args: Sequence[Expression] = attrs.field(default=(), converter=tuple[Expression, ...])

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_intrinsic_call(self)

@attrs.define
class CreateInnerTransaction(Expression):
    wtype: wtypes.WInnerTransactionFields
    fields: Mapping[TxnField, Expression] = attrs.field(converter=immutabledict)

    @fields.validator
    def _validate_fields(self, _attribute: object, fields: Mapping[TxnField, Expression]) -> None:
        for field, value in fields.items():
            if not field.valid_argument_type(value.wtype):
                raise CodeError("invalid type for field", value.source_location)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_create_inner_transaction(self)

@attrs.define
class UpdateInnerTransaction(Expression):
    itxn: Expression = attrs.field(validator=expression_has_wtype(wtypes.WInnerTransactionFields))
    fields: Mapping[TxnField, Expression] = attrs.field(converter=immutabledict)
    wtype: WType = attrs.field(default=wtypes.void_wtype, init=False)

    @fields.validator
    def _validate_fields(self, _attribute: object, fields: Mapping[TxnField, Expression]) -> None:
        for field, value in fields.items():
            if not field.valid_argument_type(value.wtype):
                raise CodeError("invalid type for field", value.source_location)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_update_inner_transaction(self)

@attrs.frozen
class GroupTransactionReference(Expression):
    index: Expression = attrs.field(validator=wtype_is_uint64)
    wtype: wtypes.WGroupTransaction

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_group_transaction_reference(self)

@attrs.define
class CheckedMaybe(Expression):
    """Allows evaluating a maybe type i.e. tuple[_T, bool] as _T, but with the assertion that
    the 2nd bool element is true"""

    expr: Expression
    comment: str
    wtype: wtypes.WType = attrs.field(init=False)
    source_location: SourceLocation = attrs.field(init=False)

    @source_location.default
    def _source_location(self) -> SourceLocation:
        return self.expr.source_location

    @wtype.default
    def _wtype(self) -> wtypes.WType:
        match self.expr.wtype:
            case wtypes.WTuple(types=(wtype, wtypes.bool_wtype)):
                return wtype
            case _:
                raise InternalError(
                    f"{type(self).__name__}.expr: expression of WType {self.expr.wtype} received,"
                    f" expected tuple[_T, bool]"
                )

    @classmethod
    def from_tuple_items(
        cls,
        expr: Expression,
        check: Expression,
        source_location: SourceLocation,
        comment: str,
    ) -> typing.Self:
        if check.wtype != wtypes.bool_wtype:
            raise InternalError(
                "Check condition for CheckedMaybe should be a boolean", source_location
            )
        tuple_expr = TupleExpression.from_items((expr, check), source_location)
        return cls(expr=tuple_expr, comment=comment)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_checked_maybe(self)

@attrs.frozen
class TupleExpression(Expression):
    items: Sequence[Expression] = attrs.field(converter=tuple[Expression, ...])
    wtype: wtypes.WTuple = attrs.field()

    @classmethod
    def from_items(cls, items: Sequence[Expression], location: SourceLocation) -> typing.Self:
        return cls(
            items=items,
            wtype=wtypes.WTuple((i.wtype for i in items), location),
            source_location=location,
        )

    @wtype.validator
    def _wtype_validator(self, _attribute: object, wtype: wtypes.WTuple) -> None:
        if tuple(it.wtype for it in self.items) != wtype.types:
            raise CodeError("Tuple type mismatch", self.source_location)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_tuple_expression(self)

@attrs.frozen
class TupleItemExpression(Expression):
    """Represents tuple element access.

    Note: this is its own item (vs IndexExpression) for two reasons:
    1. It's not a valid lvalue (tuples are immutable)
    2. The index must always be a literal, and can be negative
    """

    base: Expression
    index: int
    wtype: wtypes.WType = attrs.field(init=False)

    @wtype.default
    def _wtype(self) -> wtypes.WType:
        base_wtype = self.base.wtype
        if not isinstance(base_wtype, wtypes.WTuple | wtypes.ARC4Tuple):
            raise InternalError(
                f"Tuple item expression should be for a tuple type, got {base_wtype}",
                self.source_location,
            )
        try:
            wtype = base_wtype.types[self.index]
        except IndexError as ex:
            raise CodeError("invalid index into tuple expression", self.source_location) from ex
        return wtype

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_tuple_item_expression(self)

@attrs.frozen
class VarExpression(Expression):
    name: str

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_var_expression(self)

@attrs.frozen(kw_only=True)
class InnerTransactionField(Expression):
    itxn: Expression = attrs.field(validator=expression_has_wtype(wtypes.WInnerTransaction))
    field: TxnField
    array_index: Expression | None = None

    def __attrs_post_init__(self) -> None:
        has_array = self.array_index is not None
        if has_array != self.field.is_array:
            raise InternalError(
                f"Inconsistent field and array_index combination: "
                f"{self.field} and {'' if has_array else 'no '} array provided",
                self.source_location,
            )
        if self.wtype.scalar_type != self.field.avm_type:
            raise InternalError(
                f"wtype of field {self.field.immediate} is {self.field.wtype}"
                f" which is not compatible with specified result type {self.wtype}",
                self.source_location,
            )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_inner_transaction_field(self)

@attrs.define
class SubmitInnerTransaction(Expression):
    itxns: Sequence[Expression] = attrs.field(converter=tuple[Expression, ...])
    wtype: WType = attrs.field(init=False)

    @wtype.default
    def _wtype(self) -> wtypes.WType:
        txn_types = []
        for expr in self.itxns:
            if not isinstance(expr.wtype, wtypes.WInnerTransactionFields):
                raise CodeError("invalid expression type for submit", expr.source_location)
            txn_types.append(wtypes.WInnerTransaction.from_type(expr.wtype.transaction_type))
        try:
            (single_txn,) = txn_types
        except ValueError:
            return wtypes.WTuple(txn_types, self.source_location)
        else:
            return single_txn

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_submit_inner_transaction(self)

@attrs.frozen
class FieldExpression(Expression):
    base: Expression = attrs.field(
        validator=expression_has_wtype(wtypes.WStructType, wtypes.ARC4Struct, wtypes.WTuple)
    )
    name: str
    wtype: wtypes.WType = attrs.field(init=False)

    @wtype.default
    def _wtype_factory(self) -> wtypes.WType:
        dataclass_type = self.base.wtype
        assert isinstance(dataclass_type, wtypes.WStructType | wtypes.ARC4Struct | wtypes.WTuple)
        try:
            return dataclass_type.fields[self.name]
        except KeyError:
            raise CodeError(f"invalid field for {dataclass_type}", self.source_location) from None

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_field_expression(self)

@attrs.frozen
class IndexExpression(Expression):
    base: Expression = attrs.field(
        validator=expression_has_wtype(
            wtypes.bytes_wtype,
            wtypes.ARC4StaticArray,
            wtypes.ARC4DynamicArray,
            # NOTE: tuples (native or arc4) use TupleItemExpression instead
        )
    )
    index: Expression = attrs.field(validator=expression_has_wtype(wtypes.uint64_wtype))

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_index_expression(self)

@attrs.frozen
class SliceExpression(Expression):
    base: Expression = attrs.field(
        validator=expression_has_wtype(
            wtypes.bytes_wtype,
            wtypes.WTuple,
        )
    )

    begin_index: Expression | None
    end_index: Expression | None

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_slice_expression(self)

@attrs.frozen
class IntersectionSliceExpression(Expression):
    """
    Returns the intersection of the slice indexes and the base
    """

    base: Expression = attrs.field(
        validator=expression_has_wtype(
            wtypes.bytes_wtype,
            wtypes.WTuple,
        )
    )

    begin_index: Expression | int | None
    end_index: Expression | int | None

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_intersection_slice_expression(self)

@attrs.frozen
class AppStateExpression(Expression):
    key: Expression = attrs.field(validator=expression_has_wtype(wtypes.state_key))
    exists_assertion_message: str | None
    """TEAL comment that will be emitted in a checked-read scenario"""

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_app_state_expression(self)

@attrs.frozen
class AppAccountStateExpression(Expression):
    key: Expression = attrs.field(validator=expression_has_wtype(wtypes.state_key))
    exists_assertion_message: str | None
    """TEAL comment that will be emitted in a checked-read scenario"""
    account: Expression = attrs.field(
        validator=expression_has_wtype(wtypes.account_wtype, wtypes.uint64_wtype)
    )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_app_account_state_expression(self)

@attrs.frozen
class BoxValueExpression(Expression):
    key: Expression = attrs.field(validator=expression_has_wtype(wtypes.box_key))
    exists_assertion_message: str | None
    """TEAL comment that will be emitted in a checked-read scenario"""

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_box_value_expression(self)

@attrs.frozen
class SingleEvaluation(Expression):
    """
    This node wraps an underlying expression and effectively caches the result of that lowering,
    such that regardless of how many times the SingleEvaluation object
    (or any object comparing equal to it) appears in the AWST,
    the underlying source expression will only be evaluated once.
    """

    source: Expression
    _id: int = attrs.field()
    wtype: WType = attrs.field(init=False, eq=False)
    source_location: SourceLocation = attrs.field(eq=False)

    @_id.default
    def _default_id(self) -> int:
        return id(self)

    @wtype.default
    def _wtype(self) -> WType:
        return self.source.wtype

    @source_location.default
    def _default_source_location(self) -> SourceLocation:
        return self.source.source_location

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_single_evaluation(self)

@attrs.frozen
class ReinterpretCast(Expression):
    """Convert an expression to an AVM equivalent type.

    Note: the validation of this isn't done until IR construction"""

    expr: Expression

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_reinterpret_cast(self)

StorageExpression = AppStateExpression | AppAccountStateExpression | BoxValueExpression
# Expression types that are valid on the left hand side of assignment *statements*
# Note that some of these can be recursive/nested, eg:
# obj.field[index].another_field = 123
Lvalue = VarExpression | FieldExpression | IndexExpression | TupleExpression | StorageExpression

@attrs.frozen
class NewArray(Expression):
    wtype: wtypes.WArray | wtypes.ARC4Array
    values: Sequence[Expression] = attrs.field(default=(), converter=tuple[Expression, ...])

    @values.validator
    def _check_element_types(self, _attribute: object, value: tuple[Expression, ...]) -> None:
        if any(expr.wtype != self.wtype.element_type for expr in value):
            raise ValueError(
                f"All array elements should have array type: {self.wtype.element_type}"
            )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_new_array(self)

@attrs.frozen
class ConditionalExpression(Expression):
    """A "ternary" operator with conditional evaluation - the true and false expressions must only
    be evaluated if they will be the result of expression.
    """

    condition: Expression = attrs.field(validator=[wtype_is_bool])
    true_expr: Expression
    false_expr: Expression
    wtype: WType = attrs.field()

    @wtype.default
    def _wtype(self) -> WType:
        if self.true_expr.wtype != self.false_expr.wtype:
            raise CodeError(
                f"true and false expressions of conditional have differing types:"
                f" {self.true_expr.wtype} and {self.false_expr.wtype}",
                self.source_location,
            )
        return self.true_expr.wtype

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_conditional_expression(self)

@attrs.frozen
class AssignmentStatement(Statement):
    """
    A single assignment statement e.g. `a = 1`.

    Multi-assignment statements like `a = b = 1` should be split in the AST pass.

    Will validate that target and value are of the same type, and that said type is usable
    as an l-value.
    """

    target: Lvalue
    value: Expression

    def __attrs_post_init__(self) -> None:
        if self.value.wtype != self.target.wtype:
            raise CodeError(
                "assignment target type differs from expression value type",
                self.source_location,
            )
        if self.value.wtype == wtypes.void_wtype:
            raise CodeError("void type cannot be assigned", self.source_location)

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_assignment_statement(self)

@attrs.frozen
class AssignmentExpression(Expression):
    """
    This both assigns value to target and returns the value as the result of the expression.

    Will validate that target and value are of the same type, and that said type is usable
    as an l-value.
    """

    target: Lvalue = attrs.field()
    value: Expression = attrs.field()
    wtype: wtypes.WType = attrs.field(init=False)

    @wtype.default
    def _wtype(self) -> wtypes.WType:
        return self.target.wtype

    @value.validator
    def _value_validator(self, _attribute: object, value: Expression) -> None:
        if value.wtype != self.target.wtype:
            raise CodeError(
                "assignment target type differs from expression value type",
                self.source_location,
            )
        if value.wtype == wtypes.void_wtype:
            raise CodeError("void type cannot be assigned", self.source_location)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_assignment_expression(self)

class EqualityComparison(enum.StrEnum):
    eq = "=="
    ne = "!="

class NumericComparison(enum.StrEnum):
    eq = "=="  # 😩 why can't Python have enum inheritance
    ne = "!="
    lt = "<"
    lte = "<="
    gt = ">"
    gte = ">="

numeric_comparable = expression_has_wtype(
    wtypes.uint64_wtype,
    wtypes.biguint_wtype,
    wtypes.bool_wtype,
    wtypes.asset_wtype,
    wtypes.application_wtype,
)

@attrs.frozen
class NumericComparisonExpression(Expression):
    """Compare two numeric types.

    Any type promotion etc should be done at the source language level,
    this operation expects both arguments to already be in the same type.
    This is to insulate against language-specific differences in type promotion rules,
    or equality comparisons with bool, and so on.
    """

    wtype: WType = attrs.field(default=wtypes.bool_wtype, init=False)

    # TODO: make these names consistent with other expressions
    lhs: Expression = attrs.field(validator=[numeric_comparable])
    operator: NumericComparison
    rhs: Expression = attrs.field(validator=[numeric_comparable])

    def __attrs_post_init__(self) -> None:
        if self.lhs.wtype != self.rhs.wtype:
            raise InternalError(
                "numeric comparison between different wtypes:"
                f" {self.lhs.wtype} and {self.rhs.wtype}",
                self.source_location,
            )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_numeric_comparison_expression(self)

bytes_comparable = expression_has_wtype(
    wtypes.bytes_wtype,
    wtypes.account_wtype,
    wtypes.string_wtype,
    wtypes.ARC4Type,
)

@attrs.frozen
class BytesComparisonExpression(Expression):
    wtype: WType = attrs.field(default=wtypes.bool_wtype, init=False)

    lhs: Expression = attrs.field(validator=[bytes_comparable])
    operator: EqualityComparison
    rhs: Expression = attrs.field(validator=[bytes_comparable])

    def __attrs_post_init__(self) -> None:
        if self.lhs.wtype != self.rhs.wtype:
            raise InternalError(
                "bytes comparison between different wtypes:"
                f" {self.lhs.wtype} and {self.rhs.wtype}",
                self.source_location,
            )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_bytes_comparison_expression(self)

@attrs.frozen
class SubroutineID:
    target: str

@attrs.frozen(kw_only=True)
class InstanceMethodTarget:
    member_name: str

@attrs.frozen(kw_only=True)
class InstanceSuperMethodTarget:
    member_name: str

@attrs.frozen(kw_only=True)
class ContractMethodTarget:
    cref: ContractReference
    member_name: str

SubroutineTarget = (
    SubroutineID | InstanceMethodTarget | InstanceSuperMethodTarget | ContractMethodTarget
)

@attrs.frozen
class CallArg:
    name: str | None  # if None, then passed positionally
    value: Expression

@attrs.frozen
class SubroutineCallExpression(Expression):
    target: SubroutineTarget
    args: Sequence[CallArg] = attrs.field(converter=tuple[CallArg, ...])

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_subroutine_call_expression(self)

@attrs.frozen
class PuyaLibData:
    id: str
    params: Mapping[str, wtypes.WType]
    wtype: wtypes.WType

class PuyaLibFunction(enum.Enum):
    ensure_budget = PuyaLibData(
        id="_puya_lib.util.ensure_budget",
        params={"required_budget": wtypes.uint64_wtype, "fee_source": wtypes.uint64_wtype},
        wtype=wtypes.void_wtype,
    )
    is_substring = PuyaLibData(
        id="_puya_lib.bytes_.is_substring",
        params={"item": wtypes.bytes_wtype, "sequence": wtypes.bytes_wtype},
        wtype=wtypes.bool_wtype,
    )

@attrs.define
class PuyaLibCall(Expression):
    func: PuyaLibFunction
    args: Sequence[CallArg] = attrs.field(default=(), converter=tuple[CallArg, ...])
    wtype: wtypes.WType = attrs.field(init=False)

    @wtype.default
    def _wtype(self) -> wtypes.WType:
        return self.func.value.wtype

    @args.validator
    def _args_validator(self, _: object, args: Sequence[CallArg]) -> None:
        if len(self.func.value.params) != len(args):
            raise CodeError(
                f"provided args does not match arity for {self.func.name}", self.source_location
            )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_puya_lib_call(self)

@enum.unique
class UInt64BinaryOperator(enum.StrEnum):
    add = "+"
    sub = "-"
    mult = "*"
    floor_div = "//"
    mod = "%"
    pow = "**"
    lshift = "<<"
    rshift = ">>"
    bit_or = "|"
    bit_xor = "^"
    bit_and = "&"
    # unsupported:
    # / aka ast.Div
    # @ aka ast.MatMult

@enum.unique
class BigUIntBinaryOperator(enum.StrEnum):
    add = "+"
    sub = "-"
    mult = "*"
    floor_div = "//"
    mod = "%"
    bit_or = "|"
    bit_xor = "^"
    bit_and = "&"
    # unsupported:
    # ** aka ast.Pow
    # / aka ast.Div
    # @ aka ast.MatMult
    # << aka ast.LShift
    # >> aka ast.RShift

@enum.unique
class BytesBinaryOperator(enum.StrEnum):
    add = "+"
    bit_or = "|"
    bit_xor = "^"
    bit_and = "&"

@enum.unique
class BytesUnaryOperator(enum.StrEnum):
    bit_invert = "~"

@enum.unique
class UInt64UnaryOperator(enum.StrEnum):
    bit_invert = "~"

@enum.unique
class UInt64PostfixUnaryOperator(enum.StrEnum):
    increment = "++"
    decrement = "--"

@attrs.frozen
class UInt64UnaryOperation(Expression):
    op: UInt64UnaryOperator
    expr: Expression = attrs.field(validator=[wtype_is_uint64])
    wtype: WType = attrs.field(default=wtypes.uint64_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_uint64_unary_operation(self)

@attrs.frozen
class UInt64PostfixUnaryOperation(Expression):
    op: UInt64PostfixUnaryOperator
    target: Lvalue = attrs.field(validator=[wtype_is_uint64])
    wtype: WType = attrs.field(default=wtypes.uint64_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_uint64_postfix_unary_operation(self)

@enum.unique
class BigUIntPostfixUnaryOperator(enum.StrEnum):
    increment = "++"
    decrement = "--"

@attrs.frozen
class BigUIntPostfixUnaryOperation(Expression):
    op: BigUIntPostfixUnaryOperator
    target: Expression = attrs.field(validator=[wtype_is_biguint])
    wtype: WType = attrs.field(default=wtypes.biguint_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_biguint_postfix_unary_operation(self)

@attrs.frozen
class BytesUnaryOperation(Expression):
    op: BytesUnaryOperator
    expr: Expression = attrs.field(validator=[wtype_is_bytes])
    wtype: WType = attrs.field(default=wtypes.bytes_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_bytes_unary_operation(self)

@attrs.frozen
class UInt64BinaryOperation(Expression):
    left: Expression = attrs.field(validator=[wtype_is_uint64])
    op: UInt64BinaryOperator
    right: Expression = attrs.field(validator=[wtype_is_uint64])
    wtype: WType = attrs.field(default=wtypes.uint64_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_uint64_binary_operation(self)

@attrs.frozen
class BigUIntBinaryOperation(Expression):
    left: Expression = attrs.field(validator=[wtype_is_biguint])
    op: BigUIntBinaryOperator
    right: Expression = attrs.field(validator=[wtype_is_biguint])
    wtype: WType = attrs.field(default=wtypes.biguint_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_biguint_binary_operation(self)

@attrs.frozen
class BytesBinaryOperation(Expression):
    left: Expression = attrs.field(
        validator=[expression_has_wtype(wtypes.bytes_wtype, wtypes.string_wtype)]
    )
    op: BytesBinaryOperator
    right: Expression = attrs.field(
        validator=[expression_has_wtype(wtypes.bytes_wtype, wtypes.string_wtype)]
    )
    wtype: WType = attrs.field(init=False)

    @right.validator
    def _check_right(self, _attribute: object, right: Expression) -> None:
        if right.wtype != self.left.wtype:
            raise CodeError(
                f"Bytes operation on differing types,"
                f" lhs is {self.left.wtype}, rhs is {self.right.wtype}",
                self.source_location,
            )

    @wtype.default
    def _wtype_factory(self) -> wtypes.WType:
        return self.left.wtype

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_bytes_binary_operation(self)

@enum.unique
class BinaryBooleanOperator(enum.StrEnum):
    and_ = "and"
    or_ = "or"

@attrs.frozen
class BooleanBinaryOperation(Expression):
    left: Expression = attrs.field(validator=[wtype_is_bool])
    op: BinaryBooleanOperator
    right: Expression = attrs.field(validator=[wtype_is_bool])
    wtype: WType = attrs.field(default=wtypes.bool_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_boolean_binary_operation(self)

@attrs.frozen
class Not(Expression):
    expr: Expression = attrs.field(validator=[wtype_is_bool])
    wtype: WType = attrs.field(default=wtypes.bool_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_not_expression(self)

@attrs.frozen
class UInt64AugmentedAssignment(Statement):
    target: Lvalue = attrs.field(validator=[wtype_is_uint64])
    op: UInt64BinaryOperator
    value: Expression = attrs.field(validator=[wtype_is_uint64])

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_uint64_augmented_assignment(self)

@attrs.frozen
class BigUIntAugmentedAssignment(Statement):
    target: Lvalue = attrs.field(validator=[wtype_is_biguint])
    op: BigUIntBinaryOperator
    value: Expression = attrs.field(validator=[wtype_is_biguint])

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_biguint_augmented_assignment(self)

@attrs.frozen
class BytesAugmentedAssignment(Statement):
    target: Lvalue = attrs.field(
        validator=[
            expression_has_wtype(wtypes.bytes_wtype, wtypes.string_wtype, wtypes.arc4_string_alias)
        ]
    )
    op: BytesBinaryOperator
    value: Expression = attrs.field(
        validator=[
            expression_has_wtype(wtypes.bytes_wtype, wtypes.string_wtype, wtypes.arc4_string_alias)
        ]
    )

    @value.validator
    def _check_value(self, _attribute: object, value: Expression) -> None:
        if value.wtype != self.target.wtype:
            raise CodeError(
                f"Augmented assignment of differing types,"
                f" expected {self.target.wtype}, got {value.wtype}",
                value.source_location,
            )

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_bytes_augmented_assignment(self)

@attrs.frozen
class Emit(Expression):
    signature: str
    value: Expression = attrs.field(validator=expression_has_wtype(wtypes.ARC4Struct))
    wtype: WType = attrs.field(default=wtypes.void_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_emit(self)

@attrs.frozen
class Range(Expression):
    wtype: WType = attrs.field(default=wtypes.uint64_range_wtype, init=False)
    start: Expression = attrs.field(validator=[wtype_is_uint64])
    stop: Expression = attrs.field(validator=[wtype_is_uint64])
    step: Expression = attrs.field(validator=[wtype_is_uint64])

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_range(self)

@attrs.frozen
class Enumeration(Expression):
    expr: Expression
    wtype: wtypes.WEnumeration = attrs.field(init=False)

    @wtype.default
    def _wtype(self) -> wtypes.WEnumeration:
        return wtypes.WEnumeration(self.expr.wtype)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_enumeration(self)

@attrs.frozen
class Reversed(Expression):
    expr: Expression
    wtype: WType = attrs.field(init=False)

    @wtype.default
    def _wtype(self) -> WType:
        return self.expr.wtype

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_reversed(self)

@attrs.frozen
class ForInLoop(Statement):
    sequence: Expression
    items: Lvalue  # item variable(s)
    loop_body: Block

    def accept(self, visitor: StatementVisitor[T]) -> T:
        return visitor.visit_for_in_loop(self)

@attrs.frozen
class StateGet(Expression):
    """
    Get value or default if unset - note that for get without a default,
    can just use the underlying StateExpression
    """

    field: StorageExpression
    default: Expression = attrs.field()
    wtype: WType = attrs.field(init=False)

    @default.validator
    def _check_default(self, _attribute: object, default: Expression) -> None:
        if self.field.wtype != default.wtype:
            raise CodeError(
                "Default state value should match storage type", default.source_location
            )

    @wtype.default
    def _wtype_factory(self) -> WType:
        return self.field.wtype

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_state_get(self)

@attrs.frozen
class StateGetEx(Expression):
    field: StorageExpression
    wtype: wtypes.WTuple = attrs.field(init=False)

    @wtype.default
    def _wtype_factory(self) -> wtypes.WTuple:
        return wtypes.WTuple(
            (self.field.wtype, wtypes.bool_wtype),
            self.source_location,
        )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_state_get_ex(self)

@attrs.frozen
class StateExists(Expression):
    field: StorageExpression
    wtype: WType = attrs.field(default=wtypes.bool_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_state_exists(self)

@attrs.frozen
class StateDelete(Expression):
    field: StorageExpression
    wtype: WType = attrs.field(default=wtypes.void_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_state_delete(self)

@attrs.frozen
class NewStruct(Expression):
    wtype: wtypes.WStructType | wtypes.ARC4Struct
    values: Mapping[str, Expression] = attrs.field(converter=immutabledict)

    @values.validator
    def _validate_values(self, _instance: object, values: Mapping[str, Expression]) -> None:
        if values.keys() != self.wtype.fields.keys():
            raise CodeError("Invalid argument(s)", self.source_location)
        for field_name, field_value in self.values.items():
            expected_wtype = self.wtype.fields[field_name]
            if field_value.wtype != expected_wtype:
                raise CodeError("Invalid argument type(s)", self.source_location)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_new_struct(self)

@attrs.frozen
class RootNode(Node, ABC):
    @property
    @abstractmethod
    def id(self) -> str: ...

    @abstractmethod
    def accept(self, visitor: RootNodeVisitor[T]) -> T: ...

@attrs.frozen(kw_only=True)
class SubroutineArgument:
    name: str
    source_location: SourceLocation
    wtype: WType = attrs.field()

    @wtype.validator
    def _wtype_validator(self, _attribute: object, wtype: WType) -> None:
        if wtype == wtypes.void_wtype:
            raise CodeError("void type arguments are not supported", self.source_location)

@attrs.frozen
class MethodDocumentation:
    description: str | None = None
    args: immutabledict[str, str] = attrs.field(default={}, converter=immutabledict)
    returns: str | None = None

@attrs.frozen
class Function(Node, ABC):
    args: Sequence[SubroutineArgument] = attrs.field(converter=tuple[SubroutineArgument, ...])
    return_type: WType
    body: Block
    documentation: MethodDocumentation
    inline: bool | None = None

    @property
    @abstractmethod
    def short_name(self) -> str: ...

    @property
    @abstractmethod
    def full_name(self) -> str: ...

@attrs.frozen(kw_only=True)
class Subroutine(Function, RootNode):
    id: str
    name: str

    @property
    def short_name(self) -> str:
        return self.name

    @property
    def full_name(self) -> str:
        return self.id

    def accept(self, visitor: RootNodeVisitor[T]) -> T:
        return visitor.visit_subroutine(self)

AWST: typing.TypeAlias = Sequence[RootNode]

@attrs.frozen
class ContractMemberNode(Node, ABC):
    @property
    @abc.abstractmethod
    def member_name(self) -> str: ...

    @abc.abstractmethod
    def accept(self, visitor: ContractMemberVisitor[T]) -> T: ...

@attrs.frozen(kw_only=True)
class ContractMethod(Function, ContractMemberNode):
    cref: ContractReference
    member_name: str
    arc4_method_config: "ARC4MethodConfig | None"

    @property
    def short_name(self) -> str:
        return self.member_name

    @property
    def full_name(self) -> str:
        return f"{self.cref}.{self.member_name}"

    def accept(self, visitor: ContractMemberVisitor[T]) -> T:
        return visitor.visit_contract_method(self)

@enum.unique
class AppStorageKind(enum.Enum):
    app_global = enum.auto()
    account_local = enum.auto()
    box = enum.auto()

@attrs.frozen
class AppStorageDefinition(ContractMemberNode):
    member_name: str
    kind: AppStorageKind
    storage_wtype: WType
    key_wtype: WType | None
    """if not None, then this is a map rather than singular"""
    key: BytesConstant
    """for maps, this is the prefix"""
    description: str | None

    def accept(self, visitor: ContractMemberVisitor[T]) -> T:
        return visitor.visit_app_storage_definition(self)

def _validate_avm_version(node: Node, _: object, avm_version: int | None) -> None:
    if avm_version is not None and avm_version not in SUPPORTED_AVM_VERSIONS:
        raise CodeError(
            "unsupported AVM version",
            node.source_location,
        )

@attrs.frozen(kw_only=True)
class LogicSignature(RootNode):
    id: LogicSigReference
    short_name: str
    program: Subroutine = attrs.field()
    docstring: str | None
    avm_version: int | None = attrs.field(validator=_validate_avm_version)

    @program.validator
    def _validate_program(self, _instance: object, program: Subroutine) -> None:
        if program.args:
            raise CodeError(
                "logicsig should not take any args",
                program.args[0].source_location,
            )
        if program.return_type not in (wtypes.uint64_wtype, wtypes.bool_wtype):
            raise CodeError(
                "Invalid return type for logicsig method, should be either bool or UInt64.",
                program.source_location,
            )

    def accept(self, visitor: RootNodeVisitor[T]) -> T:
        return visitor.visit_logic_signature(self)

@attrs.frozen
class CompiledContract(Expression):
    contract: ContractReference
    allocation_overrides: Mapping[TxnField, Expression] = attrs.field(
        factory=immutabledict, converter=immutabledict
    )
    prefix: str | None = None
    """
    Prefix will either be the value specified here or PuyaOptions.template_vars_prefix
    if prefix is None

    The prefix is then prefixed with the template_variables keys on this node to determine the
    final template variable name
    """
    template_variables: Mapping[str, Expression] = attrs.field(
        factory=immutabledict, converter=immutabledict
    )
    """
    template variables combined with their prefix defined on this node take precedence over
    template variables of the same key defined on PuyaOptions
    """

    @allocation_overrides.validator
    def _allocation_overrides(
        self, _attribute: object, value: Mapping[TxnField, Expression]
    ) -> None:
        if value.keys() - {
            TxnField.ExtraProgramPages,
            TxnField.GlobalNumUint,
            TxnField.GlobalNumByteSlice,
            TxnField.LocalNumUint,
            TxnField.LocalNumByteSlice,
        }:
            raise InternalError("only allocation fields can be overridden", self.source_location)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_compiled_contract(self)

@attrs.frozen
class CompiledLogicSig(Expression):
    logic_sig: LogicSigReference
    prefix: str | None = None
    template_variables: Mapping[str, Expression] = attrs.field(
        converter=immutabledict, factory=immutabledict
    )

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_compiled_logicsig(self)

@attrs.frozen(kw_only=True)
class StateTotals:
    global_uints: int | None = None
    local_uints: int | None = None
    global_bytes: int | None = None
    local_bytes: int | None = None

@attrs.frozen
class ARC4Router(Expression):
    wtype: WType = attrs.field(default=wtypes.bool_wtype, init=False)

    def accept(self, visitor: ExpressionVisitor[T]) -> T:
        return visitor.visit_arc4_router(self)

@attrs.frozen(kw_only=True)
class Contract(RootNode):
    id: ContractReference
    """The fully qualified ID, must be unique within a compilation run."""
    name: str
    """The short / friendly name for the Contract, used in output file naming and in ARC-32"""
    description: str | None
    """The (user supplied) contract description, currently only used in ARC-32 output"""
    method_resolution_order: Sequence[ContractReference]
    """
    The lookup order to use when resolving calls to contract methods.
    Shouldn't include the current contract, but otherwise should be exhaustive,
    ie no recursive lookup is implemented for bases of bases.
    Front-ends for languages with multiple-inheritance must linearize their hierarchy in the
    correct order, which is why bases of bases are not resolved, since they may end up in a
    different order depending on the contract that is ultimately being compiled.
    """
    approval_program: ContractMethod = attrs.field()
    """
    The entry point to the approval program, can also appear in methods, but this is not required.
    """
    clear_program: ContractMethod = attrs.field()
    """
    The entry point to the clear-state program, can appear in methods, but not required.
    """
    methods: Sequence[ContractMethod] = attrs.field(converter=tuple[ContractMethod, ...])
    """
    All the methods in this contract and in it's entire hierarchy.
    A Sequence for serialization purposes, order doesn't matter.
    """
    app_state: Sequence[AppStorageDefinition] = attrs.field(
        converter=tuple[AppStorageDefinition, ...]
    )
    """
    All the app storage on this contract.
    A Sequence for serialization purposes, order doesn't matter.
    """
    state_totals: StateTotals | None
    """State totals which can override in part or in full those implied by `app_state`."""
    reserved_scratch_space: Set[int]
    """Scratch slots that the contract is explicitly setting aside for direct/explicit usage."""
    avm_version: int | None = attrs.field(validator=_validate_avm_version)
    """AVM version to target, defaults to options.target_avm_version"""

    @approval_program.validator
    def check_approval(self, _attribute: object, approval: ContractMethod) -> None:
        if approval.args:
            raise CodeError(
                "approval method should not take any args (other than self)",
                approval.source_location,
            )
        if approval.return_type not in (wtypes.uint64_wtype, wtypes.bool_wtype):
            raise CodeError(
                "Invalid return type for approval method, should be either bool or UInt64.",
                approval.source_location,
            )
        if approval.arc4_method_config:
            raise CodeError(
                "approval method should not be marked as an ABI method",
                approval.source_location,
            )

    @clear_program.validator
    def check_clear(self, _attribute: object, clear: ContractMethod) -> None:
        if clear.args:
            raise CodeError(
                "clear-state method should not take any args (other than self)",
                clear.source_location,
            )
        if clear.return_type not in (wtypes.uint64_wtype, wtypes.bool_wtype):
            raise CodeError(
                "Invalid return type for clear-state method, should be either bool or UInt64.",
                clear.source_location,
            )
        if clear.arc4_method_config:
            raise CodeError(
                "clear-state method should not be marked as an ABI method",
                clear.source_location,
            )

    @cached_property
    def all_methods(self) -> Sequence[ContractMethod]:
        return unique((self.approval_program, self.clear_program, *self.methods))

    def accept(self, visitor: RootNodeVisitor[T]) -> T:
        return visitor.visit_contract(self)

    @typing.overload
    def resolve_contract_method(self, name: str) -> ContractMethod | None: ...

    @typing.overload
    def resolve_contract_method(
        self,
        name: str,
        source_location: SourceLocation,
        *,
        start: ContractReference,
        skip: bool = False,
    ) -> ContractMethod | None: ...

    def resolve_contract_method(
        self,
        name: str,
        source_location: SourceLocation | None = None,
        *,
        start: ContractReference | None = None,
        skip: bool = False,
    ) -> ContractMethod | None:
        mro = [self.id, *self.method_resolution_order]
        if start:
            try:
                curr_idx = mro.index(start)
            except ValueError:
                raise CodeError(
                    "call to base method outside current hierarchy", source_location
                ) from None
            mro = mro[curr_idx:]
        if skip:
            mro = mro[1:]
        for cref in mro:
            for method in self.methods:
                if method.member_name == name and method.cref == cref:
                    return method
        return None

class ARC4CreateOption(enum.Enum):
    allow = enum.auto()
    require = enum.auto()
    disallow = enum.auto()

@attrs.frozen(kw_only=True)
class ARC4BareMethodConfig:
    source_location: SourceLocation
    allowed_completion_types: Sequence[OnCompletionAction] = attrs.field(
        default=(OnCompletionAction.NoOp,),
        converter=tuple[OnCompletionAction, ...],
        validator=attrs.validators.min_len(1),
    )
    create: ARC4CreateOption = ARC4CreateOption.disallow

@attrs.frozen(kw_only=True)
class ABIMethodArgConstantDefault:
    value: Expression

@attrs.frozen(kw_only=True)
class ABIMethodArgMemberDefault:
    name: str

ABIMethodArgDefault = ABIMethodArgMemberDefault | ABIMethodArgConstantDefault

@attrs.frozen(kw_only=True)
class ARC4ABIMethodConfig:
    source_location: SourceLocation
    allowed_completion_types: Sequence[OnCompletionAction] = attrs.field(
        default=(OnCompletionAction.NoOp,),
        converter=tuple[OnCompletionAction],
        validator=attrs.validators.min_len(1),
    )
    create: ARC4CreateOption = ARC4CreateOption.disallow
    name: str
    readonly: bool = False
    default_args: immutabledict[str, ABIMethodArgDefault] = immutabledict()
    """Mapping is from parameter -> source"""

ARC4MethodConfig = ARC4BareMethodConfig | ARC4ABIMethodConfig


import decimal
import enum
import functools
import typing
from collections.abc import Mapping

import cattrs
from cattrs import ClassValidationError, IterableValidationError, transform_error
from cattrs.preconf.json import make_converter
from cattrs.strategies import configure_tagged_union, include_subclasses
from immutabledict import immutabledict

from puya import log
from puya.awst import nodes, txn_fields, wtypes
from puya.errors import PuyaError

logger = log.get_logger(__name__)

def _unstructure_optional_enum_literal(value: object) -> object:
    if value is None:
        return None
    if not isinstance(value, enum.Enum):
        raise TypeError("expected enum value")
    return value.value

@functools.cache
def _get_converter() -> cattrs.preconf.json.JsonConverter:
    converter = make_converter()

    # literals with optional enum
    converter.register_unstructure_hook_factory(
        cattrs.converters.is_literal_containing_enums, lambda _: _unstructure_optional_enum_literal
    )

    # TxnField and PuyaLibFunction as name
    for enum_type in (txn_fields.TxnField, nodes.PuyaLibFunction):
        converter.register_unstructure_hook(enum_type, lambda v: v.name)
        converter.register_structure_hook(enum_type, lambda v, t: t[v])

    # decimals as str
    converter.register_unstructure_hook(decimal.Decimal, str)
    converter.register_structure_hook(decimal.Decimal, lambda v, _: decimal.Decimal(v))

    # nodes.Switch has a mapping of Expression -> Block
    # which can't be serialized with that structure as a JSON object
    # need to convert into a list of pairs instead
    def is_switch_cases(typ: object) -> bool:
        if typing.get_origin(typ) is Mapping:
            args = typing.get_args(typ)
            return args == (nodes.Expression, nodes.Block)
        return False

    def unstructure_switch_cases(value: Mapping[nodes.Expression, nodes.Block]) -> object:
        return converter.unstructure(value.items(), list[tuple[nodes.Expression, nodes.Block]])

    def structure_switch_cases(value: object, _: type) -> object:
        items = converter.structure(value, list[tuple[nodes.Expression, nodes.Block]])
        return immutabledict(items)

    converter.register_unstructure_hook_func(is_switch_cases, unstructure_switch_cases)
    converter.register_structure_hook_func(is_switch_cases, structure_switch_cases)

    # register AWST types and unions, order is important to ensure correct configuration
    union_strategy = configure_tagged_union
    include_subclasses(wtypes.WType, converter, union_strategy=union_strategy)
    union_strategy(wtypes.WStructType | wtypes.ARC4Struct, converter)
    union_strategy(nodes.SubroutineTarget, converter)
    include_subclasses(nodes.Expression, converter, union_strategy=union_strategy)
    union_strategy(nodes.Lvalue, converter)
    union_strategy(nodes.StorageExpression, converter)
    union_strategy(nodes.CompileTimeConstantExpression, converter)
    include_subclasses(nodes.Statement, converter, union_strategy=union_strategy)
    include_subclasses(nodes.RootNode, converter, union_strategy=union_strategy)
    return converter

def awst_to_json(awst: nodes.AWST) -> str:
    return _get_converter().dumps(awst, indent=4)

def _find_and_log_puya_errors(err: ClassValidationError | IterableValidationError) -> None:
    for ex in err.exceptions:
        match ex:
            case PuyaError():
                logger.error(ex.msg, location=ex.location)
            case ClassValidationError() | IterableValidationError():
                _find_and_log_puya_errors(ex)

def awst_from_json(json: str) -> nodes.AWST:
    try:
        return _get_converter().loads(json, nodes.AWST)  # type: ignore[type-abstract]
    except (ClassValidationError, IterableValidationError) as err:
        _find_and_log_puya_errors(err)
        logger.debug("Deserialization error: \n" + "\n".join(transform_error(err)))
        raise ValueError(
            "Error during deserialization of AWST json. See debug log for details"
        ) from err


import base64
import typing
from collections.abc import Iterable, Iterator, Mapping

from puya.awst import nodes, wtypes
from puya.awst.visitors import (
    ContractMemberVisitor,
    ExpressionVisitor,
    RootNodeVisitor,
    StatementVisitor,
)
from puya.errors import InternalError

class ToCodeVisitor(
    RootNodeVisitor[list[str]],
    StatementVisitor[list[str]],
    ExpressionVisitor[str],
    ContractMemberVisitor[list[str]],
):
    def __init__(self) -> None:
        self._seen_single_evals = dict[nodes.SingleEvaluation, int]()

    def _single_eval_index(self, tmp: nodes.SingleEvaluation) -> int:
        return self._seen_single_evals.setdefault(tmp, len(self._seen_single_evals))

    @typing.override
    def visit_array_concat(self, expr: nodes.ArrayConcat) -> str:
        left = expr.left.accept(self)
        right = expr.right.accept(self)
        return f"{left} + {right}"

    @typing.override
    def visit_array_extend(self, expr: nodes.ArrayExtend) -> str:
        base = expr.base.accept(self)
        value = expr.other.accept(self)
        return f"{base}.extend({value})"

    @typing.override
    def visit_array_pop(self, expr: nodes.ArrayPop) -> str:
        base = expr.base.accept(self)
        return f"{base}.pop()"

    @typing.override
    def visit_copy(self, expr: nodes.Copy) -> str:
        value = expr.value.accept(self)
        return f"{value}.copy()"

    @typing.override
    def visit_reversed(self, expr: nodes.Reversed) -> str:
        sequence = expr.expr.accept(self)
        return f"reversed({sequence})"

    def visit_module(self, module: nodes.AWST) -> str:
        result = list[str]()
        for stmt in module:
            lines = stmt.accept(self)
            result.extend(lines)
        return "\n".join(result).strip()

    @typing.override
    def visit_arc4_decode(self, expr: nodes.ARC4Decode) -> str:
        return f"arc4_decode({expr.value.accept(self)}, {expr.wtype})"

    @typing.override
    def visit_arc4_encode(self, expr: nodes.ARC4Encode) -> str:
        return f"arc4_encode({expr.value.accept(self)}, {expr.wtype})"

    @typing.override
    def visit_reinterpret_cast(self, expr: nodes.ReinterpretCast) -> str:
        return f"reinterpret_cast<{expr.wtype}>({expr.expr.accept(self)})"

    @typing.override
    def visit_single_evaluation(self, expr: nodes.SingleEvaluation) -> str:
        # only render source the first time it is encountered
        source = "" if expr in self._seen_single_evals else f", source={expr.source.accept(self)}"
        eval_id = self._single_eval_index(expr)

        return "".join(
            (
                f"SINGLE_EVAL(id={eval_id}",
                source,
                ")",
            )
        )

    @typing.override
    def visit_app_state_expression(self, expr: nodes.AppStateExpression) -> str:
        return f"GlobalState[{expr.key.accept(self)}]"

    @typing.override
    def visit_app_account_state_expression(self, expr: nodes.AppAccountStateExpression) -> str:
        return f"LocalState[{expr.key.accept(self)}, {expr.account.accept(self)}]"

    @typing.override
    def visit_box_value_expression(self, expr: nodes.BoxValueExpression) -> str:
        return f"Box[{expr.key.accept(self)}]"

    @typing.override
    def visit_new_array(self, expr: nodes.NewArray) -> str:
        args = ", ".join(a.accept(self) for a in expr.values)
        return f"new {expr.wtype}({args})"

    @typing.override
    def visit_new_struct(self, expr: nodes.NewStruct) -> str:
        args = ", ".join([f"{name}=" + value.accept(self) for name, value in expr.values.items()])
        return f"new {expr.wtype}({args})"

    @typing.override
    def visit_enumeration(self, expr: nodes.Enumeration) -> str:
        sequence = expr.expr.accept(self)
        return f"enumerate({sequence})"

    @typing.override
    def visit_bytes_comparison_expression(self, expr: nodes.BytesComparisonExpression) -> str:
        return f"{expr.lhs.accept(self)} {expr.operator} {expr.rhs.accept(self)}"

    @typing.override
    def visit_subroutine_call_expression(self, expr: nodes.SubroutineCallExpression) -> str:
        match expr.target:
            case nodes.InstanceMethodTarget(member_name=member_name):
                target = f"this::{member_name}"
            case nodes.InstanceSuperMethodTarget(member_name=member_name):
                target = f"super::{member_name}"
            case nodes.ContractMethodTarget(cref=cref, member_name=member_name):
                target = "::".join((cref, member_name))
            case nodes.SubroutineID(target):
                pass
            case unhandled:
                typing.assert_never(unhandled)
        args = ", ".join(
            [(f"{a.name}=" if a.name else "") + a.value.accept(self) for a in expr.args]
        )
        return f"{target}({args})"

    @typing.override
    def visit_bytes_binary_operation(self, expr: nodes.BytesBinaryOperation) -> str:
        return f"{expr.left.accept(self)} {expr.op.value} {expr.right.accept(self)}"

    @typing.override
    def visit_boolean_binary_operation(self, expr: nodes.BooleanBinaryOperation) -> str:
        return f"{expr.left.accept(self)} {expr.op.value} {expr.right.accept(self)}"

    @typing.override
    def visit_not_expression(self, expr: nodes.Not) -> str:
        return f"!({expr.expr.accept(self)})"

    @typing.override
    def visit_bytes_augmented_assignment(
        self, statement: nodes.BytesAugmentedAssignment
    ) -> list[str]:
        return [
            f"{statement.target.accept(self)} {statement.op.value}= {statement.value.accept(self)}"
        ]

    @typing.override
    def visit_range(self, range_node: nodes.Range) -> str:
        range_args = ", ".join(
            [r.accept(self) for r in [range_node.start, range_node.stop, range_node.step]]
        )
        return f"range({range_args})"

    @typing.override
    def visit_for_in_loop(self, statement: nodes.ForInLoop) -> list[str]:
        sequence = statement.sequence.accept(self)
        loop_body = statement.loop_body.accept(self)
        item_vars = statement.items.accept(self)
        return [
            f"for {item_vars} in {sequence} {{",
            *_indent(loop_body),
            "}",
        ]

    @typing.override
    def visit_subroutine(self, statement: nodes.Subroutine) -> list[str]:
        args = ", ".join([f"{a.name}: {a.wtype}" for a in statement.args])
        body = statement.body.accept(self)
        return [
            "",
            f"subroutine {statement.name}({args}): {statement.return_type}",
            "{",
            *_indent(body),
            "}",
        ]

    @typing.override
    def visit_app_storage_definition(self, defn: nodes.AppStorageDefinition) -> list[str]:
        raise InternalError("app storage is converted as part of class")

    @typing.override
    def visit_contract(self, c: nodes.Contract) -> list[str]:
        body = [
            "method_resolution_order: (",
            *_indent(f"{cref}," for cref in c.method_resolution_order),
            ")",
        ]
        if c.app_state:
            state_by_kind = dict[nodes.AppStorageKind, list[nodes.AppStorageDefinition]]()
            for state in c.app_state:
                state_by_kind.setdefault(state.kind, []).append(state)
            for kind_name, kind in (
                ("globals", nodes.AppStorageKind.app_global),
                ("locals", nodes.AppStorageKind.account_local),
                ("boxes", nodes.AppStorageKind.box),
            ):
                state_of_kind = state_by_kind.pop(kind, [])
                if state_of_kind:
                    body.extend(
                        [
                            f"{kind_name} {{",
                            *_indent(
                                (
                                    f"[{s.key.accept(self)}]: {s.key_wtype} => {s.storage_wtype}"
                                    if s.key_wtype is not None
                                    else f"[{s.key.accept(self)}]: {s.storage_wtype}"
                                )
                                for s in state_of_kind
                            ),
                            "}",
                        ]
                    )
            if state_by_kind:
                raise InternalError(
                    f"Unhandled app state kinds: {', '.join(map(str, state_by_kind.keys()))}",
                    c.source_location,
                )
        if c.reserved_scratch_space:
            body.extend(
                [
                    "reserved_scratch_space {",
                    *_indent([", ".join(_collapse_sequential_ranges(c.reserved_scratch_space))]),
                    "}",
                ]
            )
        for sub in c.all_methods:
            lines = sub.accept(self)
            body.extend(lines)

        if body and not body[0].strip():
            body = body[1:]

        header = ["contract", c.name]

        return [
            "",
            " ".join(header),
            "{",
            *_indent(body),
            "}",
        ]

    @typing.override
    def visit_logic_signature(self, statement: nodes.LogicSignature) -> list[str]:
        body = statement.program.body.accept(self)
        return [
            "",
            f"logicsig {statement.id}",
            "{",
            *_indent(body),
            "}",
        ]

    @typing.override
    def visit_contract_method(self, statement: nodes.ContractMethod) -> list[str]:
        body = statement.body.accept(self)
        args = ", ".join([f"{a.name}: {a.wtype}" for a in statement.args])
        match statement.arc4_method_config:
            case None:
                deco = "subroutine"
            case nodes.ARC4BareMethodConfig():
                deco = "baremethod"
            case nodes.ARC4ABIMethodConfig(name=config_name):
                if statement.member_name != config_name:
                    deco = f"abimethod[name_override={config_name}]"
                else:
                    deco = "abimethod"
            case other:
                typing.assert_never(other)

        return [
            "",
            f"{deco} {statement.full_name}({args}): {statement.return_type}",
            "{",
            *_indent(body),
            "}",
        ]

    @typing.override
    def visit_assignment_expression(self, expr: nodes.AssignmentExpression) -> str:
        return f"{expr.target.accept(self)}: {expr.target.wtype} := {expr.value.accept(self)}"

    @typing.override
    def visit_assignment_statement(self, stmt: nodes.AssignmentStatement) -> list[str]:
        return [f"{stmt.target.accept(self)}: {stmt.target.wtype} = {stmt.value.accept(self)}"]

    @typing.override
    def visit_uint64_binary_operation(self, expr: nodes.UInt64BinaryOperation) -> str:
        return f"{expr.left.accept(self)} {expr.op.value} {expr.right.accept(self)}"

    @typing.override
    def visit_biguint_binary_operation(self, expr: nodes.BigUIntBinaryOperation) -> str:
        return f"{expr.left.accept(self)} b{expr.op.value} {expr.right.accept(self)}"

    @typing.override
    def visit_uint64_unary_operation(self, expr: nodes.UInt64UnaryOperation) -> str:
        return f"{expr.op.value}({expr.expr.accept(self)})"

    @typing.override
    def visit_bytes_unary_operation(self, expr: nodes.BytesUnaryOperation) -> str:
        return f"b{expr.op.value}({expr.expr.accept(self)})"

    @typing.override
    def visit_integer_constant(self, expr: nodes.IntegerConstant) -> str:
        if expr.teal_alias:
            return expr.teal_alias
        match expr.wtype:
            case wtypes.uint64_wtype:
                suffix = "u"
            case wtypes.biguint_wtype:
                suffix = "n"
            case wtypes.ARC4UIntN(n=n):
                suffix = f"_arc4u{n}"
            case _:
                raise InternalError(
                    f"Numeric type not implemented: {expr.wtype}", expr.source_location
                )
        return f"{expr.value}{suffix}"

    @typing.override
    def visit_decimal_constant(self, expr: nodes.DecimalConstant) -> str:
        d = str(expr.value)
        if expr.wtype.n <= 64:
            suffix = f"arc4u{expr.wtype.n}x{expr.wtype.m}"
        else:
            suffix = f"arc4n{expr.wtype.n}x{expr.wtype.m}"
        return f"{d}{suffix}"

    @typing.override
    def visit_bool_constant(self, expr: nodes.BoolConstant) -> str:
        return "true" if expr.value else "false"

    @typing.override
    def visit_bytes_constant(self, expr: nodes.BytesConstant) -> str:
        match expr.encoding:
            case nodes.BytesEncoding.utf8:
                return _bytes_str(expr.value)
            case nodes.BytesEncoding.base32:
                return f'b32<"{base64.b32encode(expr.value).decode("ascii")}">'
            case nodes.BytesEncoding.base64:
                return f'b64<"{base64.b64encode(expr.value).decode("ascii")}">'
            case nodes.BytesEncoding.base16 | nodes.BytesEncoding.unknown:
                return f'hex<"{expr.value.hex().upper()}">'

    @typing.override
    def visit_string_constant(self, expr: nodes.StringConstant) -> str:
        return repr(expr.value)

    @typing.override
    def visit_void_constant(self, expr: nodes.VoidConstant) -> str:
        return "void"

    @typing.override
    def visit_method_constant(self, expr: nodes.MethodConstant) -> str:
        return f'Method("{expr.value}")'

    @typing.override
    def visit_address_constant(self, expr: nodes.AddressConstant) -> str:
        return f'Address("{expr.value}")'

    @typing.override
    def visit_compiled_contract(self, expr: nodes.CompiledContract) -> str:
        template_vars_fragment = self._template_vars_fragment(expr.prefix, expr.template_variables)
        overrides = ", ".join(
            f"{k.name}={v.accept(self)}" for k, v in expr.allocation_overrides.items()
        )
        return f"compiled_contract({expr.contract},{overrides},{template_vars_fragment})"

    @typing.override
    def visit_compiled_logicsig(self, expr: nodes.CompiledLogicSig) -> str:
        template_vars_fragment = self._template_vars_fragment(expr.prefix, expr.template_variables)
        return f"compiled_logicsig({expr.logic_sig!r}{template_vars_fragment})"

    def _template_vars_fragment(
        self, prefix: str | None, variables: Mapping[str, nodes.Expression]
    ) -> str:
        variables_str = ", ".join(f"{k!r}: {v.accept(self)}" for k, v in variables.items())
        return f", {prefix=!r}, variables={{{variables_str}}}"

    @typing.override
    def visit_conditional_expression(self, expr: nodes.ConditionalExpression) -> str:
        condition = expr.condition.accept(self)
        true = expr.true_expr.accept(self)
        false = expr.false_expr.accept(self)
        return f"({condition}) ? ({true}) : ({false})"

    @typing.override
    def visit_numeric_comparison_expression(self, expr: nodes.NumericComparisonExpression) -> str:
        return f"{expr.lhs.accept(self)} {expr.operator.value} {expr.rhs.accept(self)}"

    @typing.override
    def visit_var_expression(self, expr: nodes.VarExpression) -> str:
        return expr.name

    @typing.override
    def visit_checked_maybe(self, expr: nodes.CheckedMaybe) -> str:
        return f"checked_maybe({expr.expr.accept(self)})"

    @typing.override
    def visit_intrinsic_call(self, expr: nodes.IntrinsicCall) -> str:
        result = expr.op_code
        if expr.immediates:
            result += "<" + ", ".join([str(immediate) for immediate in expr.immediates]) + ">"
        result += "("
        if expr.stack_args:
            result += ", ".join([stack_arg.accept(self) for stack_arg in expr.stack_args])
        result += ")"
        return result

    @typing.override
    def visit_puya_lib_call(self, expr: nodes.PuyaLibCall) -> str:
        result = expr.func.value.id
        result += "("
        if expr.args:
            result += ", ".join(
                [(f"{a.name}=" if a.name else "") + a.value.accept(self) for a in expr.args]
            )
        result += ")"
        return result

    @typing.override
    def visit_group_transaction_reference(self, ref: nodes.GroupTransactionReference) -> str:
        if ref.wtype.transaction_type is None:
            type_ = "any"
        else:
            type_ = ref.wtype.transaction_type.name
        return f"group_transaction(index={ref.index.accept(self)}, type={type_})"

    @typing.override
    def visit_create_inner_transaction(self, expr: nodes.CreateInnerTransaction) -> str:
        fields = []
        for field, value in expr.fields.items():
            fields.append(f"{field.immediate}={value.accept(self)}")
        return f"create_inner_transaction({', '.join(fields)})"

    @typing.override
    def visit_update_inner_transaction(self, expr: nodes.UpdateInnerTransaction) -> str:
        fields = []
        for field, value in expr.fields.items():
            fields.append(f"{field.immediate}={value.accept(self)}")
        return f"update_inner_transaction({expr.itxn.accept(self)},{', '.join(fields)})"

    @typing.override
    def visit_submit_inner_transaction(self, call: nodes.SubmitInnerTransaction) -> str:
        itxns = f'{", ".join(itxn.accept(self) for itxn in call.itxns)}'
        return f"submit_txn({itxns})"

    @typing.override
    def visit_inner_transaction_field(self, itxn_field: nodes.InnerTransactionField) -> str:
        txn = itxn_field.itxn.accept(self)
        result = f"{txn}.{itxn_field.field.immediate}"
        if itxn_field.array_index is not None:
            index = itxn_field.array_index.accept(self)
            result = f"{result}[{index}]"
        return result

    @typing.override
    def visit_tuple_expression(self, expr: nodes.TupleExpression) -> str:
        items = ", ".join([item.accept(self) for item in expr.items])
        return f"({items})"

    @typing.override
    def visit_tuple_item_expression(self, expr: nodes.TupleItemExpression) -> str:
        base = expr.base.accept(self)
        return f"{base}[{expr.index}]"

    @typing.override
    def visit_field_expression(self, expr: nodes.FieldExpression) -> str:
        base = expr.base.accept(self)
        return f"{base}.{expr.name}"

    @typing.override
    def visit_index_expression(self, expr: nodes.IndexExpression) -> str:
        return f"{expr.base.accept(self)}[{expr.index.accept(self)}]"

    @typing.override
    def visit_slice_expression(self, expr: nodes.SliceExpression) -> str:
        start = expr.begin_index.accept(self) if expr.begin_index else ""
        stop = expr.end_index.accept(self) if expr.end_index else ""

        return f"{expr.base.accept(self)}[{start}:{stop}]"

    @typing.override
    def visit_intersection_slice_expression(self, expr: nodes.IntersectionSliceExpression) -> str:
        start = (
            expr.begin_index.accept(self)
            if isinstance(expr.begin_index, nodes.Expression)
            else (expr.begin_index if expr.begin_index is not None else "")
        )
        stop = (
            expr.end_index.accept(self)
            if isinstance(expr.end_index, nodes.Expression)
            else (expr.end_index if expr.end_index is not None else "")
        )

        return f"{expr.base.accept(self)}[{start}:{stop}]"

    @typing.override
    def visit_block(self, statement: nodes.Block) -> list[str]:
        statements = [line for statement in statement.body for line in statement.accept(self)]
        if statement.label:
            return [f"{statement.label}:", *statements]
        return statements

    @typing.override
    def visit_goto(self, statement: nodes.Goto) -> list[str]:
        return [f"goto {statement.target}"]

    @typing.override
    def visit_if_else(self, statement: nodes.IfElse) -> list[str]:
        if_branch = statement.if_branch.accept(self)
        if_block = [
            f"if ({statement.condition.accept(self)}) {{",
            *_indent(if_branch),
        ]
        if statement.else_branch is not None:
            else_branch = statement.else_branch.accept(self)
            else_block = ["} else {", *_indent(else_branch)]
        else:
            else_block = []
        return [*if_block, *else_block, "}"]

    @typing.override
    def visit_switch(self, statement: nodes.Switch) -> list[str]:
        match_block = [f"switch ({statement.value.accept(self)}) {{"]
        for case_value, case_block in statement.cases.items():
            value = case_value.accept(self)
            block = case_block.accept(self)
            match_block.extend(
                _indent(
                    [
                        f"case {value}: {{",
                        *_indent(block),
                        "}",
                    ]
                )
            )
        if statement.default_case:
            default_block = statement.default_case.accept(self)
            match_block.extend(
                _indent(
                    [
                        "case _: {",
                        *_indent(default_block),
                        "}",
                    ]
                )
            )
        match_block.append("}")
        return match_block

    @typing.override
    def visit_while_loop(self, statement: nodes.WhileLoop) -> list[str]:
        loop_body = statement.loop_body.accept(self)
        return [
            f"while ({statement.condition.accept(self)}) {{",
            *_indent(loop_body),
            "}",
        ]

    @typing.override
    def visit_loop_exit(self, _statement: nodes.LoopExit) -> list[str]:
        return ["break"]

    @typing.override
    def visit_return_statement(self, statement: nodes.ReturnStatement) -> list[str]:
        if not statement.value:
            return ["return"]
        return [f"return {statement.value.accept(self)}"]

    @typing.override
    def visit_assert_expression(self, statement: nodes.AssertExpression) -> str:
        error_message = "" if statement.error_message is None else f'"{statement.error_message}"'
        if not statement.condition:
            result = "err("
            if error_message:
                result += error_message
            result += ")"
        else:
            result = f"assert({statement.condition.accept(self)}"
            if error_message:
                result += f", comment={error_message}"
            result += ")"
        return result

    @typing.override
    def visit_loop_continue(self, _statement: nodes.LoopContinue) -> list[str]:
        return ["continue"]

    @typing.override
    def visit_expression_statement(self, statement: nodes.ExpressionStatement) -> list[str]:
        return [
            statement.expr.accept(self),
        ]

    @typing.override
    def visit_uint64_augmented_assignment(
        self, statement: nodes.UInt64AugmentedAssignment
    ) -> list[str]:
        return [
            f"{statement.target.accept(self)} {statement.op.value}= {statement.value.accept(self)}"
        ]

    @typing.override
    def visit_biguint_augmented_assignment(
        self, statement: nodes.BigUIntAugmentedAssignment
    ) -> list[str]:
        return [
            f"{statement.target.accept(self)} {statement.op.value}= {statement.value.accept(self)}"
        ]

    @typing.override
    def visit_state_get_ex(self, expr: nodes.StateGetEx) -> str:
        return f"STATE_GET_EX({expr.field.accept(self)})"

    @typing.override
    def visit_state_delete(self, statement: nodes.StateDelete) -> str:
        return f"STATE_DELETE({statement.field.accept(self)})"

    @typing.override
    def visit_state_get(self, expr: nodes.StateGet) -> str:
        return f"STATE_GET({expr.field.accept(self)}, default={expr.default.accept(self)})"

    @typing.override
    def visit_state_exists(self, expr: nodes.StateExists) -> str:
        return f"STATE_EXISTS({expr.field.accept(self)})"

    @typing.override
    def visit_template_var(self, expr: nodes.TemplateVar) -> str:
        return f"TemplateVar[{expr.wtype}]({expr.name})"

    @typing.override
    def visit_biguint_postfix_unary_operation(
        self, expr: nodes.BigUIntPostfixUnaryOperation
    ) -> str:
        return f"{expr.target.accept(self)}{expr.op}"

    @typing.override
    def visit_uint64_postfix_unary_operation(self, expr: nodes.UInt64PostfixUnaryOperation) -> str:
        return f"{expr.target.accept(self)}{expr.op}"

    @typing.override
    def visit_arc4_router(self, expr: nodes.ARC4Router) -> str:
        return "arc4_router()"

    @typing.override
    def visit_emit(self, expr: nodes.Emit) -> str:
        return f"emit({expr.signature!r}, {expr.value.accept(self)})"

def _indent(lines: Iterable[str], indent_size: str = "  ") -> Iterator[str]:
    yield from (f"{indent_size}{line}" for line in lines)

def _bytes_str(b: bytes) -> str:
    return repr(b)[1:]

def _collapse_sequential_ranges(nums: Iterable[int]) -> Iterable[str]:
    ranges = list[tuple[int, int]]()
    for num in sorted(nums):
        if ranges and num == ranges[-1][1] + 1:
            ranges[-1] = (ranges[-1][0], num)
        else:
            ranges.append((num, num))
    for start, stop in ranges:
        if start == stop:
            yield str(start)
        else:
            yield f"{start}..{stop}"


# ruff: noqa: PIE796
import enum
import typing

import attrs

from puya.avm import AVMType
from puya.awst import wtypes

__all__ = [
    "TxnField",
]

@attrs.frozen(eq=False, hash=False)
class _TxnFieldData:
    wtype: wtypes.WType
    num_values: int = attrs.field(default=1, validator=attrs.validators.ge(1), kw_only=True)
    is_inner_param: bool = attrs.field(default=True, kw_only=True)

@enum.unique
class TxnField(enum.Enum):
    def __init__(self, data: _TxnFieldData):
        # _name_ is set by the EnumType metaclass during construction,
        # and refers to the class member name
        self.immediate: typing.Final = self._name_
        assert data.wtype.scalar_type is not None
        self.avm_type: typing.Final = data.wtype.scalar_type
        typing.assert_type(self.avm_type, typing.Literal[AVMType.uint64, AVMType.bytes])
        self.wtype: typing.Final = data.wtype
        self.num_values: typing.Final = data.num_values
        self.is_inner_param: typing.Final = data.is_inner_param

    @property
    def is_array(self) -> bool:
        return self.num_values > 1

    def valid_argument_type(self, wtype: wtypes.WType) -> bool:
        if not self.is_array:
            return wtype.scalar_type == self.avm_type
        else:
            return isinstance(wtype, wtypes.WTuple) and all(
                item_wtype.scalar_type == self.avm_type for item_wtype in wtype.types
            )

    def __repr__(self) -> str:
        return (
            f"{type(self).__name__}("
            f"immediate={self.immediate!r},"
            f" wtype={self.wtype},"
            f" num_values={self.num_values!r},"
            f" is_inner_param={self.is_inner_param!r}"
            ")"
        )

    Sender = _TxnFieldData(wtypes.account_wtype)
    Fee = _TxnFieldData(wtypes.uint64_wtype)
    FirstValid = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    FirstValidTime = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    LastValid = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    Note = _TxnFieldData(wtypes.bytes_wtype)
    Lease = _TxnFieldData(wtypes.bytes_wtype, is_inner_param=False)
    Receiver = _TxnFieldData(wtypes.account_wtype)
    Amount = _TxnFieldData(wtypes.uint64_wtype)
    CloseRemainderTo = _TxnFieldData(wtypes.account_wtype)
    VotePK = _TxnFieldData(wtypes.bytes_wtype)
    SelectionPK = _TxnFieldData(wtypes.bytes_wtype)
    VoteFirst = _TxnFieldData(wtypes.uint64_wtype)
    VoteLast = _TxnFieldData(wtypes.uint64_wtype)
    VoteKeyDilution = _TxnFieldData(wtypes.uint64_wtype)
    Type = _TxnFieldData(wtypes.bytes_wtype)
    TypeEnum = _TxnFieldData(wtypes.uint64_wtype)
    XferAsset = _TxnFieldData(wtypes.asset_wtype)
    AssetAmount = _TxnFieldData(wtypes.uint64_wtype)
    AssetSender = _TxnFieldData(wtypes.account_wtype)
    AssetReceiver = _TxnFieldData(wtypes.account_wtype)
    AssetCloseTo = _TxnFieldData(wtypes.account_wtype)
    GroupIndex = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    TxID = _TxnFieldData(wtypes.bytes_wtype, is_inner_param=False)
    # v2
    ApplicationID = _TxnFieldData(wtypes.application_wtype)
    OnCompletion = _TxnFieldData(wtypes.uint64_wtype)
    NumAppArgs = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    NumAccounts = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    ApprovalProgram = _TxnFieldData(wtypes.bytes_wtype)
    ClearStateProgram = _TxnFieldData(wtypes.bytes_wtype)
    RekeyTo = _TxnFieldData(wtypes.account_wtype)
    ConfigAsset = _TxnFieldData(wtypes.asset_wtype)
    ConfigAssetTotal = _TxnFieldData(wtypes.uint64_wtype)
    ConfigAssetDecimals = _TxnFieldData(wtypes.uint64_wtype)
    ConfigAssetDefaultFrozen = _TxnFieldData(wtypes.bool_wtype)
    ConfigAssetUnitName = _TxnFieldData(wtypes.bytes_wtype)
    ConfigAssetName = _TxnFieldData(wtypes.bytes_wtype)
    ConfigAssetURL = _TxnFieldData(wtypes.bytes_wtype)
    ConfigAssetMetadataHash = _TxnFieldData(wtypes.bytes_wtype)
    ConfigAssetManager = _TxnFieldData(wtypes.account_wtype)
    ConfigAssetReserve = _TxnFieldData(wtypes.account_wtype)
    ConfigAssetFreeze = _TxnFieldData(wtypes.account_wtype)
    ConfigAssetClawback = _TxnFieldData(wtypes.account_wtype)
    FreezeAsset = _TxnFieldData(wtypes.asset_wtype)
    FreezeAssetAccount = _TxnFieldData(wtypes.account_wtype)
    FreezeAssetFrozen = _TxnFieldData(wtypes.bool_wtype)
    # v3
    NumAssets = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    NumApplications = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    GlobalNumUint = _TxnFieldData(wtypes.uint64_wtype)
    GlobalNumByteSlice = _TxnFieldData(wtypes.uint64_wtype)
    LocalNumUint = _TxnFieldData(wtypes.uint64_wtype)
    LocalNumByteSlice = _TxnFieldData(wtypes.uint64_wtype)
    # v4
    ExtraProgramPages = _TxnFieldData(wtypes.uint64_wtype)
    # v5
    Nonparticipation = _TxnFieldData(wtypes.bool_wtype)
    NumLogs = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    CreatedAssetID = _TxnFieldData(wtypes.asset_wtype, is_inner_param=False)
    CreatedApplicationID = _TxnFieldData(wtypes.application_wtype, is_inner_param=False)
    # v6
    LastLog = _TxnFieldData(wtypes.bytes_wtype, is_inner_param=False)
    StateProofPK = _TxnFieldData(wtypes.bytes_wtype)
    # v7
    NumApprovalProgramPages = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    NumClearStateProgramPages = _TxnFieldData(wtypes.uint64_wtype, is_inner_param=False)
    # array fields
    # TODO: allow configuring as these are consensus values
    # v2
    ApplicationArgs = _TxnFieldData(wtypes.bytes_wtype, num_values=16)
    Accounts = _TxnFieldData(wtypes.account_wtype, num_values=4)
    # v3
    Assets = _TxnFieldData(wtypes.asset_wtype, num_values=8)
    Applications = _TxnFieldData(wtypes.application_wtype, num_values=8)
    # v5
    Logs = _TxnFieldData(wtypes.bytes_wtype, num_values=32, is_inner_param=False)
    # v7
    ApprovalProgramPages = _TxnFieldData(wtypes.bytes_wtype, num_values=4)
    ClearStateProgramPages = _TxnFieldData(wtypes.bytes_wtype, num_values=4)