import attrs

from puya.teal import models
from puya.teal._util import preserve_stack_manipulations
from puya.teal.optimize._data import (
    COMMUTATIVE_OPS,
    LOAD_OP_CODES,
    LOAD_OP_CODES_INCL_OFFSET,
    ORDERING_OPS,
    STORE_OPS_INCL_OFFSET,
)
from puya.utils import invert_ordered_binary_op

def peephole(block: models.TealBlock, opt_level: int) -> bool:
    start_idx = 0
    stack_height = block.entry_stack_height
    any_modified = False
    result = block.ops.copy()
    while start_idx < len(result):
        modified = False
        window: slice | None = None
        if opt_level > 0 and not modified and start_idx < len(result) - 3:
            window = slice(start_idx, start_idx + 4)
            new_values, modified = _optimize_quadruplet(*result[window])
        if opt_level > 0 and not modified and start_idx < len(result) - 2:
            window = slice(start_idx, start_idx + 3)
            new_values, modified = _optimize_triplet(*result[window], stack_height=stack_height)
        if not modified and start_idx < len(result) - 1:
            window = slice(start_idx, start_idx + 2)
            new_values, modified = _optimize_pair(*result[window])
        if not modified:
            window = slice(start_idx, start_idx + 1)
            new_values, modified = optimize_single(*result[window])
        if modified:
            assert window is not None
            any_modified = True
            preserve_stack_manipulations(result, window, new_values)
        else:
            stack_height += result[start_idx].stack_height_delta
            start_idx += 1  # go to next
    block.ops[:] = result
    return any_modified

def is_redundant_rotate(a: models.TealOp, b: models.TealOp) -> bool:
    match a, b:
        case models.Cover(n=a_n), models.Uncover(n=b_n) if a_n == b_n:
            return True
        case models.Uncover(n=a_n), models.Cover(n=b_n) if a_n == b_n:
            return True

    return is_stack_swap(a) and is_stack_swap(b)

def is_stack_swap(op: models.TealOp) -> bool:
    return op.op_code == "swap" or (op.op_code in ("cover", "uncover") and op.immediates[0] == 1)

def optimize_single(a: models.TealOp) -> tuple[list[models.TealOp], bool]:
    if a.op_code in ("cover", "uncover") and a.immediates == (0,):
        return [], True
    if a.op_code == "dig" and a.immediates == (0,):
        return [
            models.Dup(
                source_location=a.source_location,
                stack_manipulations=a.stack_manipulations,
            )
        ], True
    if a.op_code == "popn" and a.immediates == (1,):
        return [
            models.Pop(
                source_location=a.source_location,
                stack_manipulations=a.stack_manipulations,
            )
        ], True
    return [a], False

def _optimize_pair(a: models.TealOp, b: models.TealOp) -> tuple[list[models.TealOp], bool]:
    if is_redundant_rotate(a, b):
        return [], True

    if is_stack_swap(a):
        # `swap; pop` -> `bury 1`
        if b.op_code == "pop":
            return [models.Bury(n=1, source_location=b.source_location)], True
        if b.op_code in COMMUTATIVE_OPS:
            return [b], True
        if b.op_code in ORDERING_OPS:
            inverse_ordering_op = invert_ordered_binary_op(b.op_code)
            return [attrs.evolve(b, op_code=inverse_ordering_op)], True

    match a, b:
        # `frame_dig n; frame_bury n` is redundant
        case models.FrameDig(n=dig_n), models.FrameBury(n=bury_n) if dig_n == bury_n:
            return [], True
        # `frame_bury n; frame_dig n` can be simplified to dup; frame_bury n
        case models.FrameBury(n=dig_n), models.FrameDig(n=bury_n) if dig_n == bury_n:
            return [models.Dup(source_location=None), a], True
        # `dup; swap` -> `dup`
        case models.TealOp(op_code="dup" | "dupn"), maybe_swap if is_stack_swap(maybe_swap):
            return [a], True
        # combine consecutive dup/dupn's
        case models.TealOp(op_code="dup" | "dupn"), models.TealOp(op_code="dup" | "dupn"):
            (n1,) = a.immediates or (1,)
            assert isinstance(n1, int)
            (n2,) = b.immediates or (1,)
            assert isinstance(n2, int)
            return [models.DupN(n=n1 + n2, source_location=a.source_location)], True
        # combine consecutive pop/popn's
        case models.TealOp(op_code="pop" | "popn"), models.TealOp(op_code="pop" | "popn"):
            (n1,) = a.immediates or (1,)
            assert isinstance(n1, int)
            (n2,) = b.immediates or (1,)
            assert isinstance(n2, int)
            return [models.PopN(n=n1 + n2, source_location=a.source_location)], True
        # `dig 1; dig 1` -> `dup2`
        case models.TealOpUInt8(op_code="dig", n=1), models.TealOpUInt8(op_code="dig", n=1):
            return [models.Dup2(source_location=a.source_location or b.source_location)], True
    return [a, b], False

def _optimize_triplet(
    a: models.TealOp, b: models.TealOp, c: models.TealOp, *, stack_height: int
) -> tuple[list[models.TealOp], bool]:
    if _frame_digs_overlap_with_ops(stack_height, a, b, c):
        return [a, b, c], False

    # `'cover 3; cover 3; swap` -> `uncover 2; uncover 3`
    if (
        is_stack_swap(c)
        and (a.op_code == "cover" and a.immediates[0] == 3)
        and (b.op_code == "cover" and b.immediates[0] == 3)
    ):
        return [
            models.Uncover(n=2, source_location=a.source_location),
            models.Uncover(n=3, source_location=b.source_location),
        ], True

    # `swap; (consumes=0, produces=1); uncover 2` -> `(consume=0, produces=1); swap`
    if (
        is_stack_swap(a)
        and (c.op_code == "uncover" and c.immediates[0] == 2)
        and (
            b.op_code in LOAD_OP_CODES_INCL_OFFSET
            # only count digs if they go below the swap
            and (b.op_code != "dig" or int(b.immediates[0]) >= 2)
        )
    ):
        return [b, a], True

    # <load A>; <load B>; swap -> <load B>; <load A>
    if (
        is_stack_swap(c)
        and a.op_code in LOAD_OP_CODES_INCL_OFFSET
        and b.op_code in LOAD_OP_CODES_INCL_OFFSET
        # cant swap dig 0, which will become a dup anyway
        and (not isinstance(b, models.Dig) or b.n)
    ):
        if isinstance(a, models.Dig):
            a = attrs.evolve(a, n=a.n + 1)
        if isinstance(b, models.Dig):
            new_n = b.n - 1
            if new_n == 0:
                b = models.Dup(source_location=b.source_location)
            else:
                b = attrs.evolve(b, n=new_n)
        return [b, a], True

    # swap; <store A>; <store B> -> <store B>; <store A>
    if (
        is_stack_swap(a)
        and b.op_code in STORE_OPS_INCL_OFFSET
        and c.op_code in STORE_OPS_INCL_OFFSET
    ):
        height_below_swap = stack_height - 2
        if not (
            (b.op_code == "frame_bury" and int(b.immediates[0]) >= height_below_swap)
            or (c.op_code == "frame_bury" and int(c.immediates[0]) >= height_below_swap)
            or (
                # can't swap ops if store order is important
                # e.g. itxn_field ApplicationArgs or frame_bury -1
                b.op_code in ("frame_bury", "itxn_field")
                and b.op_code == c.op_code
                and b.immediates == c.immediates
            )
        ):
            return [c, b], True

    match a, b, c:
        # `uncover 2; swap; uncover 2` is equivalent to `swap`
        case models.Uncover(n=2), maybe_swap, models.Uncover(n=2) if is_stack_swap(maybe_swap):
            return [maybe_swap], True
        # `dup; cover 2; swap` can be replaced by `dup; uncover 2`
        case models.Dup(), models.Cover(n=2), maybe_swap if is_stack_swap(maybe_swap):
            return [
                a,
                models.Uncover(n=2, source_location=b.source_location or c.source_location),
            ], True

    # `uncover n; dup; cover n+1` can be replaced with `dig n`
    # this occurs when the x-stack becomes the l-stack
    if (
        a.op_code == "uncover"
        and b.op_code == "dup"
        and c.op_code == "cover"
        and isinstance((n := a.immediates[0]), int)
        and (n + 1) == c.immediates[0]
    ):
        return [
            models.Dig(
                n=n, source_location=a.source_location or b.source_location or c.source_location
            )
        ], True
    return [a, b, c], False

def _frame_digs_overlap_with_ops(stack_height: int, *ops: models.TealOp) -> bool:
    """
    Check to see if there is a frame_dig in the sequence that could be impacted
    if ops were re-ordered/eliminated/otherwise optimized.
    """
    curr_height = min_stack_height = stack_height
    for op in ops:
        if op.op_code == "frame_dig":
            n = op.immediates[0]
            assert isinstance(n, int)
            if n >= min_stack_height:
                return True
        curr_height -= op.consumes
        min_stack_height = min(curr_height, min_stack_height)
        curr_height += op.produces
    return False

def _optimize_quadruplet(
    a: models.TealOp, b: models.TealOp, c: models.TealOp, d: models.TealOp
) -> tuple[list[models.TealOp], bool]:
    # `swap; <re-orderable load op>; swap; uncover 2` -> `dig|uncover n; cover 2`
    if (
        is_stack_swap(a)
        and (
            b.op_code in LOAD_OP_CODES
            or (b.op_code in ("dig", "uncover") and int(b.immediates[0]) >= 2)
            or (b.op_code == "frame_dig" and int(b.immediates[0]) < 0)
        )
        and is_stack_swap(c)
        and (d.op_code == "uncover" and d.immediates[0] == 2)
    ):
        return [b, models.Cover(n=2, source_location=d.source_location)], True
    return [a, b, c, d], False


from collections.abc import Sequence

from puya.teal import models
from puya.teal._util import preserve_stack_manipulations

def _simplify_repeated_rotation_ops(
    maybe_simplify: list[models.Cover | models.Uncover],
) -> tuple[Sequence[models.Cover | models.Uncover], bool]:
    first = maybe_simplify[0]
    is_cover = isinstance(first, models.Cover)
    n = first.n
    number_of_ops = len(maybe_simplify)
    number_of_inverse = n + 1 - number_of_ops
    while number_of_inverse < 0:
        number_of_inverse += n + 1
    if number_of_inverse >= number_of_ops:
        return maybe_simplify, False
    inverse_op: models.Cover | models.Uncover = (
        models.Uncover(n=n, source_location=first.source_location)
        if is_cover
        else models.Cover(n=n, source_location=first.source_location)
    )
    simplified = [inverse_op] * number_of_inverse
    return simplified, True

def simplify_repeated_rotation_ops(block: models.TealBlock) -> bool:
    result = block.ops.copy()
    maybe_simplify = list[models.Cover | models.Uncover]()
    modified = False
    end_idx = 0
    while end_idx < len(result):
        op = result[end_idx]
        if isinstance(op, models.Cover | models.Uncover) and (
            not maybe_simplify or op == maybe_simplify[0]
        ):
            maybe_simplify.append(op)
        else:
            if maybe_simplify:
                maybe_simplified, modified_ = _simplify_repeated_rotation_ops(maybe_simplify)
                modified = modified or modified_
                preserve_stack_manipulations(
                    result, slice(end_idx - len(maybe_simplify), end_idx), maybe_simplified
                )
                end_idx += len(maybe_simplified) - len(maybe_simplify)
                maybe_simplify = []
            if isinstance(op, models.Cover | models.Uncover):
                maybe_simplify.append(op)
        end_idx += 1
    if maybe_simplify:
        maybe_simplified, modified_ = _simplify_repeated_rotation_ops(maybe_simplify)
        modified = modified or modified_
        idx = len(result)
        preserve_stack_manipulations(
            result, slice(idx - len(maybe_simplify), idx), maybe_simplified
        )
    block.ops[:] = result
    return modified

