# pylint:disable=line-too-long,missing-class-docstring,no-self-use
from __future__ import annotations
import logging
from typing import Generic, cast, TypeVar

from collections.abc import Iterable
from collections import defaultdict
import contextlib

import claripy
import archinfo
from archinfo import RegisterName
from unique_log_filter import UniqueLogFilter

import angr
from .errors import AngrTypeError
from .sim_type import (
    NamedTypeMixin,
    SimType,
    SimTypeChar,
    SimTypePointer,
    SimTypeFixedSizeArray,
    SimTypeArray,
    SimTypeString,
    SimTypeFunction,
    SimTypeFloat,
    SimTypeDouble,
    SimTypeReg,
    SimStruct,
    SimStructValue,
    SimTypeInt,
    SimTypeNum,
    SimUnion,
    SimTypeBottom,
    parse_signature,
    SimTypeReference,
    SimTypeRef,
    SimTypeBool,
)
from .state_plugins.sim_action_object import SimActionObject

l = logging.getLogger(name=__name__)
l.addFilter(UniqueLogFilter())

T = TypeVar("T", bound="SimFunctionArgument")


class PointerWrapper:
    def __init__(self, value, buffer=False):
        self.value = value
        self.buffer = buffer


class AllocHelper:
    def __init__(self, ptrsize):
        self.base = claripy.BVS("alloc_base", ptrsize)
        self.ptr = self.base
        self.stores = {}
        self.store_asts = {}

    def alloc(self, size):
        out = self.ptr
        self.ptr += size
        return out

    def dump(self, val, state, loc=None):
        if loc is None:
            loc = self.stack_loc(val, state.arch)
        self.stores[self.ptr] = (val, loc)
        return self.alloc(self.calc_size(val, state.arch))

    def translate(self, val, base):
        if type(val) is SimStructValue:
            return SimStructValue(
                val.struct, {field: self.translate(subval, base) for field, subval in val._values.items()}
            )
        if isinstance(val, claripy.ast.Bits):
            return claripy.replace(val, self.base, base)
        if type(val) is list:
            return [self.translate(subval, base) for subval in val]
        raise TypeError(type(val))

    def apply(self, state, base):
        for ptr, (val, loc) in self.stores.items():
            translated_val = self.translate(val, base)
            translated_ptr = self.translate(ptr, base)
            loc.set_value(state, translated_val, stack_base=translated_ptr)

    def size(self):
        val = self.translate(self.ptr, claripy.BVV(0, len(self.ptr)))
        assert isinstance(val, claripy.ast.Base) and val.op == "BVV"
        assert isinstance(val.args[0], int)
        return abs(val.args[0])

    @classmethod
    def calc_size(cls, val, arch):
        if type(val) is SimStructValue:
            return val.struct.size // arch.byte_width
        if isinstance(val, claripy.ast.Bits):
            return len(val) // arch.byte_width
        if type(val) is list:
            # TODO real strides
            if len(val) == 0:
                return 0
            return cls.calc_size(val[0], arch) * len(val)
        raise TypeError(type(val))

    @classmethod
    def stack_loc(cls, val, arch, offset=0):
        if isinstance(val, claripy.ast.Bits):
            return SimStackArg(offset, len(val) // arch.byte_width)
        if type(val) is list:
            # TODO real strides
            if len(val) == 0:
                return SimArrayArg([])
            stride = cls.calc_size(val[0], arch)
            return SimArrayArg([cls.stack_loc(subval, arch, offset + i * stride) for i, subval in enumerate(val)])
        if type(val) is SimStructValue:
            return SimStructArg(
                val.struct,
                {
                    field: cls.stack_loc(subval, arch, offset + val.struct.offsets[field])
                    for field, subval in val._values.items()
                },
            )
        raise TypeError(type(val))


def refine_locs_with_struct_type(
    arch: archinfo.Arch,
    locs: list,
    arg_type: SimType,
    offset: int = 0,
    treat_bot_as_int=True,
    treat_unsupported_as_int=True,
):
    # CONTRACT FOR USING THIS METHOD: locs must be a list of locs which are all wordsize
    # ADDITIONAL NUANCE: this will not respect the need for big-endian integers to be stored at the end of words.
    # that's why this is named with_struct_type, because it will blindly trust the offsets given to it.

    if treat_bot_as_int and isinstance(arg_type, SimTypeBottom):
        arg_type = SimTypeInt(label=arg_type.label).with_arch(arch)

    if isinstance(arg_type, (SimTypeReg, SimTypeNum, SimTypeFloat)):
        assert arg_type.size is not None
        seen_bytes = 0
        pieces = []
        while seen_bytes < arg_type.size // arch.byte_width:
            start_offset = offset + seen_bytes
            chunk = start_offset // arch.bytes
            chunk_offset = start_offset % arch.bytes
            chunk_remaining = arch.bytes - chunk_offset
            type_remaining = arg_type.size // arch.byte_width - seen_bytes
            use_bytes = min(chunk_remaining, type_remaining)
            pieces.append(locs[chunk].refine(size=use_bytes, offset=chunk_offset))
            seen_bytes += use_bytes

        piece = pieces[0] if len(pieces) == 1 else SimComboArg(pieces)
        if isinstance(arg_type, SimTypeFloat):
            piece.is_fp = True
        return piece
    if isinstance(arg_type, SimTypeFixedSizeArray):
        assert arg_type.elem_type.size is not None and arg_type.length is not None
        # TODO explicit stride
        locs_list = [
            refine_locs_with_struct_type(
                arch, locs, arg_type.elem_type, offset=offset + i * arg_type.elem_type.size // arch.byte_width
            )
            for i in range(arg_type.length)
        ]
        return SimArrayArg(locs_list)
    if isinstance(arg_type, SimStruct):
        locs_dict = {
            field: refine_locs_with_struct_type(arch, locs, field_ty, offset=offset + arg_type.offsets[field])
            for field, field_ty in arg_type.fields.items()
        }
        return SimStructArg(arg_type, locs_dict)
    if isinstance(arg_type, SimUnion):
        # Treat a SimUnion as functionality equivalent to its longest member
        for member in arg_type.members.values():
            if member.size == arg_type.size:
                return refine_locs_with_struct_type(arch, locs, member, offset)

    # for all other types, we basically treat them as integers until someone implements proper layouting logic
    if treat_unsupported_as_int:
        arg_type = SimTypeInt().with_arch(arch)
        return refine_locs_with_struct_type(
            arch,
            locs,
            arg_type,
            offset=offset,
            treat_bot_as_int=treat_bot_as_int,
            treat_unsupported_as_int=treat_unsupported_as_int,
        )

    raise TypeError(f"I don't know how to lay out a {arg_type}")


class SerializableIterator:
    def __iter__(self):
        return self

    def __next__(self):
        raise NotImplementedError

    def getstate(self):
        raise NotImplementedError

    def setstate(self, state):
        raise NotImplementedError


class SerializableListIterator(SerializableIterator):
    def __init__(self, lst):
        self._lst = lst
        self._index = 0

    def __next__(self):
        if self._index >= len(self._lst):
            raise StopIteration
        result = self._lst[self._index]
        self._index += 1
        return result

    def getstate(self):
        return self._index

    def setstate(self, state):
        self._index = state


class SerializableCounter(SerializableIterator):
    def __init__(self, start, stride, mapping=lambda x: x):
        self._next = start
        self._stride = stride
        self._mapping = mapping

    def __next__(self):
        result = self._mapping(self._next)
        self._next += self._stride
        return result

    def getstate(self):
        return self._next

    def setstate(self, state):
        self._next = state


class SimFunctionArgument:
    """
    Represent a generic function argument.

    :ivar int size:    The size of the argument, in number of bytes.
    :ivar bool is_fp:  Whether loads from this location should return a floating point bitvector
    """

    def __init__(self, size: int, is_fp: bool = False):
        self.size = size
        self.is_fp = is_fp

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return hash(("function_argument", self.size))

    def check_value_set(self, value, arch):
        if not isinstance(value, claripy.ast.Base) and self.size is None:
            raise TypeError("Only claripy objects may be stored through SimFunctionArgument when size is not provided")
        if self.size is not None and isinstance(value, claripy.ast.Base) and self.size * arch.byte_width < value.length:
            raise TypeError(f"{value} doesn't fit in an argument of size {self.size}")
        if isinstance(value, int):
            value = claripy.BVV(value, self.size * arch.byte_width)
        if isinstance(value, float):
            if self.size not in (4, 8):
                raise ValueError(f"What do I do with a float {self.size} bytes long")
            value = claripy.FPV(value, claripy.FSORT_FLOAT if self.size == 4 else claripy.FSORT_DOUBLE)
        return value.raw_to_bv()  # type:ignore

    def check_value_get(self, value):
        if self.is_fp:
            return value.raw_to_fp()
        return value

    def set_value(self, state, value, **kwargs):
        raise NotImplementedError

    def get_value(self, state, **kwargs):
        raise NotImplementedError

    def refine(self, size, arch=None, offset=None, is_fp=None):
        raise NotImplementedError

    def get_footprint(self) -> Iterable[SimRegArg | SimStackArg]:
        """
        Return a list of SimRegArg and SimStackArgs that are the base components used for this location
        """
        raise NotImplementedError


class SimRegArg(SimFunctionArgument):
    """
    Represents a function argument that has been passed in a register.

    :ivar string reg_name:    The name of the represented register.
    :ivar int size:           The size of the data to store, in number of bytes.
    :ivar reg_offset:         The offset into the register to start storing data.
    :ivar clear_entire_reg:   Whether a store to this register should zero the unused parts of the register.
    :ivar bool is_fp:  Whether loads from this location should return a floating point bitvector
    """

    def __init__(self, reg_name: RegisterName, size: int, reg_offset=0, is_fp=False, clear_entire_reg=False):
        super().__init__(size, is_fp)
        self.reg_name = reg_name
        self.reg_offset = reg_offset
        self.clear_entire_reg = clear_entire_reg

    def get_footprint(self):
        return {self}

    def __repr__(self):
        return f"<{self.reg_name}>"

    def __eq__(self, other):
        return (
            type(other) is SimRegArg
            and self.reg_name == other.reg_name
            and self.reg_offset == other.reg_offset
            and self.size == other.size
        )

    def __hash__(self):
        return hash((self.size, self.reg_name, self.reg_offset))

    def check_offset(self, arch) -> int:
        return arch.registers[self.reg_name][0] + self.reg_offset

    def set_value(self, state, value, **kwargs):  # pylint: disable=unused-argument,arguments-differ
        value = self.check_value_set(value, state.arch)
        offset = self.check_offset(state.arch)
        if self.clear_entire_reg:
            state.registers.store(self.reg_name, 0)
        state.registers.store(offset, value, size=self.size)

    def get_value(self, state, **kwargs):  # pylint: disable=unused-argument,arguments-differ
        offset = self.check_offset(state.arch)
        return self.check_value_get(state.registers.load(offset, size=self.size))

    def refine(self, size, arch=None, offset=None, is_fp=None):
        passed_offset_none = offset is None
        if offset is None:
            if arch is None:
                raise ValueError("Need to specify either offset or arch in order to refine a register argument")
            offset = 0 if arch.register_endness == "Iend_LE" else self.size - size
        if is_fp is None:
            is_fp = self.is_fp
        return SimRegArg(self.reg_name, size, self.reg_offset + offset, is_fp, clear_entire_reg=passed_offset_none)

    def sse_extend(self):
        return SimRegArg(self.reg_name, self.size, self.reg_offset + self.size, is_fp=self.is_fp)


class SimStackArg(SimFunctionArgument):
    """
    Represents a function argument that has been passed on the stack.

    :var int stack_offset:    The position of the argument relative to the stack pointer after the function prelude.
    :ivar int size:           The size of the argument, in number of bytes.
    :ivar bool is_fp:  Whether loads from this location should return a floating point bitvector
    """

    def __init__(self, stack_offset: int, size: int, is_fp: bool = False):
        SimFunctionArgument.__init__(self, size, is_fp)
        self.stack_offset: int = stack_offset

    def get_footprint(self):
        return {self}

    def __repr__(self):
        return f"[{self.stack_offset:#x}]"

    def __eq__(self, other):
        return type(other) is SimStackArg and self.stack_offset == other.stack_offset

    def __hash__(self):
        return hash((self.size, self.stack_offset))

    def set_value(self, state, value, stack_base=None, **kwargs):  # pylint: disable=arguments-differ
        value = self.check_value_set(value, state.arch)
        if stack_base is None:
            stack_base = state.regs.sp
        state.memory.store(stack_base + self.stack_offset, value, endness=state.arch.memory_endness)

    def get_value(self, state, stack_base=None, **kwargs):  # pylint: disable=arguments-differ
        if stack_base is None:
            stack_base = state.regs.sp
        value = state.memory.load(stack_base + self.stack_offset, endness=state.arch.memory_endness, size=self.size)
        return self.check_value_get(value)

    def refine(self, size, arch=None, offset=None, is_fp=None):
        if offset is None:
            if arch is None:
                raise ValueError("Need to specify either offset or arch in order to refine a stack argument")
            offset = 0 if arch.register_endness == "Iend_LE" else self.size - size
        if is_fp is None:
            is_fp = self.is_fp
        return SimStackArg(self.stack_offset + offset, size, is_fp)


class SimComboArg(SimFunctionArgument, Generic[T]):
    """
    An argument which spans multiple storage locations. Locations should be given least-significant first.
    """

    def __init__(self, locations: list[T], is_fp=False):
        super().__init__(sum(x.size for x in locations), is_fp=is_fp)
        self.locations = locations

    def get_footprint(self):
        return {y for x in self.locations for y in x.get_footprint()}

    def __repr__(self):
        return f"SimComboArg({self.locations!r})"

    def __eq__(self, other):
        return type(other) is SimComboArg and all(a == b for a, b in zip(self.locations, other.locations))

    def set_value(self, state, value, **kwargs):  # pylint:disable=arguments-differ
        value = self.check_value_set(value, state.arch)
        cur = 0
        for loc in self.locations:
            size_bits = loc.size * state.arch.byte_width
            loc.set_value(state, value[cur + size_bits - 1 : cur], **kwargs)
            cur += size_bits

    def get_value(self, state, **kwargs):  # pylint:disable=arguments-differ
        vals = []
        for loc in reversed(self.locations):
            vals.append(loc.get_value(state, **kwargs))
        return self.check_value_get(claripy.Concat(*vals))


class SimStructArg(SimFunctionArgument):
    """
    An argument which de/serializes a struct from a list of storage locations

    :ivar struct:   The simtype describing the structure
    :ivar locs:     The storage locations to use
    """

    def __init__(self, struct: SimStruct, locs: dict[str, SimFunctionArgument]):
        super().__init__(sum(loc.size for loc in locs.values()))
        self.struct = struct
        self.locs = locs

    def get_footprint(self):
        regs: defaultdict[str, set[SimRegArg]] = defaultdict(set)
        others: set[SimRegArg | SimStackArg] = set()
        for loc in self.locs.values():
            for footloc in loc.get_footprint():
                if isinstance(footloc, SimRegArg):
                    regs[footloc.reg_name].add(footloc)
                else:
                    others.add(footloc)

        for reg, locset in regs.items():
            min_offset = min(loc.reg_offset for loc in locset)
            max_offset = max(loc.reg_offset + loc.size for loc in locset)
            others.add(SimRegArg(reg, max_offset - min_offset, min_offset))

        return others

    def get_single_footprint(self) -> SimStackArg | SimRegArg | SimComboArg:
        if self.struct._arch is None:
            raise TypeError("Can't tell the size of a struct without an arch")
        stack_min = None
        stack_max = None
        regs = []
        for field in self.struct.fields:
            loc = self.locs[field]
            if isinstance(loc, SimStackArg):
                if stack_min is None or stack_max is None:
                    stack_min = loc.stack_offset
                    stack_max = loc.stack_offset
                else:
                    # sanity check that arguments are laid out in order...
                    assert loc.stack_offset >= stack_max
                    stack_max = loc.stack_offset + loc.size
            elif isinstance(loc, SimRegArg):
                regs.append(loc)
            else:
                assert False, "Why would a struct have layout elements other than stack and reg?"

        # things to consider...
        # what happens if we return the concat of two registers but there's slack space missing?
        # an example of this would be big-endian struct { long a; int b; }
        # do any CCs do this??
        # for now assume no

        if stack_min is not None:
            if regs:
                assert (
                    False
                ), "Unknown CC argument passing structure - why are we passing both regs and stack at the same time?"
            return SimStackArg(stack_min, self.struct.size // self.struct._arch.byte_width)
        if not regs:
            assert False, "huh??????"
        if len(regs) == 1:
            return regs[0]
        return SimComboArg(regs)

    def get_value(self, state, **kwargs):
        return SimStructValue(
            self.struct, {field: getter.get_value(state, **kwargs) for field, getter in self.locs.items()}
        )

    def set_value(self, state, value, **kwargs):
        for field, setter in self.locs.items():
            setter.set_value(state, value[field], **kwargs)


class SimArrayArg(SimFunctionArgument):
    def __init__(self, locs):
        super().__init__(sum(loc.size for loc in locs))
        self.locs = locs

    def get_footprint(self):
        return {y for x in self.locs for y in x.get_footprint()}

    def get_value(self, state, **kwargs):
        return [getter.get_value(state, **kwargs) for getter in self.locs]

    def set_value(self, state, value, **kwargs):
        if len(value) != len(self.locs):
            raise TypeError(f"Expected {len(self.locs)} elements, got {len(value)}")
        for subvalue, setter in zip(value, self.locs):
            setter.set_value(state, subvalue, **kwargs)


class SimReferenceArgument(SimFunctionArgument):
    """
    A function argument which is passed by reference.

    :ivar ptr_loc:      The location the reference's pointer is stored
    :ivar main_loc:     A SimStackArgument describing how to load the argument's value as if it were stored at offset
                        zero on the stack. It will be passed ``stack_base=ptr_loc.get_value(state)``
    """

    def __init__(self, ptr_loc: SimFunctionArgument, main_loc: SimFunctionArgument):
        super().__init__(ptr_loc.size)  # ???
        self.ptr_loc = ptr_loc
        self.main_loc = main_loc

    def get_footprint(self):
        return self.main_loc.get_footprint()

    def get_value(self, state, **kwargs):
        ptr_val = self.ptr_loc.get_value(state, **kwargs)
        return self.main_loc.get_value(state, stack_base=ptr_val, **kwargs)

    def set_value(self, state, value, **kwargs):
        ptr_val = self.ptr_loc.get_value(state, **kwargs)
        self.main_loc.set_value(state, value, stack_base=ptr_val, **kwargs)


class ArgSession:
    """
    A class to keep track of the state accumulated in laying parameters out into memory
    """

    __slots__ = (
        "both_iter",
        "cc",
        "fp_iter",
        "int_iter",
    )

    def __init__(self, cc):
        self.cc = cc
        self.fp_iter = cc.fp_args
        self.int_iter = cc.int_args
        self.both_iter = cc.memory_args

    def getstate(self):
        return (self.fp_iter.getstate(), self.int_iter.getstate(), self.both_iter.getstate())

    def setstate(self, state):
        fp, int_, both = state
        self.fp_iter.setstate(fp)
        self.int_iter.setstate(int_)
        self.both_iter.setstate(both)


class UsercallArgSession:
    """
    An argsession for use with SimCCUsercall
    """

    __slots__ = (
        "cc",
        "real_args",
    )

    def __init__(self, cc):
        self.cc = cc
        self.real_args = SerializableListIterator(self.cc.args)

    def getstate(self):
        return self.real_args.getstate()

    def setstate(self, state):
        self.real_args.setstate(state)


class SimCC:
    """
    A calling convention allows you to extract from a state the data passed from function to
    function by calls and returns. Most of the methods provided by SimCC that operate on a state
    assume that the program is just after a call but just before stack frame allocation, though
    this may be overridden with the `stack_base` parameter to each individual method.

    This is the base class for all calling conventions.
    """

    def __init__(self, arch: archinfo.Arch):
        """
        :param arch:        The Archinfo arch for this CC
        """
        self.arch = arch

    #
    # Here are all the things a subclass needs to specify!
    #

    ARG_REGS: list[str] = []  # A list of all the registers used for integral args, in order (names or offsets)
    FP_ARG_REGS: list[str] = []  # A list of all the registers used for floating point args, in order
    STACKARG_SP_BUFF = 0  # The amount of stack space reserved between the saved return address
    # (if applicable) and the arguments. Probably zero.
    STACKARG_SP_DIFF = 0  # The amount of stack space reserved for the return address
    CALLER_SAVED_REGS: list[str] = []  # Caller-saved registers
    RETURN_ADDR: SimFunctionArgument | None = (
        None  # The location where the return address is stored, as a SimFunctionArgument
    )
    RETURN_VAL: SimFunctionArgument | None = (
        None  # The location where the return value is stored, as a SimFunctionArgument
    )
    OVERFLOW_RETURN_VAL: SimFunctionArgument | None = (
        None  # The second half of the location where a double-length return value is stored
    )
    FP_RETURN_VAL: SimFunctionArgument | None = (
        None  # The location where floating-point argument return values are stored
    )
    ARCH: type[archinfo.Arch] | None = (
        None  # The archinfo.Arch class for which this CC is most likely relevant, if related
    )
    # archinfo.Arch classes for which this CC is relevant, in addition to self.ARCH.
    # you should access cls.arches() to get a list of all arches for which this CC is relevant
    EXTRA_ARCHES: tuple[type[archinfo.Arch], ...] = ()
    CALLEE_CLEANUP = False  # Whether the callee has to deallocate the stack space for the arguments

    STACK_ALIGNMENT = 1  # the alignment requirement of the stack pointer at function start BEFORE call

    #
    # Here are several things you MAY want to override to change your cc's convention
    #

    @property
    def int_args(self):
        """
        Iterate through all the possible arg positions that can only be used to store integer or pointer values.

        Returns an iterator of SimFunctionArguments
        """
        if self.ARG_REGS is None:
            raise NotImplementedError
        return SerializableListIterator([SimRegArg(reg, self.arch.bytes) for reg in self.ARG_REGS])

    @property
    def memory_args(self):
        """
        Iterate through all the possible arg positions that can be used to store any kind of argument.

        Returns an iterator of SimFunctionArguments
        """
        start = self.STACKARG_SP_BUFF + self.STACKARG_SP_DIFF
        return SerializableCounter(start, self.arch.bytes, lambda offset: SimStackArg(offset, self.arch.bytes))

    @property
    def fp_args(self):
        """
        Iterate through all the possible arg positions that can only be used to store floating point values.

        Returns an iterator of SimFunctionArguments
        """
        if self.FP_ARG_REGS is None:
            raise NotImplementedError
        return SerializableListIterator([SimRegArg(reg, self.arch.bytes) for reg in self.FP_ARG_REGS])

    def is_fp_arg(self, arg):
        """
        This should take a SimFunctionArgument instance and return whether or not that argument is a floating-point
        argument.

        Returns True for MUST be a floating point arg,
                False for MUST NOT be a floating point arg,
                None for when it can be either.
        """
        if arg in self.int_args:
            return False
        if arg in self.fp_args or arg == self.FP_RETURN_VAL:
            return True
        return None

    ArgSession = ArgSession  # import this from global scope so SimCC subclasses can subclass it if they like

    def arg_session(self, ret_ty: SimType | None):
        """
        Return an arg session.

        A session provides the control interface necessary to describe how integral and floating-point arguments are
        laid out into memory. The default behavior is that there are a finite list of int-only and fp-only argument
        slots, and an infinite number of generic slots, and when an argument of a given type is requested, the most
        slot available is used. If you need different behavior, subclass ArgSession.

        You need to provide the return type of the function in order to kick off an arg layout session.
        """
        session = self.ArgSession(self)
        if self.return_in_implicit_outparam(ret_ty):
            self.next_arg(session, SimTypePointer(SimTypeBottom()).with_arch(self.arch))
        return session

    def return_in_implicit_outparam(self, ty) -> bool:  # pylint:disable=unused-argument
        return False

    def stack_space(self, args):
        """
        :param args:        A list of SimFunctionArguments

        :returns:           The number of bytes that should be allocated on the stack to store all these args,
                            NOT INCLUDING the return address.
        """
        out = self.STACKARG_SP_DIFF
        for arg in args:
            if isinstance(arg, SimStackArg):
                out = max(out, arg.stack_offset + self.arch.bytes)

        out += self.STACKARG_SP_BUFF
        return out

    def return_val(self, ty, perspective_returned=False):
        """
        The location the return value is stored, based on its type.
        """
        if ty._arch is None:
            ty = ty.with_arch(self.arch)
        if isinstance(ty, (SimStruct, SimUnion, SimTypeFixedSizeArray)):
            raise AngrTypeError(
                f"{self} doesn't know how to return aggregate types ({type(ty)}). Consider overriding return_val to "
                "implement its ABI logic"
            )
        if self.return_in_implicit_outparam(ty):
            if perspective_returned:
                assert self.RETURN_VAL is not None
                ptr_loc = self.RETURN_VAL
            else:
                ptr_loc = self.next_arg(self.ArgSession(self), SimTypePointer(SimTypeBottom()).with_arch(self.arch))
            return SimReferenceArgument(
                ptr_loc, SimStackArg(0, ty.size // self.arch.byte_width, is_fp=isinstance(ty, SimTypeFloat))
            )

        if isinstance(ty, SimTypeFloat) and self.FP_RETURN_VAL is not None:
            return self.FP_RETURN_VAL.refine(size=ty.size // self.arch.byte_width, arch=self.arch, is_fp=True)

        if self.RETURN_VAL is None or isinstance(ty, SimTypeBottom):
            return None
        if ty.size > self.RETURN_VAL.size * self.arch.byte_width:
            assert self.OVERFLOW_RETURN_VAL is not None
            return SimComboArg([self.RETURN_VAL, self.OVERFLOW_RETURN_VAL])
        return self.RETURN_VAL.refine(size=ty.size // self.arch.byte_width, arch=self.arch, is_fp=False)

    @property
    def return_addr(self):
        """
        The location the return address is stored.
        """
        return self.RETURN_ADDR

    def next_arg(self, session: ArgSession, arg_type: SimType):
        if isinstance(arg_type, (SimTypeArray, SimTypeFixedSizeArray)):  # hack
            arg_type = SimTypePointer(arg_type.elem_type).with_arch(self.arch)
        if isinstance(arg_type, (SimStruct, SimUnion, SimTypeFixedSizeArray)):
            raise TypeError(
                f"{self} doesn't know how to store aggregate type {type(arg_type)}. Consider overriding next_arg to "
                "implement its ABI logic"
            )
        if isinstance(arg_type, SimTypeBottom):
            # This is usually caused by failures or mistakes during type inference
            l.warning("Function argument type cannot be BOT. Treating it as a 32-bit int.")
            arg_type = SimTypeInt().with_arch(self.arch)
        is_fp = isinstance(arg_type, SimTypeFloat)
        assert arg_type.size is not None
        size = arg_type.size // self.arch.byte_width
        try:
            arg = next(session.fp_iter) if is_fp else next(session.int_iter)
        except StopIteration:
            try:
                arg = next(session.both_iter)
            except StopIteration as err:
                raise TypeError("Accessed too many arguments - exhausted all positions?") from err

        if size > arg.size:
            if isinstance(arg, SimStackArg):
                arg_size = arg.size
                locations = [arg]
                while arg_size < size:
                    next_arg = next(session.both_iter)
                    arg_size += next_arg.size
                    locations.append(next_arg)
                return SimComboArg(locations, is_fp=is_fp)
            raise ValueError(
                f"{self} doesn't know how to store large types. Consider overriding"
                " next_arg to implement its ABI logic"
            )
        return arg.refine(size, is_fp=is_fp, arch=self.arch)

    #
    # Useful functions!
    #

    @staticmethod
    def is_fp_value(val):
        return (
            isinstance(val, (float, claripy.ast.FP))
            or (isinstance(val, claripy.ast.Base) and val.op.startswith("fp"))  # type: ignore
            or (
                isinstance(val, claripy.ast.Base)
                and val.op == "Reverse"  # type:ignore
                and val.args[0].op.startswith("fp")  # type:ignore
            )
        )

    @staticmethod
    def guess_prototype(args, prototype=None):
        """
        Come up with a plausible SimTypeFunction for the given args (as would be passed to e.g. setup_callsite).

        You can pass a variadic function prototype in the `base_type` parameter and all its arguments will be used,
        only guessing types for the variadic arguments.
        """
        if type(prototype) is str:
            prototype = parse_signature(prototype)
        elif prototype is None:
            l.warning("Guessing call prototype. Please specify prototype.")

        charp = SimTypePointer(SimTypeChar())
        result = prototype if prototype is not None else SimTypeFunction([], charp)
        for arg in args[len(result.args) :]:
            if type(arg) in (int, bytes, PointerWrapper):
                result.args += (charp,)
            elif type(arg) is float:
                result.args += (SimTypeDouble(),)
            elif isinstance(arg, claripy.ast.BV):
                result.args += (SimTypeNum(len(arg), False),)
            elif isinstance(arg, claripy.ast.FP):
                if arg.sort == claripy.FSORT_FLOAT:
