"""
Looking up addresses for function names / symbols, and
vice-versa.

Uses IDA when available if there isn't sufficient symbol
information available.
"""

from __future__ import annotations

import re

import gdb

import pwndbg.gdblib.android
import pwndbg.gdblib.arch
import pwndbg.gdblib.elf
import pwndbg.gdblib.file
import pwndbg.gdblib.memory
import pwndbg.gdblib.qemu
import pwndbg.gdblib.remote
import pwndbg.gdblib.stack
import pwndbg.gdblib.vmmap
import pwndbg.integration
import pwndbg.lib.cache

# Symbol lookup only throws exceptions on errors, not if it failed to
# look up a symbol. We want to raise these errors so we can handle them
# properly, but there are some we haven't figured out how to fix yet, so
# we ignore those here
skipped_exceptions = None

skipped_exceptions = (
    # This exception is being thrown by the Go typeinfo tests, we should
    # investigate why this is happening and see if we can explicitly check
    # for it with `gdb.selected_frame()`
    "No frame selected",
    # If we try to look up a TLS variable when there is no TLS, this
    # exception occurs. Ideally we should come up with a way to check for
    # this case before calling `gdb.lookup_symbol`
    "Cannot find thread-local",
    # This reproduced on GDB 12.1 and caused #1878
    "Symbol requires a frame to compute its value",
)


def _get_debug_file_directory() -> str:
    """
    Retrieve the debug file directory path.

    The debug file directory path ('show debug-file-directory') is a comma-
    separated list of directories which GDB will look in to find the binaries
    currently loaded.
    """
    result = gdb.execute("show debug-file-directory", to_string=True, from_tty=False)
    expr = r'The directory where separate debug symbols are searched for is "(.*)".\n'

    match = re.search(expr, result)

    if match:
        return match.group(1)
    return ""


def _set_debug_file_directory(d: str) -> None:
    gdb.execute(f"set debug-file-directory {d}", to_string=True, from_tty=False)


def _add_debug_file_directory(d: str) -> None:
    current = _get_debug_file_directory()
    if current:
        _set_debug_file_directory(f"{current}:{d}")
    else:
        _set_debug_file_directory(d)


if "/usr/lib/debug" not in _get_debug_file_directory():
    _add_debug_file_directory("/usr/lib/debug")


@pwndbg.lib.cache.cache_until("objfile")
def get(address: int | gdb.Value, gdb_only: bool = False) -> str:
    """
    Retrieve the name for the symbol located at `address` - either from GDB or from integration provider
    Passing `gdb_only=True`

    Empty string if no symbol
    """
    # Note: we do not return "" on `address < pwndbg.gdblib.memory.MMAP_MIN_ADDR`
    # because this may be used to find out the symbol name on PIE binaries that weren't started yet
    # and then their symbol addresses can be found by GDB on their (non-rebased) offsets

    # Fast path: GDB's `info symbol` returns 'Numeric constant too large' here
    if address >= ((1 << 64) - 1):
        return ""

    # This sucks, but there's not a GDB API for this.
    # Workaround for a bug with Rust language, see #2094
    try:
        result = gdb.execute(f"info symbol 0x{address:x}", to_string=True, from_tty=False)
    except gdb.error:
        return ""

    if not gdb_only and result.startswith("No symbol"):
        address = int(address)
        return pwndbg.integration.provider.get_symbol(address) or ""

    # If there are newlines, which means that there are multiple symbols for the address
    # then use the first one (see also #1610)
    result = result[: result.index("\n")]

    # See https://github.com/bminor/binutils-gdb/blob/d1702fea87aa62dff7de465464097dba63cc8c0f/gdb/printcmd.c#L1594-L1624
    # The most often encountered formats looks like this:
    #   "main in section .text of /bin/bash"
    #   "main + 3 in section .text of /bin/bash"
    #   "system + 1 in section .text of /lib/x86_64-linux-gnu/libc.so.6"
    #   "No symbol matches system-1"
    # But there are some others that we have to account for as well
    if " in section " in result:
        loc_string, _ = result.split(" in section ")
    elif " in load address range of " in result:
        loc_string, _ = result.split(" in load address range of ")
    elif " overlay section " in result:
        result, _ = result.split(" overlay section ")
        loc_string, _ = result.split(" in ")
    else:
        loc_string = ""

    # If there is 'main + 87' we want to replace it with 'main+87' etc.
    return loc_string.replace(" + ", "+")


@pwndbg.lib.cache.cache_until("objfile")
def address(symbol: str) -> int | None:
    """
    Get the address for `symbol`
    """
    try:
        symbol_obj = gdb.lookup_symbol(symbol)[0]
        if symbol_obj:
            return int(symbol_obj.value().address)
    except gdb.error as e:
        if all(x not in str(e) for x in skipped_exceptions):
            raise e

    try:
        # Unfortunately, `gdb.lookup_symbol` does not seem to handle all
        # symbols, so we need to fallback to using `gdb.parse_and_eval`. See
        # https://sourceware.org/pipermail/gdb/2022-October/050362.html
        # (We tried parsing the output of the `info address` before, but there were some issues. See #1628 and #1666)
        if "\\" in symbol:
            # Is it possible that happens? Probably not, but just in case
            raise ValueError(f"Symbol {symbol!r} contains a backslash")
        sanitized_symbol_name = symbol.replace("'", "\\'")
        return int(gdb.parse_and_eval(f"&'{sanitized_symbol_name}'"))

    except gdb.error:
        return None


@pwndbg.lib.cache.cache_until("objfile", "thread")
def static_linkage_symbol_address(symbol: str) -> int | None:
    """
    Get the address for static linkage `symbol`
    """

    try:
        symbol_obj = gdb.lookup_static_symbol(symbol)
        return int(symbol_obj.value().address) if symbol_obj else None
    except gdb.error:
        return None


@pwndbg.lib.cache.cache_until("stop", "start")
def selected_frame_source_absolute_filename():
    """
    Retrieve the symbol table’s source absolute file name from the selected frame.

    In case of missing symbol table or frame information, None is returned.
    """
    try:
        frame = gdb.selected_frame()
    except gdb.error:
        return None

    if not frame:
        return None

    sal = frame.find_sal()
    if not sal:
        return None

    symtab = sal.symtab
    if not symtab:
        return None

    return symtab.fullname()


def parse_and_eval(expression: str) -> gdb.Value | None:
    """Error handling wrapper for GDBs parse_and_eval function"""
    try:
        return gdb.parse_and_eval(expression)
    except gdb.error:
        return None