def simplify_swap_ops(block: models.TealBlock) -> bool:
    result = list[models.TealOp]()
    modified = False
    for op in block.ops:
        if isinstance(op, models.Cover | models.Uncover) and (op.n == 1):
            modified = True
            result.append(
                models.Swap(
                    source_location=op.source_location, stack_manipulations=op.stack_manipulations
                )
            )
        else:
            result.append(op)
    block.ops[:] = result
    return modified


import functools
import itertools
import typing
from collections.abc import Sequence

import attrs

from puya import log
from puya.teal import models
from puya.teal._util import preserve_stack_manipulations

TealOpSequence = tuple[models.TealOpUInt8, ...]
logger = log.get_logger(__name__)

class InvalidOpSequenceError(Exception):
    pass

@attrs.define
class TealStack:
    stack: list[int]

    @classmethod
    def from_stack_size(cls, stack_size: int) -> "TealStack":
        return cls(stack=list(range(stack_size)))

    def apply(self, ops: Sequence[models.TealOpUInt8]) -> "TealStack":
        stack = TealStack(self.stack.copy())
        for op in ops:
            n = op.n
            if n:
                index = len(stack.stack) - n - 1
                if index < 0 or index >= len(stack.stack):
                    raise InvalidOpSequenceError
                match op.op_code:
                    case "cover":
                        stack.stack.insert(index, stack.stack.pop())
                    case "uncover":
                        stack.stack.append(stack.stack.pop(index))
                    case _:
                        raise InvalidOpSequenceError
        return stack

@functools.cache
def simplify_rotation_ops(original_ops: TealOpSequence) -> TealOpSequence | None:
    num_rot_ops = len(original_ops)
    max_rot_op_n = 0
    for o in original_ops:
        max_rot_op_n = max(max_rot_op_n, o.n)

    original_stack = TealStack.from_stack_size(max_rot_op_n + 1)

    expected = original_stack.apply(original_ops)
    # entire sequence can be removed!
    if expected == original_stack:
        return ()

    possible_rotation_ops = get_possible_rotation_ops(max_rot_op_n)
    original_stack_result = original_stack.apply(original_ops)

    # TODO: use a non-bruteforce approach and/or capture common simplifications as data
    for num_rotation_ops in range(num_rot_ops):
        for maybe_ops in itertools.permutations(possible_rotation_ops, num_rotation_ops):
            try:
                stack = original_stack.apply(maybe_ops)
            except InvalidOpSequenceError:
                continue
            if expected == stack:
                assert original_stack_result == original_stack.apply(maybe_ops)
                return tuple(attrs.evolve(op, source_location=None) for op in maybe_ops)
    return None

@functools.cache
def get_possible_rotation_ops(n: int) -> TealOpSequence:
    possible_ops = list[models.TealOpUInt8]()
    for i in range(1, n + 1):
        possible_ops.append(models.Cover(i, source_location=None))
        possible_ops.append(models.Uncover(i, source_location=None))
    return tuple(possible_ops)

ROTATION_SIMPLIFY_OPS = frozenset(
    [
        "cover",
        "uncover",
    ]
)

def repeated_rotation_ops_search(teal_ops: list[models.TealOp]) -> list[models.TealOp]:
    maybe_remove_rotations = list[models.TealOpUInt8]()
    result = list[models.TealOp]()
    for teal_op in teal_ops:
        if teal_op.op_code in ROTATION_SIMPLIFY_OPS:
            maybe_remove_rotations.append(typing.cast(models.TealOpUInt8, teal_op))
        else:
            maybe_simplified = _maybe_simplified(maybe_remove_rotations)
            maybe_remove_rotations = []
            result.extend(maybe_simplified)
            result.append(teal_op)
    result.extend(_maybe_simplified(maybe_remove_rotations))
    return result

def _maybe_simplified(
    maybe_remove_rotations: list[models.TealOpUInt8], window_size: int = 5
) -> Sequence[models.TealOpUInt8]:
    if len(maybe_remove_rotations) < 2:
        return maybe_remove_rotations

    for start_idx in range(len(maybe_remove_rotations) - 1):
        window_slice = slice(start_idx, start_idx + window_size + 1)
        window = maybe_remove_rotations[window_slice]
        simplified = simplify_rotation_ops(tuple(window))
        if simplified is not None:
            logger.debug(
                f"Replaced '{'; '.join(map(str, maybe_remove_rotations))}'"
                f" with '{'; '.join(map(str, simplified))}',"
                f" reducing by {len(maybe_remove_rotations) - len(simplified)} ops by search"
            )
            result_ = maybe_remove_rotations.copy()
            preserve_stack_manipulations(result_, window_slice, simplified)
            assert result_ != maybe_remove_rotations
            return result_
    return maybe_remove_rotations


import textwrap

import attrs

from puya.context import ArtifactCompileContext
from puya.teal import models

def emit_teal(context: ArtifactCompileContext, program: models.TealProgram) -> str:
    indent = " " * 4

    result = [
        f"#pragma version {program.avm_version}",
        "#pragma typetrack false",
        "",
    ]
    for idx, subroutine in enumerate(program.all_subroutines):
        if idx > 0:
            result.append("")
        result.append(f"// {subroutine.signature}")

        for block in subroutine.blocks:
            last_location = None
            result.append(f"{block.label}:")
            for teal_op in block.ops:
                if context.options.debug_level and (op_loc := teal_op.source_location):
                    whole_lines_location = attrs.evolve(
                        op_loc.with_comments(), column=None, end_column=None
                    )
                    if whole_lines_location != last_location:
                        last_location = whole_lines_location
                        src = context.try_get_source(whole_lines_location)
                        if src is not None:
                            result.append(f"{indent}// {whole_lines_location}")
                            lines = textwrap.dedent("\n".join(src)).splitlines()
                            result.extend(f"{indent}// {line.rstrip()}" for line in lines)

                result.append(f"{indent}{teal_op.teal()}")
            result.append("")
    return "\n".join(result)


import struct
import typing
from collections import defaultdict
from collections.abc import Callable, Iterable, Mapping, Sequence

from puya import log
from puya.compilation_artifacts import DebugEvent
from puya.errors import InternalError
from puya.parse import SourceLocation
from puya.teal import models as teal
from puya.ussemble import models
from puya.ussemble.context import AssembleContext
from puya.ussemble.debug import build_debug_info
from puya.ussemble.op_spec import OP_SPECS
from puya.ussemble.op_spec_models import ImmediateEnum, ImmediateKind

logger = log.get_logger(__name__)
_BRANCHING_OPS = {
    op.name
    for op in OP_SPECS.values()
    if any(i in (ImmediateKind.label, ImmediateKind.label_array) for i in op.immediates)
}

def assemble_bytecode_and_debug_info(
    ctx: AssembleContext, program: teal.TealProgram
) -> models.AssembledProgram:
    function_block_ids = {s.blocks[0].label: s.signature.name for s in program.all_subroutines}

    version_bytes = _encode_varuint(program.avm_version)
    pc_events = defaultdict[int, DebugEvent](lambda: DebugEvent())
    pc_ops = dict[int, models.AVMOp]()
    label_pcs = dict[str, int]()

    # pc includes version header
    pc = len(version_bytes)
    # first pass lowers teal ops, calculate pcs, and captures debug info
    for subroutine in program.all_subroutines:
        current_event = pc_events[pc]
        current_event["subroutine"] = subroutine.signature.name
        current_event["params"] = {
            p.local_id: p.atype.name or "" for p in subroutine.signature.parameters
        }
        stack = list[str]()
        for block in subroutine.blocks:
            current_event = pc_events[pc]
            # update stack with correct values on entry to a block
            f_stack_height = block.entry_stack_height - len(block.x_stack_in)
            stack[f_stack_height:] = block.x_stack_in
            label_pcs[block.label] = pc
            current_event["block"] = block.label
            current_event["stack_in"] = stack.copy()
            defined = set[str]()

            for op in block.ops:
                current_event = pc_events[pc]
                avm_op = _lower_op(ctx, op)
                # actual label offsets can't be determined until all PC values are known
                # so just use a placeholder value initially
                op_size = len(_encode_op(avm_op, get_label_offset=lambda _: 0))
                assert op_size, "expected non empty bytecode"
                _add_op_debug_events(
                    current_event,
                    function_block_ids,
                    op,
                    # note: stack & defined are mutated
                    stack,
                    defined,
                )
                pc_ops[pc] = avm_op
                pc += op_size

    # all pc values, including pc after final op
    pcs = [*pc_ops, pc]
    # second pass assembles final byte code
    bytecode = [version_bytes]
    for op_index, avm_op in enumerate(pc_ops.values()):

        def get_label_offset(label: models.Label) -> int:
            # label offset is the signed PC difference
            # between the label PC location and the end of the current op
            return label_pcs[label.name] - pcs[op_index + 1]  # noqa: B023

        bytecode.append(_encode_op(avm_op, get_label_offset=get_label_offset))

    return models.AssembledProgram(
        bytecode=b"".join(bytecode),
        debug_info=build_debug_info(ctx, pc_ops, pc_events),
        template_variables={
            var: ctx.provided_template_variables.get(var, (None, None))[0]
            for var in ctx.template_variable_types
        },
    )

def _add_op_debug_events(
    event: DebugEvent,
    subroutine_ids: Mapping[str, str],
    op: teal.TealOp,
    stack: list[str],
    defined: set[str],
) -> None:
    stack_in = stack.copy()
    num_defined = len(defined)
    if op.op_code == "callsub":
        (func_block,) = op.immediates
        assert isinstance(func_block, str), "expected label"
        event["callsub"] = subroutine_ids[func_block]
    elif op.op_code == "retsub":
        event["retsub"] = True
    if op.error_message:
        event["error"] = op.error_message
    event["op"] = op.teal()

    for sm in op.stack_manipulations:
        match sm:
            case teal.StackConsume(n=n):
                for _ in range(n):
                    stack.pop()
            case teal.StackExtend() as se:
                stack.extend(se.local_ids)
            case teal.StackDefine() as sd:
                defined.update(sd.local_ids)
            case teal.StackInsert() as si:
                index = len(stack) - si.depth
                stack.insert(index, si.local_id)
            case teal.StackPop() as sp:
                index = len(stack) - sp.depth - 1
                stack.pop(index)
            case _:
                typing.assert_never(sm)

    if len(defined) != num_defined:
        event["defined_out"] = sorted(set(defined) & set(stack))
    if stack_in != stack:
        event["stack_out"] = stack.copy()

