import typing
from collections.abc import Sequence

import mypy.nodes

from puya import algo_constants, log
from puya.awst import wtypes
from puya.awst.nodes import (
    ARC4Decode,
    ARC4Encode,
    ArrayConcat,
    BytesAugmentedAssignment,
    BytesBinaryOperator,
    Expression,
    Statement,
    StringConstant,
)
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb._bytes_backed import BytesBackedInstanceExpressionBuilder
from puyapy.awst_build.eb._utils import compare_expr_bytes, dummy_statement
from puyapy.awst_build.eb.arc4._base import ARC4TypeBuilder, arc4_bool_bytes
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
)
from puyapy.awst_build.eb.string import StringExpressionBuilder

__all__ = [
    "ARC4StringTypeBuilder",
    "ARC4StringExpressionBuilder",
]

logger = log.get_logger(__name__)

class ARC4StringTypeBuilder(ARC4TypeBuilder):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.ARC4StringType, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case str(literal_value):
                try:
                    bytes_value = literal_value.encode("utf8")
                except UnicodeEncodeError as ex:
                    logger.error(  # noqa: TRY400
                        f"invalid UTF-8 string (encoding error: {ex})",
                        location=literal.source_location,
                    )
                else:
                    if len(bytes_value) > (algo_constants.MAX_BYTES_LENGTH - 2):
                        logger.error(
                            "encoded string exceeds max byte array length",
                            location=literal.source_location,
                        )
                return _arc4_str_literal(literal_value, location)
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.at_most_one_arg(args, location)
        match arg:
            case InstanceBuilder(pytype=pytypes.StrLiteralType):
                return arg.resolve_literal(ARC4StringTypeBuilder(location))
            case None:
                return _arc4_str_literal("", location)
            case _:
                arg = expect.argument_of_type_else_dummy(arg, pytypes.StringType)
                return _from_native(arg, location)

class ARC4StringExpressionBuilder(
    NotIterableInstanceExpressionBuilder, BytesBackedInstanceExpressionBuilder
):
    def __init__(self, expr: Expression):
        super().__init__(pytypes.ARC4StringType, expr)

    @typing.override
    def augmented_assignment(
        self, op: BuilderBinaryOp, rhs: InstanceBuilder, location: SourceLocation
    ) -> Statement:
        if op != BuilderBinaryOp.add:
            logger.error(f"unsupported operator for type: {op.value!r}", location=location)
            return dummy_statement(location)

        rhs = rhs.resolve_literal(ARC4StringTypeBuilder(rhs.source_location))
        if pytypes.StringType <= rhs.pytype:
            value = _from_native(rhs, rhs.source_location).resolve()
        else:
            value = expect.argument_of_type_else_dummy(rhs, self.pytype).resolve()

        return BytesAugmentedAssignment(
            target=self.resolve_lvalue(),
            op=BytesBinaryOperator.add,
            value=value,
            source_location=location,
        )

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        if op != BuilderBinaryOp.add:
            return NotImplemented

        other = other.resolve_literal(ARC4StringTypeBuilder(other.source_location))
        if pytypes.ARC4StringType <= other.pytype:
            other_expr = other.resolve()
        elif pytypes.StringType <= other.pytype:
            other_expr = _from_native(other, other.source_location).resolve()
        else:
            return NotImplemented

        lhs = self.resolve()
        rhs = other_expr
        if reverse:
            (lhs, rhs) = (rhs, lhs)

        return ARC4StringExpressionBuilder(
            ArrayConcat(
                left=lhs,
                right=rhs,
                wtype=wtypes.arc4_string_alias,
                source_location=location,
            )
        )

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        other = other.resolve_literal(ARC4StringTypeBuilder(other.source_location))
        if pytypes.ARC4StringType <= other.pytype:
            lhs: InstanceBuilder = self
        elif pytypes.StringType <= other.pytype:
            # when comparing arc4 to native, easier to convert by stripping length prefix
            lhs = _string_to_native(self, self.source_location)
        else:
            return NotImplemented
        return compare_expr_bytes(
            lhs=lhs.resolve(),
            op=op,
            rhs=other.resolve(),
            source_location=location,
        )

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return arc4_bool_bytes(
            self,
            false_bytes=b"\x00\x00",
            negate=negate,
            location=location,
        )

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "native":
                return _string_to_native(self, location)
            case _:
                return super().member_access(name, location)

def _string_to_native(
    builder: InstanceBuilder, location: SourceLocation
) -> StringExpressionBuilder:
    assert pytypes.ARC4StringType <= builder.pytype
    return StringExpressionBuilder(
        ARC4Decode(
            value=builder.resolve(),
            wtype=pytypes.StringType.wtype,
            source_location=location,
        )
    )

def _arc4_str_literal(value: str, location: SourceLocation) -> InstanceBuilder:
    return ARC4StringExpressionBuilder(
        StringConstant(value=value, source_location=location, wtype=wtypes.arc4_string_alias)
    )

def _from_native(eb: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
    assert pytypes.StringType <= eb.pytype
    return ARC4StringExpressionBuilder(
        ARC4Encode(
            value=eb.resolve(),
            wtype=wtypes.arc4_string_alias,
            source_location=location,
        )
    )


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst import wtypes
from puya.awst.nodes import Copy, Expression, FieldExpression, NewStruct
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder, NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb._bytes_backed import (
    BytesBackedInstanceExpressionBuilder,
    BytesBackedTypeBuilder,
)
from puyapy.awst_build.eb._utils import compare_bytes, constant_bool_and_error, dummy_value
from puyapy.awst_build.eb.arc4._base import CopyBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import BuilderComparisonOp, InstanceBuilder, NodeBuilder
from puyapy.awst_build.utils import get_arg_mapping

logger = log.get_logger(__name__)

class ARC4StructTypeBuilder(BytesBackedTypeBuilder[pytypes.StructType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.StructType)
        assert pytypes.ARC4StructBaseType < typ
        super().__init__(typ, location)

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        pytype = self.produces()
        field_mapping, any_missing = get_arg_mapping(
            required_positional_names=list(pytype.fields),
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
        )
        if any_missing:
            return dummy_value(pytype, location)

        values = {
            field_name: expect.argument_of_type_else_dummy(
                field_mapping[field_name], field_type
            ).resolve()
            for field_name, field_type in pytype.fields.items()
        }
        assert isinstance(pytype.wtype, wtypes.ARC4Struct)
        expr = NewStruct(wtype=pytype.wtype, values=values, source_location=location)
        return ARC4StructExpressionBuilder(expr, pytype)

class ARC4StructExpressionBuilder(
    NotIterableInstanceExpressionBuilder[pytypes.StructType],
    BytesBackedInstanceExpressionBuilder[pytypes.StructType],
):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.StructType)
        super().__init__(typ, expr)

    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case field_name if field := self.pytype.fields.get(field_name):
                result_expr = FieldExpression(
                    base=self.resolve(),
                    name=field_name,
                    source_location=location,
                )
                return builder_for_instance(field, result_expr)
            case "copy":
                return CopyBuilder(self.resolve(), location, self.pytype)
            case "_replace":
                return _Replace(self, self.pytype, location)
            case _:
                return super().member_access(name, location)

    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        return compare_bytes(self=self, op=op, other=other, source_location=location)

    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return constant_bool_and_error(value=True, location=location, negate=negate)

class _Replace(FunctionBuilder):
    def __init__(
        self,
        instance: ARC4StructExpressionBuilder,
        struct_type: pytypes.StructType,
        location: SourceLocation,
    ):
        super().__init__(location)
        self.instance = instance
        self.struct_type = struct_type

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        pytype = self.struct_type
        field_mapping, _ = get_arg_mapping(
            optional_kw_only=list(pytype.fields),
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
        )
        base_expr = self.instance.single_eval().resolve()
        values = dict[str, Expression]()
        for field_name, field_pytype in pytype.fields.items():
            new_value = field_mapping.get(field_name)
            if new_value is not None:
                item_builder = expect.argument_of_type_else_dummy(new_value, field_pytype)
                item = item_builder.resolve()
            else:
                field_wtype = field_pytype.checked_wtype(location)
                item = FieldExpression(base=base_expr, name=field_name, source_location=location)
                if not field_wtype.immutable:
                    logger.error(
                        f"mutable field {field_name!r} requires explicit copy", location=location
                    )
                    # implicitly create a copy node so that there is only one error
                    item = Copy(value=item, source_location=location)
            values[field_name] = item
        new_tuple = NewStruct(values=values, wtype=pytype.wtype, source_location=location)
        return ARC4StructExpressionBuilder(new_tuple, pytype)


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst.nodes import ARC4Decode, ARC4Encode, Expression, TupleItemExpression
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import GenericTypeBuilder
from puyapy.awst_build.eb._bytes_backed import BytesBackedInstanceExpressionBuilder
from puyapy.awst_build.eb._utils import compare_bytes, constant_bool_and_error, dummy_value
from puyapy.awst_build.eb.arc4._base import ARC4TypeBuilder, CopyBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    BuilderComparisonOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    StaticSizedCollectionBuilder,
)
from puyapy.awst_build.eb.tuple import TupleExpressionBuilder

