import typing

from puya import log
from puya.awst.nodes import (
    BinaryBooleanOperator,
    BoolConstant,
    BytesConstant,
    BytesEncoding,
    Expression,
    Lvalue,
    Statement,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import intrinsic_factory, pytypes
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    InstanceBuilder,
    LiteralBuilder,
    TypeBuilder,
)
from puyapy.awst_build.utils import fold_binary_expr, fold_unary_expr
from puyapy.models import ConstantValue

logger = log.get_logger(__name__)

class LiteralBuilderImpl(LiteralBuilder):
    def __init__(self, value: ConstantValue, source_location: SourceLocation):
        super().__init__(source_location)
        self._value = value
        match value:
            case bool():
                typ: pytypes.PyType = pytypes.BoolType
            case int():
                typ = pytypes.IntLiteralType
            case str():
                typ = pytypes.StrLiteralType
            case bytes():
                typ = pytypes.BytesLiteralType
            case _:
                typing.assert_never(value)
        self._pytype = typ

    @property
    def value(self) -> ConstantValue:
        return self._value

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

    @typing.override
    def resolve(self) -> Expression:
        if isinstance(self.value, bool):
            return BoolConstant(value=self.value, source_location=self.source_location)
        raise CodeError("a Python literal is not valid at this location", self.source_location)

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

    @typing.override
    def try_resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder | None:
        return converter.try_convert_literal(literal=self, location=converter.source_location)

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

    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        match self.value:
            case str(str_value):
                return BytesConstant(
                    value=str_value.encode(), encoding=BytesEncoding.utf8, source_location=location
                )
            case bytes(bytes_value):
                return BytesConstant(
                    value=bytes_value, encoding=BytesEncoding.unknown, source_location=location
                )
            case bool():
                return intrinsic_factory.itob(self.resolve(), location)
        raise CodeError(f"cannot serialize literal of type {self.pytype}", 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) -> LiteralBuilder:
        folded = fold_unary_expr(location, op.value, self.value)
        return LiteralBuilderImpl(value=folded, source_location=location)

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> LiteralBuilder:
        if not isinstance(other, LiteralBuilder):
            return NotImplemented
        folded = fold_binary_expr(location, op.value, self.value, other.value)
        return LiteralBuilderImpl(value=folded, source_location=location)

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> LiteralBuilder:
        if not isinstance(other, LiteralBuilder):
            return NotImplemented
        lhs, rhs = self.value, other.value
        if reverse:
            lhs, rhs = rhs, lhs
        folded = fold_binary_expr(location, op.value, lhs, rhs)
        return LiteralBuilderImpl(value=folded, source_location=location)

    @typing.override
    def bool_binary_op(
        self, other: InstanceBuilder, op: BinaryBooleanOperator, location: SourceLocation
    ) -> InstanceBuilder:
        if not isinstance(other, LiteralBuilder):
            return super().bool_binary_op(other, op, location)
        folded = fold_binary_expr(location, op.value, self.value, other.value)
        return LiteralBuilderImpl(value=folded, source_location=location)

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

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> LiteralBuilder:
        if not isinstance(item, LiteralBuilder):
            raise CodeError("cannot perform containment check with non-constant value", location)
        try:
            folded = item.value in self.value  # type: ignore[operator]
        except Exception as ex:
            raise CodeError(str(ex), location) from ex
        return LiteralBuilderImpl(value=folded, source_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) -> LiteralBuilder:
        if not isinstance(index, LiteralBuilder):
            raise CodeError("cannot index literal with non-constant value", location)
        try:
            folded = self.value[index.value]  # type: ignore[index]
        except Exception as ex:
            raise CodeError(str(ex), location) from ex
        return LiteralBuilderImpl(value=folded, source_location=location)

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> LiteralBuilder:
        def _constant_slice_arg(index: InstanceBuilder | None) -> ConstantValue | None:
            if index is None:
                return None
            if not isinstance(index, LiteralBuilder):
                raise CodeError("cannot slice literal with non-constant value", location)
            return index.value

        begin = _constant_slice_arg(begin_index)
        end = _constant_slice_arg(end_index)
        stride_ = _constant_slice_arg(stride)
        try:
            folded = self.value[begin:end:stride_]  # type: ignore[index,misc]
        except Exception as ex:
            raise CodeError(str(ex), location) from ex
        return LiteralBuilderImpl(value=folded, source_location=location)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> LiteralBuilder:
        # TODO: support stuff like int.from_bytes etc
        raise CodeError("unsupported member access from literal", location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        if isinstance(self.value, bool):
            value = self.value
            warn = False
        else:
            value = bool(self.value)
            warn = True
        if negate:
            value = not value
        if warn:
            logger.warning(f"expression is always {value}", location=location)
        return LiteralBuilderImpl(value=value, source_location=location)

    @typing.override
    def single_eval(self) -> InstanceBuilder:
        return self


import functools
from collections.abc import Callable

from puya.awst.nodes import Expression
from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puyapy.awst_build import constants, intrinsic_data, pytypes
from puyapy.awst_build.eb import (
    arc4,
    array,
    biguint,
    bool as bool_,
    bytes as bytes_,
    compiled,
    ensure_budget,
    intrinsics,
    log,
    none,
    storage,
    string,
    struct,
    template_variables,
    transaction,
    tuple as tuple_,
    uint64,
    uint64_enums,
    unsigned_builtins,
)
from puyapy.awst_build.eb.interface import CallableBuilder, InstanceBuilder
from puyapy.awst_build.eb.reference_types import account, application, asset

__all__ = [
    "builder_for_instance",
    "builder_for_type",
]

CallableBuilderFromSourceFactory = Callable[[SourceLocation], CallableBuilder]

FUNC_NAME_TO_BUILDER: dict[str, CallableBuilderFromSourceFactory] = {
    "algopy.arc4.arc4_signature": intrinsics.Arc4SignatureBuilder,
    "algopy._util.ensure_budget": ensure_budget.EnsureBudgetBuilder,
    "algopy._util.log": log.LogBuilder,
    "algopy.arc4.emit": arc4.EmitBuilder,
    "algopy.itxn.submit_txns": transaction.SubmitInnerTransactionExpressionBuilder,
    "algopy._compiled.compile_contract": compiled.CompileContractFunctionBuilder,
    "algopy._compiled.compile_logicsig": compiled.CompileLogicSigFunctionBuilder,
    "algopy.arc4.arc4_create": arc4.ARC4CreateFunctionBuilder,
    "algopy.arc4.arc4_update": arc4.ARC4UpdateFunctionBuilder,
    constants.CLS_ARC4_ABI_CALL: arc4.ABICallGenericTypeBuilder,
    "algopy._template_variables.TemplateVar": (
        template_variables.GenericTemplateVariableExpressionBuilder
    ),
    **{
        (fullname := "".join((constants.ALGOPY_OP_PREFIX, name))): functools.partial(
            intrinsics.IntrinsicFunctionExpressionBuilder, fullname, mappings
        )
        for name, mappings in intrinsic_data.FUNC_TO_AST_MAPPER.items()
    },
}

PYTYPE_TO_TYPE_BUILDER: dict[pytypes.PyType, CallableBuilderFromSourceFactory] = {
    pytypes.NoneType: none.NoneTypeBuilder,
    pytypes.BoolType: bool_.BoolTypeBuilder,
    pytypes.GenericTupleType: tuple_.GenericTupleTypeBuilder,
    pytypes.reversedGenericType: unsigned_builtins.ReversedFunctionExpressionBuilder,
    pytypes.urangeType: unsigned_builtins.UnsignedRangeBuilder,
    pytypes.uenumerateGenericType: unsigned_builtins.UnsignedEnumerateBuilder,
    pytypes.OpUpFeeSourceType: uint64_enums.OpUpFeeSourceTypeBuilder,
    pytypes.GenericBoxType: storage.BoxGenericTypeExpressionBuilder,
    pytypes.BoxRefType: storage.BoxRefTypeBuilder,
    pytypes.GenericBoxMapType: storage.BoxMapGenericTypeExpressionBuilder,
    pytypes.GenericLocalStateType: storage.LocalStateGenericTypeBuilder,
    pytypes.GenericGlobalStateType: storage.GlobalStateGenericTypeBuilder,
    pytypes.ARC4AddressType: arc4.AddressTypeBuilder,
    pytypes.ARC4BoolType: arc4.ARC4BoolTypeBuilder,
    pytypes.ARC4ByteType: functools.partial(arc4.UIntNTypeBuilder, pytypes.ARC4ByteType),
    pytypes.GenericARC4DynamicArrayType: arc4.DynamicArrayGenericTypeBuilder,
    pytypes.GenericARC4StaticArrayType: arc4.StaticArrayGenericTypeBuilder,
    pytypes.ARC4StringType: arc4.ARC4StringTypeBuilder,
    pytypes.GenericARC4TupleType: arc4.ARC4TupleGenericTypeBuilder,
    pytypes.ARC4DynamicBytesType: arc4.DynamicBytesTypeBuilder,
    pytypes.AccountType: account.AccountTypeBuilder,
    pytypes.GenericArrayType: array.ArrayGenericTypeBuilder,
    pytypes.AssetType: asset.AssetTypeBuilder,
    pytypes.ApplicationType: application.ApplicationTypeBuilder,
    pytypes.BigUIntType: biguint.BigUIntTypeBuilder,
    pytypes.BytesType: bytes_.BytesTypeBuilder,
    pytypes.StringType: string.StringTypeBuilder,
    pytypes.UInt64Type: uint64.UInt64TypeBuilder,
    pytypes.TransactionTypeType: uint64_enums.TransactionTypeTypeBuilder,
    pytypes.OnCompleteActionType: uint64_enums.OnCompletionActionTypeBuilder,
    **{
        op_enum_typ: functools.partial(intrinsics.IntrinsicEnumTypeBuilder, op_enum_typ)
        for op_enum_typ in pytypes.OpEnumTypes
    },
    **{
        op_namespace_typ: functools.partial(
            intrinsics.IntrinsicNamespaceTypeBuilder, op_namespace_typ
        )
        for op_namespace_typ in pytypes.OpNamespaceTypes
    },
    **{
        gtxn_pytyp: functools.partial(transaction.GroupTransactionTypeBuilder, gtxn_pytyp)
        for gtxn_pytyp in (
            pytypes.GroupTransactionBaseType,
            *pytypes.GroupTransactionTypes.values(),
        )
    },
    **{
        itxn_fieldset_pytyp: functools.partial(
            transaction.InnerTxnParamsTypeBuilder, itxn_fieldset_pytyp
        )
        for itxn_fieldset_pytyp in pytypes.InnerTransactionFieldsetTypes.values()
    },
    **{
        itxn_result_pytyp: functools.partial(
            transaction.InnerTransactionTypeBuilder, itxn_result_pytyp
        )
        for itxn_result_pytyp in pytypes.InnerTransactionResultTypes.values()
    },
}

CallableBuilderFromPyTypeAndSourceFactory = Callable[
    [pytypes.PyType, SourceLocation], CallableBuilder
]

PYTYPE_GENERIC_TO_TYPE_BUILDER: dict[
    pytypes.PyType | None, CallableBuilderFromPyTypeAndSourceFactory
] = {
    pytypes.uenumerateGenericType: (
        # TODO: fixme, should accept type
        lambda _, loc: unsigned_builtins.UnsignedEnumerateBuilder(loc)
    ),
    pytypes.reversedGenericType: (
        # TODO: fixme, should accept type
        lambda _, loc: unsigned_builtins.ReversedFunctionExpressionBuilder(loc)
    ),
    pytypes.GenericTemplateVarType: template_variables.TemplateVariableExpressionBuilder,
    pytypes.GenericABICallWithReturnType: arc4.ABICallTypeBuilder,
    pytypes.GenericLocalStateType: storage.LocalStateTypeBuilder,
    pytypes.GenericGlobalStateType: storage.GlobalStateTypeBuilder,
    pytypes.GenericBoxType: storage.BoxTypeBuilder,
    pytypes.GenericBoxMapType: storage.BoxMapTypeBuilder,
    pytypes.GenericARC4TupleType: arc4.ARC4TupleTypeBuilder,
    pytypes.GenericTupleType: tuple_.TupleTypeBuilder,
    pytypes.GenericArrayType: array.ArrayTypeBuilder,
    pytypes.GenericARC4UFixedNxMType: arc4.UFixedNxMTypeBuilder,
    pytypes.GenericARC4BigUFixedNxMType: arc4.UFixedNxMTypeBuilder,
    pytypes.GenericARC4UIntNType: arc4.UIntNTypeBuilder,
    pytypes.GenericARC4BigUIntNType: arc4.UIntNTypeBuilder,
    pytypes.GenericARC4DynamicArrayType: arc4.DynamicArrayTypeBuilder,
    pytypes.GenericARC4StaticArrayType: arc4.StaticArrayTypeBuilder,
}

PYTYPE_BASE_TO_TYPE_BUILDER: dict[pytypes.PyType, CallableBuilderFromPyTypeAndSourceFactory] = {
    pytypes.ARC4StructBaseType: arc4.ARC4StructTypeBuilder,
    pytypes.StructBaseType: struct.StructSubclassExpressionBuilder,
    pytypes.NamedTupleBaseType: tuple_.NamedTupleTypeBuilder,
}

PYTYPE_TO_BUILDER: dict[pytypes.PyType, Callable[[Expression], InstanceBuilder]] = {
    pytypes.ARC4BoolType: arc4.ARC4BoolExpressionBuilder,
    pytypes.ARC4StringType: arc4.ARC4StringExpressionBuilder,
    pytypes.ARC4DynamicBytesType: arc4.DynamicBytesExpressionBuilder,
    pytypes.ARC4ByteType: functools.partial(arc4.UIntNExpressionBuilder, typ=pytypes.ARC4ByteType),
    pytypes.ARC4AddressType: arc4.AddressExpressionBuilder,
    pytypes.AccountType: account.AccountExpressionBuilder,
    pytypes.ApplicationType: application.ApplicationExpressionBuilder,
    pytypes.AssetType: asset.AssetExpressionBuilder,
    pytypes.BigUIntType: biguint.BigUIntExpressionBuilder,
    pytypes.BoolType: bool_.BoolExpressionBuilder,
    pytypes.BytesType: bytes_.BytesExpressionBuilder,
    pytypes.CompiledContractType: compiled.CompiledContractExpressionBuilder,
    pytypes.CompiledLogicSigType: compiled.CompiledLogicSigExpressionBuilder,
    pytypes.StringType: string.StringExpressionBuilder,
    pytypes.UInt64Type: uint64.UInt64ExpressionBuilder,
    pytypes.NoneType: none.NoneExpressionBuilder,
    pytypes.NeverType: none.NoneExpressionBuilder,  # we treat Never as None/void synonym for now
    pytypes.BoxRefType: storage.BoxRefProxyExpressionBuilder,
    # bound
    **{
        uint64_enum_typ: functools.partial(
            uint64.UInt64ExpressionBuilder, enum_type=uint64_enum_typ
        )
        for uint64_enum_typ in (
            pytypes.OpUpFeeSourceType,
            pytypes.TransactionTypeType,
            pytypes.OnCompleteActionType,
        )
    },
    **{
        gtxn_pytyp: functools.partial(
            transaction.GroupTransactionExpressionBuilder, typ=gtxn_pytyp
        )
        for gtxn_pytyp in (
            pytypes.GroupTransactionBaseType,
            *pytypes.GroupTransactionTypes.values(),
        )
    },
    **{
        itxn_fieldset_pytyp: functools.partial(
            transaction.InnerTxnParamsExpressionBuilder, itxn_fieldset_pytyp
        )
        for itxn_fieldset_pytyp in pytypes.InnerTransactionFieldsetTypes.values()
    },
    **{
        itxn_result_pytyp: functools.partial(
            transaction.InnerTransactionExpressionBuilder, typ=itxn_result_pytyp
        )
        for itxn_result_pytyp in pytypes.InnerTransactionResultTypes.values()
    },
}

InstanceBuilderFromExpressionAndPyTypeFactory = Callable[
    [Expression, pytypes.PyType], InstanceBuilder
]

PYTYPE_GENERIC_TO_BUILDER: dict[
    pytypes.PyType | None, InstanceBuilderFromExpressionAndPyTypeFactory
] = {
    pytypes.GenericTupleType: tuple_.TupleExpressionBuilder,
    pytypes.GenericBoxType: storage.BoxProxyExpressionBuilder,
    pytypes.GenericBoxMapType: storage.BoxMapProxyExpressionBuilder,
    pytypes.GenericArrayType: array.ArrayExpressionBuilder,
    pytypes.GenericARC4DynamicArrayType: arc4.DynamicArrayExpressionBuilder,
    pytypes.GenericARC4StaticArrayType: arc4.StaticArrayExpressionBuilder,
    pytypes.GenericARC4TupleType: arc4.ARC4TupleExpressionBuilder,
    pytypes.GenericARC4UFixedNxMType: arc4.UFixedNxMExpressionBuilder,
    pytypes.GenericARC4BigUFixedNxMType: arc4.UFixedNxMExpressionBuilder,
    pytypes.GenericARC4UIntNType: arc4.UIntNExpressionBuilder,
    pytypes.GenericARC4BigUIntNType: arc4.UIntNExpressionBuilder,
    pytypes.GenericGlobalStateType: storage.GlobalStateExpressionBuilder,
    pytypes.GenericLocalStateType: storage.LocalStateExpressionBuilder,
}

PYTYPE_BASE_TO_BUILDER: dict[pytypes.PyType, InstanceBuilderFromExpressionAndPyTypeFactory] = {
    pytypes.ARC4StructBaseType: arc4.ARC4StructExpressionBuilder,
    pytypes.StructBaseType: struct.StructExpressionBuilder,
    pytypes.NamedTupleBaseType: tuple_.TupleExpressionBuilder,
}

def builder_for_instance(pytyp: pytypes.PyType, expr: Expression) -> InstanceBuilder:
    if eb := PYTYPE_TO_BUILDER.get(pytyp):
        return eb(expr)
    if eb_param_generic := PYTYPE_GENERIC_TO_BUILDER.get(pytyp.generic):
        return eb_param_generic(expr, pytyp)
    for base in pytyp.mro:
        if eb_base := PYTYPE_BASE_TO_BUILDER.get(base):
            return eb_base(expr, pytyp)
    if isinstance(pytyp, pytypes.UnionType):
        raise CodeError("type unions are unsupported at this location", expr.source_location)
    raise InternalError(f"no builder for instance: {pytyp}", expr.source_location)

def builder_for_type(pytyp: pytypes.PyType, expr_loc: SourceLocation) -> CallableBuilder:
    if tb := PYTYPE_TO_TYPE_BUILDER.get(pytyp):
        return tb(expr_loc)
    if tb_param_generic := PYTYPE_GENERIC_TO_TYPE_BUILDER.get(pytyp.generic):
        return tb_param_generic(pytyp, expr_loc)
    for base in pytyp.mro:
        if tb_base := PYTYPE_BASE_TO_TYPE_BUILDER.get(base):
            return tb_base(pytyp, expr_loc)
    if isinstance(pytyp, pytypes.UnionType):
        raise CodeError("type unions are unsupported at this location", expr_loc)
    raise InternalError(f"no builder for type: {pytyp}", expr_loc)


from puya import log
from puya.awst import wtypes
from puya.awst.nodes import (
    BytesComparisonExpression,
    EqualityComparison,
    Expression,
    ExpressionStatement,
    ReinterpretCast,
    Statement,
    VarExpression,
)
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    InstanceBuilder,
    LiteralBuilder,
)