def _lower_op(ctx: AssembleContext, op: teal.TealOp) -> models.AVMOp:
    loc = op.source_location
    match op:
        case teal.TemplateVar() | teal.Int() | teal.Byte():
            raise InternalError(f"{op} should have been eliminated during TEAL phase", loc)
        case teal.IntBlock(constants=constants):
            return models.AVMOp(
                op_code=op.op_code,
                immediates=[_resolve_template_vars(ctx, int, constants.items())],
                source_location=loc,
            )
        case teal.BytesBlock(constants=constants):
            return models.AVMOp(
                op_code=op.op_code,
                immediates=[
                    _resolve_template_vars(ctx, bytes, [(b, es[1]) for b, es in constants.items()])
                ],
                source_location=loc,
            )
        case teal.PushBytes(value=bytes_value):
            return models.AVMOp(
                op_code=op.op_code,
                immediates=[bytes_value],
                source_location=loc,
            )
        case teal.PushBytess(values=values):
            return models.AVMOp(
                op_code=op.op_code,
                immediates=[[t[0] for t in values]],
                source_location=loc,
            )
        case teal.PushInts(values=values):
            return models.AVMOp(
                op_code=op.op_code,
                immediates=[values],
                source_location=loc,
            )
        case teal.CallSub(target=label_id):
            return models.AVMOp(
                op_code=op.op_code,
                immediates=[models.Label(name=label_id)],
                source_location=loc,
            )
        case teal.TealOp(op_code="b" | "bz" | "bnz", immediates=[str(label_id)]):
            return models.AVMOp(
                op_code=op.op_code,
                immediates=[models.Label(name=label_id)],
                source_location=loc,
            )
        case teal.TealOp(
            op_code="switch" | "match" as op_code, immediates=label_ids
        ) if _is_sequence(label_ids, str):
            return models.AVMOp(
                op_code=op_code,
                immediates=[[models.Label(label_id) for label_id in label_ids]],
                source_location=loc,
            )
        case teal.TealOp() if op.op_code not in _BRANCHING_OPS:
            return models.AVMOp(op_code=op.op_code, immediates=op.immediates, source_location=loc)
        case _:
            raise InternalError(f"invalid teal op: {op}", loc)

def _resolve_template_vars[T: (int, bytes)](
    ctx: AssembleContext, typ: type[T], values: Iterable[tuple[T | str, SourceLocation | None]]
) -> Sequence[T]:
    result = []
    for value_or_template, var_loc in values:
        if not isinstance(value_or_template, str):
            value = value_or_template
        else:
            try:
                maybe_value, val_loc = ctx.provided_template_variables[value_or_template]
            except KeyError:
                # if bytecode isn't required for this program, then a dummy value is sufficient
                bytecode_required = ctx.options.output_bytecode and (
                    ctx.artifact_ref in ctx.compilation_set
                )
                if ctx.is_reference or bytecode_required:
                    logger.error(  # noqa: TRY400
                        f"template variable not defined: {value_or_template}", location=var_loc
                    )
                value = typ()
            else:
                if isinstance(maybe_value, typ):
                    value = maybe_value
                else:
                    logger.error(
                        f"invalid template value type for {value_or_template!r},"
                        f" expected {typ.__name__}",
                        location=val_loc or var_loc,
                    )
                    value = typ()
        result.append(value)
    return result

def _encode_op(op: models.AVMOp, *, get_label_offset: Callable[[models.Label], int]) -> bytes:
    op_spec = op.op_spec
    bytecode = _encode_uint8(op.op_spec.code)
    for immediate_kind, immediate in zip(op_spec.immediates, op.immediates, strict=True):
        match immediate_kind:
            case ImmediateKind.uint8 if isinstance(immediate, int):
                bytecode += _encode_uint8(immediate)
            case ImmediateKind.int8 if isinstance(immediate, int):
                bytecode += _encode_int8(immediate)
            case ImmediateEnum(codes=enum_codes) if isinstance(immediate, str):
                immediate_code = enum_codes[immediate]
                bytecode += _encode_uint8(immediate_code)
            case ImmediateKind.bytes if isinstance(immediate, bytes):
                bytecode += _encode_bytes(immediate)
            case ImmediateKind.varuint if isinstance(immediate, int):
                bytecode += _encode_varuint(immediate)
            case ImmediateKind.varuint_array if _is_sequence(immediate, int):
                bytecode += _encode_varuint_array(immediate)
            case ImmediateKind.bytes_array if _is_sequence(immediate, bytes):
                bytecode += _encode_bytes_array(immediate)
            case ImmediateKind.label if isinstance(immediate, models.Label):
                offset = get_label_offset(immediate)
                bytecode += _encode_label(offset)
            case ImmediateKind.label_array if _is_sequence(immediate, models.Label):
                offsets = [get_label_offset(label) for label in immediate]
                bytecode += _encode_label_array(offsets)
            case _:
                raise InternalError(f"Invalid op: {op}")
    return bytecode

_encode_uint8 = struct.Struct(">B").pack
_encode_int8 = struct.Struct(">b").pack
_encode_label = struct.Struct(">h").pack

def _encode_varuint(value: int) -> bytes:
    bits = value & 0x7F
    value >>= 7
    result = b""
    while value:
        result += _encode_uint8(0x80 | bits)
        bits = value & 0x7F
        value >>= 7
    return result + _encode_uint8(bits)

def _encode_bytes(value: bytes) -> bytes:
    return _encode_varuint(len(value)) + value

def _encode_varuint_array(values: Sequence[int]) -> bytes:
    return b"".join((_encode_varuint(len(values)), *map(_encode_varuint, values)))

def _encode_label_array(values: Sequence[int]) -> bytes:
    # note: op spec describes a label array size as a varuint
    #       however actual algod go implementation is just a single byte
    #       additionally max number of labels is 255
    return b"".join((_encode_uint8(len(values)), *map(_encode_label, values)))

def _encode_bytes_array(values: Sequence[bytes]) -> bytes:
    return b"".join(
        (
            _encode_varuint(len(values)),
            *map(_encode_bytes, values),
        ),
    )

def _is_sequence[_T](maybe: object, typ: type[_T]) -> typing.TypeGuard[Sequence[_T]]:
    return isinstance(maybe, Sequence) and all(isinstance(m, typ) for m in maybe)


import typing
from collections.abc import Mapping
from functools import cached_property

import attrs

from puya.avm import AVMType
from puya.compilation_artifacts import TemplateValue
from puya.context import CompileContext
from puya.program_refs import ContractReference, LogicSigReference

@attrs.frozen(kw_only=True)
class AssembleContext(CompileContext):
    artifact_ref: ContractReference | LogicSigReference
    is_reference: bool
    template_variable_types: Mapping[str, typing.Literal[AVMType.uint64, AVMType.bytes]]
    """Template variables that are required and their types"""
    template_constants: Mapping[str, TemplateValue] | None
    """Template variables provided via compilation"""

    @cached_property
    def provided_template_variables(self) -> Mapping[str, TemplateValue]:
        return {
            **{k: (v, None) for k, v in self.options.template_variables.items()},
            **(self.template_constants or {}),
        }

    @cached_property
    def offset_pc_from_constant_blocks(self) -> bool:
        # only need to offset PC if there are any unspecified template variables
        return not (self.provided_template_variables.keys() >= self.template_variable_types.keys())


from collections.abc import Mapping, Sequence

from puya.compilation_artifacts import DebugEvent, DebugInfo
from puya.parse import SourceLocation
from puya.ussemble import models
from puya.ussemble.context import AssembleContext
from puya.utils import normalize_path

def build_debug_info(
    ctx: AssembleContext,
    pc_ops: Mapping[int, models.AVMOp],
    pc_events: Mapping[int, DebugEvent],
) -> DebugInfo:
    op_pc_offset = pc_offset = 0
    if ctx.offset_pc_from_constant_blocks:
        for idx, (pc, node) in enumerate(pc_ops.items()):
            # stop at first op that is not a constant block
            if node.op_code not in ("intcblock", "bytecblock"):
                # only need to set pc_offset if there was a constant block present (i.e. idx != 0)
                # this is because the first op will be after the version byte and so will always
                # have a pc of 1, but a op_pc_offset of 0 means the values are not offset
                if idx:
                    op_pc_offset = idx
                    pc_offset = pc
                break
    events = {pc - pc_offset: event for pc, event in pc_events.items() if pc >= pc_offset}
    source_map = {
        pc - pc_offset: node.source_location for pc, node in pc_ops.items() if pc >= pc_offset
    }

    files = sorted(map(normalize_path, {s.file for s in source_map.values() if s and s.file}))
    mappings = _get_src_mappings(source_map, files)

    return DebugInfo(
        version=3,
        sources=files,
        mappings=";".join(mappings),
        op_pc_offset=op_pc_offset,
        pc_events=events,
    )

def _get_src_mappings(
    source_map: Mapping[int, SourceLocation | None],
    files: Sequence[str],
) -> list[str]:
    mappings = []
    previous_source_index = 0
    previous_line = 0
    previous_column = 0
    for pc in range(max(source_map) + 1):
        loc = source_map.get(pc)
        if not loc or not loc.file:
            mappings.append("")
            continue
        source_index = files.index(normalize_path(loc.file))
        line = loc.line - 1  # make 0-indexed
        column = loc.column or 0

        mappings.append(
            _base64vlq_encode(
                0,  # generated col offset, always 0 for AVM byte code
                source_index - previous_source_index,
                line - previous_line,
                column - previous_column,
            )
        )
        previous_source_index = source_index
        previous_line = line
        previous_column = column
    return mappings

def _base64vlq_encode(*values: int) -> str:
    """Encode integers to a VLQ value"""
    results = []

    b64_chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    b64_encode = b64_chars.decode().__getitem__
    shift = 5
    flag = 1 << shift
    mask = flag - 1

    for v in values:
        # add sign bit
        v = (abs(v) << 1) | int(v < 0)
        while True:
            to_encode = v & mask
            v >>= shift
            results.append(b64_encode(to_encode | (v and flag)))
            if not v:
                break
    return "".join(results)


import typing
from collections.abc import Mapping

from puya.avm import AVMType
from puya.compilation_artifacts import TemplateValue
from puya.context import CompileContext
from puya.program_refs import ContractReference, LogicSigReference
from puya.teal import models as teal
from puya.ussemble import models
from puya.ussemble.assemble import assemble_bytecode_and_debug_info
from puya.ussemble.context import AssembleContext
from puya.utils import attrs_extend

def assemble_program(
    ctx: CompileContext,
    ref: ContractReference | LogicSigReference,
    program: teal.TealProgram,
    *,
    template_constants: Mapping[str, TemplateValue] | None = None,
    is_reference: bool = False,
) -> models.AssembledProgram:
    referenced_template_vars = _gather_template_variables(program)
    assemble_ctx = attrs_extend(
        AssembleContext,
        ctx,
        artifact_ref=ref,
        is_reference=is_reference,
        template_variable_types=referenced_template_vars,
        template_constants=template_constants,
    )
    return assemble_bytecode_and_debug_info(assemble_ctx, program)

def _gather_template_variables(
    program: teal.TealProgram,
) -> Mapping[str, typing.Literal[AVMType.uint64, AVMType.bytes]]:
    return {
        t: AVMType.uint64 if isinstance(op, teal.IntBlock) else AVMType.bytes
        for sub in program.all_subroutines
        for block in sub.blocks
        for op in block.ops
        if isinstance(op, teal.IntBlock | teal.BytesBlock)
        for t in op.constants
        if isinstance(t, str)
    }


from collections.abc import Mapping, Sequence

import attrs

