# pylint:disable=wrong-import-position,wrong-import-order
from __future__ import annotations
import enum
from typing import TYPE_CHECKING, Literal, cast
from collections.abc import Sequence
from collections import defaultdict, OrderedDict
import logging
import functools
import contextlib

import pyvex
import claripy
from archinfo.arch_arm import is_arm_arch
from claripy.annotation import UninitializedAnnotation

from angr import sim_options as o
from angr import BP, BP_BEFORE, BP_AFTER
from angr.misc.ux import once
from angr.concretization_strategies import SimConcretizationStrategyAny
from angr.knowledge_plugins.cfg import IndirectJump, IndirectJumpType
from angr.engines.vex.claripy import ccall
from angr.engines.light import SimEngineNostmtVEX, SpOffset, RegisterOffset
from angr.errors import AngrError, SimError
from angr.blade import Blade
from angr.annocfg import AnnotatedCFG
from angr.exploration_techniques.slicecutor import Slicecutor
from angr.exploration_techniques.local_loop_seer import LocalLoopSeer
from angr.exploration_techniques.explorer import Explorer
from angr.utils.constants import DEFAULT_STATEMENT
from angr.analyses.propagator.top_checker_mixin import ClaripyDataVEXEngineMixin
from angr.engines.vex.claripy.datalayer import value
from .resolver import IndirectJumpResolver
from .constant_value_manager import ConstantValueManager

try:
    from angr.engines import pcode
except ImportError:
    pcode = None

if TYPE_CHECKING:
    from angr.knowledge_plugins import Function

l = logging.getLogger(name=__name__)


class NotAJumpTableNotification(AngrError):
    """
    Exception raised to indicate this is not (or does not appear to be) a jump table.
    """


class UninitReadMeta:
    """
    Uninitialized read remapping details.
    """

    uninit_read_base = 0xC000000


class AddressTransformationTypes(int, enum.Enum):
    """
    Address transformation operations.
    """

    Assignment = 0
    SignedExtension = 1
    UnsignedExtension = 2
    Truncation = 3
    Or1 = 4
    ShiftLeft = 5
    ShiftRight = 6
    Add = 7
    Load = 8


class AddressTransformation:
    """
    Describe and record an address transformation operation.
    """

    def __init__(self, op: AddressTransformationTypes, operands: list, first_load: bool = False):
        self.op = op
        self.operands = operands
        self.first_load = first_load

    def __repr__(self):
        return f"<Transformation: {self.op} {self.operands}>"


class AddressOperand:
    """
    The class for the singleton class AddressSingleton. It represents the address being transformed before using as an
    indirect jump target.
    """

    def __repr__(self):
        return "ADDR"


AddressSingleton = AddressOperand()


class Tmp:
    """
    For modeling Tmp variables.
    """

    def __init__(self, tmp_idx):
        self.tmp_idx = tmp_idx


class JumpTargetBaseAddr:
    """
    Model for jump targets and their data origin.
    """

    def __init__(
        self, stmt_loc, stmt: pyvex.stmt.IRStmt, tmp: int, base_addr: int | None = None, tmp_1: int | None = None
    ):
        self.stmt_loc = stmt_loc
        self.stmt = stmt
        self.tmp = tmp
        self.tmp_1 = tmp_1
        self.base_addr = base_addr

        assert base_addr is not None or tmp_1 is not None

    @property
    def base_addr_available(self):
        return self.base_addr is not None


#
# Jump table pre-check
#

