from __future__ import annotations
from typing import TYPE_CHECKING, Any
from collections.abc import Callable, Generator
import logging
from collections import defaultdict

import archinfo
from archinfo.arch_arm import is_arm_arch
import claripy
import networkx

from angr.utils.graph import GraphUtils
from angr.analyses import ForwardAnalysis
from .cfg.cfg_job_base import BlockID, FunctionKey, CFGJobBase
from angr import sim_options
from angr.engines.procedure import ProcedureEngine
from angr.engines import SimSuccessors
from angr.errors import (
    AngrDelayJobNotice,
    AngrSkipJobNotice,
    AngrVFGError,
    AngrError,
    AngrVFGRestartAnalysisNotice,
    AngrJobMergingFailureNotice,
    SimValueError,
    SimIRSBError,
    SimError,
)
from angr.procedures import SIM_PROCEDURES
from angr.state_plugins.callstack import CallStack
from angr.analyses import AnalysesHub
from angr.sim_state import SimState
from . import Analysis, CFGEmulated

if TYPE_CHECKING:
    from angr.knowledge_base import KnowledgeBase

l = logging.getLogger(name=__name__)


class VFGJob(CFGJobBase):
    """
    A job descriptor that contains local variables used during VFG analysis.
    """

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self.call_stack_suffix: list = []
        self.vfg_node: VFGNode | None = None
        self.is_call_jump = None
        self.call_target = None
        self.dbg_exit_status = {}
        self.is_return_jump = None

        self.sim_successors: SimSuccessors | None = None

        # if this job has a call successor, do we plan to skip the call successor or not
        self.call_skipped = False
        # if the call is skipped, calling stack of the skipped function is saved in `call_context_key`
        self.call_function_key: FunctionKey | None = None

        self.call_task: CallAnalysis | None = None

    @property
    def block_id(self) -> BlockID | None:
        return self._block_id

    def callstack_repr(self, kb: KnowledgeBase):
        s = []
        for i in range(0, len(self.call_stack_suffix), 2):
            call_site, func_addr = (
                self.call_stack_suffix[i],
                self.call_stack_suffix[i + 1],
            )  # pylint:disable=unsubscriptable-object
            if func_addr is None:
                continue

            call_site_str = f"{call_site:#x}" if call_site is not None else "None"

            if func_addr in kb.functions:
                s.append(f"{kb.functions[func_addr].name}[{call_site_str}]")
            else:
                s.append(f"{func_addr:#x}[{call_site_str}]")

        return "//".join(s)


class PendingJob:
    """
    Describes a pending job during VFG analysis.
    """

    __slots__ = (
        "block_id",
        "call_stack",
        "src_block_id",
        "src_ins_addr",
        "src_stmt_idx",
        "state",
    )

    def __init__(
        self,
        block_id: BlockID,
        state: SimState,
        call_stack: CallStack,
        src_block_id: BlockID,
        src_stmt_idx: int,
        src_ins_addr: int,
    ) -> None:
        self.block_id = block_id
        self.state = state
        self.call_stack = call_stack
        self.src_block_id = src_block_id
        self.src_stmt_idx = src_stmt_idx
        self.src_ins_addr = src_ins_addr


class AnalysisTask:
    """
    An analysis task describes a task that should be done before popping this task out of the task stack and discard it.
    """

    def __init__(self) -> None:
        pass

    @property
    def done(self):
        raise NotImplementedError


class FunctionAnalysis(AnalysisTask):
    """
    Analyze a function, generate fix-point states from all endpoints of that function, and then merge them to one state.
    """

    def __init__(self, function_address: int, return_address: int | None) -> None:
        super().__init__()

        self.function_address = function_address
        self.return_address = return_address

        self.call_analysis: AnalysisTask | None = None

        # tracks all jobs that are live currently
        self.jobs = []

    def __repr__(self):
        return f"<Function @ {self.function_address:#08x} with {len(self.jobs)} jobs>"

    #
    # Properties
    #

    @property
    def done(self) -> bool:
        return not self.jobs