from puya.compilation_artifacts import DebugInfo
from puya.parse import SourceLocation
from puya.ussemble.op_spec import OP_SPECS
from puya.ussemble.op_spec_models import OpSpec

@attrs.frozen(str=False)
class Label:
    name: str

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

@attrs.frozen(str=False, eq=False)
class AVMOp:
    source_location: SourceLocation | None
    op_code: str
    immediates: Sequence[
        Sequence[int] | int | Sequence[bytes] | bytes | Sequence[Label] | Label | str
    ]

    @property
    def op_spec(self) -> OpSpec:
        return OP_SPECS[self.op_code]

@attrs.frozen
class AssembledProgram:
    bytecode: bytes
    debug_info: DebugInfo
    template_variables: Mapping[str, int | bytes | None]
    """Indicates template variable values used in compilation"""


from puya.ussemble.op_spec_models import ImmediateEnum, ImmediateKind, OpSpec

OP_SPECS = {
    "err": OpSpec(name="err", code=0, immediates=[]),
    "sha256": OpSpec(name="sha256", code=1, immediates=[]),
    "keccak256": OpSpec(name="keccak256", code=2, immediates=[]),
    "sha512_256": OpSpec(name="sha512_256", code=3, immediates=[]),
    "ed25519verify": OpSpec(name="ed25519verify", code=4, immediates=[]),
    "ecdsa_verify": OpSpec(
        name="ecdsa_verify",
        code=5,
        immediates=[ImmediateEnum(codes={"Secp256k1": 0, "Secp256r1": 1})],
    ),
    "ecdsa_pk_decompress": OpSpec(
        name="ecdsa_pk_decompress",
        code=6,
        immediates=[ImmediateEnum(codes={"Secp256k1": 0, "Secp256r1": 1})],
    ),
    "ecdsa_pk_recover": OpSpec(
        name="ecdsa_pk_recover",
        code=7,
        immediates=[ImmediateEnum(codes={"Secp256k1": 0, "Secp256r1": 1})],
    ),
    "+": OpSpec(name="+", code=8, immediates=[]),
    "-": OpSpec(name="-", code=9, immediates=[]),
    "/": OpSpec(name="/", code=10, immediates=[]),
    "*": OpSpec(name="*", code=11, immediates=[]),
    "<": OpSpec(name="<", code=12, immediates=[]),
    ">": OpSpec(name=">", code=13, immediates=[]),
    "<=": OpSpec(name="<=", code=14, immediates=[]),
    ">=": OpSpec(name=">=", code=15, immediates=[]),
    "&&": OpSpec(name="&&", code=16, immediates=[]),
    "||": OpSpec(name="||", code=17, immediates=[]),
    "==": OpSpec(name="==", code=18, immediates=[]),
    "!=": OpSpec(name="!=", code=19, immediates=[]),
    "!": OpSpec(name="!", code=20, immediates=[]),
    "len": OpSpec(name="len", code=21, immediates=[]),
    "itob": OpSpec(name="itob", code=22, immediates=[]),
    "btoi": OpSpec(name="btoi", code=23, immediates=[]),
    "%": OpSpec(name="%", code=24, immediates=[]),
    "|": OpSpec(name="|", code=25, immediates=[]),
    "&": OpSpec(name="&", code=26, immediates=[]),
    "^": OpSpec(name="^", code=27, immediates=[]),
    "~": OpSpec(name="~", code=28, immediates=[]),
    "mulw": OpSpec(name="mulw", code=29, immediates=[]),
    "addw": OpSpec(name="addw", code=30, immediates=[]),
    "divmodw": OpSpec(name="divmodw", code=31, immediates=[]),
    "intcblock": OpSpec(name="intcblock", code=32, immediates=[ImmediateKind.varuint_array]),
    "intc": OpSpec(name="intc", code=33, immediates=[ImmediateKind.uint8]),
    "intc_0": OpSpec(name="intc_0", code=34, immediates=[]),
    "intc_1": OpSpec(name="intc_1", code=35, immediates=[]),
    "intc_2": OpSpec(name="intc_2", code=36, immediates=[]),
    "intc_3": OpSpec(name="intc_3", code=37, immediates=[]),
    "bytecblock": OpSpec(name="bytecblock", code=38, immediates=[ImmediateKind.bytes_array]),
    "bytec": OpSpec(name="bytec", code=39, immediates=[ImmediateKind.uint8]),
    "bytec_0": OpSpec(name="bytec_0", code=40, immediates=[]),
    "bytec_1": OpSpec(name="bytec_1", code=41, immediates=[]),
    "bytec_2": OpSpec(name="bytec_2", code=42, immediates=[]),
    "bytec_3": OpSpec(name="bytec_3", code=43, immediates=[]),
    "arg": OpSpec(name="arg", code=44, immediates=[ImmediateKind.uint8]),
    "arg_0": OpSpec(name="arg_0", code=45, immediates=[]),
    "arg_1": OpSpec(name="arg_1", code=46, immediates=[]),
    "arg_2": OpSpec(name="arg_2", code=47, immediates=[]),
    "arg_3": OpSpec(name="arg_3", code=48, immediates=[]),
    "txn": OpSpec(
        name="txn",
        code=49,
        immediates=[
            ImmediateEnum(
                codes={
                    "Sender": 0,
                    "Fee": 1,
                    "FirstValid": 2,
                    "FirstValidTime": 3,
                    "LastValid": 4,
                    "Note": 5,
                    "Lease": 6,
                    "Receiver": 7,
                    "Amount": 8,
                    "CloseRemainderTo": 9,
                    "VotePK": 10,
                    "SelectionPK": 11,
                    "VoteFirst": 12,
                    "VoteLast": 13,
                    "VoteKeyDilution": 14,
                    "Type": 15,
                    "TypeEnum": 16,
                    "XferAsset": 17,
                    "AssetAmount": 18,
                    "AssetSender": 19,
                    "AssetReceiver": 20,
                    "AssetCloseTo": 21,
                    "GroupIndex": 22,
                    "TxID": 23,
                    "ApplicationID": 24,
                    "OnCompletion": 25,
                    "ApplicationArgs": 26,
                    "NumAppArgs": 27,
                    "Accounts": 28,
                    "NumAccounts": 29,
                    "ApprovalProgram": 30,
                    "ClearStateProgram": 31,
                    "RekeyTo": 32,
                    "ConfigAsset": 33,
                    "ConfigAssetTotal": 34,
                    "ConfigAssetDecimals": 35,
                    "ConfigAssetDefaultFrozen": 36,
                    "ConfigAssetUnitName": 37,
                    "ConfigAssetName": 38,
                    "ConfigAssetURL": 39,
                    "ConfigAssetMetadataHash": 40,
                    "ConfigAssetManager": 41,
                    "ConfigAssetReserve": 42,
                    "ConfigAssetFreeze": 43,
                    "ConfigAssetClawback": 44,
                    "FreezeAsset": 45,
                    "FreezeAssetAccount": 46,
                    "FreezeAssetFrozen": 47,
                    "Assets": 48,
                    "NumAssets": 49,
                    "Applications": 50,
                    "NumApplications": 51,
                    "GlobalNumUint": 52,
                    "GlobalNumByteSlice": 53,
                    "LocalNumUint": 54,
                    "LocalNumByteSlice": 55,
                    "ExtraProgramPages": 56,
                    "Nonparticipation": 57,
                    "Logs": 58,
                    "NumLogs": 59,
                    "CreatedAssetID": 60,
                    "CreatedApplicationID": 61,
                    "LastLog": 62,
                    "StateProofPK": 63,
                    "ApprovalProgramPages": 64,
                    "NumApprovalProgramPages": 65,
                    "ClearStateProgramPages": 66,
                    "NumClearStateProgramPages": 67,
                }
            )
        ],
    ),
    "global": OpSpec(
        name="global",
        code=50,
        immediates=[
            ImmediateEnum(
                codes={
                    "MinTxnFee": 0,
                    "MinBalance": 1,
                    "MaxTxnLife": 2,
                    "ZeroAddress": 3,
                    "GroupSize": 4,
                    "LogicSigVersion": 5,
                    "Round": 6,
                    "LatestTimestamp": 7,
                    "CurrentApplicationID": 8,
                    "CreatorAddress": 9,
                    "CurrentApplicationAddress": 10,
                    "GroupID": 11,
                    "OpcodeBudget": 12,
                    "CallerApplicationID": 13,
                    "CallerApplicationAddress": 14,
                    "AssetCreateMinBalance": 15,
                    "AssetOptInMinBalance": 16,
                    "GenesisHash": 17,
                    "PayoutsEnabled": 18,
                    "PayoutsGoOnlineFee": 19,
                    "PayoutsPercent": 20,
                    "PayoutsMinBalance": 21,
                    "PayoutsMaxBalance": 22,
                }
            )
        ],
    ),
    "gtxn": OpSpec(
        name="gtxn",
        code=51,
        immediates=[
            ImmediateKind.uint8,
            ImmediateEnum(
                codes={
                    "Sender": 0,
                    "Fee": 1,
                    "FirstValid": 2,
                    "FirstValidTime": 3,
                    "LastValid": 4,
                    "Note": 5,
                    "Lease": 6,
                    "Receiver": 7,
                    "Amount": 8,
                    "CloseRemainderTo": 9,
                    "VotePK": 10,
                    "SelectionPK": 11,
                    "VoteFirst": 12,
                    "VoteLast": 13,
                    "VoteKeyDilution": 14,
                    "Type": 15,
                    "TypeEnum": 16,
                    "XferAsset": 17,
                    "AssetAmount": 18,
                    "AssetSender": 19,
                    "AssetReceiver": 20,
                    "AssetCloseTo": 21,
                    "GroupIndex": 22,
                    "TxID": 23,
                    "ApplicationID": 24,
                    "OnCompletion": 25,
                    "ApplicationArgs": 26,
                    "NumAppArgs": 27,
                    "Accounts": 28,
                    "NumAccounts": 29,
                    "ApprovalProgram": 30,
                    "ClearStateProgram": 31,
                    "RekeyTo": 32,
                    "ConfigAsset": 33,
                    "ConfigAssetTotal": 34,
                    "ConfigAssetDecimals": 35,
                    "ConfigAssetDefaultFrozen": 36,
                    "ConfigAssetUnitName": 37,
                    "ConfigAssetName": 38,
                    "ConfigAssetURL": 39,
                    "ConfigAssetMetadataHash": 40,
                    "ConfigAssetManager": 41,
                    "ConfigAssetReserve": 42,
                    "ConfigAssetFreeze": 43,
                    "ConfigAssetClawback": 44,
                    "FreezeAsset": 45,
                    "FreezeAssetAccount": 46,
                    "FreezeAssetFrozen": 47,
                    "Assets": 48,
                    "NumAssets": 49,
                    "Applications": 50,
                    "NumApplications": 51,
                    "GlobalNumUint": 52,
                    "GlobalNumByteSlice": 53,
                    "LocalNumUint": 54,
                    "LocalNumByteSlice": 55,
                    "ExtraProgramPages": 56,
                    "Nonparticipation": 57,
                    "Logs": 58,
                    "NumLogs": 59,
                    "CreatedAssetID": 60,
                    "CreatedApplicationID": 61,
                    "LastLog": 62,
                    "StateProofPK": 63,
                    "ApprovalProgramPages": 64,
                    "NumApprovalProgramPages": 65,
                    "ClearStateProgramPages": 66,
                    "NumClearStateProgramPages": 67,
                }
            ),
        ],
    ),
    "load": OpSpec(name="load", code=52, immediates=[ImmediateKind.uint8]),
    "store": OpSpec(name="store", code=53, immediates=[ImmediateKind.uint8]),
    "txna": OpSpec(
        name="txna",
        code=54,
        immediates=[
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            ),
            ImmediateKind.uint8,
        ],
    ),
    "gtxna": OpSpec(
        name="gtxna",
        code=55,
        immediates=[
            ImmediateKind.uint8,
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            ),
            ImmediateKind.uint8,
        ],
    ),
    "gtxns": OpSpec(
        name="gtxns",
        code=56,
        immediates=[
            ImmediateEnum(
                codes={
                    "Sender": 0,
                    "Fee": 1,
                    "FirstValid": 2,
                    "FirstValidTime": 3,
                    "LastValid": 4,
                    "Note": 5,
                    "Lease": 6,
                    "Receiver": 7,
                    "Amount": 8,
                    "CloseRemainderTo": 9,
                    "VotePK": 10,
                    "SelectionPK": 11,
                    "VoteFirst": 12,
                    "VoteLast": 13,
                    "VoteKeyDilution": 14,
                    "Type": 15,
                    "TypeEnum": 16,
                    "XferAsset": 17,
                    "AssetAmount": 18,
                    "AssetSender": 19,
                    "AssetReceiver": 20,
                    "AssetCloseTo": 21,
                    "GroupIndex": 22,
                    "TxID": 23,
                    "ApplicationID": 24,
                    "OnCompletion": 25,
                    "ApplicationArgs": 26,
                    "NumAppArgs": 27,
                    "Accounts": 28,
                    "NumAccounts": 29,
                    "ApprovalProgram": 30,
                    "ClearStateProgram": 31,
                    "RekeyTo": 32,
                    "ConfigAsset": 33,
                    "ConfigAssetTotal": 34,
                    "ConfigAssetDecimals": 35,
                    "ConfigAssetDefaultFrozen": 36,
                    "ConfigAssetUnitName": 37,
                    "ConfigAssetName": 38,
                    "ConfigAssetURL": 39,
                    "ConfigAssetMetadataHash": 40,
                    "ConfigAssetManager": 41,
                    "ConfigAssetReserve": 42,
                    "ConfigAssetFreeze": 43,
                    "ConfigAssetClawback": 44,
                    "FreezeAsset": 45,
                    "FreezeAssetAccount": 46,
                    "FreezeAssetFrozen": 47,
                    "Assets": 48,
                    "NumAssets": 49,
                    "Applications": 50,
                    "NumApplications": 51,
                    "GlobalNumUint": 52,
                    "GlobalNumByteSlice": 53,
                    "LocalNumUint": 54,
                    "LocalNumByteSlice": 55,
                    "ExtraProgramPages": 56,
                    "Nonparticipation": 57,
                    "Logs": 58,
                    "NumLogs": 59,
                    "CreatedAssetID": 60,
                    "CreatedApplicationID": 61,
                    "LastLog": 62,
                    "StateProofPK": 63,
                    "ApprovalProgramPages": 64,
                    "NumApprovalProgramPages": 65,
                    "ClearStateProgramPages": 66,
                    "NumClearStateProgramPages": 67,
                }
            )
        ],
    ),
    "gtxnsa": OpSpec(
        name="gtxnsa",
        code=57,
        immediates=[
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            ),
            ImmediateKind.uint8,
        ],
    ),
    "gload": OpSpec(name="gload", code=58, immediates=[ImmediateKind.uint8, ImmediateKind.uint8]),
    "gloads": OpSpec(name="gloads", code=59, immediates=[ImmediateKind.uint8]),
    "gaid": OpSpec(name="gaid", code=60, immediates=[ImmediateKind.uint8]),
    "gaids": OpSpec(name="gaids", code=61, immediates=[]),
    "loads": OpSpec(name="loads", code=62, immediates=[]),
    "stores": OpSpec(name="stores", code=63, immediates=[]),
    "bnz": OpSpec(name="bnz", code=64, immediates=[ImmediateKind.label]),
    "bz": OpSpec(name="bz", code=65, immediates=[ImmediateKind.label]),
    "b": OpSpec(name="b", code=66, immediates=[ImmediateKind.label]),
    "return": OpSpec(name="return", code=67, immediates=[]),
    "assert": OpSpec(name="assert", code=68, immediates=[]),
    "bury": OpSpec(name="bury", code=69, immediates=[ImmediateKind.uint8]),
    "popn": OpSpec(name="popn", code=70, immediates=[ImmediateKind.uint8]),
    "dupn": OpSpec(name="dupn", code=71, immediates=[ImmediateKind.uint8]),
    "pop": OpSpec(name="pop", code=72, immediates=[]),
    "dup": OpSpec(name="dup", code=73, immediates=[]),
    "dup2": OpSpec(name="dup2", code=74, immediates=[]),
    "dig": OpSpec(name="dig", code=75, immediates=[ImmediateKind.uint8]),
    "swap": OpSpec(name="swap", code=76, immediates=[]),
    "select": OpSpec(name="select", code=77, immediates=[]),
    "cover": OpSpec(name="cover", code=78, immediates=[ImmediateKind.uint8]),
    "uncover": OpSpec(name="uncover", code=79, immediates=[ImmediateKind.uint8]),
    "concat": OpSpec(name="concat", code=80, immediates=[]),
    "substring": OpSpec(
        name="substring", code=81, immediates=[ImmediateKind.uint8, ImmediateKind.uint8]
    ),
    "substring3": OpSpec(name="substring3", code=82, immediates=[]),
    "getbit": OpSpec(name="getbit", code=83, immediates=[]),
    "setbit": OpSpec(name="setbit", code=84, immediates=[]),
    "getbyte": OpSpec(name="getbyte", code=85, immediates=[]),
    "setbyte": OpSpec(name="setbyte", code=86, immediates=[]),
    "extract": OpSpec(
        name="extract", code=87, immediates=[ImmediateKind.uint8, ImmediateKind.uint8]
    ),
    "extract3": OpSpec(name="extract3", code=88, immediates=[]),
    "extract_uint16": OpSpec(name="extract_uint16", code=89, immediates=[]),
    "extract_uint32": OpSpec(name="extract_uint32", code=90, immediates=[]),
    "extract_uint64": OpSpec(name="extract_uint64", code=91, immediates=[]),
    "replace2": OpSpec(name="replace2", code=92, immediates=[ImmediateKind.uint8]),
    "replace3": OpSpec(name="replace3", code=93, immediates=[]),
    "base64_decode": OpSpec(
        name="base64_decode",
        code=94,
        immediates=[ImmediateEnum(codes={"URLEncoding": 0, "StdEncoding": 1})],
    ),
    "json_ref": OpSpec(
        name="json_ref",
        code=95,
        immediates=[ImmediateEnum(codes={"JSONString": 0, "JSONUint64": 1, "JSONObject": 2})],
    ),
    "balance": OpSpec(name="balance", code=96, immediates=[]),
    "app_opted_in": OpSpec(name="app_opted_in", code=97, immediates=[]),
    "app_local_get": OpSpec(name="app_local_get", code=98, immediates=[]),
    "app_local_get_ex": OpSpec(name="app_local_get_ex", code=99, immediates=[]),
    "app_global_get": OpSpec(name="app_global_get", code=100, immediates=[]),
    "app_global_get_ex": OpSpec(name="app_global_get_ex", code=101, immediates=[]),
    "app_local_put": OpSpec(name="app_local_put", code=102, immediates=[]),
    "app_global_put": OpSpec(name="app_global_put", code=103, immediates=[]),
    "app_local_del": OpSpec(name="app_local_del", code=104, immediates=[]),
    "app_global_del": OpSpec(name="app_global_del", code=105, immediates=[]),
    "asset_holding_get": OpSpec(
        name="asset_holding_get",
        code=112,
        immediates=[ImmediateEnum(codes={"AssetBalance": 0, "AssetFrozen": 1})],
    ),
    "asset_params_get": OpSpec(
        name="asset_params_get",
        code=113,
        immediates=[
            ImmediateEnum(
                codes={
                    "AssetTotal": 0,
                    "AssetDecimals": 1,
                    "AssetDefaultFrozen": 2,
                    "AssetUnitName": 3,
                    "AssetName": 4,
                    "AssetURL": 5,
                    "AssetMetadataHash": 6,
                    "AssetManager": 7,
                    "AssetReserve": 8,
                    "AssetFreeze": 9,
                    "AssetClawback": 10,
                    "AssetCreator": 11,
                }
            )
        ],
    ),
    "app_params_get": OpSpec(
        name="app_params_get",
        code=114,
        immediates=[
            ImmediateEnum(
                codes={
                    "AppApprovalProgram": 0,
                    "AppClearStateProgram": 1,
                    "AppGlobalNumUint": 2,
                    "AppGlobalNumByteSlice": 3,
                    "AppLocalNumUint": 4,
                    "AppLocalNumByteSlice": 5,
                    "AppExtraProgramPages": 6,
                    "AppCreator": 7,
                    "AppAddress": 8,
                }
            )
        ],
    ),
    "acct_params_get": OpSpec(
        name="acct_params_get",
        code=115,
        immediates=[
            ImmediateEnum(
                codes={
                    "AcctBalance": 0,
                    "AcctMinBalance": 1,
                    "AcctAuthAddr": 2,
                    "AcctTotalNumUint": 3,
                    "AcctTotalNumByteSlice": 4,
                    "AcctTotalExtraAppPages": 5,
                    "AcctTotalAppsCreated": 6,
                    "AcctTotalAppsOptedIn": 7,
                    "AcctTotalAssetsCreated": 8,
                    "AcctTotalAssets": 9,
                    "AcctTotalBoxes": 10,
                    "AcctTotalBoxBytes": 11,
                    "AcctIncentiveEligible": 12,
                    "AcctLastProposed": 13,
                    "AcctLastHeartbeat": 14,
                }
            )
        ],
    ),
    "voter_params_get": OpSpec(
        name="voter_params_get",
        code=116,
        immediates=[ImmediateEnum(codes={"VoterBalance": 0, "VoterIncentiveEligible": 1})],
    ),
    "online_stake": OpSpec(name="online_stake", code=117, immediates=[]),
    "min_balance": OpSpec(name="min_balance", code=120, immediates=[]),
    "pushbytes": OpSpec(name="pushbytes", code=128, immediates=[ImmediateKind.bytes]),
    "pushint": OpSpec(name="pushint", code=129, immediates=[ImmediateKind.varuint]),
    "pushbytess": OpSpec(name="pushbytess", code=130, immediates=[ImmediateKind.bytes_array]),
    "pushints": OpSpec(name="pushints", code=131, immediates=[ImmediateKind.varuint_array]),
    "ed25519verify_bare": OpSpec(name="ed25519verify_bare", code=132, immediates=[]),
    "falcon_verify": OpSpec(name="falcon_verify", code=133, immediates=[]),
    "sumhash512": OpSpec(name="sumhash512", code=134, immediates=[]),
    "callsub": OpSpec(name="callsub", code=136, immediates=[ImmediateKind.label]),
    "retsub": OpSpec(name="retsub", code=137, immediates=[]),
    "proto": OpSpec(name="proto", code=138, immediates=[ImmediateKind.uint8, ImmediateKind.uint8]),
    "frame_dig": OpSpec(name="frame_dig", code=139, immediates=[ImmediateKind.int8]),
    "frame_bury": OpSpec(name="frame_bury", code=140, immediates=[ImmediateKind.int8]),
    "switch": OpSpec(name="switch", code=141, immediates=[ImmediateKind.label_array]),
    "match": OpSpec(name="match", code=142, immediates=[ImmediateKind.label_array]),
    "shl": OpSpec(name="shl", code=144, immediates=[]),
    "shr": OpSpec(name="shr", code=145, immediates=[]),
    "sqrt": OpSpec(name="sqrt", code=146, immediates=[]),
    "bitlen": OpSpec(name="bitlen", code=147, immediates=[]),
    "exp": OpSpec(name="exp", code=148, immediates=[]),
    "expw": OpSpec(name="expw", code=149, immediates=[]),
    "bsqrt": OpSpec(name="bsqrt", code=150, immediates=[]),
    "divw": OpSpec(name="divw", code=151, immediates=[]),
    "sha3_256": OpSpec(name="sha3_256", code=152, immediates=[]),
    "b+": OpSpec(name="b+", code=160, immediates=[]),
    "b-": OpSpec(name="b-", code=161, immediates=[]),
    "b/": OpSpec(name="b/", code=162, immediates=[]),
    "b*": OpSpec(name="b*", code=163, immediates=[]),
    "b<": OpSpec(name="b<", code=164, immediates=[]),
    "b>": OpSpec(name="b>", code=165, immediates=[]),
    "b<=": OpSpec(name="b<=", code=166, immediates=[]),
    "b>=": OpSpec(name="b>=", code=167, immediates=[]),
    "b==": OpSpec(name="b==", code=168, immediates=[]),
    "b!=": OpSpec(name="b!=", code=169, immediates=[]),
    "b%": OpSpec(name="b%", code=170, immediates=[]),
    "b|": OpSpec(name="b|", code=171, immediates=[]),
    "b&": OpSpec(name="b&", code=172, immediates=[]),
    "b^": OpSpec(name="b^", code=173, immediates=[]),
    "b~": OpSpec(name="b~", code=174, immediates=[]),
    "bzero": OpSpec(name="bzero", code=175, immediates=[]),
    "log": OpSpec(name="log", code=176, immediates=[]),
    "itxn_begin": OpSpec(name="itxn_begin", code=177, immediates=[]),
    "itxn_field": OpSpec(
        name="itxn_field",
        code=178,
        immediates=[
            ImmediateEnum(
                codes={
                    "Sender": 0,
                    "Fee": 1,
                    "Note": 5,
                    "Receiver": 7,
                    "Amount": 8,
                    "CloseRemainderTo": 9,
                    "VotePK": 10,
                    "SelectionPK": 11,
                    "VoteFirst": 12,
                    "VoteLast": 13,
                    "VoteKeyDilution": 14,
                    "Type": 15,
                    "TypeEnum": 16,
                    "XferAsset": 17,
                    "AssetAmount": 18,
                    "AssetSender": 19,
                    "AssetReceiver": 20,
                    "AssetCloseTo": 21,
                    "ApplicationID": 24,
                    "OnCompletion": 25,
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "ApprovalProgram": 30,
                    "ClearStateProgram": 31,
                    "RekeyTo": 32,
                    "ConfigAsset": 33,
                    "ConfigAssetTotal": 34,
                    "ConfigAssetDecimals": 35,
                    "ConfigAssetDefaultFrozen": 36,
                    "ConfigAssetUnitName": 37,
                    "ConfigAssetName": 38,
                    "ConfigAssetURL": 39,
                    "ConfigAssetMetadataHash": 40,
                    "ConfigAssetManager": 41,
                    "ConfigAssetReserve": 42,
                    "ConfigAssetFreeze": 43,
                    "ConfigAssetClawback": 44,
                    "FreezeAsset": 45,
                    "FreezeAssetAccount": 46,
                    "FreezeAssetFrozen": 47,
                    "Assets": 48,
                    "Applications": 50,
                    "GlobalNumUint": 52,
                    "GlobalNumByteSlice": 53,
                    "LocalNumUint": 54,
                    "LocalNumByteSlice": 55,
                    "ExtraProgramPages": 56,
                    "Nonparticipation": 57,
                    "StateProofPK": 63,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            )
        ],
    ),
    "itxn_submit": OpSpec(name="itxn_submit", code=179, immediates=[]),
    "itxn": OpSpec(
        name="itxn",
        code=180,
        immediates=[
            ImmediateEnum(
                codes={
                    "Sender": 0,
                    "Fee": 1,
                    "FirstValid": 2,
                    "FirstValidTime": 3,
                    "LastValid": 4,
                    "Note": 5,
                    "Lease": 6,
                    "Receiver": 7,
                    "Amount": 8,
                    "CloseRemainderTo": 9,
                    "VotePK": 10,
                    "SelectionPK": 11,
                    "VoteFirst": 12,
                    "VoteLast": 13,
                    "VoteKeyDilution": 14,
                    "Type": 15,
                    "TypeEnum": 16,
                    "XferAsset": 17,
                    "AssetAmount": 18,
                    "AssetSender": 19,
                    "AssetReceiver": 20,
                    "AssetCloseTo": 21,
                    "GroupIndex": 22,
                    "TxID": 23,
                    "ApplicationID": 24,
                    "OnCompletion": 25,
                    "ApplicationArgs": 26,
                    "NumAppArgs": 27,
                    "Accounts": 28,
                    "NumAccounts": 29,
                    "ApprovalProgram": 30,
                    "ClearStateProgram": 31,
                    "RekeyTo": 32,
                    "ConfigAsset": 33,
                    "ConfigAssetTotal": 34,
                    "ConfigAssetDecimals": 35,
                    "ConfigAssetDefaultFrozen": 36,
                    "ConfigAssetUnitName": 37,
                    "ConfigAssetName": 38,
                    "ConfigAssetURL": 39,
                    "ConfigAssetMetadataHash": 40,
                    "ConfigAssetManager": 41,
                    "ConfigAssetReserve": 42,
                    "ConfigAssetFreeze": 43,
                    "ConfigAssetClawback": 44,
                    "FreezeAsset": 45,
                    "FreezeAssetAccount": 46,
                    "FreezeAssetFrozen": 47,
                    "Assets": 48,
                    "NumAssets": 49,
                    "Applications": 50,
                    "NumApplications": 51,
                    "GlobalNumUint": 52,
                    "GlobalNumByteSlice": 53,
                    "LocalNumUint": 54,
                    "LocalNumByteSlice": 55,
                    "ExtraProgramPages": 56,
                    "Nonparticipation": 57,
                    "Logs": 58,
                    "NumLogs": 59,
                    "CreatedAssetID": 60,
                    "CreatedApplicationID": 61,
                    "LastLog": 62,
                    "StateProofPK": 63,
                    "ApprovalProgramPages": 64,
                    "NumApprovalProgramPages": 65,
                    "ClearStateProgramPages": 66,
                    "NumClearStateProgramPages": 67,
                }
            )
        ],
    ),
    "itxna": OpSpec(
        name="itxna",
        code=181,
        immediates=[
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            ),
            ImmediateKind.uint8,
        ],
    ),
    "itxn_next": OpSpec(name="itxn_next", code=182, immediates=[]),
    "gitxn": OpSpec(
        name="gitxn",
        code=183,
        immediates=[
            ImmediateKind.uint8,
            ImmediateEnum(
                codes={
                    "Sender": 0,
                    "Fee": 1,
                    "FirstValid": 2,
                    "FirstValidTime": 3,
                    "LastValid": 4,
                    "Note": 5,
                    "Lease": 6,
                    "Receiver": 7,
                    "Amount": 8,
                    "CloseRemainderTo": 9,
                    "VotePK": 10,
                    "SelectionPK": 11,
                    "VoteFirst": 12,
                    "VoteLast": 13,
                    "VoteKeyDilution": 14,
                    "Type": 15,
                    "TypeEnum": 16,
                    "XferAsset": 17,
                    "AssetAmount": 18,
                    "AssetSender": 19,
                    "AssetReceiver": 20,
                    "AssetCloseTo": 21,
                    "GroupIndex": 22,
                    "TxID": 23,
                    "ApplicationID": 24,
                    "OnCompletion": 25,
                    "ApplicationArgs": 26,
                    "NumAppArgs": 27,
                    "Accounts": 28,
                    "NumAccounts": 29,
                    "ApprovalProgram": 30,
                    "ClearStateProgram": 31,
                    "RekeyTo": 32,
                    "ConfigAsset": 33,
                    "ConfigAssetTotal": 34,
                    "ConfigAssetDecimals": 35,
                    "ConfigAssetDefaultFrozen": 36,
                    "ConfigAssetUnitName": 37,
                    "ConfigAssetName": 38,
                    "ConfigAssetURL": 39,
                    "ConfigAssetMetadataHash": 40,
                    "ConfigAssetManager": 41,
                    "ConfigAssetReserve": 42,
                    "ConfigAssetFreeze": 43,
                    "ConfigAssetClawback": 44,
                    "FreezeAsset": 45,
                    "FreezeAssetAccount": 46,
                    "FreezeAssetFrozen": 47,
                    "Assets": 48,
                    "NumAssets": 49,
                    "Applications": 50,
                    "NumApplications": 51,
                    "GlobalNumUint": 52,
                    "GlobalNumByteSlice": 53,
                    "LocalNumUint": 54,
                    "LocalNumByteSlice": 55,
                    "ExtraProgramPages": 56,
                    "Nonparticipation": 57,
                    "Logs": 58,
                    "NumLogs": 59,
                    "CreatedAssetID": 60,
                    "CreatedApplicationID": 61,
                    "LastLog": 62,
                    "StateProofPK": 63,
                    "ApprovalProgramPages": 64,
                    "NumApprovalProgramPages": 65,
                    "ClearStateProgramPages": 66,
                    "NumClearStateProgramPages": 67,
                }
            ),
        ],
    ),
    "gitxna": OpSpec(
        name="gitxna",
        code=184,
        immediates=[
            ImmediateKind.uint8,
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            ),
            ImmediateKind.uint8,
        ],
    ),
    "box_create": OpSpec(name="box_create", code=185, immediates=[]),
    "box_extract": OpSpec(name="box_extract", code=186, immediates=[]),
    "box_replace": OpSpec(name="box_replace", code=187, immediates=[]),
    "box_del": OpSpec(name="box_del", code=188, immediates=[]),
    "box_len": OpSpec(name="box_len", code=189, immediates=[]),
    "box_get": OpSpec(name="box_get", code=190, immediates=[]),
    "box_put": OpSpec(name="box_put", code=191, immediates=[]),
    "txnas": OpSpec(
        name="txnas",
        code=192,
        immediates=[
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            )
        ],
    ),
    "gtxnas": OpSpec(
        name="gtxnas",
        code=193,
        immediates=[
            ImmediateKind.uint8,
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            ),
        ],
    ),
    "gtxnsas": OpSpec(
        name="gtxnsas",
        code=194,
        immediates=[
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            )
        ],
    ),
    "args": OpSpec(name="args", code=195, immediates=[]),
    "gloadss": OpSpec(name="gloadss", code=196, immediates=[]),
    "itxnas": OpSpec(
        name="itxnas",
        code=197,
        immediates=[
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            )
        ],
    ),
    "gitxnas": OpSpec(
        name="gitxnas",
        code=198,
        immediates=[
            ImmediateKind.uint8,
            ImmediateEnum(
                codes={
                    "ApplicationArgs": 26,
                    "Accounts": 28,
                    "Assets": 48,
                    "Applications": 50,
                    "Logs": 58,
                    "ApprovalProgramPages": 64,
                    "ClearStateProgramPages": 66,
                }
            ),
        ],
    ),
    "vrf_verify": OpSpec(
        name="vrf_verify", code=208, immediates=[ImmediateEnum(codes={"VrfAlgorand": 0})]
    ),
    "block": OpSpec(
        name="block",
        code=209,
        immediates=[
            ImmediateEnum(
                codes={
                    "BlkSeed": 0,
                    "BlkTimestamp": 1,
                    "BlkProposer": 2,
                    "BlkFeesCollected": 3,
                    "BlkBonus": 4,
                    "BlkBranch": 5,
                    "BlkFeeSink": 6,
                    "BlkProtocol": 7,
                    "BlkTxnCounter": 8,
                    "BlkProposerPayout": 9,
                }
            )
        ],
    ),
    "box_splice": OpSpec(name="box_splice", code=210, immediates=[]),
    "box_resize": OpSpec(name="box_resize", code=211, immediates=[]),
    "ec_add": OpSpec(
        name="ec_add",
        code=224,
        immediates=[
            ImmediateEnum(codes={"BN254g1": 0, "BN254g2": 1, "BLS12_381g1": 2, "BLS12_381g2": 3})
        ],
    ),
    "ec_scalar_mul": OpSpec(
        name="ec_scalar_mul",
        code=225,
        immediates=[
            ImmediateEnum(codes={"BN254g1": 0, "BN254g2": 1, "BLS12_381g1": 2, "BLS12_381g2": 3})
        ],
    ),
    "ec_pairing_check": OpSpec(
        name="ec_pairing_check",
        code=226,
        immediates=[
            ImmediateEnum(codes={"BN254g1": 0, "BN254g2": 1, "BLS12_381g1": 2, "BLS12_381g2": 3})
        ],
    ),
    "ec_multi_scalar_mul": OpSpec(
        name="ec_multi_scalar_mul",
        code=227,
        immediates=[
            ImmediateEnum(codes={"BN254g1": 0, "BN254g2": 1, "BLS12_381g1": 2, "BLS12_381g2": 3})
        ],
    ),
    "ec_subgroup_check": OpSpec(
        name="ec_subgroup_check",
        code=228,
        immediates=[
            ImmediateEnum(codes={"BN254g1": 0, "BN254g2": 1, "BLS12_381g1": 2, "BLS12_381g2": 3})
        ],
    ),
    "ec_map_to": OpSpec(
        name="ec_map_to",
        code=229,
        immediates=[
            ImmediateEnum(codes={"BN254g1": 0, "BN254g2": 1, "BLS12_381g1": 2, "BLS12_381g2": 3})
        ],
    ),
    "mimc": OpSpec(
        name="mimc",
        code=230,
        immediates=[ImmediateEnum(codes={"BN254Mp110": 0, "BLS12_381Mp111": 1})],
    ),
}