_x86_ct = ccall.data["X86"]["CondTypes"]
_amd64_ct = ccall.data["AMD64"]["CondTypes"]
EXPECTED_COND_TYPES = {
    "X86": {
        _x86_ct["CondB"],
        _x86_ct["CondNB"],
        _x86_ct["CondBE"],
        _x86_ct["CondNBE"],
        _x86_ct["CondL"],
        _x86_ct["CondNL"],
        _x86_ct["CondLE"],
        _x86_ct["CondNLE"],
    },
    "AMD64": {
        _amd64_ct["CondB"],
        _amd64_ct["CondNB"],
        _amd64_ct["CondBE"],
        _amd64_ct["CondNBE"],
        _amd64_ct["CondL"],
        _amd64_ct["CondNL"],
        _amd64_ct["CondLE"],
        _amd64_ct["CondNLE"],
    },
    "ARM": {
        ccall.ARMCondHS,
        ccall.ARMCondLO,
        ccall.ARMCondHI,
        ccall.ARMCondLS,
        ccall.ARMCondGE,
        ccall.ARMCondLT,
        ccall.ARMCondGT,
        ccall.ARMCondLE,
    },
    "AARCH64": {
        ccall.ARM64CondCS,
        ccall.ARM64CondCC,
        ccall.ARM64CondHI,
        ccall.ARM64CondLS,
        ccall.ARM64CondGE,
        ccall.ARM64CondLT,
        ccall.ARM64CondGT,
        ccall.ARM64CondLE,
    },
}


class JumpTableProcessorState:
    """
    The state used in JumpTableProcessor.
    """

    __slots__ = (
        "_registers",
        "_stack",
        "_tmpvar_source",
        "arch",
        "is_jumptable",
        "regs_to_initialize",
        "stmts_to_instrument",
    )

    def __init__(self, arch):
        self.arch = arch

        self._registers = {}
        self._stack = {}
        self._tmpvar_source = {}  # a mapping from temporary variables to their origins

        self.is_jumptable: bool | None = None  # is the current slice representing a jump table?
        self.stmts_to_instrument = []  # Store/Put statements that we should instrument
        self.regs_to_initialize = []  # registers that we should initialize


class RegOffsetAnnotation(claripy.Annotation):
    """
    Register Offset annotation.
    """

    __slots__ = ("reg_offset",)

    def __init__(self, reg_offset: RegisterOffset):
        self.reg_offset = reg_offset

    @property
    def relocatable(self):
        return False

    @property
    def eliminatable(self):
        return False


binop_handler = SimEngineNostmtVEX[JumpTableProcessorState, claripy.ast.BV, JumpTableProcessorState].binop_handler


