# pylint:disable=missing-class-docstring,too-many-boolean-expressions
from __future__ import annotations
from itertools import chain
from collections.abc import Iterable
import logging
from typing import cast

from archinfo.types import RegisterOffset
import claripy
import ailment
from claripy import FSORT_DOUBLE, FSORT_FLOAT

from angr.engines.light import SpOffset
from angr.engines.light.engine import SimEngineNostmtAIL
from angr.errors import SimEngineError, SimMemoryMissingError
from angr.calling_conventions import default_cc, SimRegArg, SimTypeBottom
from angr.storage.memory_mixins.paged_memory.pages.multi_values import MultiValues, mv_is_bv
from angr.knowledge_plugins.key_definitions.atoms import Atom, Register, Tmp, MemoryLocation
from angr.knowledge_plugins.key_definitions.constants import OP_BEFORE, OP_AFTER
from angr.knowledge_plugins.key_definitions.live_definitions import Definition, LiveDefinitions
from angr.code_location import CodeLocation, ExternalCodeLocation
from .subject import SubjectType
from .rd_state import ReachingDefinitionsState
from .function_handler import FunctionHandler, FunctionCallData

l = logging.getLogger(name=__name__)


class SimEngineRDAIL(
    SimEngineNostmtAIL[
        ReachingDefinitionsState, MultiValues[claripy.ast.BV | claripy.ast.FP], None, ReachingDefinitionsState
    ]
):
    def __init__(
        self,
        project,
        function_handler: FunctionHandler,
        stack_pointer_tracker=None,
        use_callee_saved_regs_at_return=True,
        bp_as_gpr: bool = False,
    ):
        super().__init__(project)
        self._function_handler = function_handler
        self._visited_blocks = None
        self._dep_graph = None
        self._stack_pointer_tracker = stack_pointer_tracker
        self._use_callee_saved_regs_at_return = use_callee_saved_regs_at_return
        self.bp_as_gpr = bp_as_gpr

    def _is_top(self, expr):
        """
        MultiValues are not really "top" in the stricter sense. They are just a collection of values,
        some of which might be top
        """
        return False

    def _top(self, bits) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        return MultiValues(self.state.top(bits))

    def process(
        self, state, *, dep_graph=None, visited_blocks=None, block=None, fail_fast=False, whitelist=None, **kwargs
    ):
        self._dep_graph = dep_graph
        self._visited_blocks = visited_blocks

        try:
            result_state = super().process(state, whitelist=whitelist, block=block)
        except SimEngineError:
            if fail_fast is True:
                raise
            result_state = state
        return result_state

    def _process_block_end(self, block, stmt_data, whitelist):
        return self.state

    #
    # Private methods
    #

    def _expr_bv(self, expr: ailment.expression.Expression) -> MultiValues[claripy.ast.BV]:
        result = self._expr(expr)
        assert mv_is_bv(result)
        return result

    def _expr_pair(
        self, arg0: ailment.expression.Expression, arg1: ailment.expression.Expression
    ) -> (
        tuple[MultiValues[claripy.ast.BV], MultiValues[claripy.ast.BV]]
        | tuple[MultiValues[claripy.ast.FP], MultiValues[claripy.ast.FP]]
    ):
        r0 = self._expr(arg0)
        r1 = self._expr(arg1)
        assert type(r0) is type(r1)
        return r0, r1  # type: ignore

    def _external_codeloc(self):
        return ExternalCodeLocation(self.state.codeloc.context)

    def _set_codeloc(self):
        # TODO do we want a better mechanism to specify context updates?
        new_codeloc = CodeLocation(
            self.block.addr,
            self.stmt_idx,
            ins_addr=self.ins_addr,
            block_idx=self.block.idx,
            context=self.state.codeloc.context,
        )
        self.state.move_codelocs(new_codeloc)
        self.state.analysis.model.at_new_stmt(new_codeloc)

    #
    # AIL statement handlers
    #

    def _stmt(self, stmt):
        if self.state.analysis:
            self.state.analysis.stmt_observe(self.stmt_idx, stmt, self.block, self.state, OP_BEFORE)
            self.state.analysis.insn_observe(self.ins_addr, stmt, self.block, self.state, OP_BEFORE)

        self._set_codeloc()
        super()._stmt(stmt)

        if self.state.analysis:
            self.state.analysis.stmt_observe(self.stmt_idx, stmt, self.block, self.state, OP_AFTER)
            self.state.analysis.insn_observe(self.ins_addr, stmt, self.block, self.state, OP_AFTER)

    def _handle_stmt_Assignment(self, stmt):
        src = self._expr(stmt.src)
        dst = stmt.dst

        if isinstance(dst, ailment.Tmp):
            self.state.kill_and_add_definition(Tmp(dst.tmp_idx, dst.size), src)
            self.tmps[dst.tmp_idx] = src

        elif isinstance(dst, ailment.Register):
            reg = Register(RegisterOffset(dst.reg_offset), dst.size)
            self.state.kill_and_add_definition(reg, src)

            if dst.reg_offset == self.arch.sp_offset:
                self.state._sp_adjusted = True
                # TODO: Special logic that frees all definitions above the current stack pointer
        else:
            l.warning("Unsupported type of Assignment dst %s.", type(dst).__name__)

    def _handle_stmt_Store(self, stmt: ailment.Stmt.Store) -> None:
        data = self._expr(stmt.data)
        addr = self._expr_bv(stmt.addr)
        size: int = stmt.size
        if stmt.guard is not None:
            self._expr(stmt.guard)

        addr_v = addr.one_value()
        if addr_v is not None and not self.state.is_top(addr_v):
            if self.state.is_stack_address(addr_v):
                stack_offset = self.state.get_stack_offset(addr_v)
                if stack_offset is not None:
                    memory_location = MemoryLocation(SpOffset(self.arch.bits, stack_offset), size, endness=stmt.endness)
                else:
                    memory_location = None
            elif self.state.is_heap_address(addr_v):
                memory_location = None
            else:
                memory_location = MemoryLocation(addr_v.concrete_value, size, endness=stmt.endness)

            if memory_location is not None:
                self.state.kill_and_add_definition(memory_location, data, endness=stmt.endness)

    def _handle_stmt_Jump(self, stmt):
        _ = self._expr(stmt.target)

    def _handle_stmt_ConditionalJump(self, stmt):
        _ = self._expr(stmt.condition)  # pylint:disable=unused-variable
        if stmt.true_target is not None:
            _ = self._expr(stmt.true_target)  # pylint:disable=unused-variable
        if stmt.false_target is not None:
            _ = self._expr(stmt.false_target)  # pylint:disable=unused-variable

        ip = Register(cast(RegisterOffset, self.arch.ip_offset), self.arch.bytes)
        self.state.kill_definitions(ip)

    def _handle_stmt_Call(self, stmt: ailment.Stmt.Call):
        data = self._handle_Call_base(stmt, is_expr=False)
        src = data.ret_values
        if src is None:
            return

        dst = stmt.ret_expr
        if isinstance(dst, ailment.Tmp):
            _, defs = self.state.kill_and_add_definition(Tmp(dst.tmp_idx, dst.size), src, uses=data.ret_values_deps)
            self.tmps[dst.tmp_idx] = src

        elif isinstance(dst, ailment.Register):
            full_reg_offset, full_reg_size = self.arch.registers[
                self.arch.register_names[RegisterOffset(dst.reg_offset)]
            ]
            if dst.size != full_reg_size:
                # we need to extend the value to overwrite the entire register
                otv = {}
                next_off = 0
                if full_reg_offset < dst.reg_offset:
                    otv[0] = {claripy.BVV(0, (dst.reg_offset - full_reg_offset) * 8)}
                    next_off = dst.reg_offset - full_reg_offset
                for off, items in src.items():
                    otv[next_off + off] = set(items)
                next_off += len(src) // 8
                if next_off < full_reg_size:
                    otv[next_off] = {claripy.BVV(0, (full_reg_size - next_off) * 8)}
                src = MultiValues(offset_to_values=otv)
            reg = Register(full_reg_offset, full_reg_size)
            _, defs = self.state.kill_and_add_definition(reg, src, uses=data.ret_values_deps)
        else:
            defs = set()

        if self.state.analysis:
            self.state.analysis.function_calls[data.callsite_codeloc].ret_defns.update(defs)

    def _handle_Call_base(self, stmt: ailment.Stmt.Call, is_expr: bool = False) -> FunctionCallData:
        if isinstance(stmt.target, ailment.Expr.Expression):
            target = self._expr(stmt.target)  # pylint:disable=unused-variable
            func_name = None
        elif isinstance(stmt.target, str):
            func_name = stmt.target
            target = None
        else:
            target = stmt.target
            func_name = None

        ip = Register(cast(RegisterOffset, self.arch.ip_offset), self.arch.bytes)
        self.state.kill_definitions(ip)

        statement = self.block.statements[self.stmt_idx]
        caller_will_handle_single_ret = True
        if hasattr(statement, "dst") and statement.dst != stmt.ret_expr:
            caller_will_handle_single_ret = False

        data = FunctionCallData(
            self.state.codeloc,
            self._function_handler.make_function_codeloc(
                target, self.state.codeloc, self.state.analysis.model.func_addr
            ),
            target,
            cc=stmt.calling_convention,
            prototype=stmt.prototype,
            name=func_name,
            args_values=[self._expr(arg) for arg in stmt.args] if stmt.args is not None else None,
            redefine_locals=stmt.args is None and not is_expr,
            caller_will_handle_single_ret=caller_will_handle_single_ret,
            ret_atoms={Atom.from_ail_expr(stmt.ret_expr, self.arch)} if stmt.ret_expr is not None else None,
        )

        self._function_handler.handle_function(self.state, data)

        if hasattr(stmt, "arg_defs"):
            for arg_def in stmt.arg_defs:
                arg_def: Definition
                if arg_def in self.state.all_definitions:
                    self.state.kill_definitions(arg_def.atom)

        # kill all cc_ops
        if "cc_op" in self.arch.registers:

            def killreg(name: str):
                offset, size = self.arch.registers[name]
                self.state.kill_definitions(Register(offset, size))

            killreg("cc_op")
            killreg("cc_dep1")
            killreg("cc_dep2")
            killreg("cc_ndep")

        return data

    def _handle_stmt_Return(self, stmt: ailment.Stmt.Return):  # pylint:disable=unused-argument
        cc = None
        prototype = None
        if self.state.analysis.subject.type == SubjectType.Function:
            cc = self.state.analysis.subject.content.calling_convention
            prototype = self.state.analysis.subject.content.prototype
            # import ipdb; ipdb.set_trace()

        if cc is None:
            # fall back to the default calling convention
            cc_cls = default_cc(
                self.project.arch.name,
                platform=self.project.simos.name if self.project.simos is not None else None,
                default=None,
            )
            if cc_cls is None:
                l.warning("Unknown default calling convention for architecture %s.", self.project.arch.name)
                cc = None
            else:
                cc = cc_cls(self.project.arch)

        if self._use_callee_saved_regs_at_return and cc is not None:
            # handle callee-saved registers: add uses for these registers so that the restoration statements are not
            # considered dead assignments.
            for reg in self.arch.register_list:
                if (
                    reg.general_purpose
                    and reg.name not in cc.CALLER_SAVED_REGS
                    and reg.name not in cc.ARG_REGS
                    and reg.vex_offset
                    not in {
                        self.arch.sp_offset,
                        self.arch.bp_offset,
                        self.arch.ip_offset,
                    }
                    and (isinstance(cc.RETURN_VAL, SimRegArg) and reg.name != cc.RETURN_VAL.reg_name)
                ):
                    self.state.add_register_use(reg.vex_offset, reg.size)

        if stmt.ret_exprs:
            # Handle return expressions
            for ret_expr in stmt.ret_exprs:
                self._expr(ret_expr)
            return

        # No return expressions are available.
        # consume registers that are potentially useful

        # return value
        if (
            cc is not None
            and prototype is not None
            and prototype.returnty is not None
            and not isinstance(prototype.returnty, SimTypeBottom)
        ):
            ret_val = cc.return_val(prototype.returnty)
            if isinstance(ret_val, SimRegArg):
                if ret_val.clear_entire_reg:
                    offset, size = cc.arch.registers[ret_val.reg_name]
                else:
                    offset = cc.arch.registers[ret_val.reg_name][0] + ret_val.reg_offset
                    size = ret_val.size
                self.state.add_register_use(offset, size)
            else:
                l.error("Cannot handle CC with non-register return value location")

        # base pointer
        # TODO: Check if the stack base pointer is used as a stack base pointer in this function or not
        self.state.add_register_use(self.project.arch.bp_offset, self.project.arch.bytes)
        # We don't add sp since stack pointers are supposed to be get rid of in AIL. this is definitely a hack though
        # self.state.add_use(Register(self.project.arch.sp_offset, self.project.arch.bits // 8))

    def _handle_stmt_DirtyStatement(self, stmt: ailment.Stmt.DirtyStatement):
        self._expr(stmt.dirty)

    #
    # AIL expression handlers
    #

    def _handle_expr_Tmp(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        self.state.add_tmp_use(expr.tmp_idx)

        try:
            return self.tmps[expr.tmp_idx]
        except KeyError:
            return self._top(expr.bits)

    def _handle_expr_Call(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        data = self._handle_Call_base(expr, is_expr=True)
        result = data.ret_values

        if result is None:
            return self._top(expr.bits)

        # truncate result if needed
        if len(result) > expr.bits:
            assert mv_is_bv(result)
            result = cast(
                MultiValues[claripy.ast.BV | claripy.ast.FP],
                result.extract((len(result) - expr.bits) // 8, expr.bits // 8, "Iend_BE"),
            )

        if data.ret_values_deps is not None:
            for dep in data.ret_values_deps:
                result = self.state.annotate_mv_with_def(result, dep)
        return result

    def _handle_expr_Register(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        self.state: ReachingDefinitionsState

        reg_offset = expr.reg_offset
        size = expr.size
        # bits = size * 8

        # Special handling for SP and BP
        if self._stack_pointer_tracker is not None:
            if reg_offset == self.arch.sp_offset:
                sb_offset = self._stack_pointer_tracker.offset_before(self.ins_addr, self.arch.sp_offset)
                if sb_offset is not None:
                    return MultiValues(v=self.state._initial_stack_pointer() + sb_offset)
            elif reg_offset == self.arch.bp_offset and not self.bp_as_gpr:
                sb_offset = self._stack_pointer_tracker.offset_before(self.ins_addr, self.arch.bp_offset)
                if sb_offset is not None:
                    return MultiValues(v=self.state._initial_stack_pointer() + sb_offset)

        reg_atom = Register(RegisterOffset(reg_offset), size)

        # first check if it is ever defined
        try:
            value: MultiValues = self.state.registers.load(reg_offset, size=size)
        except SimMemoryMissingError as ex:
            # the full value does not exist, but we handle partial existence, too
            missing_defs = None
            if ex.missing_size != size:
                existing_values = []
                i = 0
                while i < size:
                    try:
                        value: MultiValues = self.state.registers.load(reg_offset + i, size=1)
                    except SimMemoryMissingError as ex_:
                        i += ex_.missing_size
                        continue
                    i += 1
                    existing_values.append(value)
                # extract existing definitions
                for existing_value in existing_values:
                    for vs in existing_value.values():
                        for v in vs:
                            if missing_defs is None:
                                missing_defs = self.state.extract_defs(v)
                            else:
                                missing_defs = chain(missing_defs, self.state.extract_defs(v))

            if missing_defs is not None:
                self.state.add_register_use_by_defs(missing_defs, expr=expr)

            top = self.state.top(size * self.state.arch.byte_width)
            # annotate it
            extloc = self._external_codeloc()
            top = self.state.annotate_with_def(top, Definition(reg_atom, extloc))
            value = MultiValues(top)
            # write it back
            self.state.kill_and_add_definition(reg_atom, value, override_codeloc=extloc)

        # extract Definitions
        defs: Iterable[Definition] | None = None
        for vs in value.values():
            for v in vs:
                defs = self.state.extract_defs(v) if defs is None else chain(defs, self.state.extract_defs(v))

        if defs is None:
            # define it right away as an external dependency
            self.state.kill_and_add_definition(reg_atom, value, override_codeloc=self._external_codeloc())
        else:
            self.state.add_register_use_by_defs(defs, expr=expr)

        return value

    def _handle_expr_Load(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        addrs = self._expr_bv(expr.addr)

        size = expr.size
        bits = expr.bits
        if expr.guard is not None:
            assert expr.alt is not None
            self._expr(expr.guard)
            self._expr(expr.alt)

        # convert addrs from MultiValues to a list of valid addresses
        if addrs.count() == 1:
            addrs_v = next(iter(addrs.values()))
        else:
            top = self.state.top(bits)
            # annotate it
            extloc = self._external_codeloc()
            dummy_atom = MemoryLocation(0, size, endness=expr.endness)
            def_ = Definition(dummy_atom, extloc)
            top = self.state.annotate_with_def(top, def_)
            # add use
            self.state.add_memory_use_by_def(def_, expr=expr)
            return MultiValues(top)

        result: MultiValues | None = None
        for addr in addrs_v:
            if not isinstance(addr, claripy.ast.Base):
                continue
            if addr.concrete:
                # a concrete address
                concrete_addr: int = addr.concrete_value
                try:
                    vs: MultiValues = self.state.memory.load(concrete_addr, size=size, endness=expr.endness)
                    defs = set(LiveDefinitions.extract_defs_from_mv(vs))
                except SimMemoryMissingError:
                    continue

                self.state.add_memory_use_by_defs(defs, expr=expr)
                result = result.merge(vs) if result is not None else vs
            elif self.state.is_stack_address(addr):
                stack_offset = self.state.get_stack_offset(addr)
                if stack_offset is not None:
                    stack_addr = self.state.live_definitions.stack_offset_to_stack_addr(stack_offset)
                    try:
                        vs: MultiValues = self.state.stack.load(stack_addr, size=size, endness=expr.endness)
                        defs = set(LiveDefinitions.extract_defs_from_mv(vs))
                    except SimMemoryMissingError:
                        continue

                    # XXX should be add_stack_use_by_defs?
                    self.state.add_memory_use_by_defs(defs, expr=expr)
                    result = result.merge(vs) if result is not None else vs
            else:
                # XXX does ail not support heap tracking?
                l.debug("Memory address %r undefined or unsupported at pc %#x.", addr, self.ins_addr)

        if result is None:
            top = self.state.top(bits)
            # TODO: Annotate top with a definition
            result = MultiValues(top)

        return result

    def _handle_expr_Convert(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        to_conv: MultiValues = self._expr(expr.operand)
        bits = expr.to_bits
        size = bits // self.arch.byte_width

        if (
            to_conv.count() == 1
            and 0 in to_conv
            and expr.from_type == ailment.Expr.Convert.TYPE_INT
            and expr.to_type == ailment.Expr.Convert.TYPE_INT
        ):
            values = to_conv[0]
        else:
            top = self.state.top(expr.to_bits)
            # annotate it
            dummy_atom = MemoryLocation(0, size, endness=self.arch.memory_endness)
            def_ = Definition(dummy_atom, self._external_codeloc())
            top = self.state.annotate_with_def(top, def_)
            # add use
            self.state.add_memory_use_by_def(def_, expr=expr)
            return MultiValues(top)

        converted = set()
        for v in values:
            if expr.to_bits < expr.from_bits:
                conv = v[expr.to_bits - 1 : 0]
            elif expr.to_bits > expr.from_bits:
                conv = claripy.ZeroExt(expr.to_bits - expr.from_bits, v)
            else:
                conv = v
            converted.add(conv)

        return MultiValues(offset_to_values={0: converted})

    def _handle_expr_Reinterpret(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        _: MultiValues = self._expr(expr.operand)
        bits = expr.to_bits

        # we currently do not support floating-point operations. therefore, we return TOP directly
        reinterpreted = self.state.top(bits)

        return MultiValues(reinterpreted)

    def _handle_unop_Default(self, expr):
        return self._top(expr.bits)

    _handle_unop_Reference = _handle_unop_Default
    _handle_unop_Clz = _handle_unop_Default
    _handle_unop_Ctz = _handle_unop_Default
    _handle_unop_Dereference = _handle_unop_Default
    _handle_unop_GetMSBs = _handle_unop_Default
    _handle_unop_unpack = _handle_unop_Default
    _handle_unop_Sqrt = _handle_unop_Default
    _handle_unop_RSqrtEst = _handle_unop_Default

    def _handle_expr_ITE(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        _: MultiValues = self._expr(expr.cond)
        iftrue: MultiValues = self._expr(expr.iftrue)
        _: MultiValues = self._expr(expr.iffalse)
        top = self.state.top(len(iftrue))
        return MultiValues(top)

    def _handle_unop_Not(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        operand = self._expr_bv(expr.operand)
        bits = expr.bits

        operand_v = operand.one_value()

        if operand_v is not None and operand_v.concrete:
            return MultiValues(~operand_v)  # pylint:disable=invalid-unary-operand-type
        return MultiValues(self.state.top(bits))

    def _handle_unop_Neg(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        operand: MultiValues = self._expr(expr.operand)
        bits = expr.bits

        operand_v = operand.one_value()

        if operand_v is not None and operand_v.concrete:
            return MultiValues(-operand_v)  # pylint:disable=invalid-unary-operand-type
        return MultiValues(self.state.top(bits))

    def _handle_unop_BitwiseNeg(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        operand = self._expr_bv(expr.operand)
        bits = expr.bits

        operand_v = operand.one_value()

        if operand_v is not None and operand_v.concrete:
            return MultiValues(offset_to_values={0: {~operand_v}})
        return MultiValues(offset_to_values={0: {self.state.top(bits)}})

    def _handle_binop_Add(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        expr0, expr1 = self._expr_pair(expr.operands[0], expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            # adding two single values together
            if (expr0_v.concrete or self.state.is_stack_address(expr0_v)) and expr1_v.concrete:
                r = MultiValues(expr0_v + expr1_v)  # type: ignore
        elif expr0_v is None and expr1_v is not None:
            # adding a single value to a multivalue
            if (
                expr0.count() == 1
                and 0 in expr0
                and all(v.concrete or self.state.is_stack_address(v) for v in expr0[0])
            ):
                vs = {v + expr1_v for v in expr0[0]}  # type: ignore
                r = MultiValues(offset_to_values={0: vs})
        elif expr0_v is not None and expr1_v is None:
            # adding a single value to a multivalue
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                vs = {v + expr0_v for v in expr1[0]}  # type: ignore
                r = MultiValues(offset_to_values={0: vs})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    def _handle_binop_Sub(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        expr0, expr1 = self._expr_pair(expr.operands[0], expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            if (expr0_v.concrete or self.state.is_stack_address(expr0_v)) and expr1_v.concrete:
                r = MultiValues(expr0_v - expr1_v)  # type: ignore
        elif expr0_v is None and expr1_v is not None:
            # subtracting a single value from a multivalue
            if (
                expr0.count() == 1
                and 0 in expr0
                and all(v.concrete or self.state.is_stack_address(v) for v in expr0[0])
            ):
                vs = {v - expr1_v for v in expr0[0]}  # type: ignore
                r = MultiValues(offset_to_values={0: vs})
        elif expr0_v is not None and expr1_v is None:
            # subtracting a single value from a multivalue
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                vs = {expr0_v - v for v in expr1[0]}  # type: ignore
                r = MultiValues(offset_to_values={0: vs})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    def _handle_binop_Default(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        arg0, arg1 = expr.operands

        self._expr(arg0)
        self._expr(arg1)
        bits = expr.bits

        return MultiValues(self.state.top(bits))

    _handle_binop_AddV = _handle_binop_Add
    _handle_binop_Div = _handle_binop_Default
    _handle_binop_MulV = _handle_binop_Default
    _handle_binop_MulHiV = _handle_binop_Default
    _handle_binop_Mod = _handle_binop_Default
    _handle_binop_AddF = _handle_binop_Default
    _handle_binop_DivF = _handle_binop_Default
    _handle_binop_DivV = _handle_binop_Default
    _handle_binop_MulF = _handle_binop_Default
    _handle_binop_SubF = _handle_binop_Default
    _handle_binop_SubV = _handle_binop_Default
    _handle_binop_InterleaveLOV = _handle_binop_Default
    _handle_binop_InterleaveHIV = _handle_binop_Default
    _handle_binop_CasCmpEQ = _handle_binop_Default
    _handle_binop_CasCmpNE = _handle_binop_Default
    _handle_binop_ExpCmpNE = _handle_binop_Default
    _handle_binop_SarNV = _handle_binop_Default
    _handle_binop_ShrNV = _handle_binop_Default
    _handle_binop_ShlNV = _handle_binop_Default
    _handle_binop_CmpEQV = _handle_binop_Default
    _handle_binop_CmpNEV = _handle_binop_Default
    _handle_binop_CmpGEV = _handle_binop_Default
    _handle_binop_CmpGTV = _handle_binop_Default
    _handle_binop_CmpLEV = _handle_binop_Default
    _handle_binop_CmpLTV = _handle_binop_Default
    _handle_binop_MinV = _handle_binop_Default
    _handle_binop_MaxV = _handle_binop_Default
    _handle_binop_QAddV = _handle_binop_Default
    _handle_binop_QNarrowBinV = _handle_binop_Default
    _handle_binop_PermV = _handle_binop_Default
    _handle_binop_Set = _handle_binop_Default

    def _handle_binop_Mul(self, expr):
        expr0 = self._expr(expr.operands[0])
        expr1 = self._expr(expr.operands[1])
        bits = expr.bits

        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None and expr0_v.concrete and expr1_v.concrete:
            r = MultiValues(offset_to_values={0: {expr0_v * expr1_v}})  # type: ignore
        else:
            r = MultiValues(offset_to_values={0: {self.state.top(bits)}})

        return r

    def _handle_binop_Mull(self, expr):
        expr0 = self._expr(expr.operands[0])
        expr1 = self._expr(expr.operands[1])
        bits = expr.bits

        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None and expr0_v.concrete and expr1_v.concrete:
            xt = expr.bits // 2
            if expr.signed:
                r = MultiValues(
                    offset_to_values={0: {expr0_v.sign_extend(xt) * expr1_v.sign_extend(xt)}}  # type: ignore
                )
            else:
                r = MultiValues(
                    offset_to_values={0: {expr0_v.zero_extend(xt) * expr1_v.zero_extend(xt)}}  # type: ignore
                )
        else:
            r = MultiValues(offset_to_values={0: {self.state.top(bits)}})

        return r

    def _handle_binop_Shr(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        expr0 = self._expr_bv(expr.operands[0])
        expr1 = self._expr_bv(expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            if expr0_v.concrete and expr1_v.concrete:
                r = MultiValues(claripy.LShR(expr0_v, expr1_v.concrete_value))
        elif expr0_v is None and expr1_v is not None:
            # each value in expr0 >> expr1_v
            if expr0.count() == 1 and 0 in expr0 and all(v.concrete for v in expr0[0]) and expr1_v.concrete:
                vs = {
                    (claripy.LShR(v, expr1_v.concrete_value) if v.concrete else self.state.top(bits)) for v in expr0[0]
                }
                r = MultiValues(offset_to_values={0: vs})
        elif expr0_v is not None and expr1_v is None:
            # expr0_v >> each value in expr1
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                vs = {
                    (claripy.LShR(expr0_v, v.concrete_value) if v.concrete else self.state.top(bits)) for v in expr1[0]
                }
                r = MultiValues(offset_to_values={0: vs})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    def _handle_binop_Sar(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        expr0 = self._expr_bv(expr.operands[0])
        expr1 = self._expr_bv(expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            if expr0_v.concrete and expr1_v.concrete:
                r = MultiValues(expr0_v >> expr1_v.concrete_value)
        elif expr0_v is None and expr1_v is not None:
            # each value in expr0 >> expr1_v
            if expr0.count() == 1 and 0 in expr0 and all(v.concrete for v in expr0[0]) and expr1_v.concrete:
                vs = {
                    (claripy.LShR(v, expr1_v.concrete_value) if v.concrete else self.state.top(bits)) for v in expr0[0]
                }
                r = MultiValues(offset_to_values={0: vs})
        elif expr0_v is not None and expr1_v is None:
            # expr0_v >> each value in expr1
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                vs = {
                    (claripy.LShR(expr0_v, v.concrete_value) if v.concrete else self.state.top(bits)) for v in expr1[0]
                }
                r = MultiValues(offset_to_values={0: vs})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    def _handle_binop_Shl(self, expr):
        expr0 = self._expr_bv(expr.operands[0])
        expr1 = self._expr_bv(expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            if expr0_v.concrete and expr1_v.concrete:
                r = MultiValues(expr0_v << expr1_v.concrete_value)
        elif expr0_v is None and expr1_v is not None:
            # each value in expr0 << expr1_v
            if expr0.count() == 1 and 0 in expr0 and all(v.concrete for v in expr0[0]) and expr1_v.concrete:
                vs = {((v << expr1_v.concrete_value) if v.concrete else self.state.top(bits)) for v in expr0[0]}
                r = MultiValues(offset_to_values={0: vs})
        elif expr0_v is not None and expr1_v is None:
            # expr0_v >> each value in expr1
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                vs = {((expr0_v << v.concrete_value) if v.concrete else self.state.top(bits)) for v in expr1[0]}
                r = MultiValues(offset_to_values={0: vs})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    _handle_binop_Rol = _handle_binop_Default
    _handle_binop_Ror = _handle_binop_Default

    def _handle_binop_And(self, expr):
        expr0 = self._expr_bv(expr.operands[0])
        expr1 = self._expr_bv(expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            # special handling for stack alignment
            if self.state.is_stack_address(expr0_v):
                r = MultiValues(expr0_v)
            else:
                if expr0_v.concrete and expr1_v.concrete:
                    r = MultiValues(expr0_v & expr1_v)
        elif expr0_v is None and expr1_v is not None:
            # expr1_v & each value in expr0
            if expr0.count() == 1 and 0 in expr0 and all(v.concrete for v in expr0[0]):
                r = MultiValues(offset_to_values={0: {v & expr1_v for v in expr0[0]}})
        elif expr0_v is not None and expr1_v is None:
            # expr0_v & each value in expr1
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                r = MultiValues(offset_to_values={0: {expr0_v & v for v in expr1[0]}})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    def _handle_binop_Or(self, expr):
        expr0: MultiValues = self._expr(expr.operands[0])
        expr1: MultiValues = self._expr(expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            if expr0_v.concrete and expr1_v.concrete:
                r = MultiValues(expr0_v | expr1_v)
        elif expr0_v is None and expr1_v is not None:
            # expr1_v | each value in expr0
            if expr0.count() == 1 and 0 in expr0 and all(v.concrete for v in expr0[0]):
                vs = {v | expr1_v for v in expr0[0]}
                r = MultiValues(offset_to_values={0: vs})
        elif expr0_v is not None and expr1_v is None:
            # expr0_v | each value in expr1
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                vs = {expr0_v | v for v in expr1[0]}
                r = MultiValues(offset_to_values={0: vs})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    def _handle_binop_LogicalAnd(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        expr0: MultiValues = self._expr(expr.operands[0])
        expr1: MultiValues = self._expr(expr.operands[1])
        bits = expr.bits

        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        # TODO: can maybe be smarter about this. if we can determine that expr0 is never falsey, we can just return it,
        # TODO: or if it's always falsey we can return expr1 (did I get this backwards?)
        if expr0_v is None or expr1_v is None:
            return MultiValues(self.state.top(bits))

        return MultiValues(claripy.If(expr0_v == 0, expr0_v, expr1_v))

    def _handle_binop_LogicalOr(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        expr0: MultiValues = self._expr(expr.operands[0])
        expr1: MultiValues = self._expr(expr.operands[1])
        bits = expr.bits

        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is None or expr1_v is None:
            return MultiValues(self.state.top(bits))

        return MultiValues(claripy.If(expr0_v != 0, expr0_v, expr1_v))

    def _handle_binop_LogicalXor(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        expr0: MultiValues = self._expr(expr.operands[0])
        expr1: MultiValues = self._expr(expr.operands[1])
        bits = expr.bits

        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is None or expr1_v is None:
            return MultiValues(self.state.top(bits))

        return MultiValues(claripy.If(expr0_v != 0, expr1_v, expr0_v))

    def _handle_binop_Xor(self, expr):
        expr0: MultiValues = self._expr(expr.operands[0])
        expr1: MultiValues = self._expr(expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            if expr0_v.concrete and expr1_v.concrete:
                r = MultiValues(expr0_v ^ expr1_v)
        elif expr0_v is None and expr1_v is not None:
            # expr1_v ^ each value in expr0
            if expr0.count() == 1 and 0 in expr0 and all(v.concrete for v in expr0[0]):
                vs = {v ^ expr1_v for v in expr0[0]}
                r = MultiValues(offset_to_values={0: vs})
        elif expr0_v is not None and expr1_v is None:
            # expr0_v ^ each value in expr1
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                vs = {expr0_v ^ v for v in expr1[0]}
                r = MultiValues(offset_to_values={0: vs})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    def _handle_binop_Carry(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        _ = self._expr(expr.operands[0])
        _ = self._expr(expr.operands[1])
        bits = expr.bits
        return MultiValues(self.state.top(bits))

    def _handle_binop_SCarry(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        _ = self._expr(expr.operands[0])
        _ = self._expr(expr.operands[1])
        bits = expr.bits
        return MultiValues(self.state.top(bits))

    def _handle_binop_SBorrow(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        _ = self._expr(expr.operands[0])
        _ = self._expr(expr.operands[1])
        bits = expr.bits
        return MultiValues(self.state.top(bits))

    def _handle_binop_Concat(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        expr0 = self._expr_bv(expr.operands[0])
        expr1 = self._expr_bv(expr.operands[1])
        bits = expr.bits

        r: MultiValues[claripy.ast.BV | claripy.ast.FP] | None = None
        expr0_v = expr0.one_value()
        expr1_v = expr1.one_value()

        if expr0_v is not None and expr1_v is not None:
            if expr0_v.concrete and expr1_v.concrete:
                r = MultiValues(claripy.Concat(expr0_v, expr1_v))
        elif expr0_v is None and expr1_v is not None:
            # concatenate expr1_v with each value in expr0
            if expr0.count() == 1 and 0 in expr0 and all(v.concrete for v in expr0[0]):
                r = MultiValues(offset_to_values={0: {claripy.Concat(v, expr1_v) for v in expr0[0]}})
        elif expr0_v is not None and expr1_v is None:
            # concatenate expr0_v with each value in expr1
            if expr1.count() == 1 and 0 in expr1 and all(v.concrete for v in expr1[0]):
                r = MultiValues(offset_to_values={0: {claripy.Concat(expr0_v, v) for v in expr1[0]}})
        else:
            r = MultiValues(self.state.top(bits))

        if r is None:
            r = MultiValues(self.state.top(bits))

        return r

    def _handle_binop_Cmp(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        op0 = self._expr(expr.operands[0])
        op1 = self._expr(expr.operands[1])

        if op0 is None:
            _ = expr.operands[0]
        if op1 is None:
            _ = expr.operands[1]

        top = self.state.top(expr.bits)
        return MultiValues(top)

    _handle_binop_CmpF = _handle_binop_Cmp
    _handle_binop_CmpEQ = _handle_binop_Cmp
    _handle_binop_CmpNE = _handle_binop_Cmp
    _handle_binop_CmpLE = _handle_binop_Cmp
    _handle_binop_CmpLEs = _handle_binop_Cmp
    _handle_binop_CmpLT = _handle_binop_Cmp
    _handle_binop_CmpLTs = _handle_binop_Cmp
    _handle_binop_CmpGE = _handle_binop_Cmp
    _handle_binop_CmpGEs = _handle_binop_Cmp
    _handle_binop_CmpGT = _handle_binop_Cmp
    _handle_binop_CmpGTs = _handle_binop_Cmp
    _handle_binop_CmpORD = _handle_binop_Cmp

    def _handle_binop_ExpCmpNE(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        self._expr(expr.operands[0])
        self._expr(expr.operands[1])

        return MultiValues(self.state.top(expr.bits))

    def _handle_unop_Clz(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        self._expr(expr.operand)
        return MultiValues(self.state.top(expr.bits))

    def _handle_expr_Const(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        self.state.mark_const(expr.value, expr.size)
        if isinstance(expr.value, float):
            sort = None
            if expr.bits == 64:
                sort = FSORT_DOUBLE
            elif expr.bits == 32:
                sort = FSORT_FLOAT
            return MultiValues(claripy.FPV(expr.value, sort))
        return MultiValues(claripy.BVV(expr.value, expr.bits))

    def _handle_expr_StackBaseOffset(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        stack_addr = self.state.stack_address(expr.offset)
        return MultiValues(stack_addr)

    def _ail_handle_VEXCCallExpression(self, expr: ailment.Expr.VEXCCallExpression) -> MultiValues:
        for operand in expr.operands:
            self._expr(operand)

        top = self.state.top(expr.bits)
        return MultiValues(top)

    def _handle_expr_Phi(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        return self._top(expr.bits)  # TODO

    def _handle_expr_VEXCCallExpression(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        return self._top(expr.bits)  # TODO

    def _handle_expr_VirtualVariable(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        return self._top(expr.bits)  # TODO

    def _handle_expr_DirtyExpression(self, expr) -> MultiValues[claripy.ast.BV | claripy.ast.FP]:
        if isinstance(expr.dirty_expr, ailment.expression.VEXCCallExpression):
            for operand in expr.dirty_expr.operands:
                self._expr(operand)

        return MultiValues(self.state.top(expr.bits))

    def _handle_expr_BasePointerOffset(self, expr):
        return self._top(expr.bits)

    def _handle_expr_MultiStatementExpression(self, expr):
        return self._top(expr.bits)