class CallAnalysis(AnalysisTask):
    """
    Analyze a call by analyze all functions this call might be calling, collect all final states generated by analyzing
    those functions, and merge them into one state.
    """

    def __init__(
        self,
        address: int,
        return_address: None,
        function_analysis_tasks: list[Any] | None = None,
        mergeable_plugins: tuple[str, str] | None = None,
    ) -> None:
        super().__init__()

        self.address = address
        self.return_address = return_address
        self.function_analysis_tasks = function_analysis_tasks if function_analysis_tasks is not None else []
        self._mergeable_plugins = mergeable_plugins

        self.skipped = False
        self._final_jobs = []

    def __repr__(self):
        return f"<Call @ {self.address:#08x} with {len(self.function_analysis_tasks)} function tasks>"

    #
    # Properties
    #

    @property
    def done(self) -> bool:
        return all(task.done for task in self.function_analysis_tasks)

    #
    # Public methods
    #

    def register_function_analysis(self, task: FunctionAnalysis) -> None:
        assert isinstance(task, FunctionAnalysis)

        self.function_analysis_tasks.append(task)
        task.call_analysis = self

    def add_final_job(self, job: VFGJob) -> None:
        self._final_jobs.append(job)

    def merge_jobs(self) -> VFGJob:
        assert self._final_jobs

        job = self._final_jobs[0]

        for other in self._final_jobs[1:]:
            job.state = job.state.merge(other.state, plugin_whitelist=self._mergeable_plugins)[0]

        return job


class VFGNode:
    """
    A descriptor of nodes in a Value-Flow Graph
    """

    def __init__(self, addr: int, key: BlockID, state: SimState | None = None) -> None:
        """
        Constructor.

        :param int addr:
        :param BlockID key:
        :param SimState state:
        """
        self.key = key
        self.addr = addr
        self.state: SimState | None = None
        self.widened_state: SimState | None = None
        self.narrowing_times: int = 0
        self.all_states: list[SimState] = []
        self.events: list = []
        self.input_variables: list = []
        self.actions: list = []
        self.final_states: list[SimState] = []

        if state:
            self.all_states.append(state)
            self.state = state

    def __hash__(self) -> int:
        return hash(self.key)

    def __eq__(self, o):
        return (
            type(self) is type(o)
            and self.key == o.key
            and self.addr == o.addr
            and self.state == o.state
            and self.actions == o.actions
            and self.events == o.events
            and self.narrowing_times == o.narrowing_times
            and self.all_states == o.all_states
            and self.widened_state == o.widened_state
            and self.input_variables == o.input_variables
        )

    def __repr__(self):
        return f"VFGNode[{self.addr:#x}] <{self.key!r}>"

    def append_state(self, s, is_widened_state=False):
        """
        Appended a new state to this VFGNode.
        :param s: The new state to append
        :param is_widened_state: Whether it is a widened state or not.
        """

        if not is_widened_state:
            self.all_states.append(s)
            self.state = s

        else:
            self.widened_state = s