import enum
from collections.abc import Mapping

import attrs

class ImmediateKind(enum.StrEnum):
    uint8 = enum.auto()
    int8 = enum.auto()
    label = enum.auto()
    varuint = enum.auto()
    bytes = enum.auto()

    # array types
    label_array = enum.auto()
    varuint_array = enum.auto()
    bytes_array = enum.auto()

    def __repr__(self) -> str:
        return f"{type(self).__name__}.{self.name}"

@attrs.frozen
class ImmediateEnum:
    codes: Mapping[str, int]

@attrs.frozen
class OpSpec:
    name: str
    code: int
    immediates: list[ImmediateKind | ImmediateEnum]


import base64
import contextlib
import functools
import math
import os
import typing
from collections.abc import Callable, Iterable, Iterator, MutableMapping, MutableSet, Sequence, Set
from pathlib import Path

import attrs

from puya.algo_constants import (
    ADDRESS_CHECKSUM_LENGTH,
    ENCODED_ADDRESS_LENGTH,
    MAX_APP_PAGE_SIZE,
    MAX_BYTES_LENGTH,
    MAX_UINT64,
    PUBLIC_KEY_HASH_LENGTH,
)

@attrs.frozen
class Address:
    address: str
    public_key: bytes = b""
    check_sum: bytes = b""
    is_valid: bool = False

    @classmethod
    def from_public_key(cls, public_key: bytes) -> typing.Self:
        check_sum = sha512_256_hash(public_key)[-ADDRESS_CHECKSUM_LENGTH:]
        address_bytes = public_key + check_sum
        address = base64.b32encode(address_bytes).decode("utf8").rstrip("=")
        assert len(address) == ENCODED_ADDRESS_LENGTH
        return cls(
            address=address,
            public_key=public_key,
            check_sum=check_sum,
            is_valid=True,
        )

    @classmethod
    def parse(cls, address: str) -> typing.Self:
        # Pad address so it's a valid b32 string
        padded_address = address + (6 * "=")
        if not (len(address) == ENCODED_ADDRESS_LENGTH and valid_base32(padded_address)):
            return cls(address)
        address_bytes = base64.b32decode(padded_address)
        if len(address_bytes) != PUBLIC_KEY_HASH_LENGTH + ADDRESS_CHECKSUM_LENGTH:
            return cls(address)

        public_key_hash = address_bytes[:PUBLIC_KEY_HASH_LENGTH]
        check_sum = address_bytes[PUBLIC_KEY_HASH_LENGTH:]
        verified_address = cls.from_public_key(public_key_hash)
        return cls(
            address=address,
            public_key=public_key_hash,
            check_sum=check_sum,
            is_valid=verified_address.check_sum == check_sum,
        )