logger = log.get_logger(__name__)

class ARC4TupleGenericTypeBuilder(GenericTypeBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.exactly_one_arg(args, location, default=expect.default_raise)
        match arg:
            case InstanceBuilder(pytype=pytypes.TupleType(items=items)):
                typ = pytypes.GenericARC4TupleType.parameterise(items, location)
                return ARC4TupleExpressionBuilder(
                    ARC4Encode(value=arg.resolve(), wtype=typ.wtype, source_location=location), typ
                )
            case _:
                # don't know expected type, so raise
                expect.not_this_type(arg, default=expect.default_raise)

class ARC4TupleTypeBuilder(ARC4TypeBuilder[pytypes.ARC4TupleType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.ARC4TupleType)
        super().__init__(typ, location)

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        typ = self.produces()
        native_type = pytypes.GenericTupleType.parameterise(typ.items, location)
        arg = expect.exactly_one_arg(
            args, location, default=expect.default_dummy_value(native_type)
        )
        wtype = typ.wtype
        if not wtype.can_encode_type(arg.pytype.checked_wtype(location)):
            arg = expect.not_this_type(arg, default=expect.default_dummy_value(native_type))
        return ARC4TupleExpressionBuilder(
            ARC4Encode(value=arg.resolve(), wtype=wtype, source_location=location), typ
        )

class ARC4TupleExpressionBuilder(
    BytesBackedInstanceExpressionBuilder[pytypes.ARC4TupleType], StaticSizedCollectionBuilder
):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.ARC4TupleType)
        super().__init__(typ, expr)

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        match index:
            case LiteralBuilder(value=int(index_value)):
                pass
            case InstanceBuilder(pytype=pytypes.IntLiteralType):
                raise CodeError("tuple index must be a simple int literal", index.source_location)
            case other:
                expect.not_this_type(other, default=expect.default_raise)
        try:
            item_typ = self.pytype.items[index_value]
        except IndexError:
            raise CodeError("index out of bounds", index.source_location) from None
        return builder_for_instance(
            item_typ,
            TupleItemExpression(
                base=self.resolve(),
                index=index_value,
                source_location=location,
            ),
        )

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return constant_bool_and_error(value=True, location=location, negate=negate)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "native":
                native_pytype = pytypes.GenericTupleType.parameterise(self.pytype.items, location)
                native_wtype = native_pytype.checked_wtype(location)
                result_expr: Expression = ARC4Decode(
                    value=self.resolve(),
                    wtype=native_wtype,
                    source_location=location,
                )
                return TupleExpressionBuilder(result_expr, native_pytype)
            case "copy":
                return CopyBuilder(self.resolve(), location, self.pytype)
            case _:
                return super().member_access(name, location)

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        return compare_bytes(self=self, op=op, other=other, source_location=location)

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        logger.error(
            "item containment with ARC4 tuples is currently unsupported", location=location
        )
        return dummy_value(pytypes.BoolType, location)

    @typing.override
    def iterate(self) -> typing.Never:
        # could only support for homogenous types anyway, in which case use a StaticArray?
        raise CodeError("iterating ARC4 tuples is currently unsupported", self.source_location)

    @typing.override
    def iterate_static(self) -> Sequence[InstanceBuilder]:
        base = self.single_eval().resolve()
        return [
            builder_for_instance(
                item_type,
                TupleItemExpression(base=base, index=idx, source_location=self.source_location),
            )
            for idx, item_type in enumerate(self.pytype.items)
        ]

    @typing.override
    def iterable_item_type(self) -> typing.Never:
        self.iterate()

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise CodeError("slicing ARC4 tuples is currently unsupported", location)


import decimal
import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst import wtypes
from puya.awst.nodes import DecimalConstant, Expression
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb._bytes_backed import BytesBackedInstanceExpressionBuilder
from puyapy.awst_build.eb._utils import compare_bytes
from puyapy.awst_build.eb.arc4._base import ARC4TypeBuilder, arc4_bool_bytes
from puyapy.awst_build.eb.interface import (
    BuilderComparisonOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
)

__all__ = [
    "UFixedNxMTypeBuilder",
    "UFixedNxMExpressionBuilder",
]

logger = log.get_logger(__name__)

class UFixedNxMTypeBuilder(ARC4TypeBuilder):
    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case str(literal_value):
                result = self._str_to_decimal_constant(
                    literal_value, error_location=literal.source_location, location=location
                )
                return UFixedNxMExpressionBuilder(result, self.produces())
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.at_most_one_arg(args, location)
        if arg is None:
            result = self._str_to_decimal_constant("0.0", location=location)
            return UFixedNxMExpressionBuilder(result, self.produces())

        arg = expect.argument_of_type(arg, pytypes.StrLiteralType, default=expect.default_raise)
        return arg.resolve_literal(UFixedNxMTypeBuilder(self.produces(), location))

    def _str_to_decimal_constant(
        self,
        literal_value: str,
        *,
        location: SourceLocation,
        error_location: SourceLocation | None = None,
    ) -> DecimalConstant:
        error_location = location or error_location
        fixed_wtype = self.produces().wtype
        assert isinstance(fixed_wtype, wtypes.ARC4UFixedNxM)

        with decimal.localcontext(
            decimal.Context(
                prec=160,
                traps=[
                    decimal.Rounded,
                    decimal.InvalidOperation,
                    decimal.Overflow,
                    decimal.DivisionByZero,
                ],
            )
        ):
            try:
                d = decimal.Decimal(literal_value)
            except ArithmeticError:
                logger.error("invalid decimal literal", location=error_location)  # noqa: TRY400
                d = decimal.Decimal()
            try:
                q = d.quantize(decimal.Decimal(f"1e-{fixed_wtype.m}"))
            except ArithmeticError:
                logger.error(  # noqa: TRY400
                    "invalid decimal constant (wrong precision)", location=error_location
                )
                q = decimal.Decimal("0." + "0" * fixed_wtype.m)

            sign, digits, exponent = q.as_tuple()
            if sign != 0:  # is negative
                logger.error(
                    "invalid decimal constant (value is negative)", location=error_location
                )
            if not isinstance(exponent, int):  # is infinite
                logger.error(
                    "invalid decimal constant (value is infinite)", location=error_location
                )
            adjusted_int = int("".join(map(str, digits)))
            if adjusted_int.bit_length() > fixed_wtype.n:
                logger.error("invalid decimal constant (too many bits)", location=error_location)
            result = DecimalConstant(value=q, wtype=fixed_wtype, source_location=location)
            return result

class UFixedNxMExpressionBuilder(
    NotIterableInstanceExpressionBuilder[pytypes.ARC4UFixedNxMType],
    BytesBackedInstanceExpressionBuilder[pytypes.ARC4UFixedNxMType],
):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.ARC4UFixedNxMType)
        assert typ.generic in (
            pytypes.GenericARC4UFixedNxMType,
            pytypes.GenericARC4BigUFixedNxMType,
        )
        super().__init__(typ, expr)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return arc4_bool_bytes(
            self,
            false_bytes=b"\x00" * (self.pytype.bits // 8),
            negate=negate,
            location=location,
        )

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        other = other.resolve_literal(UFixedNxMTypeBuilder(self.pytype, other.source_location))
        return compare_bytes(op=op, self=self, other=other, source_location=location)


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst import wtypes
from puya.awst.nodes import (
    ARC4Decode,
    ARC4Encode,
    Expression,
    IntegerConstant,
    NumericComparison,
    NumericComparisonExpression,
    ReinterpretCast,
)
from puya.parse import SourceLocation
from puyapy.awst_build import intrinsic_factory, pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb._bytes_backed import BytesBackedInstanceExpressionBuilder
from puyapy.awst_build.eb.arc4._base import ARC4TypeBuilder, arc4_bool_bytes
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    BuilderComparisonOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
)

__all__ = [
    "UIntNTypeBuilder",
    "UIntNExpressionBuilder",
]

logger = log.get_logger(__name__)