logger = log.get_logger(__name__)

def dummy_value(pytype: pytypes.PyType, location: SourceLocation) -> InstanceBuilder:
    if isinstance(pytype, pytypes.LiteralOnlyType):
        from puyapy.awst_build.eb._literals import LiteralBuilderImpl

        return LiteralBuilderImpl(pytype.python_type(), location)
    expr = VarExpression(name="", wtype=pytype.checked_wtype(location), source_location=location)
    return builder_for_instance(pytype, expr)

def dummy_statement(location: SourceLocation) -> Statement:
    return ExpressionStatement(
        VarExpression(
            name="",
            wtype=wtypes.void_wtype,
            source_location=location,
        )
    )

def resolve_negative_literal_index(
    index: InstanceBuilder, length: InstanceBuilder, location: SourceLocation
) -> InstanceBuilder:
    match index:
        case LiteralBuilder(value=int(int_index)) if int_index < 0:
            return length.binary_op(
                index.unary_op(BuilderUnaryOp.negative, location),
                BuilderBinaryOp.sub,
                location,
                reverse=False,
            )
        case _:
            from puyapy.awst_build.eb.uint64 import UInt64TypeBuilder

            return index.resolve_literal(UInt64TypeBuilder(index.source_location))

def constant_bool_and_error(
    *, value: bool, location: SourceLocation, negate: bool = False
) -> InstanceBuilder:
    """
    Returns a constant bool instance builder for the specified value and negate combination.

    Always emits an error as either allowing the expression would result in a semantic
    compatability issue, or indicates the user has most likely made a mistake
    """
    from puyapy.awst_build.eb._literals import LiteralBuilderImpl

    if negate:
        value = not value
    logger.error(f"expression is always {value}", location=location)
    return LiteralBuilderImpl(value=value, source_location=location)