def valid_base32(s: str) -> bool:
    """check if s is a valid base32 encoding string and fits into AVM bytes type"""
    try:
        value = base64.b32decode(s)
    except ValueError:
        return False
    return valid_bytes(value)
    # regex from PyTEAL, appears to be RFC-4648
    # ^(?:[A-Z2-7]{8})*(?:([A-Z2-7]{2}([=]{6})?)|([A-Z2-7]{4}([=]{4})?)|([A-Z2-7]{5}([=]{3})?)|([A-Z2-7]{7}([=]{1})?))?  # noqa: E501

def valid_base16(s: str) -> bool:
    try:
        value = base64.b16decode(s)
    except ValueError:
        return False
    return valid_bytes(value)

def valid_base64(s: str) -> bool:
    """check if s is a valid base64 encoding string and fits into AVM bytes type"""
    try:
        value = base64.b64decode(s, validate=True)
    except ValueError:
        return False
    return valid_bytes(value)
    # regex from PyTEAL, appears to be RFC-4648
    # ^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$

def valid_bytes(value: bytes) -> bool:
    return len(value) <= MAX_BYTES_LENGTH

def valid_int64(value: int) -> bool:
    return bool(0 <= value <= MAX_UINT64)

def valid_address(address: str) -> bool:
    """check if address is a valid address with checksum"""
    return Address.parse(address).is_valid