class UIntNTypeBuilder(ARC4TypeBuilder[pytypes.ARC4UIntNType]):
    def __init__(self, pytype: pytypes.PyType, location: SourceLocation):
        assert isinstance(pytype, pytypes.ARC4UIntNType)
        super().__init__(pytype, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        pytype = self.produces()
        match literal.value:
            case int(int_value):
                if int_value < 0 or int_value.bit_length() > pytype.bits:
                    logger.error(f"invalid {pytype} value", location=literal.source_location)
                # take int() of the value since it could match a bool also
                expr = IntegerConstant(
                    value=int(int_value), wtype=pytype.wtype, source_location=location
                )
                return UIntNExpressionBuilder(expr, pytype)
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.at_most_one_arg(args, location)
        typ = self.produces()
        wtype = typ.wtype
        match arg:
            case InstanceBuilder(pytype=pytypes.IntLiteralType):
                return arg.resolve_literal(UIntNTypeBuilder(typ, location))
            case None:
                expr: Expression = IntegerConstant(value=0, wtype=wtype, source_location=location)
            case _:
                encodeable = expect.argument_of_type_else_dummy(
                    arg, pytypes.UInt64Type, pytypes.BigUIntType, pytypes.BoolType
                )
                expr = ARC4Encode(
                    value=encodeable.resolve(), wtype=wtype, source_location=location
                )
        return UIntNExpressionBuilder(expr, typ)

class UIntNExpressionBuilder(
    NotIterableInstanceExpressionBuilder[pytypes.ARC4UIntNType],
    BytesBackedInstanceExpressionBuilder[pytypes.ARC4UIntNType],
):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.ARC4UIntNType)
        super().__init__(typ, expr)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "native":
                result_expr = ARC4Decode(
                    value=self.resolve(),
                    wtype=self.pytype.native_type.wtype,
                    source_location=location,
                )
                return builder_for_instance(self.pytype.native_type, result_expr)
            case _:
                return super().member_access(name, location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return arc4_bool_bytes(
            self,
            false_bytes=b"\x00" * (self.pytype.bits // 8),
            location=location,
            negate=negate,
        )

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        other = other.resolve_literal(UIntNTypeBuilder(self.pytype, other.source_location))
        if pytypes.BigUIntType <= other.pytype:
            other_expr = other.resolve()
        elif other.pytype.is_type_or_subtype(pytypes.BoolType, pytypes.UInt64Type):
            other_expr = intrinsic_factory.itob_as(other.resolve(), wtypes.biguint_wtype, location)
        elif isinstance(other.pytype, pytypes.ARC4UIntNType):
            other_expr = ReinterpretCast(
                expr=other.resolve(),
                wtype=wtypes.biguint_wtype,
                source_location=other.source_location,
            )
        else:
            return NotImplemented
        cmp_expr = NumericComparisonExpression(
            operator=NumericComparison(op.value),
            lhs=ReinterpretCast(
                expr=self.resolve(),
                wtype=wtypes.biguint_wtype,
                source_location=self.source_location,
            ),
            rhs=other_expr,
            source_location=location,
        )
        return BoolExpressionBuilder(cmp_expr)


import typing
from collections.abc import Sequence

import mypy.nodes

from puya.awst import wtypes
from puya.awst.nodes import Expression
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb._base import GenericTypeBuilder
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder, TypeBuilder

class ArrayGenericTypeBuilder(GenericTypeBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise NotImplementedError

class ArrayTypeBuilder(TypeBuilder[pytypes.ArrayType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.ArrayType)
        assert typ.generic == pytypes.GenericArrayType
        wtype = typ.wtype
        assert isinstance(wtype, wtypes.WArray)
        self._wtype = wtype
        super().__init__(typ, location)

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise NotImplementedError

def ArrayExpressionBuilder(expr: Expression, typ: pytypes.PyType) -> typing.Never:  # noqa: N802
    raise NotImplementedError


import typing
from collections.abc import Sequence

import attrs
import mypy.nodes

from puya import algo_constants, log
from puya.awst import wtypes
from puya.awst.nodes import (
    BigUIntAugmentedAssignment,
    BigUIntBinaryOperation,
    BigUIntBinaryOperator,
    BigUIntConstant,
    Expression,
    NumericComparison,
    NumericComparisonExpression,
    Statement,
)
from puya.parse import SourceLocation
from puyapy.awst_build import intrinsic_factory, pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb._bytes_backed import (
    BytesBackedInstanceExpressionBuilder,
    BytesBackedTypeBuilder,
)
from puyapy.awst_build.eb._utils import dummy_statement
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
)

logger = log.get_logger(__name__)

class BigUIntTypeBuilder(BytesBackedTypeBuilder):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.BigUIntType, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case int(int_value):
                pytype = self.produces()
                if int_value < 0 or int_value.bit_length() > algo_constants.MAX_BIGUINT_BITS:
                    logger.error(f"invalid {pytype} value", location=literal.source_location)
                expr = BigUIntConstant(value=int(int_value), source_location=location)
                return BigUIntExpressionBuilder(expr)
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.at_most_one_arg(args, location)
        match arg:
            case InstanceBuilder(pytype=pytypes.IntLiteralType):
                return arg.resolve_literal(converter=BigUIntTypeBuilder(location))
            case None:
                value: Expression = BigUIntConstant(value=0, source_location=location)
            case _:
                arg = expect.argument_of_type_else_dummy(arg, pytypes.UInt64Type)
                value = _uint64_to_biguint(arg, location)
        return BigUIntExpressionBuilder(value)

class BigUIntExpressionBuilder(
    NotIterableInstanceExpressionBuilder, BytesBackedInstanceExpressionBuilder
):
    def __init__(self, expr: Expression):
        super().__init__(pytypes.BigUIntType, expr)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        cmp_expr = NumericComparisonExpression(
            lhs=self.resolve(),
            operator=NumericComparison.eq if negate else NumericComparison.ne,
            rhs=BigUIntConstant(value=0, source_location=location),
            source_location=location,
        )
        return BoolExpressionBuilder(cmp_expr)

    @typing.override
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> InstanceBuilder:
        if op == BuilderUnaryOp.positive:
            # unary + is allowed, but for the current types it has no real impact
            # so just expand the existing expression to include the unary operator
            return BigUIntExpressionBuilder(attrs.evolve(self.resolve(), source_location=location))
        return super().unary_op(op, location)

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        other = other.resolve_literal(converter=BigUIntTypeBuilder(other.source_location))
        if pytypes.BigUIntType <= other.pytype:
            other_expr = other.resolve()
        elif pytypes.UInt64Type <= other.pytype:
            other_expr = _uint64_to_biguint(other, location)
        else:
            return NotImplemented
        cmp_expr = NumericComparisonExpression(
            source_location=location,
            lhs=self.resolve(),
            operator=NumericComparison(op.value),
            rhs=other_expr,
        )
        return BoolExpressionBuilder(cmp_expr)

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        biguint_op = _translate_biguint_math_operator(op, location)
        if biguint_op is None:
            return NotImplemented
        other = other.resolve_literal(converter=BigUIntTypeBuilder(other.source_location))
        if pytypes.BigUIntType <= other.pytype:
            other_expr = other.resolve()
        elif pytypes.UInt64Type <= other.pytype:
            other_expr = _uint64_to_biguint(other, location)
        else:
            return NotImplemented
        lhs = self.resolve()
        rhs = other_expr
        if reverse:
            (lhs, rhs) = (rhs, lhs)
        bin_op_expr = BigUIntBinaryOperation(
            source_location=location, left=lhs, op=biguint_op, right=rhs
        )
        return BigUIntExpressionBuilder(bin_op_expr)

    @typing.override
    def augmented_assignment(
        self, op: BuilderBinaryOp, rhs: InstanceBuilder, location: SourceLocation
    ) -> Statement:
        biguint_op = _translate_biguint_math_operator(op, location)
        if biguint_op is None:
            logger.error(f"unsupported operator for type: {op.value!r}", location=location)
            return dummy_statement(location)
        if pytypes.UInt64Type <= rhs.pytype:
            value = _uint64_to_biguint(rhs, location)
        else:
            value = expect.argument_of_type_else_dummy(
                rhs, self.pytype, resolve_literal=True
            ).resolve()
        target = self.resolve_lvalue()
        return BigUIntAugmentedAssignment(
            source_location=location,
            target=target,
            value=value,
            op=biguint_op,
        )

def _translate_biguint_math_operator(
    operator: BuilderBinaryOp, loc: SourceLocation
) -> BigUIntBinaryOperator | None:
    if operator is BuilderBinaryOp.div:
        logger.error(
            (
                "To maintain semantic compatibility with Python, "
                "only the truncating division operator (//) is supported "
            ),
            location=loc,
        )
        # continue traversing code to generate any further errors
        operator = BuilderBinaryOp.floor_div
    try:
        return BigUIntBinaryOperator(operator.value)
    except ValueError:
        return None

def _uint64_to_biguint(arg_in: InstanceBuilder, location: SourceLocation) -> Expression:
    return intrinsic_factory.itob_as(arg_in.resolve(), wtypes.biguint_wtype, location)


import typing

from puya.awst.nodes import (
    BinaryBooleanOperator,
    BooleanBinaryOperation,
    ConditionalExpression,
    Expression,
    Not,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    InstanceBuilder,
    NodeBuilder,
    TypeBuilder,
)
from puyapy.awst_build.utils import determine_base_type

class BinaryBoolOpBuilder(InstanceBuilder):
    """
    This builder works to defer the evaluation of a boolean binary op (ie and/or), because
    in some cases (unions, python literals) we can only successfully evaluate in certain contexts.

    For example:
        a = Bytes(...)
        b = UInt64(...)
        if a or b: # this is fine, even though `a or b` produces `Bytes | UInt64`
            ...
        c = a or b # compiler error

    You wouldn't be able to do anything with c, since in general we can't know at compile
    time what the type of c is, and the AVM doesn't provide any type introspection.
    Even if there was an op that said whether a stack item or a scratch slot etc held
    a bytes[] or a uint64, there are differences between logical types and physical types
    that need to be accounted for - for example, biguint is a bytes[] but we would need
    to use a different equality op b== instead of ==
    """

    def __init__(
        self,
        left: InstanceBuilder,
        right: InstanceBuilder,
        op: BinaryBooleanOperator,
        location: SourceLocation,
        *,
        result_type: pytypes.PyType | None = None,
    ):
        super().__init__(location)
        if result_type is None:
            # if either left or right is already a union, just produce another union
            if isinstance(left.pytype, pytypes.UnionType) or isinstance(
                right.pytype, pytypes.UnionType
            ):
                # note if left and/or right are unions this constructor will expand them for us
                result_type = pytypes.UnionType([left.pytype, right.pytype], location)
            else:
                # otherwise, left and right are both non-union types, so try and
                # compute a common base type, falling back to a union if not possible
                result_type = determine_base_type(left.pytype, right.pytype, location=location)
        self._result_type = result_type
        self._left = left
        self._right = right
        self._op = op

    @typing.override
    @property
    def pytype(self) -> pytypes.PyType:
        return self._result_type

    @typing.override
    def single_eval(self) -> InstanceBuilder:
        left = self._left.single_eval()
        right = self._right.single_eval()
        return self._evolve_builders(left, right, recalculate_type=False)

    @typing.override
    def resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder:
        left = self._left.resolve_literal(converter)
        right = self._right.resolve_literal(converter)
        return self._evolve_builders(left, right)

    @typing.override
    def try_resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder | None:
        left = self._left.try_resolve_literal(converter)
        right = self._right.try_resolve_literal(converter)
        if left is None or right is None:
            return None
        return self._evolve_builders(left, right)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        bool_left = self._left.bool_eval(self._left.source_location).resolve()
        bool_right = self._right.bool_eval(self._right.source_location).resolve()
        result_expr: Expression = BooleanBinaryOperation(
            left=bool_left, op=self._op, right=bool_right, source_location=location
        )
        if negate:
            result_expr = Not(expr=result_expr, source_location=location)
        return BoolExpressionBuilder(result_expr)

    @typing.override
    def bool_binary_op(
        self, other: InstanceBuilder, op: BinaryBooleanOperator, location: SourceLocation
    ) -> InstanceBuilder:
        return BinaryBoolOpBuilder(left=self, right=other, op=op, location=location)

    @typing.override
    def resolve(self) -> Expression:
        if isinstance(self.pytype, pytypes.UnionType):
            raise CodeError(
                "expression would produce a union type,"
                " which isn't supported unless evaluating a boolean condition",
                self.source_location,
            )
        result_wtype = self.pytype.checked_wtype(self.source_location)

        # (left:uint64 and right:uint64) => left_cache if not bool(left_cache := left) else right
        # (left:uint64 or right:uint64) => left_cache if bool(left_cache := left) else right
        left_cache = self._left.single_eval()
        condition = left_cache.bool_eval(
            self.source_location, negate=self._op is BinaryBooleanOperator.and_
        )
        expr_result = ConditionalExpression(
            condition=condition.resolve(),
            true_expr=left_cache.resolve(),
            false_expr=self._right.resolve(),
            wtype=result_wtype,
            source_location=self.source_location,
        )
        return expr_result

    def _evolve_builders(
        self, left: InstanceBuilder, right: InstanceBuilder, *, recalculate_type: bool = True
    ) -> InstanceBuilder:
        return BinaryBoolOpBuilder(
            left=left,
            right=right,
            op=self._op,
            location=self.source_location,
            result_type=None if recalculate_type else self.pytype,
        )

    # region Invalid Python syntax
    @typing.override
    def resolve_lvalue(self) -> typing.Never:
        raise CodeError(
            # message copied from Python
            "cannot assign to expression here. Maybe you meant '==' instead of '='?",
            self.source_location,
        )

    @typing.override
    def delete(self, location: SourceLocation) -> typing.Never:
        raise CodeError(
            # message copied from Python
            "cannot delete expression",
            location,
        )

    @typing.override
    def augmented_assignment(
        self, op: BuilderBinaryOp, right: InstanceBuilder, location: SourceLocation
    ) -> typing.Never:
        raise CodeError(
            # copied (and trimmed) from Python
            "illegal expression for augmented assignment",
            location,
        )

    # endregion

    # region Forward to resolved builder
    def _resolve_builder(self) -> InstanceBuilder:
        expr = self.resolve()
        return builder_for_instance(self.pytype, expr)

    @typing.override
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> InstanceBuilder:
        return self._resolve_builder().unary_op(op, location)

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        return self._resolve_builder().compare(other, op, location)

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        return self._resolve_builder().binary_op(other, op, location, reverse=reverse)

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        return self._resolve_builder().contains(item, location)

    @typing.override
    def iterate(self) -> Expression:
        return self._resolve_builder().iterate()

    @typing.override
    def iterable_item_type(self) -> pytypes.PyType:
        return self._resolve_builder().iterable_item_type()

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        return self._resolve_builder().index(index, location)

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        return self._resolve_builder().slice_index(begin_index, end_index, stride, location)

    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        return self._resolve_builder().to_bytes(location)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        return self._resolve_builder().member_access(name, location)

    # endregion


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst.nodes import (
    BinaryBooleanOperator,
    BoolConstant,
    BooleanBinaryOperation,
    Expression,
    Not,
    NumericComparison,
    NumericComparisonExpression,
)
from puya.parse import SourceLocation
from puyapy.awst_build import intrinsic_factory, pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb.interface import (
    BuilderComparisonOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    TypeBuilder,
)

logger = log.get_logger(__name__)

class BoolTypeBuilder(TypeBuilder):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.BoolType, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case bool(literal_value):
                expr = BoolConstant(value=literal_value, source_location=location)
                return BoolExpressionBuilder(expr)
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.at_most_one_arg(args, location)
        match arg:
            case None:
                false = BoolConstant(value=False, source_location=location)
                return BoolExpressionBuilder(false)
            case InstanceBuilder(pytype=pytypes.BoolType):
                return arg
            case _:
                return arg.bool_eval(location)

class BoolExpressionBuilder(NotIterableInstanceExpressionBuilder):
    def __init__(self, expr: Expression):
        super().__init__(pytypes.BoolType, expr)

    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        return intrinsic_factory.itob(self.resolve(), location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        if not negate:
            return self
        return BoolExpressionBuilder(Not(location, self.resolve()))

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        if other.pytype != pytypes.BoolType:
            return NotImplemented
        cmp_expr = NumericComparisonExpression(
            source_location=location,
            lhs=self.resolve(),
            operator=NumericComparison(op.value),
            rhs=other.resolve(),
        )
        return BoolExpressionBuilder(cmp_expr)

    @typing.override
    def bool_binary_op(
        self, other: InstanceBuilder, op: BinaryBooleanOperator, location: SourceLocation
    ) -> InstanceBuilder:
        if other.pytype != pytypes.BoolType:
            return super().bool_binary_op(other, op, location)
        result = BooleanBinaryOperation(
            left=self.resolve(),
            op=op,
            right=other.resolve(),
            source_location=location,
        )
        return BoolExpressionBuilder(result)


import base64
import typing
from collections.abc import Sequence

import mypy.nodes

from puya import algo_constants, log, utils
from puya.awst.nodes import (
    BytesAugmentedAssignment,
    BytesBinaryOperation,
    BytesBinaryOperator,
    BytesConstant,
    BytesEncoding,
    BytesUnaryOperation,
    BytesUnaryOperator,
    CallArg,
    Expression,
    IndexExpression,
    IntersectionSliceExpression,
    PuyaLibCall,
    PuyaLibFunction,
    Statement,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import intrinsic_factory, pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder, InstanceExpressionBuilder
from puyapy.awst_build.eb._utils import (
    compare_bytes,
    dummy_statement,
    dummy_value,
    resolve_negative_literal_index,
)
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    TypeBuilder,
)
from puyapy.awst_build.eb.uint64 import UInt64ExpressionBuilder

logger = log.get_logger(__name__)

class BytesTypeBuilder(TypeBuilder):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.BytesType, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case bytes(literal_value):
                if len(literal_value) > algo_constants.MAX_BYTES_LENGTH:
                    logger.error(
                        "bytes constant exceeds max length", location=literal.source_location
                    )

                expr = BytesConstant(
                    value=literal_value, encoding=BytesEncoding.unknown, source_location=location
                )
                return BytesExpressionBuilder(expr)
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.at_most_one_arg(args, location)
        match arg:
            case InstanceBuilder(pytype=pytypes.BytesLiteralType):
                return arg.resolve_literal(BytesTypeBuilder(location))
            case None:
                value = BytesConstant(
                    value=b"", encoding=BytesEncoding.unknown, source_location=location
                )
                return BytesExpressionBuilder(value)
            case other:
                return expect.not_this_type(
                    other, default=expect.default_dummy_value(self.produces())
                )

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        """Handle self.name"""
        match name:
            case "from_base32":
                return _FromEncodedStr(location, BytesEncoding.base32)
            case "from_base64":
                return _FromEncodedStr(location, BytesEncoding.base64)
            case "from_hex":
                return _FromEncodedStr(location, BytesEncoding.base16)
            case _:
                return super().member_access(name, location)

class _FromEncodedStr(FunctionBuilder):
    def __init__(
        self,
        location: SourceLocation,
        encoding: typing.Literal[BytesEncoding.base16, BytesEncoding.base32, BytesEncoding.base64],
    ):
        super().__init__(location=location)
        self.encoding = encoding

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.exactly_one_arg(args, location, default=expect.default_none)
        if arg is not None:
            encoded_value = expect.simple_string_literal(arg, default=expect.default_none)
            if encoded_value is not None:
                match self.encoding:
                    case BytesEncoding.base64:
                        if not utils.valid_base64(encoded_value):
                            logger.error("invalid base64 value", location=arg.source_location)
                            bytes_value = b""
                        else:
                            bytes_value = base64.b64decode(encoded_value)
                    case BytesEncoding.base32:
                        if not utils.valid_base32(encoded_value):
                            logger.error("invalid base32 value", location=arg.source_location)
                            bytes_value = b""
                        else:
                            bytes_value = base64.b32decode(encoded_value)
                    case BytesEncoding.base16:
                        encoded_value = encoded_value.upper()
                        if not utils.valid_base16(encoded_value):
                            logger.error("invalid base16 value", location=arg.source_location)
                            bytes_value = b""
                        else:
                            bytes_value = base64.b16decode(encoded_value)
                    case _:
                        typing.assert_never(self.encoding)
                expr = BytesConstant(
                    source_location=location,
                    value=bytes_value,
                    encoding=self.encoding,
                )
                return BytesExpressionBuilder(expr)
        return dummy_value(pytypes.BytesType, location)

class BytesExpressionBuilder(InstanceExpressionBuilder[pytypes.RuntimeType]):
    def __init__(self, expr: Expression):
        super().__init__(pytypes.BytesType, expr)

    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        return self.resolve()

    def length(self, location: SourceLocation) -> InstanceBuilder:
        len_call = intrinsic_factory.bytes_len(expr=self.resolve(), loc=location)
        return UInt64ExpressionBuilder(len_call)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> InstanceBuilder:
        match name:
            case "length":
                return self.length(location)
        raise CodeError(f"unrecognised member of {self.pytype}: {name}", location)

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        length = self.length(location)
        index = resolve_negative_literal_index(index, length, location)
        expr = IndexExpression(
            base=self.resolve(),
            index=index.resolve(),
            wtype=self.pytype.wtype,
            source_location=location,
        )
        return BytesExpressionBuilder(expr)

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        if stride is not None:
            logger.error("stride is not supported", location=stride.source_location)

        slice_expr: Expression = IntersectionSliceExpression(
            base=self.resolve(),
            begin_index=_eval_slice_component(begin_index),
            end_index=_eval_slice_component(end_index),
            wtype=self.pytype.wtype,
            source_location=location,
        )
        return BytesExpressionBuilder(slice_expr)

    @typing.override
    def iterate(self) -> Expression:
        return self.resolve()

    @typing.override
    def iterable_item_type(self) -> pytypes.PyType:
        return pytypes.BytesType

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        len_expr = intrinsic_factory.bytes_len(self.resolve(), location)
        len_builder = UInt64ExpressionBuilder(len_expr)
        return len_builder.bool_eval(location, negate=negate)

    @typing.override
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> InstanceBuilder:
        if op == BuilderUnaryOp.bit_invert:
            return BytesExpressionBuilder(
                BytesUnaryOperation(
                    expr=self.resolve(),
                    op=BytesUnaryOperator.bit_invert,
                    source_location=location,
                )
            )
        return super().unary_op(op, location)

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        item_expr = expect.argument_of_type_else_dummy(
            item, pytypes.BytesType, resolve_literal=True
        ).resolve()
        is_substring_expr = PuyaLibCall(
            func=PuyaLibFunction.is_substring,
            args=[CallArg(value=item_expr, name=None), CallArg(value=self.resolve(), name=None)],
            source_location=location,
        )
        return BoolExpressionBuilder(is_substring_expr)

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        other = other.resolve_literal(converter=BytesTypeBuilder(other.source_location))
        return compare_bytes(self=self, op=op, other=other, source_location=location)

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        bytes_op = _translate_binary_bytes_operator(op)
        if bytes_op is None:
            return NotImplemented

        other = other.resolve_literal(converter=BytesTypeBuilder(other.source_location))
        if not (pytypes.BytesType <= other.pytype):
            return NotImplemented

        lhs = self.resolve()
        rhs = other.resolve()
        if reverse:
            (lhs, rhs) = (rhs, lhs)
        bin_op_expr = BytesBinaryOperation(
            source_location=location, left=lhs, right=rhs, op=bytes_op
        )
        return BytesExpressionBuilder(bin_op_expr)

    @typing.override
    def augmented_assignment(
        self, op: BuilderBinaryOp, rhs: InstanceBuilder, location: SourceLocation
    ) -> Statement:
        bytes_op = _translate_binary_bytes_operator(op)
        if bytes_op is None:
            logger.error(f"unsupported operator for type: {op.value!r}", location=location)
            return dummy_statement(location)

        rhs = expect.argument_of_type_else_dummy(rhs, self.pytype, resolve_literal=True)
        target = self.resolve_lvalue()
        return BytesAugmentedAssignment(
            target=target,
            op=bytes_op,
            value=rhs.resolve(),
            source_location=location,
        )

def _translate_binary_bytes_operator(operator: BuilderBinaryOp) -> BytesBinaryOperator | None:
    try:
        return BytesBinaryOperator(operator.value)
    except ValueError:
        return None

def _eval_slice_component(val: NodeBuilder | None) -> Expression | None | int:
    match val:
        case None:
            return None
        case LiteralBuilder(value=int(int_value)):
            return int_value
    return expect.argument_of_type_else_dummy(val, pytypes.UInt64Type).resolve()


import typing
from collections.abc import Mapping, Sequence

import mypy.nodes

from puya.awst.nodes import CompiledContract, CompiledLogicSig, Expression
from puya.awst.txn_fields import TxnField
from puya.log import get_logger
from puya.parse import SourceLocation
from puya.program_refs import LogicSigReference
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder
from puyapy.awst_build.eb._utils import dummy_value
from puyapy.awst_build.eb.dict_ import DictLiteralBuilder
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder
from puyapy.awst_build.eb.logicsig import LogicSigExpressionBuilder
from puyapy.awst_build.eb.tuple import TupleExpressionBuilder
from puyapy.awst_build.utils import get_arg_mapping

logger = get_logger(__name__)

# these names should match pytypes CompiledContract definition
PROGRAM_FIELDS = {
    "approval_program": TxnField.ApprovalProgramPages,
    "clear_state_program": TxnField.ClearStateProgramPages,
}
APP_ALLOCATION_FIELDS = {
    "extra_program_pages": TxnField.ExtraProgramPages,
    "global_bytes": TxnField.GlobalNumByteSlice,
    "global_uints": TxnField.GlobalNumUint,
    "local_bytes": TxnField.LocalNumByteSlice,
    "local_uints": TxnField.LocalNumUint,
}

class CompiledContractExpressionBuilder(TupleExpressionBuilder):
    def __init__(self, expr: Expression) -> None:
        super().__init__(expr, pytypes.CompiledContractType)

class CompiledLogicSigExpressionBuilder(TupleExpressionBuilder):
    def __init__(self, expr: Expression) -> None:
        super().__init__(expr, pytypes.CompiledLogicSigType)

_TEMPLATE_VAR_KWARG_NAMES = [
    "template_vars",
    "template_vars_prefix",
]

class CompileContractFunctionBuilder(FunctionBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        contract_arg_name = "contract"
        arg_map, _ = get_arg_mapping(
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
            required_positional_names=[contract_arg_name],
            optional_kw_only=[
                *APP_ALLOCATION_FIELDS,
                *_TEMPLATE_VAR_KWARG_NAMES,
            ],
        )
        prefix, template_vars = _extract_prefix_template_args(arg_map)
        allocation_overrides = {}
        for python_name, field in APP_ALLOCATION_FIELDS.items():
            if arg := arg_map.get(python_name):
                allocation_overrides[field] = expect.argument_of_type_else_dummy(
                    arg, pytypes.UInt64Type, resolve_literal=True
                ).resolve()

        result_type = pytypes.CompiledContractType
        match arg_map[contract_arg_name]:
            case NodeBuilder(pytype=pytypes.TypeType(typ=pytypes.ContractType() as contract_typ)):
                contract = contract_typ.name
            case invalid_or_none:
                if invalid_or_none is None:
                    # if None (=missing), then error message already logged by get_arg_mapping
                    return dummy_value(result_type, location)
                return expect.not_this_type(
                    invalid_or_none, default=expect.default_dummy_value(result_type)
                )

        return CompiledContractExpressionBuilder(
            CompiledContract(
                contract=contract,
                allocation_overrides=allocation_overrides,
                prefix=prefix,
                template_variables=template_vars,
                wtype=result_type.wtype,
                source_location=location,
            )
        )

class CompileLogicSigFunctionBuilder(FunctionBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        logicsig_arg_name = "logicsig"
        arg_map, _ = get_arg_mapping(
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
            required_positional_names=[logicsig_arg_name],
            optional_kw_only=_TEMPLATE_VAR_KWARG_NAMES,
        )
        match arg_map.get(logicsig_arg_name):
            case LogicSigExpressionBuilder(ref=logic_sig):
                pass
            case missing_or_invalid:
                logic_sig = LogicSigReference("")  # dummy reference
                # if None (=missing), then error message already logged by get_arg_mapping
                if missing_or_invalid is not None:
                    expect.not_this_type(missing_or_invalid, default=expect.default_none)
        prefix, template_vars = _extract_prefix_template_args(arg_map)
        return CompiledLogicSigExpressionBuilder(
            CompiledLogicSig(
                logic_sig=logic_sig,
                prefix=prefix,
                template_variables=template_vars,
                wtype=pytypes.CompiledLogicSigType.wtype,
                source_location=location,
            )
        )

def _extract_prefix_template_args(
    name_args: dict[str, NodeBuilder],
) -> tuple[str | None, Mapping[str, Expression]]:
    prefix: str | None = None
    template_vars: Mapping[str, Expression] = {}

    if template_vars_node := name_args.get("template_vars"):
        if isinstance(template_vars_node, DictLiteralBuilder):
            template_vars = {k: v.resolve() for k, v in template_vars_node.mapping.items()}
        else:
            expect.not_this_type(template_vars_node, default=expect.default_none)
    if prefix_node := name_args.get("template_vars_prefix"):
        prefix = expect.simple_string_literal(prefix_node, default=expect.default_none)
    return prefix, template_vars


import typing

from puya.awst.nodes import (
    BinaryBooleanOperator,
    ConditionalExpression,
    Expression,
    Lvalue,
    Statement,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    TypeBuilder,
)

class ConditionalLiteralBuilder(InstanceBuilder):
    def __init__(
        self,
        *,
        true_literal: LiteralBuilder,
        false_literal: LiteralBuilder,
        condition: InstanceBuilder,
        location: SourceLocation,
    ):
        super().__init__(location)
        self._pytype = _common_base(true_literal.pytype, false_literal.pytype, location)
        self._true_literal = true_literal
        self._false_literal = false_literal
        self._condition = condition

    @typing.override
    @property
    def pytype(self) -> pytypes.PyType:
        return self._true_literal.pytype

    @typing.override
    def resolve(self) -> Expression:
        true_expr = self._true_literal.resolve()
        false_expr = self._false_literal.resolve()
        condition_expr = self._condition.resolve()
        return ConditionalExpression(
            condition=condition_expr,
            true_expr=true_expr,
            false_expr=false_expr,
            source_location=self.source_location,
        )

    @typing.override
    def resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder:
        true_b = converter.convert_literal(
            literal=self._true_literal, location=converter.source_location
        )
        false_b = converter.convert_literal(
            literal=self._false_literal, location=converter.source_location
        )
        return self._resolve_literals(true_b, false_b)

    @typing.override
    def try_resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder | None:
        true_b = converter.try_convert_literal(
            literal=self._true_literal, location=converter.source_location
        )
        false_b = converter.try_convert_literal(
            literal=self._false_literal, location=converter.source_location
        )
        if true_b is None or false_b is None:
            return None
        return self._resolve_literals(true_b, false_b)

    def _resolve_literals(
        self, true_b: InstanceBuilder, false_b: InstanceBuilder
    ) -> InstanceBuilder:
        result_pytype = _common_base(true_b.pytype, false_b.pytype, self.source_location)
        result_pytype = true_b.pytype
        true_expr = true_b.resolve()
        false_expr = false_b.resolve()
        condition_expr = self._condition.resolve()
        result_expr = ConditionalExpression(
            condition=condition_expr,
            true_expr=true_expr,
            false_expr=false_expr,
            source_location=self.source_location,
        )
        return builder_for_instance(result_pytype, result_expr)

    @typing.override
    def resolve_lvalue(self) -> Lvalue:
        raise CodeError("cannot assign to literal", self.source_location)

    @typing.override
    def delete(self, location: SourceLocation) -> Statement:
        raise CodeError("cannot delete literal", location)

    @typing.override
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> InstanceBuilder:
        transformed_true = self._true_literal.unary_op(op, location)
        transformed_false = self._false_literal.unary_op(op, location)
        return ConditionalLiteralBuilder(
            true_literal=transformed_true,
            false_literal=transformed_false,
            condition=self._condition,
            location=location,
        )

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        other = other.single_eval()
        transformed_true = self._true_literal.compare(other, op, location)
        transformed_false = self._false_literal.compare(other, op, location)
        if transformed_true is NotImplemented or transformed_false is NotImplemented:
            return NotImplemented
        return ConditionalLiteralBuilder(
            true_literal=transformed_true,
            false_literal=transformed_false,
            condition=self._condition,
            location=location,
        )

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        other = other.single_eval()
        transformed_true = self._true_literal.binary_op(other, op, location, reverse=reverse)
        transformed_false = self._false_literal.binary_op(other, op, location, reverse=reverse)
        if transformed_true is NotImplemented or transformed_false is NotImplemented:
            return NotImplemented
        return ConditionalLiteralBuilder(
            true_literal=transformed_true,
            false_literal=transformed_false,
            condition=self._condition,
            location=location,
        )

    @typing.override
    def bool_binary_op(
        self, other: InstanceBuilder, op: BinaryBooleanOperator, location: SourceLocation
    ) -> InstanceBuilder:
        return super().bool_binary_op(other, op, location)

    @typing.override
    def augmented_assignment(
        self, op: BuilderBinaryOp, rhs: InstanceBuilder, location: SourceLocation
    ) -> Statement:
        raise CodeError("cannot assign to literal", location)

    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        true_expr = self._true_literal.to_bytes(location)
        false_expr = self._false_literal.to_bytes(location)
        condition_expr = self._condition.resolve()
        return ConditionalExpression(
            condition=condition_expr,
            true_expr=true_expr,
            false_expr=false_expr,
            source_location=self.source_location,
        )

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        transformed_true = self._true_literal.member_access(name, location)
        transformed_false = self._false_literal.member_access(name, location)
        return ConditionalLiteralBuilder(
            true_literal=transformed_true,
            false_literal=transformed_false,
            condition=self._condition,
            location=location,
        )

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        true_expr = self._true_literal.bool_eval(location, negate=negate).resolve()
        false_expr = self._false_literal.bool_eval(location, negate=negate).resolve()
        condition_expr = self._condition.resolve()
        return BoolExpressionBuilder(
            ConditionalExpression(
                condition=condition_expr,
                true_expr=true_expr,
                false_expr=false_expr,
                source_location=location,
            )
        )

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        item = item.single_eval()
        transformed_true = self._true_literal.contains(item, location)
        transformed_false = self._false_literal.contains(item, location)
        return ConditionalLiteralBuilder(
            true_literal=transformed_true,
            false_literal=transformed_false,
            condition=self._condition,
            location=location,
        )

    @typing.override
    def iterate(self) -> typing.Never:
        raise CodeError("cannot iterate literal")

    @typing.override
    def iterable_item_type(self) -> typing.Never:
        self.iterate()

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        index = index.single_eval()
        transformed_true = self._true_literal.index(index, location)
        transformed_false = self._false_literal.index(index, location)
        return ConditionalLiteralBuilder(
            true_literal=transformed_true,
            false_literal=transformed_false,
            condition=self._condition,
            location=location,
        )

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        if begin_index is not None:
            begin_index = begin_index.single_eval()
        if end_index is not None:
            end_index = end_index.single_eval()
        if stride is not None:
            stride = stride.single_eval()
        transformed_true = self._true_literal.slice_index(
            begin_index=begin_index, end_index=end_index, stride=stride, location=location
        )
        transformed_false = self._false_literal.slice_index(
            begin_index=begin_index, end_index=end_index, stride=stride, location=location
        )
        return ConditionalLiteralBuilder(
            true_literal=transformed_true,
            false_literal=transformed_false,
            condition=self._condition,
            location=location,
        )

    @typing.override
    def single_eval(self) -> InstanceBuilder:
        condition = self._condition.single_eval()
        return ConditionalLiteralBuilder(
            true_literal=self._true_literal,
            false_literal=self._false_literal,
            condition=condition,
            location=self.source_location,
        )

def _common_base(a: pytypes.PyType, b: pytypes.PyType, location: SourceLocation) -> pytypes.PyType:
    if a <= b:
        return a
    elif b < a:
        return b
    else:
        raise CodeError("type mismatch", location)


import typing
from collections.abc import Sequence

import attrs
import mypy.nodes

from puya import log
from puya.awst.nodes import (
    AppStateExpression,
    AppStorageDefinition,
    AppStorageKind,
    InstanceMethodTarget,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb._utils import constant_bool_and_error
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder, TypeBuilder
from puyapy.awst_build.eb.storage import (
    BoxMapProxyExpressionBuilder,
    BoxProxyExpressionBuilder,
    BoxRefProxyExpressionBuilder,
    GlobalStateExpressionBuilder,
    LocalStateExpressionBuilder,
)
from puyapy.awst_build.eb.subroutine import (
    BaseClassSubroutineInvokerExpressionBuilder,
    SubroutineInvokerExpressionBuilder,
)
from puyapy.models import ContractFragmentBase

logger = log.get_logger(__name__)

class ContractTypeExpressionBuilder(TypeBuilder[pytypes.ContractType]):
    def __init__(
        self,
        pytype: pytypes.ContractType,
        fragment: ContractFragmentBase,
        location: SourceLocation,
    ):
        assert pytype.name == fragment.id
        super().__init__(pytype, location)
        self.fragment: typing.Final = fragment

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise CodeError("cannot instantiate contract classes", location)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        sym_type = self.fragment.resolve_symbol(name)
        if sym_type is None:
            return super().member_access(name, location)
        if not isinstance(sym_type, pytypes.FuncType):
            raise CodeError("static references are only supported for methods", location)
        func_type = attrs.evolve(
            sym_type,
            args=[
                pytypes.FuncArg(type=self.produces(), name=None, kind=mypy.nodes.ArgKind.ARG_POS),
                *sym_type.args,
            ],
        )
        method = self.fragment.resolve_method(name)
        if method is None:
            raise CodeError("unable to resolve method member", location)
        return BaseClassSubroutineInvokerExpressionBuilder(
            self.fragment.id, method, func_type=func_type, location=location
        )

class ContractSelfExpressionBuilder(NodeBuilder):  # TODO: this _is_ an instance, technically
    def __init__(
        self,
        fragment: ContractFragmentBase,
        pytype: pytypes.ContractType,
        location: SourceLocation,
    ):
        super().__init__(location)
        self._fragment = fragment
        self._pytype = pytype

    @typing.override
    @property
    def pytype(self) -> pytypes.ContractType:
        return self._pytype

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        sym_type = self._fragment.resolve_symbol(name)
        if sym_type is None:
            raise CodeError(f"unrecognised member of {self.pytype}: {name}", location)
        if isinstance(sym_type, pytypes.FuncType):
            return SubroutineInvokerExpressionBuilder(
                target=InstanceMethodTarget(member_name=name),
                func_type=sym_type,
                location=location,
            )
        else:
            storage = self._fragment.resolve_storage(name)
            if storage is None:
                raise CodeError("unable to resolve storage member", location)
            if storage.definition is None:
                raise CodeError("use of storage proxy before definition", location)
            return _builder_for_storage_access(sym_type, name, storage.definition, location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return constant_bool_and_error(value=True, location=location, negate=negate)

def _builder_for_storage_access(
    typ: pytypes.PyType,
    member_name: str,
    definition: AppStorageDefinition,
    location: SourceLocation,
) -> NodeBuilder:
    key = attrs.evolve(definition.key, source_location=location)
    if not isinstance(typ, pytypes.StorageProxyType | pytypes.StorageMapProxyType):
        app_global_expr = AppStateExpression(
            key=key,
            wtype=typ.checked_wtype(location),
            exists_assertion_message=f"check self.{member_name} exists",
            source_location=location,
        )
        return builder_for_instance(typ, app_global_expr)
    if typ == pytypes.BoxRefType:
        return BoxRefProxyExpressionBuilder(key, member_name)
    match definition.kind:
        case AppStorageKind.app_global:
            return GlobalStateExpressionBuilder(key, typ, member_name)
        case AppStorageKind.account_local:
            return LocalStateExpressionBuilder(key, typ, member_name)
        case AppStorageKind.box:
            if isinstance(typ, pytypes.StorageMapProxyType):
                return BoxMapProxyExpressionBuilder(key, typ, member_name)
            else:
                return BoxProxyExpressionBuilder(key, typ, member_name)


import typing
from collections.abc import Mapping

from puya import log
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build.eb._utils import constant_bool_and_error
from puyapy.awst_build.eb.interface import (
    InstanceBuilder,
    NodeBuilder,
)

logger = log.get_logger(__name__)

class DictLiteralBuilder(NodeBuilder):
    def __init__(self, mapping: Mapping[str, InstanceBuilder], location: SourceLocation):
        super().__init__(location)
        self._mapping = mapping

    @typing.override
    @property
    def pytype(self) -> None:
        return None

    @property
    def mapping(self) -> Mapping[str, InstanceBuilder]:
        return self._mapping

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> typing.Never:
        if name in dir(dict()):  # noqa: C408
            raise CodeError("method is not currently supported", location)
        raise CodeError("unrecognised member access", location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return constant_bool_and_error(value=bool(self._mapping), location=location, negate=negate)


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst.nodes import (
    CallArg,
    Expression,
    PuyaLibCall,
    PuyaLibFunction,
    UInt64Constant,
)
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder
from puyapy.awst_build.eb._utils import dummy_value
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder
from puyapy.awst_build.eb.none import NoneExpressionBuilder
from puyapy.awst_build.utils import get_arg_mapping

logger = log.get_logger(__name__)

class EnsureBudgetBuilder(FunctionBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        required_budget_arg_name = "required_budget"
        fee_source_arg_name = "fee_source"
        arg_mapping, _ = get_arg_mapping(
            required_positional_names=[required_budget_arg_name],
            optional_positional_names=[fee_source_arg_name],
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
        )
        required_budget_arg = arg_mapping.get(required_budget_arg_name)
        if required_budget_arg is None:
            required_budget = dummy_value(pytypes.UInt64Type, location)
        else:
            required_budget = expect.argument_of_type_else_dummy(
                required_budget_arg, pytypes.UInt64Type, resolve_literal=True
            )

        fee_source_arg = arg_mapping.get(fee_source_arg_name)
        if fee_source_arg is None:
            fee_source_expr: Expression = UInt64Constant(value=0, source_location=location)
        else:
            fee_source_expr = expect.argument_of_type_else_dummy(
                fee_source_arg, pytypes.OpUpFeeSourceType
            ).resolve()

        call_args = [
            CallArg(
                name=required_budget_arg_name,
                value=required_budget.resolve(),
            ),
            CallArg(
                name=fee_source_arg_name,
                value=fee_source_expr,
            ),
        ]
        call_expr = PuyaLibCall(
            func=PuyaLibFunction.ensure_budget,
            args=call_args,
            source_location=location,
        )
        return NoneExpressionBuilder(call_expr)


from puya.awst.nodes import Expression
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb.interface import CallableBuilder, InstanceBuilder

def builder_for_instance(pytyp: pytypes.PyType, expr: Expression) -> InstanceBuilder:
    from puyapy.awst_build.eb import _type_registry

    return _type_registry.builder_for_instance(pytyp, expr)

def builder_for_type(pytyp: pytypes.PyType, expr_loc: SourceLocation) -> CallableBuilder:
    from puyapy.awst_build.eb import _type_registry

    return _type_registry.builder_for_type(pytyp, expr_loc)

def try_get_builder_for_func(fullname: str, expr_loc: SourceLocation) -> CallableBuilder | None:
    from puyapy.awst_build.eb import _type_registry

    builder = _type_registry.FUNC_NAME_TO_BUILDER.get(fullname)
    if builder is not None:
        return builder(expr_loc)
    return None


from __future__ import annotations

import abc
import enum
import typing
from collections.abc import Sequence

import attrs
import mypy.nodes
import typing_extensions

from puya import log
from puya.awst.nodes import (
    BinaryBooleanOperator,
    Expression,
    Lvalue,
    Statement,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puya.utils import invert_ordered_binary_op
from puyapy.awst_build import pytypes
from puyapy.models import ConstantValue

logger = log.get_logger(__name__)

@enum.unique
class BuilderComparisonOp(enum.StrEnum):
    eq = "=="
    ne = "!="
    lt = "<"
    lte = "<="
    gt = ">"
    gte = ">="

    def reversed(self) -> BuilderComparisonOp:
        return BuilderComparisonOp(invert_ordered_binary_op(self.value))

@enum.unique
class BuilderUnaryOp(enum.StrEnum):
    positive = "+"
    negative = "-"
    bit_invert = "~"

@enum.unique
class BuilderBinaryOp(enum.StrEnum):
    add = "+"
    sub = "-"
    mult = "*"
    div = "/"
    floor_div = "//"
    mod = "%"
    pow = "**"
    mat_mult = "@"
    lshift = "<<"
    rshift = ">>"
    bit_or = "|"
    bit_xor = "^"
    bit_and = "&"

class NodeBuilder(abc.ABC):
    def __init__(self, location: SourceLocation):
        self.source_location: typing.Final = location

    @property
    @abc.abstractmethod
    def pytype(self) -> pytypes.PyType | None: ...

    @abc.abstractmethod
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        """Handle self.name"""

    @abc.abstractmethod
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        """Handle boolean-ness evaluation, possibly inverted (ie "not" unary operator)"""

class CallableBuilder(NodeBuilder, abc.ABC):
    @abc.abstractmethod
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        """Handle self(args...)"""

_TPyType_co = typing_extensions.TypeVar(
    "_TPyType_co", bound=pytypes.PyType, default=pytypes.PyType, covariant=True
)

class InstanceBuilder(NodeBuilder, typing.Generic[_TPyType_co], abc.ABC):
    @typing.override
    @property
    @abc.abstractmethod
    def pytype(self) -> _TPyType_co: ...

    @abc.abstractmethod
    def resolve(self) -> Expression:
        """Produce an expression for use as an intermediary"""

    @abc.abstractmethod
    def resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder:
        """For use prior to calling resolve(), when a known type is expected,
        and implicit conversion is possible without breaking semantic compatibility.

        Primarily, this would be as an argument to an algopy function or class, or as an
        operand to a binary operator with an algopy-typed object.

        When implementing, this function should return self if there are no literals to convert,
        or if the literals are not expected to be homogenous (e.g. in the general case of a tuple
        with literals)"""

    @abc.abstractmethod
    def try_resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder | None:
        """Similar to resolve_literal, but in the case where a conversion is possible but the
        literal values are the wrong type, don't produce any errors and return None instead.

        If no conversion is necessary (ie no literals or it's not meaningful to convert literals
        this way, such as a tuple with literals), just return the current instance.
        """

    @abc.abstractmethod
    def resolve_lvalue(self) -> Lvalue:
        """Produce an expression for the target of an assignment"""

    @abc.abstractmethod
    def delete(self, location: SourceLocation) -> Statement:
        """Handle del self"""

    @abc.abstractmethod
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> InstanceBuilder:
        """Handle {op} self"""

    @abc.abstractmethod
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        """Handle self {op} other"""

    @abc.abstractmethod
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        """Handle self {op} other"""

    @abc.abstractmethod
    def bool_binary_op(
        self, other: InstanceBuilder, op: BinaryBooleanOperator, location: SourceLocation
    ) -> InstanceBuilder:
        """Handle self and/or other"""
        from puyapy.awst_build.eb.binary_bool_op import BinaryBoolOpBuilder

        return BinaryBoolOpBuilder(left=self, right=other, op=op, location=location)

    @abc.abstractmethod
    def augmented_assignment(
        self, op: BuilderBinaryOp, rhs: InstanceBuilder, location: SourceLocation
    ) -> Statement:
        """Handle self {op}= rhs"""

    @abc.abstractmethod
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        """Handle item in self"""
        from puyapy.awst_build.eb._utils import dummy_value

        logger.error("expression is not iterable", location=self.source_location)
        return dummy_value(pytypes.BoolType, location)

    @abc.abstractmethod
    def iterate(self) -> Expression:
        """Produce target of ForInLoop"""
        raise CodeError("expression is not iterable", self.source_location)

    @abc.abstractmethod
    def iterable_item_type(self) -> pytypes.PyType:
        """The type of the item if this expression is iterable"""
        raise CodeError("expression is not iterable", self.source_location)

    @abc.abstractmethod
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        """Handle self[index]"""
        raise CodeError("expression is not a collection", location)

    @abc.abstractmethod
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        """Handle self[begin_index:end_index:stride]"""
        raise CodeError("expression is not a collection", location)

    @abc.abstractmethod
    def to_bytes(self, location: SourceLocation) -> Expression:
        """Handle conversion/cast to bytes, if possible"""

    @abc.abstractmethod
    def single_eval(self) -> InstanceBuilder:
        """wrap any underlying expressions etc. (if applicable) to avoid multiple evaluations"""

class StaticSizedCollectionBuilder(NodeBuilder, abc.ABC):
    @abc.abstractmethod
    def iterate_static(self) -> Sequence[InstanceBuilder]: ...

class LiteralBuilder(InstanceBuilder, abc.ABC):
    @property
    @abc.abstractmethod
    def value(self) -> ConstantValue: ...

    @typing.override
    @abc.abstractmethod
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> LiteralBuilder: ...

    @typing.override
    @abc.abstractmethod
    def member_access(self, name: str, location: SourceLocation) -> LiteralBuilder: ...

    @typing.override
    @abc.abstractmethod
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> LiteralBuilder: ...

    @typing.override
    @abc.abstractmethod
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> LiteralBuilder: ...

    @typing.override
    @abc.abstractmethod
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> LiteralBuilder: ...

    @typing.override
    @abc.abstractmethod
    def index(self, index: InstanceBuilder, location: SourceLocation) -> LiteralBuilder: ...

    @typing.override
    @abc.abstractmethod
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> LiteralBuilder: ...

# TODO: separate interface from implementation
class TypeBuilder(CallableBuilder, typing.Generic[_TPyType_co], abc.ABC):
    def __init__(self, pytype: _TPyType_co, location: SourceLocation):
        super().__init__(location)
        self._pytype = pytype

    @typing.final
    @typing.override
    @property
    def pytype(self) -> pytypes.TypeType:
        return pytypes.TypeType(self._pytype)

    @typing.final
    def produces(self) -> _TPyType_co:
        return self._pytype

    @typing.final
    def convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder:
        from puyapy.awst_build.eb._utils import dummy_value

        result = self.try_convert_literal(literal, location)
        if result is not None:
            return result
        logger.error(
            f"can't covert literal to {self.produces()}", location=literal.source_location
        )
        return dummy_value(self.produces(), location)

    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        """
        If the type of `literal.value` is correct, return a new instance, otherwise return `None`.
        If the value is out of range or otherwise invalid, an error is logged,
        but an instance is still returned.
        """
        return None

    @typing.override
    @typing.final
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        from puyapy.awst_build.eb._utils import constant_bool_and_error

        return constant_bool_and_error(value=True, location=location, negate=negate)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        raise CodeError(f"unrecognised member {name!r} of type '{self._pytype}'", location)

@attrs.frozen(kw_only=True)
class StorageProxyConstructorArgs:
    key: Expression | None
    key_arg_name: str
    description: str | None
    initial_value: InstanceBuilder | None = None

class StorageProxyConstructorResult(
    InstanceBuilder[pytypes.StorageProxyType | pytypes.StorageMapProxyType], abc.ABC
):
    @property
    @abc.abstractmethod
    def args(self) -> StorageProxyConstructorArgs: ...