def compare_bytes(
    *,
    self: InstanceBuilder,
    op: BuilderComparisonOp,
    other: InstanceBuilder,
    source_location: SourceLocation,
) -> InstanceBuilder:
    # defer to most derived type if not equal
    if not (other.pytype <= self.pytype):
        return NotImplemented
    return _compare_expr_bytes_unchecked(self.resolve(), op, other.resolve(), source_location)

def compare_expr_bytes(
    *,
    lhs: Expression,
    op: BuilderComparisonOp,
    rhs: Expression,
    source_location: SourceLocation,
) -> InstanceBuilder:
    if rhs.wtype != lhs.wtype:
        return NotImplemented
    return _compare_expr_bytes_unchecked(lhs, op, rhs, source_location)

def _compare_expr_bytes_unchecked(
    lhs: Expression,
    op: BuilderComparisonOp,
    rhs: Expression,
    location: SourceLocation,
) -> InstanceBuilder:
    from puyapy.awst_build.eb.bool import BoolExpressionBuilder

    try:
        eq_op = EqualityComparison(op.value)
    except ValueError:
        return NotImplemented
    cmp_expr = BytesComparisonExpression(
        lhs=lhs,
        operator=eq_op,
        rhs=rhs,
        source_location=location,
    )
    return BoolExpressionBuilder(cmp_expr)

def cast_to_bytes(expr: Expression, location: SourceLocation | None = None) -> ReinterpretCast:
    return ReinterpretCast(
        expr=expr, wtype=wtypes.bytes_wtype, source_location=location or expr.source_location
    )


# ruff: noqa: F403
from puyapy.awst_build.eb.arc4.abi_call import *
from puyapy.awst_build.eb.arc4.address import *
from puyapy.awst_build.eb.arc4.bool import *
from puyapy.awst_build.eb.arc4.dynamic_array import *
from puyapy.awst_build.eb.arc4.dynamic_bytes import *
from puyapy.awst_build.eb.arc4.emit import *
from puyapy.awst_build.eb.arc4.static_array import *
from puyapy.awst_build.eb.arc4.string import *
from puyapy.awst_build.eb.arc4.struct import *
from puyapy.awst_build.eb.arc4.tuple import *
from puyapy.awst_build.eb.arc4.ufixed import *
from puyapy.awst_build.eb.arc4.uint import *


import abc
import typing
from abc import ABC
from collections.abc import Sequence

import mypy.nodes
import typing_extensions

from puya import log
from puya.awst.nodes import (
    BytesConstant,
    BytesEncoding,
    CheckedMaybe,
    Copy,
    Expression,
    IndexExpression,
)
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
from puyapy.awst_build.eb._bytes_backed import (
    BytesBackedInstanceExpressionBuilder,
    BytesBackedTypeBuilder,
)
from puyapy.awst_build.eb._utils import (
    compare_bytes,
    compare_expr_bytes,
    dummy_value,
    resolve_negative_literal_index,
)
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import BuilderComparisonOp, InstanceBuilder, NodeBuilder

logger = log.get_logger(__name__)

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

class ARC4TypeBuilder(BytesBackedTypeBuilder[_TPyType_co], abc.ABC):
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "from_log":
                return ARC4FromLogBuilder(location, self.produces())
            case _:
                return super().member_access(name, location)

class ARC4FromLogBuilder(FunctionBuilder):
    def __init__(self, location: SourceLocation, typ: pytypes.PyType):
        super().__init__(location=location)
        self.typ = typ

    @classmethod
    def abi_expr_from_log(
        cls, typ: pytypes.PyType, value: InstanceBuilder, location: SourceLocation
    ) -> Expression:
        tmp_value = value.single_eval().resolve()
        arc4_value = intrinsic_factory.extract(
            tmp_value, start=4, loc=location, result_type=typ.checked_wtype(location)
        )
        arc4_prefix = intrinsic_factory.extract(tmp_value, start=0, length=4, loc=location)
        arc4_prefix_is_valid = compare_expr_bytes(
            lhs=arc4_prefix,
            rhs=BytesConstant(
                value=b"\x15\x1f\x7c\x75",
                source_location=location,
                encoding=BytesEncoding.base16,
            ),
            op=BuilderComparisonOp.eq,
            source_location=location,
        )
        return CheckedMaybe.from_tuple_items(
            expr=arc4_value,
            check=arc4_prefix_is_valid.resolve(),
            source_location=location,
            comment="ARC4 prefix is valid",
        )

    @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_of_type_else_dummy(args, pytypes.BytesType, location)
        result_expr = self.abi_expr_from_log(self.typ, arg, location)
        return builder_for_instance(self.typ, result_expr)

class CopyBuilder(FunctionBuilder):
    def __init__(self, expr: Expression, location: SourceLocation, typ: pytypes.PyType):
        super().__init__(location)
        self._typ = typ
        self.expr = expr

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        expect.no_args(args, location)
        expr_result = Copy(value=self.expr, source_location=location)
        return builder_for_instance(self._typ, expr_result)

def arc4_bool_bytes(
    builder: InstanceBuilder, false_bytes: bytes, location: SourceLocation, *, negate: bool
) -> InstanceBuilder:
    lhs = builder.resolve()
    false_value = BytesConstant(
        value=false_bytes,
        encoding=BytesEncoding.base16,
        wtype=lhs.wtype,
        source_location=location,
    )
    return compare_expr_bytes(
        op=BuilderComparisonOp.eq if negate else BuilderComparisonOp.ne,
        lhs=lhs,
        rhs=false_value,
        source_location=location,
    )

class _ARC4ArrayExpressionBuilder(BytesBackedInstanceExpressionBuilder[pytypes.ArrayType], ABC):
    @typing.override
    def iterate(self) -> Expression:
        if not self.pytype.items_wtype.immutable:
            # this case is an error raised during AWST validation
            # adding a front end specific message here to compliment the error message
            # raise across all front ends
            logger.info(
                "use `algopy.urange(<array>.length)` to iterate by index",
                location=self.source_location,
            )
        return self.resolve()

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

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        array_length = self.length(index.source_location)
        index = resolve_negative_literal_index(index, array_length, location)
        result_expr = IndexExpression(
            base=self.resolve(),
            index=index.resolve(),
            wtype=self.pytype.items_wtype,
            source_location=location,
        )
        return builder_for_instance(self.pytype.items, result_expr)

    @abc.abstractmethod
    def length(self, location: SourceLocation) -> InstanceBuilder: ...

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "length":
                return self.length(location)
            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
    @typing.final
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        logger.error(
            "item containment with ARC4 arrays is currently unsupported", location=location
        )
        return dummy_value(pytypes.BoolType, location)

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


import re
import typing
from collections.abc import Sequence

import attrs
import mypy.nodes

from puya import log
from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puyapy.awst_build import arc4_utils, pytypes
from puyapy.awst_build.arc4_utils import pytype_to_arc4_pytype, split_tuple_types
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._utils import dummy_value
from puyapy.awst_build.eb.factories import builder_for_type
from puyapy.awst_build.eb.interface import (
    InstanceBuilder,
    NodeBuilder,
    StaticSizedCollectionBuilder,
)
from puyapy.awst_build.utils import maybe_resolve_literal

logger = log.get_logger(__name__)

_VALID_NAME_PATTERN = re.compile("^[_A-Za-z][A-Za-z0-9_]*$")

def _pytype_to_arc4_pytype(typ: pytypes.PyType, sig: attrs.AttrsInstance) -> pytypes.PyType:
    assert isinstance(sig, ARC4Signature)

    def on_error(bad_type: pytypes.PyType) -> typing.Never:
        raise CodeError(f"invalid return type for an ARC4 method: {bad_type}", sig.source_location)

    return arc4_utils.pytype_to_arc4_pytype(typ, on_error)

def _pytypes_to_arc4_pytypes(
    types: Sequence[pytypes.PyType], sig: attrs.AttrsInstance
) -> Sequence[pytypes.PyType]:
    return tuple(_pytype_to_arc4_pytype(t, sig) for t in types)

@attrs.frozen(kw_only=True)
class ARC4Signature:
    source_location: SourceLocation | None
    method_name: str
    arg_types: Sequence[pytypes.PyType] = attrs.field(
        converter=attrs.Converter(_pytypes_to_arc4_pytypes, takes_self=True)  # type: ignore[misc]
    )
    return_type: pytypes.PyType = attrs.field(
        converter=attrs.Converter(_pytype_to_arc4_pytype, takes_self=True)  # type: ignore[misc]
    )

    @property
    def method_selector(self) -> str:
        args = ",".join(map(arc4_utils.pytype_to_arc4, self.arg_types))
        return_type = arc4_utils.pytype_to_arc4(self.return_type)
        return f"{self.method_name}({args}){return_type}"

    def convert_args(
        self,
        native_args: Sequence[NodeBuilder],
        *,
        expect_itxn_args: bool = False,
    ) -> Sequence[InstanceBuilder]:
        num_args = len(native_args)
        num_sig_args = len(self.arg_types)
        if num_sig_args != num_args:
            logger.error(
                f"expected {num_sig_args} ABI argument{'' if num_sig_args == 1 else 's'},"
                f" got {num_args}",
                location=self.source_location,
            )
        arg_types = (
            list(map(_gtxn_to_itxn, self.arg_types)) if expect_itxn_args else self.arg_types
        )
        arc4_args = [
            _implicit_arc4_conversion(arg, pt)
            for arg, pt in zip(native_args, arg_types, strict=False)
        ]
        return arc4_args