def sha512_256_hash(value: bytes) -> bytes:
    """
    Returns the SHA512/256 hash of a value. This is the hashing algorithm used
    to generate address checksums
    """
    from Cryptodome.Hash import SHA512

    sha = SHA512.new(truncate="256")
    sha.update(value)
    return sha.digest()

def method_selector_hash(method_signature: str) -> bytes:
    return sha512_256_hash(method_signature.encode("utf8"))[:4]

def attrs_extend[T: attrs.AttrsInstance](
    new_type: type[T], base_instance: attrs.AttrsInstance, **changes: object
) -> T:
    """Like attrs.evolve but allows creating a related type"""
    base_type = type(base_instance)
    old_type_fields = attrs.fields_dict(base_type)
    new_type_fields = attrs.fields(new_type)
    for a in new_type_fields:
        if not a.init:
            continue
        attr_name = a.name  # To deal with private attributes.
        init_name = a.alias
        if init_name not in changes and attr_name in old_type_fields:
            changes[init_name] = getattr(base_instance, attr_name)

    return new_type(**changes)

@functools.cache
def make_path_relative_to(*, to: Path, path: Path, walk_up: bool = False) -> str:
    with contextlib.suppress(ValueError):
        path = path.relative_to(to, walk_up=walk_up)
    return normalize_path(path)

def make_path_relative_to_cwd(path: Path) -> str:
    return make_path_relative_to(to=Path.cwd(), path=path)

def unique[T](items: Iterable[T]) -> list[T]:
    return list(dict.fromkeys(items))

class StableSet[T](MutableSet[T]):
    __slots__ = ("_data",)

    def __init__(self, *items: T) -> None:
        self._data = dict.fromkeys(items)

    @classmethod
    def from_iter(cls, items: Iterable[T]) -> "StableSet[T]":
        result = StableSet.__new__(StableSet)
        result._data = dict.fromkeys(items)  # noqa: SLF001
        return result

    def __eq__(self, other: object) -> bool:
        if isinstance(other, StableSet):
            return self._data.__eq__(other._data)
        else:
            return self._data.keys() == other

    def __ne__(self, other: object) -> bool:
        if isinstance(other, StableSet):
            return self._data.__ne__(other._data)
        else:
            return self._data.keys() != other

    def __contains__(self, x: object) -> bool:
        return self._data.__contains__(x)

    def __len__(self) -> int:
        return self._data.__len__()

    def __iter__(self) -> Iterator[T]:
        return self._data.__iter__()

    def add(self, value: T) -> None:
        self._data[value] = None

    def discard(self, value: T) -> None:
        self._data.pop(value, None)

    def intersection(self, other: Iterable[T]) -> "StableSet[T]":
        result = StableSet.__new__(StableSet)
        result._data = dict.fromkeys(k for k in self._data if k in other)  # noqa: SLF001
        return result

    def __or__(self, other: Iterable[T]) -> "StableSet[T]":  # type: ignore[override]
        result = StableSet.__new__(StableSet)
        if isinstance(other, StableSet):
            other_data = other._data
        else:
            other_data = dict.fromkeys(other)
        result._data = self._data | other_data
        return result

    def __ior__(self, other: Iterable[T]) -> typing.Self:  # type: ignore[override]
        if isinstance(other, StableSet):
            other_data = other._data
        else:
            other_data = dict.fromkeys(other)
        self._data |= other_data
        return self

    def __sub__(self, other: Set[T]) -> "StableSet[T]":
        result = StableSet.__new__(StableSet)
        if isinstance(other, StableSet):
            data: Iterable[T] = self._data.keys() - other._data.keys()
        else:
            data = (k for k in self._data if k not in other)
        result._data = dict.fromkeys(data)
        return result

    def __repr__(self) -> str:
        return type(self).__name__ + "(" + ", ".join(map(repr, self._data)) + ")"

def lazy_setdefault[T, U](m: MutableMapping[T, U], /, key: T, default: Callable[[T], U]) -> U:
    """dict.setdefault, but with a callable"""
    try:
        return m[key]
    except KeyError:
        pass
    value = default(key)
    m[key] = value
    return value

_INVERT_ORDERED_BINARY_OP = str.maketrans("<>", "><")