class JumpTableProcessor(
    SimEngineNostmtVEX[JumpTableProcessorState, claripy.ast.BV, JumpTableProcessorState],
    ClaripyDataVEXEngineMixin[JumpTableProcessorState, claripy.ast.BV, JumpTableProcessorState, None],
):  # pylint:disable=abstract-method
    """
    Implements a simple and stupid data dependency tracking for stack and register variables.

    Also determines which statements to instrument during static execution of the slice later. For example, the
    following example is not uncommon in non-optimized binaries::

            mov  [rbp+var_54], 1
        loc_4051a6:
            cmp  [rbp+var_54], 6
            ja   loc_405412 (default)
        loc_4051b0:
            mov  eax, [rbp+var_54]
            mov  rax, qword [rax*8+0x223a01]
            jmp  rax

    We want to instrument the first instruction and replace the constant 1 with a symbolic variable, otherwise we will
    not be able to recover all jump targets later in block 0x4051b0.
    """

    def __init__(self, project, indirect_jump_node_pred_addrs: set[int], bp_sp_diff=0x100):
        super().__init__(project)
        self._bp_sp_diff = bp_sp_diff  # bp - sp
        self._tsrc: set[Literal["const"] | tuple[int, int]] = (
            set()
        )  # a scratch variable to store source information for values
        self._indirect_jump_node_pred_addrs = indirect_jump_node_pred_addrs

        self._SPOFFSET_BASE = claripy.BVS("SpOffset", self.project.arch.bits, explicit_name=True)
        self._REGOFFSET_BASE: dict[int, claripy.ast.BV] = {}

    def _process_block_end(self, stmt_result, whitelist):
        return self.state

    @staticmethod
    def _is_spoffset(expr) -> bool:
        return "SpOffset" in expr.variables

    def _get_spoffset_expr(self, sp_offset: SpOffset) -> claripy.ast.BV:
        return self._SPOFFSET_BASE.annotate(RegOffsetAnnotation(sp_offset))

    @staticmethod
    def _extract_spoffset_from_expr(expr: claripy.ast.Base) -> RegisterOffset | None:
        if expr.op == "BVS":
            for anno in expr.annotations:
                if isinstance(anno, RegOffsetAnnotation):
                    return anno.reg_offset
        elif expr.op == "__add__":
            if len(expr.args) == 1:
                return JumpTableProcessor._extract_spoffset_from_expr(cast(claripy.ast.BV, expr.args[0]))
            if len(expr.args) == 2 and cast(claripy.ast.BV, expr.args[1]).op == "BVV":
                sp_offset = JumpTableProcessor._extract_spoffset_from_expr(cast(claripy.ast.BV, expr.args[0]))
                if sp_offset is not None:
                    delta = cast(claripy.ast.BV, expr.args[1]).concrete_value
                    sp_offset += delta
                    return sp_offset
        elif expr.op == "__and__" and len(expr.args) == 2 and cast(claripy.ast.BV, expr.args[1]).op == "BVV":
            # ignore all masking on SpOffsets
            return JumpTableProcessor._extract_spoffset_from_expr(cast(claripy.ast.BV, expr.args[0]))
        return None

    @staticmethod
    def _is_registeroffset(expr) -> bool:
        return "RegisterOffset" in expr.variables

    def _get_regoffset_expr(self, reg_offset: RegisterOffset, bits: int) -> claripy.ast.BV:
        if bits not in self._REGOFFSET_BASE:
            self._REGOFFSET_BASE[bits] = claripy.BVS("RegisterOffset", bits, explicit_name=True)
        return self._REGOFFSET_BASE[bits].annotate(RegOffsetAnnotation(reg_offset))

    @staticmethod
    def _extract_regoffset_from_expr(expr: claripy.ast.Base) -> RegisterOffset | None:
        if expr.op == "BVS":
            for anno in expr.annotations:
                if isinstance(anno, RegOffsetAnnotation):
                    return anno.reg_offset
        elif expr.op == "__add__":
            if len(expr.args) == 1:
                return JumpTableProcessor._extract_regoffset_from_expr(cast(claripy.ast.BV, expr.args[0]))
            if len(expr.args) == 2 and cast(claripy.ast.BV, expr.args[1]).op == "BVV":
                reg_offset = JumpTableProcessor._extract_regoffset_from_expr(cast(claripy.ast.BV, expr.args[0]))
                if reg_offset is not None:
                    delta = cast(claripy.ast.BV, expr.args[1]).concrete_value
                    reg_offset += delta
                    return reg_offset
        elif expr.op == "__and__" and len(expr.args) == 2 and cast(claripy.ast.BV, expr.args[1]).op == "BVV":
            # ignore all masking on SpOffsets
            return JumpTableProcessor._extract_spoffset_from_expr(cast(claripy.ast.BV, expr.args[0]))
        return None

    def _handle_stmt_WrTmp(self, stmt):
        self._tsrc = set()

        self.tmps[stmt.tmp] = self._expr(stmt.data)
        if self._tsrc:
            self.state._tmpvar_source[stmt.tmp] = self._tsrc

    def _handle_stmt_Put(self, stmt):
        self._tsrc = set()
        offset = stmt.offset
        data = self._expr(stmt.data)
        r = (self._tsrc, data) if self._tsrc is not None else ((self.block.addr, self.stmt_idx), data)
        self.state._registers[offset] = r

    def _handle_stmt_Store(self, stmt):
        self._tsrc = set()
        addr = self._expr(stmt.addr)
        data = self._expr(stmt.data)

        if addr is None:
            return

        if isinstance(addr, SpOffset):
            self.state._stack[addr.offset] = ((self.block.addr, self.stmt_idx), data)

    def _handle_expr_RdTmp(self, expr):
        try:
            v = self.tmps[expr.tmp]
        except KeyError:
            v = self._top(expr.result_size(self.tyenv))
        if expr.tmp in self.state._tmpvar_source:
            self._tsrc |= set(self.state._tmpvar_source[expr.tmp])
        return v

    def _handle_expr_Get(self, expr):
        if expr.offset == self.arch.bp_offset:
            v = self._get_spoffset_expr(SpOffset(self.arch.bits, self._bp_sp_diff))
        elif expr.offset == self.arch.sp_offset:
            v = self._get_spoffset_expr(SpOffset(self.arch.bits, 0))
        else:
            if expr.offset in self.state._registers:
                self._tsrc |= set(self.state._registers[expr.offset][0])
                v = self.state._registers[expr.offset][1]
            else:
                # the register does not exist
                # we initialize it here
                v = RegisterOffset(expr.result_size(self.tyenv), expr.offset, 0)
                v = self._get_regoffset_expr(v, expr.result_size(self.tyenv))
                src = (self.block.addr, self.stmt_idx)
                self._tsrc.add(src)
                self.state._registers[expr.offset] = ([src], v)

        # make sure the size matches
        # note that this is sometimes incorrect. for example, we do not differentiate between reads at ah and al...
        # but it should be good enough for now (without switching state._registers to a real SimMemory, which will
        # surely slow down stuff quite a bit)
        if v is not None:
            bits = expr.result_size(self.tyenv)
            if v.size() > bits:
                v = v[bits - 1 : 0]
            elif v.size() < bits:
                v = claripy.ZeroExt(bits - v.size(), v)
        return v

    def _handle_function(self, expr):  # pylint:disable=unused-argument,no-self-use
        return None  # This analysis is not interprocedural

    def _handle_expr_Load(self, expr):
        addr = self._expr(expr.addr)
        size = expr.result_size(self.tyenv) // 8
        return self._do_load(addr, size)

    def _handle_stmt_LoadG(self, stmt: pyvex.stmt.LoadG):
        self._tsrc = set()

        guard = self._expr(stmt.guard) != 0
        iftrue = self._do_load(self._expr(stmt.addr), stmt.addr.result_size(self.tyenv) // 8)
        iffalse = self._expr(stmt.alt)
        result = claripy.If(guard, iftrue, iffalse)
        self.tmps[stmt.dst] = result
        if self._tsrc:
            self.state._tmpvar_source[stmt.dst] = self._tsrc

    def _handle_expr_Const(self, expr):
        v = value(expr.con.type, expr.con.value)
        self._tsrc.add("const")
        if not isinstance(v, claripy.ast.BV):
            return self._top(expr.result_size(self.tyenv))
        return v

    @binop_handler
    def _handle_binop_And(self, expr):
        arg0 = self._expr(expr.args[0])
        if (
            isinstance(arg0, claripy.ast.BV)
            and self._is_registeroffset(arg0)
            and isinstance(expr.args[1], pyvex.IRExpr.Const)
        ):
            mask_value = expr.args[1].con.value
            if mask_value in {1, 3, 7, 15, 31, 63, 127, 255}:
                # 1cbbf108f44c8f4babde546d26425ca5340dccf878d306b90eb0fbec2f83ab51:0x40bd1b
                self.state.is_jumptable = True
                return arg0 & mask_value
        return self._top(expr.result_size(self.tyenv))

    @binop_handler
    def _handle_binop_CmpLE(self, expr):
        return self._handle_Comparison(*expr.args)

    @binop_handler
    def _handle_binop_CmpGE(self, expr):
        return self._handle_Comparison(*expr.args)

    @binop_handler
    def _handle_binop_CmpLT(self, expr):
        return self._handle_Comparison(*expr.args)

    @binop_handler
    def _handle_binop_CmpGT(self, expr):
        return self._handle_Comparison(*expr.args)

    def _handle_expr_CCall(self, expr):
        if isinstance(expr.args[0], pyvex.IRExpr.Const):
            cond_type_enum = expr.args[0].con.value

            if self.arch.name in {"X86", "AMD64", "AARCH64"}:
                if cond_type_enum in EXPECTED_COND_TYPES[self.arch.name]:
                    self._handle_Comparison(expr.args[2], expr.args[3])
            elif is_arm_arch(self.arch):
                if cond_type_enum in EXPECTED_COND_TYPES["ARM"]:
                    self._handle_Comparison(expr.args[2], expr.args[3])
            else:
                # other architectures
                l.warning("Please fill in EXPECTED_COND_TYPES for %s.", self.arch.name)
                self._handle_Comparison(expr.args[2], expr.args[3])

        return self._top(expr.result_size(self.tyenv))

    def _handle_expr_VECRET(self, expr):
        return self._top(expr.result_size(self.tyenv))

    def _handle_expr_GSPTR(self, expr):
        return self._top(expr.result_size(self.tyenv))

    def _handle_expr_GetI(self, expr):
        return self._top(expr.result_size(self.tyenv))

    def _handle_expr_ITE(self, expr):
        return self._top(expr.result_size(self.tyenv))

    def _handle_Comparison(self, arg0: pyvex.expr.IRExpr, arg1: pyvex.expr.IRExpr) -> claripy.ast.BV:
        if self.block.addr not in self._indirect_jump_node_pred_addrs:
            return self._top(1)

        # found the comparison
        arg0_src, arg1_src = None, None

        if isinstance(arg0, pyvex.IRExpr.RdTmp):
            if arg0.tmp in self.state._tmpvar_source:
                arg0_src = self.state._tmpvar_source[arg0.tmp]
                arg0_src = None if not arg0_src or len(arg0_src) > 1 else next(iter(arg0_src))
        elif isinstance(arg0, pyvex.IRExpr.Const):
            arg0_src = "const"
        if isinstance(arg1, pyvex.IRExpr.RdTmp):
            if arg1.tmp in self.state._tmpvar_source:
                arg1_src = self.state._tmpvar_source[arg1.tmp]
                arg1_src = None if not arg1_src or len(arg1_src) > 1 else next(iter(arg1_src))
        elif isinstance(arg1, pyvex.IRExpr.Const):
            arg1_src = "const"

        if arg0_src == "const" and arg1_src == "const":
            # comparison of two consts... there is nothing we can do
            self.state.is_jumptable = True
            return self._top(1)
        if arg0_src not in {"const", None} and arg1_src not in {"const", None}:
            # this is probably not a jump table
            return self._top(1)
        if arg1_src == "const":
            # make sure arg0_src is const
            arg0_src, arg1_src = arg1_src, arg0_src

        self.state.is_jumptable = True

        if arg0_src != "const":
            # we failed during dependency tracking so arg0_src couldn't be determined
            # but we will still try to resolve it as a jump table as a fall back
            return self._top(1)

        if isinstance(arg1_src, tuple):
            arg1_src_stmt = self.project.factory.block(arg1_src[0], cross_insn_opt=True).vex.statements[arg1_src[1]]
            if isinstance(arg1_src_stmt, pyvex.IRStmt.Store):
                # Storing a constant/variable in memory
                # We will need to overwrite it when executing the slice to guarantee the full recovery of jump table
                # targets.
                #
                # Here is an example:
                #     mov  [rbp+var_54], 1
                # loc_4051a6:
                #     cmp  [rbp+var_54], 6
                #     ja   loc_405412 (default)
                #
                # Instead of writing 1 to [rbp+var_54], we want to write a symbolic variable there instead. Otherwise
                # we will only recover the second jump target instead of all 7 targets.
                self.state.stmts_to_instrument.append(("mem_write", *arg1_src))
            elif isinstance(arg1_src_stmt, pyvex.IRStmt.WrTmp) and isinstance(arg1_src_stmt.data, pyvex.IRExpr.Load):
                # Loading a constant/variable from memory (and later the value is stored in a register)
                # Same as above, we will need to overwrite it when executing the slice to guarantee the full recovery
                # of jump table targets.
                #
                # Here is an example:
                #     mov eax, [0x625a3c]
                #     cmp eax, 0x4
                #     ja  0x40899d  (default)
                # loc_408899:
                #     mov eax, eax
                #     mov rax, qword [rax*8+0x220741]
                #     jmp rax
                #
                self.state.stmts_to_instrument.append(("mem_read", *arg1_src))
            elif isinstance(arg1_src_stmt, pyvex.IRStmt.Put):
                # Storing a constant/variable in register
                # Same as above...
                #
                # Here is an example:
                #     movzx eax, byte ptr [rax+12h]
                #     movzx eax, al
                #     cmp   eax, 0xe
                #     ja    0x405b9f (default)
                # loc_405b34:
                #     mov   eax, eax
                #     mov   rax, qword [rax*8+0x2231ae]
                #
                self.state.stmts_to_instrument.append(("reg_write", *arg1_src))

        return self._top(1)

    def _do_load(self, addr: claripy.ast.BV, size: int) -> claripy.ast.BV:
        src = (self.block.addr, self.stmt_idx)
        self._tsrc = {src}

        if self._is_spoffset(addr):
            spoffset = self._extract_spoffset_from_expr(addr)
            if spoffset is not None and spoffset.offset in self.state._stack:
                self._tsrc = {self.state._stack[spoffset.offset][0]}
                return self.state._stack[spoffset.offset][1]
        elif self._is_registeroffset(addr):
            # Load data from a register, but this register hasn't been initialized at this point
            # We will need to initialize this register during slice execution later

            # Try to get where this register is first accessed
            reg_offset = self._extract_regoffset_from_expr(addr)
            if reg_offset is not None and reg_offset.reg in self.state._registers:
                try:
                    source = next(iter(src for src in self.state._registers[reg_offset.reg][0] if src != "const"))
                    assert isinstance(source, tuple)
                    self.state.regs_to_initialize.append((*source, reg_offset.reg, reg_offset.bits))
                except StopIteration:
                    # we don't need to initialize this register
                    # it might be caused by an incorrect analysis result
                    # e.g.  PN-337140.bin 11e918  r0 comes from r4, r4 comes from r0@11e8c0, and r0@11e8c0 comes from
                    # function call sub_375c04. Since we do not analyze sub_375c04, we treat r0@11e918 as a constant 0.
                    pass

        return self._top(size)


#
# State hooks
#


class StoreHook:
    """
    Hook for memory stores.
    """

    @staticmethod
    def hook(state):
        write_length = state.inspect.mem_write_length
        if write_length is None:
            write_length = len(state.inspect.mem_write_expr)
        else:
            write_length = write_length * state.arch.byte_width
        state.inspect.mem_write_expr = claripy.BVS("instrumented_store", write_length)


class LoadHook:
    """
    Hook for memory loads.
    """

    def __init__(self):
        self._var = None

    def hook_before(self, state):
        addr = state.inspect.mem_read_address
        size = state.solver.eval(state.inspect.mem_read_length)
        self._var = claripy.BVS("instrumented_load", size * 8)
        state.memory.store(addr, self._var, endness=state.arch.memory_endness)

    def hook_after(self, state):
        state.inspect.mem_read_expr = self._var


class PutHook:
    """
    Hook for register writes.
    """

    @staticmethod
    def hook(state):
        state.inspect.reg_write_expr = claripy.BVS(
            "instrumented_put", state.solver.eval(state.inspect.reg_write_length) * 8
        )


class RegisterInitializerHook:
    """
    Hook for register init.
    """

    def __init__(self, reg_offset, reg_bits, initial_value):
        self.reg_offset = reg_offset
        self.reg_bits = reg_bits
        self.value = initial_value

    def hook(self, state):
        state.registers.store(self.reg_offset, claripy.BVV(self.value, self.reg_bits))


class BSSHook:
    """
    Hook for BSS read/write.
    """

    def __init__(self, project, bss_regions):
        self.project = project
        self._bss_regions = bss_regions
        self._written_addrs = set()

    def bss_memory_read_hook(self, state):
        if not self._bss_regions:
            return

        read_addr = state.inspect.mem_read_address
        read_length = state.inspect.mem_read_length

        if not isinstance(read_addr, int) and read_addr.symbolic:
            # don't touch it
            return

        concrete_read_addr = state.solver.eval(read_addr)
        concrete_read_length = state.solver.eval(read_length)

        for start, size in self._bss_regions:
            if start <= concrete_read_addr < start + size:
                # this is a read from the .bss section
                break
        else:
            return

        if concrete_read_addr not in self._written_addrs:
            # it was never written to before. we overwrite it with unconstrained bytes
            for i in range(0, concrete_read_length, self.project.arch.bytes):
                state.memory.store(
                    concrete_read_addr + i,
                    state.solver.Unconstrained("unconstrained", self.project.arch.bits),
                    endness=self.project.arch.memory_endness,
                )

                # job done :-)

    def bss_memory_write_hook(self, state):
        if not self._bss_regions:
            return

        write_addr = state.inspect.mem_write_address

        if not isinstance(write_addr, int) and write_addr.symbolic:
            return

        concrete_write_addr = state.solver.eval(write_addr)
        concrete_write_length = (
            state.solver.eval(state.inspect.mem_write_length)
            if state.inspect.mem_write_length is not None
            else len(state.inspect.mem_write_expr) // state.arch.byte_width
        )

        for start, size in self._bss_regions:
            if start <= concrete_write_addr < start + size:
                # hit a BSS section
                break
        else:
            return

        if concrete_write_length > 1024:
            l.warning("Writing more 1024 bytes to the BSS region, only considering the first 1024 bytes.")
            concrete_write_length = 1024

        for i in range(concrete_write_addr, concrete_write_length):
            self._written_addrs.add(i)


class MIPSGPHook:
    """
    Hooks all reads from and writes into the gp register for MIPS32 binaries.
    """

    def __init__(self, gp_offset: int, gp: int):
        self.gp_offset = gp_offset
        self.gp = gp

    def gp_register_read_hook(self, state):
        read_offset = state.inspect.reg_read_offset
        read_length = state.inspect.reg_read_length
        if state.solver.eval(read_offset) == self.gp_offset and read_length == 4:
            state.inspect.reg_read_expr = claripy.BVV(self.gp, size=32)

    def gp_register_write_hook(self, state):
        write_offset = state.inspect.reg_write_offset
        write_length = state.inspect.reg_write_length
        if state.solver.eval(write_offset) == self.gp_offset and write_length == 4:
            state.inspect.reg_write_expr = claripy.BVV(self.gp, size=32)


#
# Main class
#


class JumpTableResolver(IndirectJumpResolver):
    """
    A generic jump table resolver.

    This is a fast jump table resolution. For performance concerns, we made the following assumptions:
        - The final jump target comes from the memory.
        - The final jump target must be directly read out of the memory, without any further modification or altering.

    Progressively larger program slices will be analyzed to determine jump table location and size. If the size of the
    table cannot be determined, a *guess* will be made based on how many entries in the table *appear* valid.
    """

    def __init__(self, project, resolve_calls: bool = True):
        super().__init__(project, timeless=False)

        self.resolve_calls = resolve_calls

        self._bss_regions = None
        # the maximum number of resolved targets. Will be initialized from CFG.
        self._max_targets = 0

        # cached memory read addresses that are used to initialize uninitialized registers
        # should be cleared before every symbolic execution run on the slice
        self._cached_memread_addrs = {}

        self._find_bss_region()

    def filter(self, cfg, addr, func_addr, block, jumpkind):
        if pcode is not None and isinstance(block.vex, pcode.lifter.IRSB):  # type:ignore
            if once("pcode__indirect_jump_resolver"):
                l.warning("JumpTableResolver does not support P-Code IR yet; CFG may be incomplete.")
            return False

        if jumpkind == "Ijk_Boring":
            return True
        return bool(self.resolve_calls and jumpkind == "Ijk_Call")

    def resolve(self, cfg, addr, func_addr, block, jumpkind, func_graph_complete: bool = True, **kwargs):
        """
        Resolves jump tables.

        :param cfg: A CFG instance.
        :param int addr: IRSB address.
        :param int func_addr: The function address.
        :param pyvex.IRSB block: The IRSB.
        :return: A bool indicating whether the indirect jump is resolved successfully, and a list of resolved targets
        :rtype: tuple
        """

        if not cfg.kb.functions.contains_addr(func_addr):
            # fix for angr issue #3768
            # the function must exist in the KB
            return False, None

        func: Function = cfg.kb.functions[func_addr]
        self._max_targets = cfg._indirect_jump_target_limit

        # this is an indirect call if (1) the instruction is a call, or (2) the instruction is a tail jump (we detect
        # sp moving up to approximate)
        potential_call_table = jumpkind == "Ijk_Call" or self._sp_moved_up(block) or len(func.block_addrs_set) <= 5
        # we only perform full-function propagation for jump tables or call tables in really small functions
        if not potential_call_table or len(func.block_addrs_set) <= 5:
            cv_manager = ConstantValueManager(self.project, cfg.kb, func, addr)
        else:
            cv_manager = None

        for slice_steps in range(1, 5):
            # Perform a backward slicing from the jump target
            # Important: Do not go across function call boundaries
            b = Blade(
                cfg.graph,
                addr,
                -1,
                cfg=cfg,
                project=self.project,
                ignore_sp=False,
                ignore_bp=False,
                max_level=slice_steps,
                base_state=self.base_state,
                stop_at_calls=True,
                cross_insn_opt=True,
            )

            l.debug("Try resolving %#x with a %d-level backward slice...", addr, slice_steps)
            r, targets = self._resolve(
                cfg, addr, func, b, cv_manager, potential_call_table=False, func_graph_complete=func_graph_complete
            )
            if r:
                return r, targets

        if potential_call_table:
            b = Blade(
                cfg.graph,
                addr,
                -1,
                cfg=cfg,
                project=self.project,
                ignore_sp=False,
                ignore_bp=False,
                max_level=1,
                base_state=self.base_state,
                stop_at_calls=True,
                cross_insn_opt=True,
            )
            return self._resolve(
                cfg, addr, func, b, cv_manager, potential_call_table=True, func_graph_complete=func_graph_complete
            )

        return False, None

    #
    # Private methods
    #

    def _resolve(
        self,
        cfg,
        addr: int,
        func: Function,
        b: Blade,
        cv_manager: ConstantValueManager | None,
        potential_call_table: bool = False,
        func_graph_complete: bool = True,
    ) -> tuple[bool, Sequence[int] | None]:
        """
        Internal method for resolving jump tables.

        :param cfg:       A CFG instance.
        :param addr:      Address of the block where the indirect jump is.
        :param func:      The Function instance.
        :param b:         The generated backward slice.
        :return:          A bool indicating whether the indirect jump is resolved successfully, and a list of
                          resolved targets.
        """

        project = self.project  # short-hand
        func_addr = func.addr
        is_arm = is_arm_arch(self.project.arch)

        stmt_loc = (addr, DEFAULT_STATEMENT)
        if stmt_loc not in b.slice:
            return False, None

        (
            load_stmt_loc,
            load_stmt,
            load_size,
            stmts_to_remove,
            stmts_adding_base_addr,
            transformations,
        ) = self._find_load_statement(b, stmt_loc)
        ite_stmt, ite_stmt_loc = None, None

        if load_stmt_loc is None:
            # the load statement is not found
            # maybe it's a typical ARM-style jump table like the following:
            #   SUB    R3, R5, #34
            #   CMP    R3, #28
            #   ADDLS  PC, PC, R3,LSL#2
            if is_arm:
                ite_stmt, ite_stmt_loc, stmts_to_remove = self._find_load_pc_ite_statement(b, stmt_loc)