def _gtxn_to_itxn(pytype: pytypes.PyType) -> pytypes.PyType:
    if isinstance(pytype, pytypes.GroupTransactionType):
        return pytypes.InnerTransactionFieldsetTypes[pytype.transaction_type]
    return pytype

def get_arc4_signature(
    method: NodeBuilder, native_args: Sequence[NodeBuilder], loc: SourceLocation
) -> tuple[str, ARC4Signature]:
    method_sig = expect.simple_string_literal(method, default=expect.default_raise)
    method_name, maybe_args, maybe_returns = _split_signature(method_sig, method.source_location)
    if maybe_args is None:
        arg_types = [
            _implicit_arc4_type_conversion(
                expect.instance_builder(na, default=expect.default_raise).pytype, loc
            )
            for na in native_args
        ]
    elif maybe_args:
        arg_types = [arc4_utils.arc4_to_pytype(a, loc) for a in split_tuple_types(maybe_args)]
    else:  # args are specified but empty
        arg_types = []
    return_type = (
        arc4_utils.arc4_to_pytype(maybe_returns, loc) if maybe_returns else pytypes.NoneType
    )
    return method_sig, ARC4Signature(
        method_name=method_name, arg_types=arg_types, return_type=return_type, source_location=loc
    )

def _implicit_arc4_type_conversion(typ: pytypes.PyType, loc: SourceLocation) -> pytypes.PyType:
    match typ:
        case pytypes.StrLiteralType:
            return pytypes.ARC4StringType
        case pytypes.BytesLiteralType:
            return pytypes.ARC4DynamicBytesType
        case pytypes.IntLiteralType:
            return pytypes.ARC4UIntN_Aliases[64]
        # convert an inner txn type to the equivalent group txn type
        case pytypes.InnerTransactionFieldsetType(transaction_type=txn_type):
            return pytypes.GroupTransactionTypes[txn_type]

    def on_error(invalid_pytype: pytypes.PyType) -> typing.Never:
        raise CodeError(
            f"{invalid_pytype} is not an ARC4 type and no implicit ARC4 conversion possible", loc
        )

    return pytype_to_arc4_pytype(typ, on_error)

def _inner_transaction_type_matches(instance: pytypes.PyType, target: pytypes.PyType) -> bool:
    if not isinstance(instance, pytypes.InnerTransactionFieldsetType):
        return False
    if not isinstance(target, pytypes.InnerTransactionFieldsetType):
        return False
    return (
        instance.transaction_type == target.transaction_type
        or instance.transaction_type is None
        or target.transaction_type is None
    )

def _implicit_arc4_conversion(
    operand: NodeBuilder, target_type: pytypes.PyType
) -> InstanceBuilder:
    from puya.awst.wtypes import ARC4Type

    instance = expect.instance_builder(operand, default=expect.default_dummy_value(target_type))
    instance = _maybe_resolve_arc4_literal(instance, target_type)
    if target_type <= instance.pytype:
        return instance
    target_wtype = target_type.wtype
    if isinstance(target_type, pytypes.TransactionRelatedType):
        if _inner_transaction_type_matches(instance.pytype, target_type):
            return instance
        else:
            logger.error(
                f"expected type {target_type}, got type {instance.pytype}",
                location=instance.source_location,
            )
            return dummy_value(target_type, instance.source_location)
    if not isinstance(target_wtype, ARC4Type):
        raise InternalError(
            "implicit_operand_conversion expected target_type to be an ARC-4 type,"
            f" got {target_type}",
            instance.source_location,
        )
    if isinstance(instance.pytype.wtype, ARC4Type):
        logger.error(
            f"expected type {target_type}, got type {instance.pytype}",
            location=instance.source_location,
        )
        return dummy_value(target_type, instance.source_location)
    if not target_wtype.can_encode_type(instance.pytype.checked_wtype(instance.source_location)):
        logger.error(
            f"cannot encode {instance.pytype} to {target_type}", location=instance.source_location
        )
        return dummy_value(target_type, instance.source_location)
    if (
        isinstance(target_type, pytypes.StructType)
        and isinstance(instance.pytype, pytypes.TupleType)
        and len(target_type.types) == len(instance.pytype.items)
    ):
        # Special handling to map tuples (named and unnamed) to arc4 structs
        # instance builder for TupleType should be a StaticSizedCollectionBuilder
        assert isinstance(instance, StaticSizedCollectionBuilder)
        conversion_args = [
            _implicit_arc4_conversion(item, item_target_typ)
            for item, item_target_typ in zip(
                instance.iterate_static(), target_type.types, strict=True
            )
        ]
    else:
        conversion_args = [instance]
    target_type_builder = builder_for_type(target_type, instance.source_location)
    return target_type_builder.call(
        args=conversion_args,
        arg_names=[None] * len(conversion_args),
        arg_kinds=[mypy.nodes.ARG_POS] * len(conversion_args),
        location=instance.source_location,
    )

def _maybe_resolve_arc4_literal(
    operand: InstanceBuilder, target_type: pytypes.PyType
) -> InstanceBuilder:
    """Handles special case of resolving a literal tuple into an arc4 tuple"""
    from puyapy.awst_build.eb.tuple import TupleLiteralBuilder

    if isinstance(operand, TupleLiteralBuilder) and isinstance(target_type, pytypes.ARC4TupleType):
        resolved_items = [
            _maybe_resolve_arc4_literal(item, item_type)
            for item, item_type in zip(operand.iterate_static(), target_type.items, strict=True)
        ]
        return TupleLiteralBuilder(resolved_items, operand.source_location)
    return maybe_resolve_literal(operand, target_type)

def _split_signature(
    signature: str, location: SourceLocation
) -> tuple[str, str | None, str | None]:
    """Splits signature into name, args and returns"""
    level = 0
    last_idx = 0
    name: str = ""
    args: str | None = None
    returns: str | None = None
    for idx, tok in enumerate(signature):
        if tok == "(":
            level += 1
            if level == 1:
                if not name:
                    name = signature[:idx]
                last_idx = idx + 1
        elif tok == ")":
            level -= 1
            if level == 0:
                if args is None:
                    args = signature[last_idx:idx]
                elif returns is None:
                    returns = signature[last_idx - 1 : idx + 1]
                last_idx = idx + 1
    if last_idx < len(signature):
        remaining = signature[last_idx:]
        if remaining:
            if not name:
                name = remaining
            elif args is None:
                raise CodeError(
                    f"invalid signature, args not well defined: {name=}, {remaining=}", location
                )
            elif returns:
                raise CodeError(
                    f"invalid signature, text after returns:"
                    f" {name=}, {args=}, {returns=}, {remaining=}",
                    location,
                )
            else:
                returns = remaining
    if not name or not _VALID_NAME_PATTERN.match(name):
        logger.error(f"invalid signature: {name=}", location=location)
    return name, args, returns


import typing
from collections.abc import Iterable, Mapping, Sequence, Set

import attrs
import mypy.nodes

from puya import log
from puya.avm import OnCompletionAction, TransactionType
from puya.awst import wtypes
from puya.awst.nodes import (
    ARC4CreateOption,
    ARC4Decode,
    ARC4Encode,
    ARC4MethodConfig,
    BytesConstant,
    BytesEncoding,
    CompiledContract,
    CreateInnerTransaction,
    Expression,
    IntegerConstant,
    MethodConstant,
    SubmitInnerTransaction,
    TupleExpression,
    TupleItemExpression,
    UInt64Constant,
)
from puya.awst.txn_fields import TxnField
from puya.errors import CodeError
from puya.parse import SourceLocation, sequential_source_locations_merge
from puya.program_refs import ContractReference
from puya.utils import StableSet
from puyapy.awst_build import constants, pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder
from puyapy.awst_build.eb.arc4._base import ARC4FromLogBuilder
from puyapy.awst_build.eb.arc4._utils import ARC4Signature, get_arc4_signature
from puyapy.awst_build.eb.bytes import BytesExpressionBuilder
from puyapy.awst_build.eb.compiled import (
    APP_ALLOCATION_FIELDS,
    PROGRAM_FIELDS,
    CompiledContractExpressionBuilder,
)
from puyapy.awst_build.eb.contracts import ContractTypeExpressionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    TypeBuilder,
)
from puyapy.awst_build.eb.subroutine import BaseClassSubroutineInvokerExpressionBuilder
from puyapy.awst_build.eb.transaction import InnerTransactionExpressionBuilder
from puyapy.awst_build.eb.transaction.itxn_args import PYTHON_ITXN_ARGUMENTS
from puyapy.awst_build.eb.tuple import TupleExpressionBuilder, TupleLiteralBuilder
from puyapy.awst_build.eb.uint64 import UInt64ExpressionBuilder
from puyapy.models import (
    ARC4ABIMethodData,
    ARC4BareMethodData,
    ARC4MethodData,
    ContractFragmentBase,
    ContractFragmentMethod,
)

logger = log.get_logger(__name__)

_FIELD_TO_ITXN_ARGUMENT = {arg.field: arg for arg in PYTHON_ITXN_ARGUMENTS.values()}
_ABI_CALL_TRANSACTION_FIELDS = [
    TxnField.ApplicationID,
    TxnField.OnCompletion,
    TxnField.ApprovalProgramPages,
    TxnField.ClearStateProgramPages,
    TxnField.GlobalNumUint,
    TxnField.GlobalNumByteSlice,
    TxnField.LocalNumUint,
    TxnField.LocalNumByteSlice,
    TxnField.ExtraProgramPages,
    TxnField.Fee,
    TxnField.Sender,
    TxnField.Note,
    TxnField.RekeyTo,
]
_ARC4_CREATE_TRANSACTION_FIELDS = [
    TxnField.OnCompletion,
    TxnField.Fee,
    TxnField.Sender,
    TxnField.Note,
    TxnField.RekeyTo,
]
_ARC4_UPDATE_TRANSACTION_FIELDS = [
    TxnField.ApplicationID,
    TxnField.Fee,
    TxnField.Sender,
    TxnField.Note,
    TxnField.RekeyTo,
]
_COMPILED_KWARG = "compiled"