def invert_ordered_binary_op(op: str) -> str:
    return op.translate(_INVERT_ORDERED_BINARY_OP)

def clamp(value: int, *, low: int, high: int) -> int:
    if value < low:
        return low
    if value > high:
        return high
    return value

def bits_to_bytes(bit_size: int) -> int:
    return int(math.ceil(bit_size / 8))

def round_bits_to_nearest_bytes(bit_size: int) -> int:
    return bits_to_bytes(bit_size) * 8

@contextlib.contextmanager
def pushd(new_dir: Path) -> Iterator[None]:
    orig_dir = Path.cwd()
    os.chdir(new_dir)
    try:
        yield
    finally:
        os.chdir(orig_dir)

def normalise_path_to_str(path: Path) -> str:
    return str(path).replace("\\", "/")

def biguint_bytes_eval(value: int) -> bytes:
    byte_length = math.ceil(value.bit_length() / 8.0)
    assert byte_length <= 64, "Biguints must be 64 bytes or less"
    big_uint_bytes = value.to_bytes(byteorder="big", length=byte_length)
    return big_uint_bytes

def calculate_extra_program_pages(approval_program_length: int, clear_program_length: int) -> int:
    total_bytes = approval_program_length + clear_program_length
    return (total_bytes - 1) // MAX_APP_PAGE_SIZE

@typing.overload
def coalesce[T](arg1: T | None, arg2: T, /) -> T: ...

@typing.overload
def coalesce[T](arg1: T | None, arg2: T | None, arg3: T, /) -> T: ...

@typing.overload
def coalesce[T](*args: T | None) -> T | None: ...

def coalesce[T](*args: T | None) -> T | None:
    """Shorthand for `a if a is not None else b`, with eager evaluation as a tradeoff"""
    # REFACTOR: if there's a better way to do the above overloads, we should.
    #           the problem is you can't have a positional argument after *args,
    #           and we want to take the last one's type separately
    for arg in args:
        if arg is not None:
            return arg
    return None

def positive_index[T](idx: int, seq: Sequence[T]) -> int:
    return idx if idx >= 0 else len(seq) + idx

def set_add[T](set_: MutableSet[T], value: T) -> bool:
    """ensure item exists in a set, returning if it was added or not"""
    added = value not in set_
    set_.add(value)
    return added

def set_remove[T](set_: MutableSet[T], value: T) -> bool:
    removed = value in set_
    set_.discard(value)
    return removed

def normalize_path(path: Path) -> str:
    return str(path).replace(os.sep, "/")


import pathlib
import sys

_VENDOR_PATH = pathlib.Path(__file__).parent / "_vendor"

sys.path.insert(0, str(_VENDOR_PATH))


import argparse
import typing
from collections.abc import Sequence
from importlib.metadata import version
from pathlib import Path

from puya.algo_constants import MAINNET_AVM_VERSION, SUPPORTED_AVM_VERSIONS
from puya.log import LogLevel, configure_logging
from puya.options import LocalsCoalescingStrategy
from puyapy.compile import compile_to_teal
from puyapy.options import PuyaPyOptions
from puyapy.template import parse_template_key_value

def main() -> None:
    parser = argparse.ArgumentParser(
        prog="puyapy",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument("--version", action="version", version=f"%(prog)s {version('puyapy')}")
    parser.add_argument(
        "-O",
        "--optimization-level",
        type=int,
        choices=[0, 1, 2],
        default=1,
        help="Set optimization level of output TEAL / AVM bytecode",
    )
    parser.add_argument(
        "--output-teal",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="Output TEAL code",
    )
    parser.add_argument(
        "--output-source-map",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="Output debug source maps",
    )
    parser.add_argument(
        "--output-arc32",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="Output {contract}.arc32.json ARC-32 app spec file",
    )
    parser.add_argument(
        "--output-arc56",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output {contract}.arc56.json ARC-56 app spec file",
    )
    parser.add_argument(
        "--output-client",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output Algorand Python contract client for typed ARC4 ABI calls",
    )
    parser.add_argument(
        "--out-dir", type=Path, help="Path for outputting artefacts", default=False
    )
    parser.add_argument(
        "--log-level",
        type=LogLevel.from_string,
        choices=list(LogLevel),
        default=LogLevel.info,
        help="Minimum level to log to console",
    )
    parser.add_argument(
        "-g",  # -g chosen because it is the same option for debug in gcc
        "--debug-level",
        type=int,
        choices=[0, 1, 2],
        default=1,
        help="Output debug information level, 0 = none, 1 = debug, 2 = reserved for future use",
    )
    parser.add_argument(
        "--output-awst",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output parsed result of AWST",
    )
    parser.add_argument(
        "--output-awst-json",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output parsed result of AWST as JSON",
    )
    parser.add_argument(
        "--output-ssa-ir",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output IR (in SSA form) before optimisations",
    )
    parser.add_argument(
        "--output-optimization-ir",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output IR after each optimization",
    )
    parser.add_argument(
        "--output-destructured-ir",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output IR after SSA destructuring and before MIR",
    )
    parser.add_argument(
        "--output-memory-ir",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output MIR before lowering to TealOps",
    )
    parser.add_argument(
        "--output-bytecode",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="Output AVM bytecode",
    )
    parser.add_argument(
        "--match-algod-bytecode",
        action=_EmitDeprecated,
        dest=argparse.SUPPRESS,
        nargs=0,
        help="Deprecated: When outputting bytecode, ensure bytecode matches algod output",
    )
    parser.add_argument(
        "-T",
        "--template-var",
        dest="cli_template_definitions",
        metavar="VAR=VALUE",
        action=_ParseAndStoreTemplateVar,
        default={},
        nargs="+",
        help="Define template vars for use when assembling via --output-bytecode"
        " should be specified without the prefix (see --template-vars-prefix), e.g."
        ' -T SOME_INT=1234 SOME_BYTES=0x1A2B SOME_BOOL=True SOME_STR=\\"hello\\"',
    )
    parser.add_argument(
        "--template-vars-prefix",
        help="Define the prefix to use with --template-var",
        default="TMPL_",
    )
    parser.add_argument(
        "--target-avm-version",
        type=int,
        choices=SUPPORTED_AVM_VERSIONS,
        default=MAINNET_AVM_VERSION,
    )
    parser.add_argument(
        "--locals-coalescing-strategy",
        type=LocalsCoalescingStrategy,
        choices=list(LocalsCoalescingStrategy),
        default=LocalsCoalescingStrategy.root_operand,
        help=(
            "Strategy choice for out-of-ssa local variable coalescing. "
            "The best choice for your app is best determined through experimentation"
        ),
    )

    parser.add_argument("paths", type=Path, nargs="+", metavar="PATH")

    namespace = parser.parse_args()
    options = PuyaPyOptions(**vars(namespace))
    configure_logging(min_log_level=options.log_level)
    compile_to_teal(options)

class _EmitDeprecated(argparse.Action):
    @typing.override
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[typing.Any] | None,
        option_string: str | None = None,
    ) -> None:
        print(f"warning: {option_string} is deprecated and no longer does anything")  # noqa: T201

class _ParseAndStoreTemplateVar(argparse.Action):
    @typing.override
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[typing.Any] | None,
        option_string: str | None = None,
    ) -> None:
        mapping: dict[str, int | bytes] = dict(getattr(namespace, self.dest, {}))
        lst = []
        if isinstance(values, str):
            lst = [values]
        elif values:
            for value in values:
                assert isinstance(value, str)
                lst.append(value)
        for kv in lst:
            try:
                name, value = parse_template_key_value(kv)
            except Exception as ex:
                parser.error(str(ex))
            mapping[name] = value
        setattr(namespace, self.dest, mapping)

if __name__ == "__main__":
    main()


"""Mypy type checker command line tool."""

from __future__ import annotations

import os
import sys
import traceback

from mypy.main import main, process_options
from mypy.util import FancyFormatter

def console_entry() -> None:
    try:
        main()
        sys.stdout.flush()
        sys.stderr.flush()
    except BrokenPipeError:
        # Python flushes standard streams on exit; redirect remaining output
        # to devnull to avoid another BrokenPipeError at shutdown
        devnull = os.open(os.devnull, os.O_WRONLY)
        os.dup2(devnull, sys.stdout.fileno())
        sys.exit(2)
    except KeyboardInterrupt:
        _, options = process_options(args=sys.argv[1:])
        if options.show_traceback:
            sys.stdout.write(traceback.format_exc())
        formatter = FancyFormatter(sys.stdout, sys.stderr, False)
        msg = "Interrupted\n"
        sys.stdout.write(formatter.style(msg, color="red", bold=True))
        sys.stdout.flush()
        sys.stderr.flush()
        sys.exit(2)

if __name__ == "__main__":
    console_entry()


"""This module makes it possible to use mypy as part of a Python application.

Since mypy still changes, the API was kept utterly simple and non-intrusive.
It just mimics command line activation without starting a new interpreter.
So the normal docs about the mypy command line apply.
Changes in the command line version of mypy will be immediately usable.

Just import this module and then call the 'run' function with a parameter of
type List[str], containing what normally would have been the command line
arguments to mypy.

Function 'run' returns a Tuple[str, str, int], namely
(<normal_report>, <error_report>, <exit_status>),
in which <normal_report> is what mypy normally writes to sys.stdout,
<error_report> is what mypy normally writes to sys.stderr and exit_status is
the exit status mypy normally returns to the operating system.

Any pretty formatting is left to the caller.

The 'run_dmypy' function is similar, but instead mimics invocation of
dmypy. Note that run_dmypy is not thread-safe and modifies sys.stdout
and sys.stderr during its invocation.

Note that these APIs don't support incremental generation of error
messages.

Trivial example of code using this module:

import sys
from mypy import api

result = api.run(sys.argv[1:])

if result[0]:
    print('\nType checking report:\n')
    print(result[0])  # stdout

if result[1]:
    print('\nError report:\n')
    print(result[1])  # stderr

print('\nExit status:', result[2])

"""

from __future__ import annotations

import sys
from io import StringIO
from typing import Callable, TextIO

def _run(main_wrapper: Callable[[TextIO, TextIO], None]) -> tuple[str, str, int]:
    stdout = StringIO()
    stderr = StringIO()

    try:
        main_wrapper(stdout, stderr)
        exit_status = 0
    except SystemExit as system_exit:
        assert isinstance(system_exit.code, int)
        exit_status = system_exit.code

    return stdout.getvalue(), stderr.getvalue(), exit_status

def run(args: list[str]) -> tuple[str, str, int]:
    # Lazy import to avoid needing to import all of mypy to call run_dmypy
    from mypy.main import main

    return _run(
        lambda stdout, stderr: main(args=args, stdout=stdout, stderr=stderr, clean_exit=True)
    )

def run_dmypy(args: list[str]) -> tuple[str, str, int]:
    from mypy.dmypy.client import main

    # A bunch of effort has been put into threading stdout and stderr
    # through the main API to avoid the threadsafety problems of
    # modifying sys.stdout/sys.stderr, but that hasn't been done for
    # the dmypy client, so we just do the non-threadsafe thing.
    def f(stdout: TextIO, stderr: TextIO) -> None:
        old_stdout = sys.stdout
        old_stderr = sys.stderr
        try:
            sys.stdout = stdout
            sys.stderr = stderr
            main(args)
        finally:
            sys.stdout = old_stdout
            sys.stderr = old_stderr

    return _run(f)