class VFG(ForwardAnalysis[SimState, VFGNode, VFGJob, BlockID, SimState], Analysis):  # pylint:disable=abstract-method
    """
    This class represents a control-flow graph with static analysis result.

    Perform abstract interpretation analysis starting from the given function address. The output is an invariant at
    the beginning (or the end) of each basic block.

    Steps:

        - Generate a CFG first if CFG is not provided.
        - Identify all merge points (denote the set of merge points as Pw) in the CFG.
        - Cut those loop back edges (can be derived from Pw) so that we gain an acyclic CFG.
        - Identify all variables that are 1) from memory loading 2) from initial values, or 3) phi functions. Denote
            the set of those variables as S_{var}.
        - Start real AI analysis and try to compute a fix point of each merge point. Perform widening/narrowing only on
            variables \\in S_{var}.
    """

    # TODO: right now the graph traversal method is not optimal. A new solution is needed to minimize the iteration we
    # TODO: access each node in the graph

    def __init__(
        self,
        cfg: CFGEmulated | None = None,
        context_sensitivity_level: int = 2,
        start: int | None = None,
        function_start: int | None = None,
        interfunction_level: int = 0,
        initial_state: SimState | None = None,
        avoid_runs: list[int] | None = None,
        remove_options: set[str] | None = None,
        timeout: int | None = None,
        max_iterations_before_widening: int = 8,
        max_iterations: int = 40,
        widening_interval: int = 3,
        final_state_callback: Callable[[SimState, CallStack], Any] | None = None,
        status_callback: Callable[[VFG], Any] | None = None,
        record_function_final_states: bool = False,
    ) -> None:
        """
        :param cfg: The control-flow graph to base this analysis on. If none is provided, we will
                    construct a CFGEmulated.
        :param context_sensitivity_level: The level of context-sensitivity of this VFG.
                                        It ranges from 0 to infinity. Default 2.
        :param function_start: The address of the function to analyze.
        :param interfunction_level: The level of interfunction-ness to be
        :param initial_state: A state to use as the initial one
        :param avoid_runs: A list of runs to avoid
        :param remove_options: State options to remove from the initial state. It only works when `initial_state` is
                                None
        :param int timeout:
        :param final_state_callback: callback function when countering final state
        :param status_callback: callback function used in _analysis_core_baremetal
        """

        ForwardAnalysis.__init__(
            self, order_jobs=True, allow_merging=True, allow_widening=True, status_callback=status_callback
        )  # type:ignore

        # Related CFG.
        # We can still perform analysis if you don't specify a CFG. But providing a CFG may give you better result.
        self._cfg = cfg

        # Where to start the analysis
        self._start: int = start if start is not None else self.project.entry
        self._function_start: int = function_start if function_start is not None else self._start

        # Other parameters
        self._avoid_runs: list[int] = [] if avoid_runs is None else avoid_runs
        self._context_sensitivity_level = context_sensitivity_level
        self._interfunction_level = interfunction_level
        self._state_options_to_remove = set() if remove_options is None else remove_options
        self._timeout = timeout
        self._start_at_function = self._start == self._function_start

        self._initial_state = initial_state

        self._max_iterations_before_widening = max_iterations_before_widening
        self._max_iterations = max_iterations
        self._widening_interval = widening_interval

        self._final_state_callback = final_state_callback

        self._record_function_final_states = record_function_final_states

        self._nodes: dict[BlockID, VFGNode] = {}  # all the vfg nodes, keyed on block IDs
        self._normal_states: dict[BlockID, SimState] = (
            {}
        )  # Last available state for each program point without widening
        self._widened_states: dict[BlockID, SimState] = {}  # States on which widening has occurred

        # Initial states of each function, which is context sensitive
        # It maps function key to its states
        self._function_initial_states: defaultdict[int, dict[int, SimState]] = defaultdict(dict)
        # Final states of each function, right after `ret` is called. Also context sensitive.
        # even if a function may have multiple return sites, as long as they all return to the same place, there is
        # only one final state of that function.
        self._function_final_states: defaultdict[int, dict[int, SimState]] = defaultdict(dict)

        # All final states are put in this list
        self.final_states: list[SimState] = []

        self._state_initialization_map: defaultdict[int, list[tuple[int, int]]] = defaultdict(list)

        self._exit_targets: defaultdict[tuple[int | None, ...], list[tuple[BlockID, str]]] = defaultdict(
            list
        )  # A dict to log edges and the jumpkind between each basic block
        # A dict to record all blocks that returns to a specific address
        self._return_target_sources: defaultdict[int, list[int]] = defaultdict(list)

        self._pending_returns: dict[BlockID, PendingJob] = {}

        self._thumb_addrs: set[int] = set()  # set of all addresses that are code in thumb mode

        self._final_address: int | None = (
            None  # Address of the very last instruction. The analysis is terminated there.
        )

        self._function_merge_points: dict[int, list[int]] = {}
        self._function_widening_points: dict[int, list[int]] = {}
        self._function_node_addrs: dict[int, list[int]] = {}  # sorted in reverse post-order

        self._mergeable_plugins = ("memory", "registers")

        self._task_stack: list[FunctionAnalysis] = []

        self._tracing_times: defaultdict[BlockID, int] = defaultdict(int)

        # counters for debugging
        self._execution_counter: defaultdict[BlockID, int] = defaultdict(int)

        # Start analysis
        self._analyze()

    #
    # Internal properties
    #

    @property
    def _current_function_address(self):
        return self._task_stack[-1].function_address

    @property
    def _top_task(self) -> FunctionAnalysis | None:
        """
        Get the first task in the stack.

        :return: The top task in the stack, or None if the stack is empty.
        :rtype: FunctionAnalysis
        """
        return self._task_stack[-1] if len(self._task_stack) != 0 else None

    @property
    def _top_function_analysis_task(self):
        """
        Get the first FunctionAnalysis task in the stack.

        :return: The top function analysis task in the stack, or None if there isn't any.
        :rtype: FunctionAnalysis
        """

        for r in reversed(self._task_stack):
            if isinstance(r, FunctionAnalysis):
                return r
        return None

    @property
    def function_initial_states(self):
        return self._function_initial_states

    @property
    def function_final_states(self):
        return self._function_final_states

    #
    # Public methods
    #

    def get_any_node(self, addr: int) -> VFGNode | None:
        """
        Get any VFG node corresponding to the basic block at @addr.
        Note that depending on the context sensitivity level, there might be
        multiple nodes corresponding to different contexts. This function will
        return the first one it encounters, which might not be what you want.
        """
        for n in self.graph.nodes():
            if n.addr == addr:
                return n
        return None

    def get_all_nodes(self, addr) -> Generator[VFGNode]:
        for n in self.graph.nodes():
            if n.addr == addr:
                yield n

    def irsb_from_node(self, node):
        return self.project.factory.successors(node.state, addr=node.addr)

    #
    # Operations
    #

    def copy(self):
        new_vfg = VFG(self.project)  # type:ignore
        new_vfg._cfg = self._cfg
        new_vfg._graph = networkx.DiGraph(self.graph)
        new_vfg._nodes = self._nodes.copy()
        new_vfg._exit_targets = defaultdict(list, self._exit_targets)
        return new_vfg

    # Pickling helpers
    def __setstate__(self, s):
        self.__dict__.update(s)

    def __getstate__(self):
        return dict(self.__dict__)

    #
    # Main analysis routines, mostly overriding methods of ForwardAnalysis
    #

    def _pre_analysis(self) -> None:
        """
        Executed before analysis starts. Necessary initializations are performed here.

        :return: None
        """

        l.debug("Starting from %#x", self._start)

        # initialize the task stack
        self._task_stack = []

        # initialize the execution counter dict
        self._execution_counter = defaultdict(int)

        # Generate a CFG if no CFG is provided
        if not self._cfg:
            l.debug("Generating a CFG, since none was given...")
            # TODO: can we use a fast CFG instead? note that fast CFG does not care of context sensitivity at all, but
            # TODO: for state merging, we also don't really care about context sensitivity.
            self._cfg: CFGEmulated = self.project.analyses[CFGEmulated].prep()(
                context_sensitivity_level=self._context_sensitivity_level, starts=(self._start,)
            )

        if not self._cfg.normalized:
            l.warning(
                "The given CFG is not normalized, which might impact the performance/accuracy of the VFG analysis."
            )

        # Prepare the state
        initial_state = self._prepare_initial_state(self._start, self._initial_state)
        initial_state.ip = self._start

        if self.project.arch.name.startswith("MIPS"):
            initial_state.regs.t9 = self._start

        # clear function merge points cache
        self._function_merge_points = {}

        # Create the initial state
        state = initial_state.copy()

        if self._start_at_function:
            # set the return address to an address so we can catch it and terminate the VSA analysis
            # TODO: Properly pick an address that will not conflict with any existing code and data in the program
            self._final_address = 0x4FFF0000
            self._set_return_address(state, self._final_address)

        call_stack = None
        if not self._start_at_function:
            # we should build a custom call stack
            call_stack = CallStack()
            call_stack = call_stack.call(None, self._function_start, retn_target=self._final_address)

        job = VFGJob(
            state.addr,
            state,
            self._context_sensitivity_level,
            jumpkind="Ijk_Boring",
            final_return_address=self._final_address,
            call_stack=call_stack,
        )
        block_id = BlockID.new(state.addr, job.get_call_stack_suffix(), job.jumpkind)
        job._block_id = block_id

        self._insert_job(job)

        # create the task
        function_analysis_task = FunctionAnalysis(self._function_start, self._final_address)
        function_analysis_task.jobs.append(job)
        self._task_stack.append(function_analysis_task)

    def _job_sorting_key(self, job):
        """
        Get the sorting key of a VFGJob instance.

        :param VFGJob job: the VFGJob object.
        :return: An integer that determines the order of this job in the queue.
        :rtype: int
        """

        MAX_BLOCKS_PER_FUNCTION = 1000000

        task_functions = list(
            reversed([task.function_address for task in self._task_stack if isinstance(task, FunctionAnalysis)])
        )
        try:
            function_pos = task_functions.index(job.func_addr)
        except ValueError:
            # not in the list
            # it might be because we followed the wrong path, or there is a bug in the traversal algorithm
            # anyways, do it first
            l.warning("Function address %#x is not found in task stack.", job.func_addr)
            return 0

        try:
            block_in_function_pos = self._ordered_node_addrs(job.func_addr).index(job.addr)
        except ValueError:
            # block not found. what?
            block_in_function_pos = min(job.addr - job.func_addr, MAX_BLOCKS_PER_FUNCTION - 1)

        return block_in_function_pos + MAX_BLOCKS_PER_FUNCTION * function_pos

        # return self._cfg.get_topological_order(self._cfg.get_node(job.block_id))

    def _job_key(self, job: VFGJob) -> BlockID | None:
        """
        Return the block ID of the job. Two or more jobs owning the same block ID will be merged together.

        :param VFGJob job: The VFGJob instance.
        :return:           The block ID of the job
        :rtype:            BlockID
        """

        return job.block_id

    def _pre_job_handling(self, job: VFGJob) -> None:
        """
        Some code executed before actually processing the job.

        :param VFGJob job: the VFGJob object.
        :return: None
        """

        # did we reach the final address?
        if self._final_address is not None and job.addr == self._final_address:
            # our analysis should be terminated here
            l.debug("%s is viewed as a final state. Skip.", job)
            raise AngrSkipJobNotice

        l.debug("Handling VFGJob %s", job)

        if not self._top_task:
            l.debug("No more tasks available. Skip the job.")
            raise AngrSkipJobNotice

        assert isinstance(self._top_task, FunctionAnalysis)

        if job not in self._top_task.jobs:
            # it seems that all jobs of the top task has been done. unwind the task stack
            # make sure this job is at least recorded somewhere
            unwind_count = None
            for i, task in enumerate(reversed(self._task_stack)):
                if isinstance(task, FunctionAnalysis) and job in task.jobs:
                    # nice
                    unwind_count = i

            if unwind_count is None:
                l.debug("%s is not recorded. Skip the job.", job)
                raise AngrSkipJobNotice
            # unwind the stack till the target, unless we see any pending jobs for each new top task
            for _ in range(unwind_count):
                if isinstance(self._top_task, FunctionAnalysis):
                    # are there any pending job belonging to the current function that we should handle first?
                    pending_job_key = self._get_pending_job(self._top_task.function_address)
                    if pending_job_key is not None:
                        # ah there is
                        # analyze it first
                        self._trace_pending_job(pending_job_key)
                        l.debug(
                            "A pending job is found for function %#x. Delay %s.",
                            self._top_task.function_address,
                            job,
                        )
                        raise AngrDelayJobNotice

                task = self._task_stack.pop()

                if not task.done:
                    l.warning("Removing an unfinished task %s. Might be a bug.", task)

            assert job in self._top_task.jobs

        # check if this is considered to be a final state
        if self._final_state_callback is not None and self._final_state_callback(job.state, job.call_stack):
            l.debug("%s.state is considered as a final state. Skip the job.", job)
            self.final_states.append(job.state)
            raise AngrSkipJobNotice

        # increment the execution counter
        self._execution_counter[job.addr] += 1

        self._top_task.jobs.remove(job)

        # set up some essential variables and parameters
        job.call_stack_suffix = job.get_call_stack_suffix()  # type:ignore
        job.jumpkind = "Ijk_Boring" if job.state.history.jumpkind is None else job.state.history.jumpkind

        src_block_id = job.src_block_id
        src_exit_stmt_idx = job.src_exit_stmt_idx

        addr = job.state.solver.eval(job.state.regs.ip)
        input_state = job.state
        block_id = BlockID.new(addr, job.call_stack_suffix, job.jumpkind)

        if self._tracing_times[block_id] > self._max_iterations:
            l.debug("%s has been traced too many times. Skip", job)
            raise AngrSkipJobNotice

        self._tracing_times[block_id] += 1

        if block_id not in self._nodes:
            vfg_node = VFGNode(addr, block_id, state=input_state)
            self._nodes[block_id] = vfg_node

        else:
            vfg_node = self._nodes[block_id]

        job.vfg_node = vfg_node
        # log the current state
        vfg_node.state = input_state

        # Execute this basic block with input state, and get a new SimSuccessors instance
        # unused result var is `error_occurred`
        job.sim_successors, _, restart_analysis = self._get_simsuccessors(input_state, addr)

        if restart_analysis:
            # We should restart the analysis because of something must be changed in the very initial state
            raise AngrVFGRestartAnalysisNotice

        if job.sim_successors is None:
            # Ouch, we cannot get the SimSuccessors for some reason
            # Skip this guy
            l.debug("Cannot create SimSuccessors for %s. Skip.", job)
            raise AngrSkipJobNotice

        self._graph_add_edge(src_block_id, block_id, jumpkind=job.jumpkind, src_exit_stmt_idx=src_exit_stmt_idx)

    def _get_successors(self, job: VFGJob) -> list[SimState]:
        # Extract initial values
        state = job.state
        addr = job.addr

        # Obtain successors
        if addr not in self._avoid_runs:
            assert job.sim_successors is not None
            all_successors: list[SimState] = (
                job.sim_successors.flat_successors + job.sim_successors.unconstrained_successors
            )
        else:
            all_successors = []

        assert job.vfg_node is not None
        # save those states
        if job.vfg_node.final_states is None:
            job.vfg_node.final_states = []
        job.vfg_node.final_states.extend(all_successors)

        # Update thumb_addrs
        assert job.sim_successors
        if job.sim_successors.sort == "IRSB" and state.thumb:
            self._thumb_addrs.update(job.sim_successors.artifacts["insn_addrs"])

        if not all_successors:
            if job.sim_successors.sort == "SimProcedure" and isinstance(
                job.sim_successors.artifacts["procedure"], SIM_PROCEDURES["stubs"]["PathTerminator"]
            ):
                # If there is no valid exit in this branch and it's not
                # intentional (e.g. caused by a SimProcedure that does not
                # do_return) , we should make it return to its callsite.
                # However, we don't want to use its state as it might be
                # corrupted. Just create a link in the exit_targets map.
                retn_target = job.call_stack.current_return_target
                if retn_target is not None:
                    new_call_stack = job.call_stack_copy()
                    exit_target_tpl = (*new_call_stack.stack_suffix(self._context_sensitivity_level), retn_target)
                    self._exit_targets[(*job.call_stack_suffix, addr)].append((exit_target_tpl, "Ijk_Ret"))
            else:
                # This is intentional. We shall remove all the pending returns generated before along this path.
                self._remove_pending_return(job, self._pending_returns)

        # If this is a call exit, we shouldn't put the default exit (which
        # is artificial) into the CFG. The exits will be Ijk_Call and
        # Ijk_FakeRet, and Ijk_Call always goes first
        job.is_call_jump = any(self._is_call_jumpkind(i.history.jumpkind) for i in all_successors)
        call_targets = []
        for succ in all_successors:
            if self._is_call_jumpkind(succ.history.jumpkind):
                try:
                    call_targets.append(succ.solver.eval_one(succ._ip))
                    # finding one is enough!
                    break
                except SimValueError:
                    # this call has multiple possible targets. ignore it
                    pass

        job.call_target = None if not call_targets else call_targets[0]

        job.is_return_jump = len(all_successors) and all_successors[0].history.jumpkind == "Ijk_Ret"

        if job.is_call_jump:
            # create the call task

            # TODO: correctly fill the return address
            call_task = CallAnalysis(job.addr, None, [], mergeable_plugins=self._mergeable_plugins)
            self._task_stack.append(call_task)

            job.call_task = call_task

        return all_successors

    def _handle_successor(
        self, job: VFGJob, successor: SimState, all_successors: list[SimState]
    ) -> list[VFGJob | Any]:  # pylint:disable=arguments-renamed
        """
        Process each successor generated by the job, and return a new list of succeeding jobs.

        :param VFGJob job: The VFGJob instance.
        :param SimState successor: The succeeding state.
        :param list all_successors:  A list of all successors.
        :return: A list of newly created jobs from the successor.
        :rtype: list
        """

        # Initialize parameters
        addr = job.addr
        jumpkind = successor.history.jumpkind

        #
        # Get instruction pointer
        #

        if job.is_return_jump:
            ret_target = job.call_stack.current_return_target
            if ret_target is None:
                # We have no where to go according to our call stack. However, the callstack might be corrupted
                l.debug("According to the call stack, we have nowhere to return to.")
                return []

            successor.ip = ret_target

        # this try-except block is to handle cases where the instruction pointer is symbolic
        try:
            successor_addrs = successor.solver.eval_upto(successor.ip, 2)
        except SimValueError:
            # TODO: Should fall back to reading targets from CFG
            # It cannot be concretized currently. Maybe we could handle
            # it later, maybe it just cannot be concretized
            return []

        if len(successor_addrs) > 1:
            # multiple concrete targets
            if job.is_return_jump:
                # It might be caused by state merging
                # We may retrieve the correct ip from call stack
                successor.ip = job.call_stack.current_return_target

            else:
                return self._handle_successor_multitargets(job, successor, all_successors)

        # Now there should be one single target for the successor
        successor_addr = successor.solver.eval_one(successor.ip)

        # Get the fake ret successor
        fakeret_successor = None
        if self._is_call_jumpkind(jumpkind):
            fakeret_successor = all_successors[-1]

            # If the function we're calling into doesn't return, we should discard it
            if self._cfg is not None:
                func = self.kb.functions.function(addr=job.call_target)
                if func is not None and func.returning is False and len(all_successors) == 2:
                    del all_successors[-1]
                    fakeret_successor = None

        if self._is_call_jumpkind(jumpkind):
            # Create a new call stack for the successor
            new_call_stack = self._create_callstack(job, successor_addr, jumpkind, fakeret_successor)
            if new_call_stack is None:
                l.debug("Cannot create a new callstack for address %#x", successor_addr)
                job.dbg_exit_status[successor] = ""
                return []
            new_call_stack_suffix = new_call_stack.stack_suffix(self._context_sensitivity_level)

            new_function_key = FunctionKey.new(successor_addr, new_call_stack_suffix)
            # Save the initial state for the function
            self._save_function_initial_state(new_function_key, successor_addr, successor.copy())

            # bail out if we hit the interfunction_level cap
            if len(job.call_stack) >= self._interfunction_level:
                l.debug(
                    "We are not tracing into a new function %#08x as we hit interfunction_level limit", successor_addr
                )

                # mark it as skipped