class ARC4ClientTypeBuilder(TypeBuilder):
    def __init__(
        self, typ: pytypes.PyType, source_location: SourceLocation, fragment: ContractFragmentBase
    ):
        assert pytypes.ARC4ClientBaseType in typ.bases
        super().__init__(typ, source_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("ARC4Client subclasses cannot be instantiated", location)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        method = self.fragment.resolve_method(name)
        if method is None:
            return super().member_access(name, location)
        return ARC4ClientMethodExpressionBuilder(method, location)

class ARC4ClientMethodExpressionBuilder(FunctionBuilder):
    def __init__(
        self,
        method: ContractFragmentMethod,
        location: SourceLocation,
    ):
        super().__init__(location)
        self.method: typing.Final = method

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise CodeError(
            f"can't invoke client methods directly, use {constants.CLS_ARC4_ABI_CALL}", location
        )

class ABICallGenericTypeBuilder(FunctionBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        return _abi_call(args, arg_names, location, return_type_annotation=pytypes.NoneType)

class ABICallTypeBuilder(FunctionBuilder):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.PseudoGenericFunctionType)
        self._return_type_annotation = typ.return_type
        super().__init__(location)

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

def _get_python_kwargs(fields: Sequence[TxnField]) -> Set[str]:
    return StableSet.from_iter(
        arg for arg, param in PYTHON_ITXN_ARGUMENTS.items() if param.field in fields
    )

class _ARC4CompilationFunctionBuilder(FunctionBuilder):
    allowed_fields: Sequence[TxnField]

    @property
    def allowed_kwargs(self) -> Set[str]:
        return {_COMPILED_KWARG, *_get_python_kwargs(self.allowed_fields)}

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        # if on_completion not allowed, it must be an update
        is_update = TxnField.OnCompletion not in self.allowed_fields
        method_or_type, abi_args, kwargs = _get_method_abi_args_and_kwargs(
            args, arg_names, self.allowed_kwargs
        )
        compiled = None
        if compiled_node := kwargs.pop(_COMPILED_KWARG, None):
            compiled = expect.argument_of_type_else_dummy(
                compiled_node, pytypes.CompiledContractType
            )

        match method_or_type:
            case None:
                raise CodeError("missing required positional argument 'method'", location)
            case BaseClassSubroutineInvokerExpressionBuilder(method=fmethod, cref=contract_ref):
                method_call = _get_arc4_method_call(fmethod, abi_args, location)
            case ContractTypeExpressionBuilder(
                fragment=fragment, pytype=pytypes.TypeType(typ=typ)
            ) if pytypes.ARC4ContractBaseType < typ:
                contract_ref = fragment.id
                method_call = _get_lifecycle_method_call(
                    fragment,
                    abi_args,
                    kind="update" if is_update else "create",
                    location=method_or_type.source_location,
                )
            case other:
                expect.not_this_type(other, default=expect.default_raise)
        if compiled is None:
            compiled = CompiledContractExpressionBuilder(
                CompiledContract(
                    contract=contract_ref,
                    wtype=pytypes.CompiledContractType.wtype,
                    source_location=location,
                )
            )
        else:
            _warn_if_different_contract(
                compiled, contract_ref, related_location=method_or_type.source_location
            )
        field_nodes = {PYTHON_ITXN_ARGUMENTS[kwarg].field: node for kwarg, node in kwargs.items()}
        if is_update:
            _add_on_completion(field_nodes, OnCompletionAction.UpdateApplication, location)
        # if on_completion is not set but can be inferred from config then use that
        elif (
            TxnField.OnCompletion not in field_nodes
            and (on_completion := _get_singular_on_complete(method_call.config)) is not None
        ):
            _add_on_completion(field_nodes, on_completion, location)

        compiled = compiled.single_eval()
        for member_name, field in PROGRAM_FIELDS.items():
            field_nodes[field] = compiled.member_access(member_name, location)
        # is creating
        if not is_update:
            # add all app allocation fields
            for member_name, field in APP_ALLOCATION_FIELDS.items():
                field_nodes[field] = compiled.member_access(member_name, location)
        _validate_transaction_kwargs(
            field_nodes,
            method_call.config,
            method_location=method_or_type.source_location,
            call_location=location,
        )
        return _create_abi_call_expr(
            abi_args=method_call.arc4_args,
            arc4_return_type=method_call.arc4_return_type,
            declared_result_type=method_call.method_return_type,
            field_nodes=field_nodes,
            location=location,
        )

def _warn_if_different_contract(
    compiled: InstanceBuilder, contract: ContractReference, *, related_location: SourceLocation
) -> None:
    # naive check for mismatch between method and compiled parameters
    expr = compiled.resolve()
    if isinstance(expr, CompiledContract) and expr.contract != contract:
        logger.warning(
            "compiled parameter is for a different contract",
            location=compiled.source_location,
        )
        logger.info("other contract reference", location=related_location)

class ARC4CreateFunctionBuilder(_ARC4CompilationFunctionBuilder):
    allowed_fields: Sequence[TxnField] = _ARC4_CREATE_TRANSACTION_FIELDS

class ARC4UpdateFunctionBuilder(_ARC4CompilationFunctionBuilder):
    allowed_fields: Sequence[TxnField] = _ARC4_UPDATE_TRANSACTION_FIELDS

def _abi_call(
    args: Sequence[NodeBuilder],
    arg_names: list[str | None],
    location: SourceLocation,
    *,
    return_type_annotation: pytypes.PyType,
) -> InstanceBuilder:
    method, abi_args, kwargs = _get_method_abi_args_and_kwargs(
        args, arg_names, _get_python_kwargs(_ABI_CALL_TRANSACTION_FIELDS)
    )
    declared_result_type: pytypes.PyType
    arc4_config = None
    match method:
        case None:
            raise CodeError("missing required positional argument 'method'", location)
        case (
            ARC4ClientMethodExpressionBuilder(method=fmethod)
            | BaseClassSubroutineInvokerExpressionBuilder(method=fmethod)
        ):
            # in this case the arc4 signature and declared return type are inferred
            method_call = _get_arc4_method_call(fmethod, abi_args, location)
            arc4_args = method_call.arc4_args
            arc4_return_type = method_call.arc4_return_type
            arc4_config = method_call.config
            declared_result_type = method_call.method_return_type
            if return_type_annotation not in (declared_result_type, pytypes.NoneType):
                logger.error(
                    "mismatch between return type of method and generic parameter",
                    location=location,
                )
        case _:
            method_str, signature = get_arc4_signature(method, abi_args, location)
            declared_result_type = return_type_annotation
            if declared_result_type != pytypes.NoneType:
                # this will be validated against signature below, by comparing
                # the generated method_selector against the supplied method_str
                signature = attrs.evolve(signature, return_type=declared_result_type)
            if not signature.method_selector.startswith(method_str):
                logger.error(
                    f"method selector from args '{signature.method_selector}' "
                    f"does not match provided method selector: '{method_str}'",
                    location=method.source_location,
                )
            arc4_args = _method_selector_and_arc4_args(signature, abi_args, location)
            arc4_return_type = signature.return_type

    field_nodes = {PYTHON_ITXN_ARGUMENTS[kwarg].field: node for kwarg, node in kwargs.items()}
    # set on_completion if it can be inferred from config
    if (
        TxnField.OnCompletion not in field_nodes
        and (on_completion := _get_singular_on_complete(arc4_config)) is not None
    ):
        _add_on_completion(field_nodes, on_completion, location)

    _validate_transaction_kwargs(
        field_nodes,
        arc4_config,
        method_location=method.source_location,
        call_location=location,
    )
    return _create_abi_call_expr(
        arc4_return_type=arc4_return_type,
        abi_args=arc4_args,
        declared_result_type=declared_result_type,
        field_nodes=field_nodes,
        location=location,
    )

def _get_method_abi_args_and_kwargs(
    args: Sequence[NodeBuilder], arg_names: list[str | None], allowed_kwargs: Set[str]
) -> tuple[NodeBuilder | None, Sequence[NodeBuilder], dict[str, NodeBuilder]]:
    method: NodeBuilder | None = None
    abi_args = list[NodeBuilder]()
    kwargs = dict[str, NodeBuilder]()
    for idx, (arg_name, arg) in enumerate(zip(arg_names, args, strict=True)):
        if arg_name is None:
            if idx == 0:
                method = arg
            else:
                abi_args.append(arg)
        elif arg_name in allowed_kwargs:
            kwargs[arg_name] = arg
        else:
            logger.error("unrecognised keyword argument", location=arg.source_location)
    return method, abi_args, kwargs

@attrs.frozen
class _ARC4MethodCall:
    config: ARC4MethodConfig | None
    arc4_args: Sequence[InstanceBuilder]
    method_return_type: pytypes.PyType
    """
    Return type as declared on the method, this may not be an ARC4 type due to automatic
    type conversion
    """
    arc4_return_type: pytypes.PyType
    """
    ARC4 return type
    """

def _get_arc4_method_call(
    data: ContractFragmentMethod, abi_args: Sequence[NodeBuilder], location: SourceLocation
) -> _ARC4MethodCall:
    if data.metadata is None:
        raise CodeError("not a valid ARC4 method", location)
    return _map_arc4_method_data_to_call(data.metadata, abi_args, location)

def _map_arc4_method_data_to_call(
    data: ARC4MethodData,
    abi_args: Sequence[NodeBuilder],
    location: SourceLocation,
) -> _ARC4MethodCall:
    match data:
        case ARC4ABIMethodData() as abi_method_data:
            signature = ARC4Signature(
                method_name=abi_method_data.config.name,
                arg_types=abi_method_data.arc4_argument_types,
                return_type=abi_method_data.arc4_return_type,
                source_location=location,
            )
            return _ARC4MethodCall(
                config=abi_method_data.config,
                arc4_args=_method_selector_and_arc4_args(signature, abi_args, location),
                method_return_type=abi_method_data.return_type,
                arc4_return_type=abi_method_data.arc4_return_type,
            )
        case ARC4BareMethodData() as bare_method_data:
            _expect_bare_method_args(abi_args)
            return _ARC4MethodCall(
                config=bare_method_data.config,
                arc4_args=[],
                method_return_type=pytypes.NoneType,
                arc4_return_type=pytypes.NoneType,
            )
        case other:
            typing.assert_never(other)

def _get_lifecycle_method_call(
    fragment: ContractFragmentBase,
    abi_args: Sequence[NodeBuilder],
    kind: typing.Literal["create", "update"],
    location: SourceLocation,
) -> _ARC4MethodCall:
    if kind == "create":
        possible_methods = list(fragment.find_arc4_method_metadata(can_create=True))
    elif kind == "update":
        possible_methods = list(
            fragment.find_arc4_method_metadata(oca=OnCompletionAction.UpdateApplication)
        )

    try:
        single_method, *others = possible_methods
    except ValueError:
        raise CodeError(f"could not find {kind} method on {fragment.id}", location) from None
    if others:
        raise CodeError(
            f"found multiple {kind} methods on {fragment.id}, please specify which one to use",
            location,
        )
    method_call = _map_arc4_method_data_to_call(single_method, abi_args, location)
    # remove method_return_type from result
    # so _create_abi_call_expr does not attempt to include any decoded ARC4 result
    # as per the stubs overload for arc4_create/arc4_update with a Contract type
    return attrs.evolve(method_call, method_return_type=pytypes.NoneType)

def _method_selector_and_arc4_args(
    signature: ARC4Signature, abi_args: Sequence[NodeBuilder], location: SourceLocation
) -> Sequence[InstanceBuilder]:
    return [
        BytesExpressionBuilder(
            MethodConstant(value=signature.method_selector, source_location=location)
        ),
        *signature.convert_args(abi_args, expect_itxn_args=True),
    ]

def _create_abi_call_expr(
    *,
    abi_args: Sequence[InstanceBuilder],
    arc4_return_type: pytypes.PyType,
    declared_result_type: pytypes.PyType,
    field_nodes: dict[TxnField, NodeBuilder],
    location: SourceLocation,
) -> InstanceBuilder:
    group = []
    array_fields: dict[TxnField, list[Expression]] = {
        TxnField.ApplicationArgs: [],
        TxnField.Accounts: [],
        TxnField.Applications: [],
        TxnField.Assets: [],
    }

    def ref_to_arg(ref_field: TxnField, arg: InstanceBuilder) -> Expression:
        # TODO: what about references that are used more than once?
        implicit_offset = 1 if ref_field in (TxnField.Accounts, TxnField.Applications) else 0
        ref_list = array_fields[ref_field]
        ref_index = len(ref_list)
        ref_list.append(arg.resolve())
        return BytesConstant(
            value=(ref_index + implicit_offset).to_bytes(length=1),
            encoding=BytesEncoding.base16,
            source_location=arg.source_location,
        )

    for arg_b in abi_args:
        arg_expr = None
        match arg_b.pytype:
            case pytypes.InnerTransactionFieldsetType():
                group.append(arg_b.resolve())
                # no arg_expr as txn aren't part of the app args
            case pytypes.TransactionRelatedType():
                logger.error(
                    "only inner transaction types can be used to call another contract",
                    location=arg_b.source_location,
                )
            case pytypes.AssetType:
                arg_expr = ref_to_arg(TxnField.Assets, arg_b)
            case pytypes.AccountType:
                arg_expr = ref_to_arg(TxnField.Accounts, arg_b)
            case pytypes.ApplicationType:
                arg_expr = ref_to_arg(TxnField.Applications, arg_b)
            case _:
                arg_expr = arg_b.resolve()
        if arg_expr is not None:
            array_fields[TxnField.ApplicationArgs].append(arg_expr)

    txn_type_appl = TransactionType.appl
    fields: dict[TxnField, Expression] = {
        TxnField.Fee: UInt64Constant(value=0, source_location=location),
        TxnField.TypeEnum: UInt64Constant(
            value=txn_type_appl.value, teal_alias=txn_type_appl.name, source_location=location
        ),
    }
    for arr_field, arr_field_values in array_fields.items():
        if arr_field_values:
            if arr_field == TxnField.ApplicationArgs and len(arr_field_values) > 16:
                args_to_pack = arr_field_values[15:]
                arr_field_values[15:] = [
                    _arc4_tuple_from_items(args_to_pack, _combine_locs(args_to_pack))
                ]
            fields[arr_field] = TupleExpression.from_items(
                arr_field_values, _combine_locs(arr_field_values)
            )

    for field, field_node in field_nodes.items():
        params = _FIELD_TO_ITXN_ARGUMENT[field]
        if params is None:
            logger.error("unrecognised keyword argument", location=field_node.source_location)
        else:
            fields[field] = params.validate_and_convert(field_node).resolve()

    itxn_result_pytype = pytypes.InnerTransactionResultTypes[txn_type_appl]
    create_itxn = CreateInnerTransaction(
        fields=fields,
        wtype=pytypes.InnerTransactionFieldsetTypes[txn_type_appl].wtype,
        source_location=location,
    )
    group.append(create_itxn)
    if len(group) == 1:
        itxn_builder: InstanceBuilder = InnerTransactionExpressionBuilder(
            SubmitInnerTransaction(itxns=group, source_location=location), itxn_result_pytype
        )
    else:
        itxn_types = []
        for itxn in group:
            assert isinstance(itxn.wtype, wtypes.WInnerTransactionFields)
            itxn_types.append(pytypes.InnerTransactionResultTypes[itxn.wtype.transaction_type])
        itxn_tuple_result_pytype = pytypes.GenericTupleType.parameterise(
            itxn_types,
            location,
        )
        itxn_tuple_builder = TupleExpressionBuilder(
            SubmitInnerTransaction(itxns=group, source_location=location),
            itxn_tuple_result_pytype,
        ).single_eval()
        itxn_builder = InnerTransactionExpressionBuilder(
            TupleItemExpression(
                base=itxn_tuple_builder.resolve(),
                index=-1,
                source_location=location,
            ),
            itxn_result_pytype,
        )

    if declared_result_type == pytypes.NoneType:
        return itxn_builder
    itxn_builder = itxn_builder.single_eval()
    assert isinstance(itxn_builder, InnerTransactionExpressionBuilder)
    last_log = itxn_builder.get_field_value(TxnField.LastLog, pytypes.BytesType, location)
    abi_result = ARC4FromLogBuilder.abi_expr_from_log(arc4_return_type, last_log, location)
    # the declared result type may be different to the arc4 signature return type
    # due to automatic conversion of ARC4 -> native types
    if declared_result_type != arc4_return_type:
        abi_result = ARC4Decode(
            value=abi_result,
            wtype=declared_result_type.checked_wtype(location),
            source_location=location,
        )

    abi_result_builder = builder_for_instance(declared_result_type, abi_result)
    return TupleLiteralBuilder((abi_result_builder, itxn_builder), location)

def _combine_locs(exprs: Sequence[Expression | NodeBuilder]) -> SourceLocation:
    return sequential_source_locations_merge(a.source_location for a in exprs)

def _arc4_tuple_from_items(
    items: Sequence[Expression], source_location: SourceLocation
) -> ARC4Encode:
    # TODO: should we just allow TupleExpression to have an ARCTuple wtype?
    args_tuple = TupleExpression.from_items(items, source_location)
    return ARC4Encode(
        value=args_tuple,
        wtype=wtypes.ARC4Tuple(types=args_tuple.wtype.types, source_location=source_location),
        source_location=source_location,
    )

def _expect_bare_method_args(abi_args: Sequence[NodeBuilder]) -> None:
    if abi_args:
        logger.error("unexpected args for bare method", location=_combine_locs(abi_args))

def _validate_transaction_kwargs(
    field_nodes: Mapping[TxnField, NodeBuilder],
    arc4_config: ARC4MethodConfig | None,
    *,
    method_location: SourceLocation,
    call_location: SourceLocation,
) -> None:
    # note these values may be None which indicates their value is unknown at compile time
    on_completion = _get_on_completion(field_nodes)
    is_update = on_completion == OnCompletionAction.UpdateApplication
    is_create = _is_creating(field_nodes)

    if is_create:
        # app_id not provided but method doesn't support creating
        if arc4_config and arc4_config.create == ARC4CreateOption.disallow:
            logger.error("method cannot be used to create application", location=method_location)
        # required args for creation missing
        else:
            _check_program_fields_are_present(
                "missing required arguments to create app", field_nodes, call_location
            )
    if is_update:
        if arc4_config and (
            OnCompletionAction.UpdateApplication not in arc4_config.allowed_completion_types
        ):
            logger.error("method cannot be used to update application", location=method_location)
        else:
            _check_program_fields_are_present(
                "missing required arguments to update app", field_nodes, call_location
            )
    # on_completion not valid for arc4_config
    elif (
        on_completion is not None
        and arc4_config
        and on_completion not in arc4_config.allowed_completion_types
    ):
        arg = field_nodes[TxnField.OnCompletion]
        logger.error(
            "on completion action is not supported by ARC4 method being called",
            location=arg.source_location,
        )
        logger.info("method ARC4 configuration", location=arc4_config.source_location)

    # programs provided when known not to be creating or updating
    if is_create is False and is_update is False:
        _check_fields_not_present(
            "provided argument is only valid when creating or updating an application",
            PROGRAM_FIELDS.values(),
            field_nodes,
        )
    if is_create is False:
        _check_fields_not_present(
            "provided argument is only valid when creating an application",
            APP_ALLOCATION_FIELDS.values(),
            field_nodes,
        )

def _check_program_fields_are_present(
    error_message: str, field_nodes: Mapping[TxnField, NodeBuilder], location: SourceLocation
) -> None:
    if missing_fields := [field for field in PROGRAM_FIELDS.values() if field not in field_nodes]:
        logger.error(
            f"{error_message}: {', '.join(_get_python_kwargs(missing_fields))}",
            location=location,
        )

def _check_fields_not_present(
    error_message: str,
    fields_to_omit: Iterable[TxnField],
    field_nodes: Mapping[TxnField, NodeBuilder],
) -> None:
    for field in fields_to_omit:
        if node := field_nodes.get(field):
            logger.error(error_message, location=node.source_location)

def _get_singular_on_complete(config: ARC4MethodConfig | None) -> OnCompletionAction | None:
    if config:
        try:
            (on_complete,) = config.allowed_completion_types
        except ValueError:
            pass
        else:
            return on_complete
    return None

def _get_on_completion(field_nodes: Mapping[TxnField, NodeBuilder]) -> OnCompletionAction | None:
    """
    Returns OnCompletionAction if it is statically known, otherwise returns None
    """
    match field_nodes.get(TxnField.OnCompletion):
        case None:
            return OnCompletionAction.NoOp
        case InstanceBuilder(pytype=pytypes.OnCompleteActionType) as eb:
            value = eb.resolve()
            if isinstance(value, IntegerConstant):
                return OnCompletionAction(value.value)
    return None

def _is_creating(field_nodes: Mapping[TxnField, NodeBuilder]) -> bool | None:
    """
    Returns app_id == 0 if app_id is statically known, otherwise returns None
    """
    match field_nodes.get(TxnField.ApplicationID):
        case None:
            return True
        case LiteralBuilder(value=int(app_id)):
            return app_id == 0
    return None

def _add_on_completion(
    field_nodes: dict[TxnField, NodeBuilder],
    on_complete: OnCompletionAction,
    location: SourceLocation,
) -> None:
    # if NoOp can just omit
    if on_complete == OnCompletionAction.NoOp:
        return
    field_nodes[TxnField.OnCompletion] = UInt64ExpressionBuilder(
        UInt64Constant(
            source_location=location, value=on_complete.value, teal_alias=on_complete.name
        ),
        enum_type=pytypes.OnCompleteActionType,
    )


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log, utils
from puya.algo_constants import ENCODED_ADDRESS_LENGTH
from puya.awst import wtypes
from puya.awst.nodes import (
    AddressConstant,
    CheckedMaybe,
    Expression,
    NumericComparison,
    NumericComparisonExpression,
    ReinterpretCast,
    UInt64Constant,
)
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._bytes_backed import BytesBackedTypeBuilder
from puyapy.awst_build.eb._utils import compare_expr_bytes
from puyapy.awst_build.eb.arc4.static_array import StaticArrayExpressionBuilder
from puyapy.awst_build.eb.interface import (
    BuilderComparisonOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
)
from puyapy.awst_build.eb.reference_types.account import AccountExpressionBuilder

logger = log.get_logger(__name__)

class AddressTypeBuilder(BytesBackedTypeBuilder[pytypes.ArrayType]):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.ARC4AddressType, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case str(str_value):
                if not utils.valid_address(str_value):
                    logger.error(
                        f"Invalid address value. Address literals should be"
                        f" {ENCODED_ADDRESS_LENGTH} characters and not include base32 padding",
                        location=literal.source_location,
                    )
                expr = AddressConstant(
                    value=str_value,
                    wtype=wtypes.arc4_address_alias,
                    source_location=location,
                )
                return AddressExpressionBuilder(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)
        if arg is None:
            result = _zero_address(location)
        elif arg.pytype == pytypes.StrLiteralType:
            return arg.resolve_literal(converter=AddressTypeBuilder(location))
        elif arg.pytype == pytypes.AccountType:  # Account is a final type
            result = _address_from_native(arg)
        else:
            arg = expect.argument_of_type_else_dummy(arg, pytypes.BytesType)
            arg = arg.single_eval()
            is_correct_length = NumericComparisonExpression(
                operator=NumericComparison.eq,
                source_location=location,
                lhs=UInt64Constant(value=32, source_location=location),
                rhs=intrinsic_factory.bytes_len(arg.resolve(), location),
            )
            result = CheckedMaybe.from_tuple_items(
                expr=_address_from_native(arg),
                check=is_correct_length,
                source_location=location,
                comment="Address length is 32 bytes",
            )
        return AddressExpressionBuilder(result)

class AddressExpressionBuilder(StaticArrayExpressionBuilder):
    def __init__(self, expr: Expression):
        super().__init__(expr, pytypes.ARC4AddressType)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return compare_expr_bytes(
            lhs=self.resolve(),
            op=BuilderComparisonOp.eq if negate else BuilderComparisonOp.ne,
            rhs=_zero_address(location),
            source_location=location,
        )

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        if other.pytype == pytypes.StrLiteralType:
            rhs = other.resolve_literal(AddressTypeBuilder(other.source_location)).resolve()
        elif other.pytype == pytypes.AccountType:  # Account is a final type
            rhs = _address_from_native(other)
        elif pytypes.ARC4AddressType <= other.pytype:
            rhs = other.resolve()
        else:
            return NotImplemented
        return compare_expr_bytes(lhs=self.resolve(), op=op, rhs=rhs, source_location=location)

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

def _zero_address(location: SourceLocation) -> Expression:
    return intrinsic_factory.zero_address(location, as_type=wtypes.arc4_address_alias)

def _address_to_native(builder: InstanceBuilder) -> Expression:
    assert pytypes.ARC4AddressType <= builder.pytype
    return ReinterpretCast(
        expr=builder.resolve(),
        wtype=wtypes.account_wtype,
        source_location=builder.source_location,
    )

def _address_from_native(builder: InstanceBuilder) -> Expression:
    assert builder.pytype.is_type_or_subtype(pytypes.AccountType, pytypes.BytesType)
    return ReinterpretCast(
        expr=builder.resolve(),
        wtype=wtypes.arc4_address_alias,
        source_location=builder.source_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, BoolConstant, 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.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.interface import (
    BuilderComparisonOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
)

logger = log.get_logger(__name__)

class ARC4BoolTypeBuilder(ARC4TypeBuilder):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.ARC4BoolType, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case bool(bool_literal):
                return ARC4BoolExpressionBuilder(
                    BoolConstant(
                        value=bool_literal, source_location=location, wtype=wtypes.arc4_bool_wtype
                    ),
                )
        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:
                expr: Expression = BoolConstant(
                    value=False, source_location=location, wtype=wtypes.arc4_bool_wtype
                )
            case _:
                arg = expect.argument_of_type_else_dummy(arg, pytypes.BoolType)
                native_bool = arg.resolve()
                expr = ARC4Encode(
                    value=native_bool, wtype=wtypes.arc4_bool_wtype, source_location=location
                )
        return ARC4BoolExpressionBuilder(expr)

class ARC4BoolExpressionBuilder(
    NotIterableInstanceExpressionBuilder, BytesBackedInstanceExpressionBuilder
):
    def __init__(self, expr: Expression):
        super().__init__(pytypes.ARC4BoolType, expr)

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

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

    def _native(self, location: SourceLocation) -> BoolExpressionBuilder:
        result_expr: Expression = ARC4Decode(
            value=self.resolve(),
            wtype=pytypes.BoolType.wtype,
            source_location=location,
        )
        return BoolExpressionBuilder(result_expr)

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        if other.pytype == pytypes.BoolType:
            lhs = self._native(self.source_location)
            return lhs.compare(other, op, location)
        return compare_bytes(self=self, op=op, other=other, source_location=location)


import abc
import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst import wtypes
from puya.awst.nodes import (
    ArrayConcat,
    ArrayExtend,
    ArrayPop,
    Expression,
    ExpressionStatement,
    IntrinsicCall,
    NewArray,
    Statement,
    TupleExpression,
    UInt64Constant,
)
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 FunctionBuilder, GenericTypeBuilder
from puyapy.awst_build.eb._bytes_backed import BytesBackedTypeBuilder
from puyapy.awst_build.eb._utils import (
    dummy_statement,
    dummy_value,
)
from puyapy.awst_build.eb.arc4._base import _ARC4ArrayExpressionBuilder, arc4_bool_bytes
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import BuilderBinaryOp, InstanceBuilder, NodeBuilder
from puyapy.awst_build.eb.none import NoneExpressionBuilder
from puyapy.awst_build.eb.uint64 import UInt64ExpressionBuilder

__all__ = [
    "DynamicArrayGenericTypeBuilder",
    "DynamicArrayTypeBuilder",
    "DynamicArrayExpressionBuilder",
]

logger = log.get_logger(__name__)

class DynamicArrayGenericTypeBuilder(GenericTypeBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        if not args:
            raise CodeError("empty arrays require a type annotation to be instantiated", location)
        element_type = expect.instance_builder(args[0], default=expect.default_raise).pytype
        typ = pytypes.GenericARC4DynamicArrayType.parameterise([element_type], location)
        values = tuple(expect.argument_of_type_else_dummy(a, element_type).resolve() for a in args)
        wtype = typ.wtype
        assert isinstance(wtype, wtypes.ARC4DynamicArray)
        return DynamicArrayExpressionBuilder(
            NewArray(values=values, wtype=wtype, source_location=location), typ
        )

class DynamicArrayTypeBuilder(BytesBackedTypeBuilder[pytypes.ArrayType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.ArrayType)
        assert typ.generic == pytypes.GenericARC4DynamicArrayType
        assert typ.size is None
        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()
        values = tuple(expect.argument_of_type_else_dummy(a, typ.items).resolve() for a in args)
        wtype = typ.wtype
        assert isinstance(wtype, wtypes.ARC4DynamicArray)
        return DynamicArrayExpressionBuilder(
            NewArray(values=values, wtype=wtype, source_location=location), self._pytype
        )

class DynamicArrayExpressionBuilder(_ARC4ArrayExpressionBuilder):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.ArrayType)
        super().__init__(typ, expr)

    @typing.override
    def length(self, location: SourceLocation) -> InstanceBuilder:
        length = IntrinsicCall(
            op_code="extract_uint16",
            stack_args=[self.resolve(), UInt64Constant(value=0, source_location=location)],
            wtype=wtypes.uint64_wtype,
            source_location=location,
        )
        return UInt64ExpressionBuilder(length)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "append":
                return _Append(self.resolve(), self.pytype, location)
            case "extend":
                return _Extend(self.resolve(), self.pytype, location)
            case "pop":
                return _Pop(self.resolve(), self.pytype, location)
            case _:
                return super().member_access(name, location)

    @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 = _match_array_concat_arg(rhs, self.pytype)
        extend = ArrayExtend(
            base=self.resolve(),
            other=rhs.resolve(),
            wtype=wtypes.void_wtype,
            source_location=location,
        )
        return ExpressionStatement(expr=extend)

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        # only __add__ is implemented, not __radd__
        if op != BuilderBinaryOp.add or reverse:
            return NotImplemented

        other = _match_array_concat_arg(other, self.pytype)
        return DynamicArrayExpressionBuilder(
            ArrayConcat(
                left=self.resolve(),
                right=other.resolve(),
                wtype=self.pytype.wtype,
                source_location=location,
            ),
            self.pytype,
        )

    @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,
        )

class _ArrayFunc(FunctionBuilder, abc.ABC):
    def __init__(self, expr: Expression, typ: pytypes.ArrayType, location: SourceLocation):
        super().__init__(location)
        self.expr = expr
        self.typ = typ

class _Append(_ArrayFunc):
    @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_of_type_else_dummy(args, self.typ.items, location)
        args_expr = arg.resolve()
        args_tuple = TupleExpression.from_items([args_expr], arg.source_location)
        return NoneExpressionBuilder(
            ArrayExtend(
                base=self.expr, other=args_tuple, wtype=wtypes.void_wtype, source_location=location
            )
        )

class _Pop(_ArrayFunc):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        expect.no_args(args, location)
        result_expr = ArrayPop(
            base=self.expr, wtype=self.typ.items_wtype, source_location=location
        )
        return builder_for_instance(self.typ.items, result_expr)

class _Extend(_ArrayFunc):
    @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 None:
            other = dummy_value(self.typ, location)
        else:
            other = _match_array_concat_arg(arg, self.typ)
        return NoneExpressionBuilder(
            ArrayExtend(
                base=self.expr,
                other=other.resolve(),
                wtype=wtypes.void_wtype,
                source_location=location,
            )
        )

def _match_array_concat_arg(arg: InstanceBuilder, arr_type: pytypes.ArrayType) -> InstanceBuilder:
    expected_item_type = arr_type.items
    match arg.pytype:
        case pytypes.SequenceType(items=array_items):
            okay = expected_item_type <= array_items
        case pytypes.TupleLikeType(items=tuple_items):
            okay = all(expected_item_type <= ti for ti in tuple_items)
        case _:
            okay = False
    if okay:
        return arg
    logger.error(
        "expected an array or tuple of the same element type", location=arg.source_location
    )
    return dummy_value(arr_type, arg.source_location)


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,
    BytesConstant,
    BytesEncoding,
    Expression,
    NewArray,
)
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._bytes_backed import BytesBackedTypeBuilder
from puyapy.awst_build.eb._utils import dummy_value
from puyapy.awst_build.eb.arc4.dynamic_array import DynamicArrayExpressionBuilder
from puyapy.awst_build.eb.arc4.uint import UIntNTypeBuilder
from puyapy.awst_build.eb.bytes import BytesExpressionBuilder
from puyapy.awst_build.eb.interface import InstanceBuilder, LiteralBuilder, NodeBuilder

logger = log.get_logger(__name__)

class DynamicBytesTypeBuilder(BytesBackedTypeBuilder[pytypes.ArrayType]):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.ARC4DynamicBytesType, location)

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

                bytes_expr = BytesConstant(
                    value=bytes_literal, encoding=BytesEncoding.unknown, source_location=location
                )
                return self._from_bytes_expr(bytes_expr, 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:
        match args:
            case [InstanceBuilder(pytype=pytypes.BytesLiteralType) as lit]:
                return lit.resolve_literal(DynamicBytesTypeBuilder(location))
            case []:
                bytes_expr: Expression = BytesConstant(
                    value=b"", encoding=BytesEncoding.unknown, source_location=location
                )
            case [
                InstanceBuilder(pytype=single_arg_pytype) as eb
            ] if pytypes.BytesType <= single_arg_pytype:
                bytes_expr = eb.resolve()
            case _:
                non_literal_args = tuple(_coerce_to_byte(a).resolve() for a in args)
                return DynamicBytesExpressionBuilder(
                    NewArray(
                        values=non_literal_args, wtype=self._arc4_type, source_location=location
                    )
                )
        return self._from_bytes_expr(bytes_expr, location)

    @property
    def _arc4_type(self) -> wtypes.ARC4DynamicArray:
        typ = self.produces()
        wtype = typ.wtype
        assert isinstance(wtype, wtypes.ARC4DynamicArray)
        return wtype

    def _from_bytes_expr(self, expr: Expression, location: SourceLocation) -> InstanceBuilder:
        encode_expr = ARC4Encode(value=expr, wtype=self._arc4_type, source_location=location)
        return DynamicBytesExpressionBuilder(encode_expr)

def _coerce_to_byte(builder: NodeBuilder) -> InstanceBuilder:
    arg = expect.instance_builder(
        builder, default=expect.default_dummy_value(pytypes.ARC4ByteType)
    )
    arg = arg.resolve_literal(UIntNTypeBuilder(pytypes.ARC4ByteType, arg.source_location))
    match arg:
        # can't use expect.argument_of_type here, we need a match statement
        case InstanceBuilder(pytype=pytypes.ARC4UIntNType(bits=8)):
            return arg
        case _:
            logger.error("invalid argument type", location=arg.source_location)
            return dummy_value(pytypes.ARC4ByteType, arg.source_location)

class DynamicBytesExpressionBuilder(DynamicArrayExpressionBuilder):
    def __init__(self, expr: Expression):
        super().__init__(expr, pytypes.ARC4DynamicBytesType)

    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "native":
                return BytesExpressionBuilder(
                    ARC4Decode(
                        value=self.resolve(), wtype=wtypes.bytes_wtype, source_location=location
                    )
                )
            case _:
                return super().member_access(name, location)


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst.nodes import Emit
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.arc4_utils import pytype_to_arc4
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder
from puyapy.awst_build.eb.arc4._utils import get_arc4_signature
from puyapy.awst_build.eb.arc4.struct import ARC4StructTypeBuilder
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder
from puyapy.awst_build.eb.none import NoneExpressionBuilder

logger = log.get_logger(__name__)

class EmitBuilder(FunctionBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        first, rest = expect.at_least_one_arg(args, location, default=expect.default_raise)
        match first:
            case (
                InstanceBuilder(pytype=pytypes.StructType() as struct_type) as event_arg_eb
            ) if pytypes.ARC4StructBaseType < struct_type:
                if rest:
                    logger.error(
                        "unexpected additional arguments", location=rest[0].source_location
                    )
            case _:
                method_sig, signature = get_arc4_signature(first, rest, location)
                if signature.return_type != pytypes.NoneType or method_sig.endswith(")void"):
                    logger.error(
                        "event signatures cannot include return types",
                        location=first.source_location,
                    )
                arc4_args = signature.convert_args(rest)
                # emit requires a struct type, so generate one based on args
                struct_type = pytypes.StructType(
                    base=pytypes.ARC4StructBaseType,
                    name=signature.method_name,
                    desc=None,
                    fields={
                        f"field{idx}": arg.pytype for idx, arg in enumerate(arc4_args, start=1)
                    },
                    frozen=True,
                    source_location=location,
                )
                event_arg_eb = ARC4StructTypeBuilder(struct_type, location).call(
                    args=arc4_args,
                    arg_names=[None] * len(arc4_args),
                    arg_kinds=[mypy.nodes.ARG_POS] * len(arc4_args),
                    location=location,
                )
        event_name = struct_type.name.split(".")[-1]
        event_sig = f"{event_name}{pytype_to_arc4(event_arg_eb.pytype, location)}"
        emit = Emit(
            signature=event_sig,
            value=event_arg_eb.resolve(),
            source_location=location,
        )
        return NoneExpressionBuilder(emit)


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst import wtypes
from puya.awst.nodes import Expression, IndexExpression, NewArray, UInt64Constant
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 BytesBackedTypeBuilder
from puyapy.awst_build.eb._utils import constant_bool_and_error
from puyapy.awst_build.eb.arc4._base import _ARC4ArrayExpressionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    InstanceBuilder,
    NodeBuilder,
    StaticSizedCollectionBuilder,
)
from puyapy.awst_build.eb.uint64 import UInt64ExpressionBuilder

__all__ = [
    "StaticArrayGenericTypeBuilder",
    "StaticArrayTypeBuilder",
    "StaticArrayExpressionBuilder",
]

logger = log.get_logger(__name__)

class StaticArrayGenericTypeBuilder(GenericTypeBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        if not args:
            raise CodeError("empty arrays require a type annotation to be instantiated", location)
        element_type = expect.instance_builder(args[0], default=expect.default_raise).pytype
        array_size = len(args)
        typ = pytypes.GenericARC4StaticArrayType.parameterise(
            [element_type, pytypes.TypingLiteralType(value=array_size, source_location=None)],
            location,
        )
        values = tuple(expect.argument_of_type_else_dummy(a, element_type).resolve() for a in args)
        wtype = typ.wtype
        assert isinstance(wtype, wtypes.ARC4StaticArray)
        return StaticArrayExpressionBuilder(
            NewArray(values=values, wtype=wtype, source_location=location), typ
        )

class StaticArrayTypeBuilder(BytesBackedTypeBuilder[pytypes.ArrayType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.ArrayType)
        assert typ.generic == pytypes.GenericARC4StaticArrayType
        assert typ.size is not None
        self._size = typ.size
        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()
        n_args = expect.exactly_n_args_of_type_else_dummy(args, typ.items, location, self._size)
        wtype = typ.wtype
        assert isinstance(wtype, wtypes.ARC4StaticArray)
        return StaticArrayExpressionBuilder(
            NewArray(
                values=tuple(arg.resolve() for arg in n_args),
                wtype=wtype,
                source_location=location,
            ),
            typ,
        )

class StaticArrayExpressionBuilder(_ARC4ArrayExpressionBuilder, StaticSizedCollectionBuilder):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.ArrayType)
        size = typ.size
        assert size is not None
        self._size = size
        super().__init__(typ, expr)

    @typing.override
    def length(self, location: SourceLocation) -> InstanceBuilder:
        return UInt64ExpressionBuilder(UInt64Constant(value=self._size, source_location=location))

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

    @typing.override
    def iterate_static(self) -> Sequence[InstanceBuilder]:
        base = self.single_eval().resolve()
        return [
            builder_for_instance(
                self.pytype.items,
                IndexExpression(
                    base=base,
                    index=UInt64Constant(value=idx, source_location=self.source_location),
                    wtype=self.pytype.items_wtype,
                    source_location=self.source_location,
                ),
            )
            for idx in range(self._size)
        ]