############################################################################
# tools/pynuttx/nxgdb/utils.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.  The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################

from __future__ import annotations

import hashlib
import importlib
import json
import math
import re
import sys
import traceback
from enum import Enum
from pathlib import Path
from typing import List, Optional, Union

import gdb
from nxelf.macros import fetch_macro_info, try_expand

from .protocols.thread import Tcb

g_symbol_cache = {}
g_type_cache = {}
g_macro_ctx = None
g_backtrace_cache = {}
TypeOrStr = Union[gdb.Type, str]


class Value(gdb.Value):
    def __init__(self, obj: Union[gdb.Value, Value]):
        super().__init__(obj)

    def __isabstractmethod__(self):
        # Added to avoid getting error using __getattr__
        return False

    def __getattr__(self, key):
        if hasattr(super(), key):
            value = super().__getattribute__(key)
        else:
            value = super().__getitem__(key)

        return Value(value) if not isinstance(value, Value) else value

    def __getitem__(self, key):
        value = super().__getitem__(key)
        return Value(value) if not isinstance(value, Value) else value

    def __format__(self, format_spec: str) -> str:
        try:
            return super().__format__(format_spec)
        except TypeError:
            # Convert GDB value to python value, and then format it
            type_code_map = {
                gdb.TYPE_CODE_INT: int,
                gdb.TYPE_CODE_PTR: int,
                gdb.TYPE_CODE_ENUM: int,
                gdb.TYPE_CODE_FUNC: hex,
                gdb.TYPE_CODE_BOOL: bool,
                gdb.TYPE_CODE_FLT: float,
                gdb.TYPE_CODE_STRING: str,
                gdb.TYPE_CODE_CHAR: lambda x: chr(int(x)),
            }

            t = self.type
            while t.code == gdb.TYPE_CODE_TYPEDEF:
                t = t.target()

            type_code = t.code
            try:
                converter = type_code_map[type_code]
                return f"{converter(self):{format_spec}}"
            except KeyError:
                raise TypeError(
                    f"Unsupported type: {self.type}, {self.type.code} {self}"
                )

    @property
    def address(self) -> Value:
        value = super().address
        return value and Value(value)

    def cast(self, type: str | gdb.Type, ptr: bool = False) -> Optional["Value"]:
        try:
            gdb_type = lookup_type(type) if isinstance(type, str) else type
            if ptr:
                gdb_type = gdb_type.pointer()
            return Value(super().cast(gdb_type))
        except gdb.error:
            return None

    def dereference(self) -> Value:
        return Value(super().dereference())

    def reference_value(self) -> Value:
        return Value(super().reference_value())

    def referenced_value(self) -> Value:
        return Value(super().referenced_value())

    def rvalue_reference_value(self) -> Value:
        return Value(super().rvalue_reference_value())

    def const_value(self) -> Value:
        return Value(super().const_value())

    def dynamic_cast(self, type: gdb.Type) -> Value:
        return Value(super().dynamic_cast(type))


class Symbol:
    def __init__(self, address: Union[int, gdb.Value]):
        self.func: str = ""
        self.address: int = 0
        self._symtab_and_line: gdb.Symtab_and_line = None
        self.resolve_symbol(address)

    def resolve_symbol(self, address: Union[int, gdb.Value]) -> None:
        """Resolve symbol information for the given address"""
        if not address:
            return

        # Check cache first
        if int(address) in g_backtrace_cache:
            cached = g_backtrace_cache[int(address)]
            self.address, self.func, self._symtab_and_line = cached
            return

        # Convert to proper gdb.Value if needed
        if type(address) is int:
            address = gdb.Value(address)

        # Ensure we have a pointer type
        if address.type.code is not gdb.TYPE_CODE_PTR:
            address = address.cast(gdb.lookup_type("void").pointer())

        # Get symbol information
        self.address = int(address)
        self._symtab_and_line = gdb.find_pc_line(int(address))
        if not self.is_valid():
            return

        self.func = address.format_string(symbols=True, address=False)

        # Cache the result
        result = (self.address, self.func, self._symtab_and_line)
        g_backtrace_cache[self.address] = result

    def is_valid(self) -> bool:
        return (
            self._symtab_and_line
            and self._symtab_and_line.is_valid()
            and self._symtab_and_line.symtab
            and self._symtab_and_line.symtab.is_valid()
        )

    @property
    def funcname(self) -> str:
        return self.func

    @property
    def filename(self) -> str:
        if not self.is_valid():
            return ""
        return str(self._symtab_and_line.symtab.fullname())

    @property
    def line(self) -> int:
        if not self.is_valid():
            return 0
        return int(self._symtab_and_line.line)

    def __repr__(self) -> str:
        return (
            f"<Symbol {hex(self.address)}: {self.func} at {self.filename}:{self.line}>"
        )

    def toJSON(self):
        return {
            "address": self.address,
            "function": self.funcname,
            "source": self.filename,
            "line": self.line,
        }


class DiagnoseCategory(str, Enum):
    sched = "sched"
    memory = "memory"
    system = "system"
    power = "power"
    libuv = "libuv"
    rpc = "rpc"
    fs = "fs"
    sensor = "sensor"
    connectivity = "connectivity"
    graphics = "graphics"
    audio = "audio"
    video = "video"


def lookup_type(name, block=None) -> gdb.Type:
    """Return the type object of a type name"""
    key = (name, block)
    if key not in g_type_cache:
        try:
            g_type_cache[key] = (
                gdb.lookup_type(name, block=block) if block else gdb.lookup_type(name)
            )
        except gdb.error:
            g_type_cache[key] = None

    return g_type_cache[key]


def get_fieldnames(t: TypeOrStr) -> List[str]:
    """Return the field names of a type"""
    if isinstance(t, str):
        t = lookup_type(t)

    return [f.name for f in t.fields()]


def get_type_field(obj: Union[TypeOrStr, gdb.Value], field: str) -> gdb.Field:
    """
    Get the type field descriptor from a type or string, or value object.
    """

    if isinstance(obj, str):
        t = lookup_type(obj)
    elif isinstance(obj, gdb.Type):
        t = obj
    elif isinstance(obj, gdb.Value):
        t = obj.type
    else:
        raise gdb.GdbError(f"Unsupported type {type(obj)}")

    if not t:
        return None

    while t.code in (gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_ARRAY, gdb.TYPE_CODE_TYPEDEF):
        t = t.target()

    for f in t.fields():
        if f.name == field:
            return f

        # Check anonymous(f.name is None) fields of struct and union recursively
        if f.name is None and f.type.code in (
            gdb.TYPE_CODE_STRUCT,
            gdb.TYPE_CODE_UNION,
        ):
            if f := get_type_field(f.type, field):
                return f


def get_field_nitems(t: TypeOrStr, field: str) -> int:
    """Return the array length of a field in type, or 0 if no such field"""
    if field := get_type_field(t, field):
        return nitems(field)

    return 0


def get_static_symbol(name: str, domain=None) -> Optional[gdb.Symbol]:
    """
    Return a global symbol with static linkage by name.

    Looks up all static symbols matching the given name and returns the first
    one that is not optimized out. This is useful when multiple static symbols
    with the same name exist across different compilation units, and some may
    be optimized away by the compiler.

    Args:
        name: The symbol name to look up
        domain: Optional symbol domain (e.g., gdb.SYMBOL_VAR_DOMAIN)

    Returns:
        The first non-optimized static symbol, or None if not found
    """
    try:
        symbols = (
            gdb.lookup_static_symbols(name, domain=domain)
            if domain is not None
            else gdb.lookup_static_symbols(name)
        )

        for sym in symbols:
            if sym.value().is_optimized_out:
                continue
            return sym
    except gdb.error:
        pass

    return None


def get_global_symbol(name: str, domain=None) -> Optional[gdb.Symbol]:
    """Return the global symbol object"""
    return (
        gdb.lookup_global_symbol(name, domain=domain)
        if domain is not None
        else gdb.lookup_global_symbol(name)
    )


def get_static_var(name: str) -> Optional[gdb.Symbol]:
    return get_static_symbol(name, domain=gdb.SYMBOL_VAR_DOMAIN)


def get_global_var(name: str) -> Optional[gdb.Symbol]:
    """Return global symbol by name, including static linkage symbols"""
    return gdb.lookup_global_symbol(
        name, domain=gdb.SYMBOL_VAR_DOMAIN
    ) or get_static_var(name)


def get_global_func(name: str) -> Optional[gdb.Symbol]:
    """Return global function symbol by name"""
    return gdb.lookup_global_symbol(name, domain=gdb.SYMBOL_FUNCTION_DOMAIN)


def get_global_func_block(name: str) -> Optional[gdb.Block]:
    """Return the function block by function name"""
    # name to address
    symbol = get_global_func(name)
    if not symbol:
        return None

    address = symbol.value().address
    # address to block
    return gdb.block_for_pc(int(address))


long_type = lookup_type("long")


def dont_repeat_decorator(func):
    def wrapper(self, args, from_tty):
        try:
            self.dont_repeat()
            func(self, args, from_tty)
        except Exception as e:
            print(f"Error: {e}\n{traceback.format_exc()}")

    return wrapper


# Common Helper Functions


def get_long_type():
    """Return the cached long type object"""
    return long_type


def offset_of(typeobj: TypeOrStr, field: str) -> Union[int, None]:
    """Return the offset of a field in a structure"""
    if type(typeobj) is str:
        typeobj = gdb.lookup_type(typeobj)

    if typeobj.code is gdb.TYPE_CODE_PTR:
        typeobj = typeobj.target()

    for f in typeobj.fields():
        if f.name == field:
            if f.bitpos is None:
                break
            return f.bitpos // 8

    raise gdb.GdbError(f"Field {field} not found in type {typeobj}")


def container_of(ptr: Union[Value, int], typeobj: TypeOrStr, member: str) -> Value:
    """
    Return a pointer to the containing data structure.

    Args:
        ptr: Pointer to the member.
        t: Type of the container.
        member: Name of the member in the container.

    Returns:
        Value of the container.

    Example:
        struct foo {
            int a;
            int b;
        };
        struct foo *ptr = container_of(&ptr->b, "struct foo", "b");
    """

    if isinstance(typeobj, str):
        typeobj = lookup_type(typeobj).pointer()

    if typeobj.code is not gdb.TYPE_CODE_PTR:
        typeobj = typeobj.pointer()

    addr = Value(ptr).cast(long_type)
    return Value(addr - offset_of(typeobj, member)).cast(typeobj)


class ContainerOf(gdb.Function):
    """Return pointer to containing data structure.

    $container_of(PTR, "TYPE", "ELEMENT"): Given PTR, return a pointer to the
    data structure of the type TYPE in which PTR is the address of ELEMENT.
    Note that TYPE and ELEMENT have to be quoted as strings."""

    def __init__(self):
        super().__init__("container_of")

    def invoke(self, ptr, typename, elementname):
        return container_of(ptr, typename.string(), elementname.string())


ContainerOf()


class MacroCtx:
    """
    This is a singleton class which only initializes once to
    cache a context of macro definition which can be queried later
    TODO: we only deal with single ELF at the moment for simplicity
    If you load more object files while debugging, only the first one gets loaded
    will be used to retrieve macro information
    """

    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "instance"):
            cls.instance = super(MacroCtx, cls).__new__(cls)
        return cls.instance

    def __init__(self, filename):
        self._macro_map = {}
        self._file = filename

        self._macro_map = fetch_macro_info(filename)

    @property
    def macro_map(self):
        return self._macro_map

    @property
    def objfile(self):
        return self._file


def parse_and_eval(expression: str, global_context: bool = False):
    """Equivalent to gdb.parse_and_eval, but returns a Value object"""
    gdb_value = gdb.parse_and_eval(expression)
    return Value(gdb_value)


def gdb_eval_or_none(expression):
    """Evaluate an expression and return None if it fails"""
    try:
        value = parse_and_eval(expression)
        if value.is_optimized_out:
            return None
        return value
    except gdb.error:
        return None


def suppress_cli_notifications(suppress=True):
    """Suppress(default behavior) or unsuppress GDB CLI notifications"""
    try:
        suppressed = "is on" in gdb.execute(
            "show suppress-cli-notifications", to_string=True
        )
        if suppress != suppressed:
            gdb.execute(f"set suppress-cli-notifications {'on' if suppress else 'off'}")

        return suppressed
    except gdb.error:
        return True


def check_inferior_valid():
    """Check if the current inferior is valid"""
    inferior = gdb.selected_inferior()
    return inferior and inferior.connection and inferior.connection.is_valid()


def get_symbol_value(name, locspec="nx_start", cacheable=True):
    """Return the value of a symbol value etc: Variable, Marco"""
    global g_symbol_cache

    # If there is a current stack frame, GDB uses the macros in scope at that frame’s source code line.
    # Otherwise, GDB uses the macros in scope at the current listing location.
    # Reference: https://sourceware.org/gdb/current/onlinedocs/gdb.html/Macros.html#Macros
    try:
        if not gdb.selected_frame():
            gdb.execute(f"list {locspec}", to_string=True)
            return gdb_eval_or_none(name)
    except gdb.error:
        pass

    # Try current frame
    value = gdb_eval_or_none(name)
    if value:
        return value

    # Check if the symbol is already cached
    if cacheable and (name, locspec) in g_symbol_cache:
        return g_symbol_cache[(name, locspec)]

    # There's current frame and no definition found. We need second inferior without a valid frame
    # in order to use the list command to set the scope.
    if len(gdb.inferiors()) == 1:
        gdb.execute(
            f'add-inferior -exec "{gdb.objfiles()[0].filename}" -no-connection',
            to_string=True,
        )
        g_symbol_cache = {}

    state = suppress_cli_notifications(True)

    original_thread = None
    original_frame = None
    if check_inferior_valid():
        original_thread = gdb.selected_thread()
        if original_thread and original_thread.is_valid():
            original_frame = gdb.selected_frame()
    # Switch to inferior 2 and set the scope firstly
    gdb.execute("inferior 2", to_string=True)
    gdb.execute(f"list {locspec}", to_string=True)
    value = gdb_eval_or_none(name)
    if not value:
        # Try to expand macro by reading elf
        global g_macro_ctx
        if not g_macro_ctx:
            if len(gdb.objfiles()) > 0:
                g_macro_ctx = MacroCtx(gdb.objfiles()[0].filename)
            else:
                raise gdb.GdbError("An executable file must be provided")

        expr = try_expand(name, g_macro_ctx.macro_map)
        value = gdb_eval_or_none(expr)

    if cacheable:
        g_symbol_cache[(name, locspec)] = value

    # Switch back to inferior 1
    gdb.execute("inferior 1", to_string=True)
    if original_thread and original_thread.is_valid():
        original_thread.switch()
        if original_frame and original_frame.is_valid():
            original_frame.select()
    suppress_cli_notifications(state)
    return value


def get_field(obj: gdb.Value, field: Union[str, gdb.Field], default=None) -> gdb.Value:
    """
    Get a field value from a gdb.Value, return default if field is not found.
    """
    try:
        return obj[field] if obj else default
    except gdb.error:
        return default


def has_field(obj: Union[TypeOrStr, gdb.Value], field):
    return get_type_field(obj, field) is not None


def get_bytes(val, size):
    """Convert a gdb value to a bytes object"""
    try:
        return val.bytes[:size]
    except AttributeError:  # Sometimes we don't have gdb.Value.bytes
        inf = gdb.inferiors()[0]
        mem = inf.read_memory(val.address, size)
        return mem.tobytes()


def import_check(module, name="", errmsg=""):
    try:
        module = __import__(module, fromlist=[name])
    except ImportError as e:
        gdb.write(f"import_error: {e}\n")
        gdb.write(errmsg if errmsg else f"Error to import {module}\n")
        return None

    return getattr(module, name) if name else module


def import_reload(module, name="", errmsg=""):
    """Clear and reload the module from sys.modules"""
    if module in sys.modules:
        module = importlib.reload(sys.modules[module])
    else:
        module = import_check(
            module,
            errmsg=errmsg,
        )
    return module


def hexdump(address, size):
    address = int(address)
    inf = gdb.inferiors()[0]
    mem = inf.read_memory(address, size)
    bytes = mem.tobytes()
    for i in range(0, len(bytes), 16):
        chunk = bytes[i : i + 16]
        gdb.write(f"{i + address:08x}  ")
        hex_values = " ".join(f"{byte:02x}" for byte in chunk)
        hex_display = f"{hex_values:<47}"
        gdb.write(hex_display)
        ascii_values = "".join(
            chr(byte) if 32 <= byte <= 126 else "." for byte in chunk
        )
        gdb.write(f"  {ascii_values} \n")


def is_decimal(s):
    return re.fullmatch(r"\d+", s) is not None


def is_hexadecimal(s):
    return re.fullmatch(r"0[xX][0-9a-fA-F]+|[0-9a-fA-F]+", s) is not None


def parse_arg(arg: str) -> Union[gdb.Value, int]:
    """Parse an argument to a gdb.Value or int, return None if failed"""

    if is_decimal(arg):
        return int(arg)

    if is_hexadecimal(arg):
        return int(arg, 16)

    try:
        return parse_and_eval(f"{arg}")
    except gdb.error:
        return None


def alias(name, command):
    try:
        gdb.execute(f"alias {name} = {command}")
    except gdb.error:
        pass


def nitems(array: Union[gdb.Field, gdb.Type, gdb.Symbol]) -> int:
    array_type = array.type
    element_type = array_type.target()
    element_size = element_type.sizeof
    array_size = array_type.sizeof // element_size
    return array_size


def sizeof(t: Union[str, gdb.Type]):
    if type(t) is str:
        t = gdb.lookup_type(t)

    return t.sizeof


def log2ceil(n):
    return int(math.ceil(math.log2(n)))


def log2floor(n):
    return int(math.floor(math.log2(n)))


# Machine Specific Helper Functions


BIG_ENDIAN = 0
LITTLE_ENDIAN = 1
target_endianness = None


def get_target_endianness():
    """Return the endianness of the target"""
    global target_endianness
    if not target_endianness:
        endian = gdb.execute("show endian", to_string=True)
        if "little endian" in endian:
            target_endianness = LITTLE_ENDIAN
        elif "big endian" in endian:
            target_endianness = BIG_ENDIAN
        else:
            raise gdb.GdbError("unknown endianness '{0}'".format(str(endian)))
    return target_endianness


def read_memoryview(inf, start, length):
    """Read memory from the target and return a memoryview object"""
    m = inf.read_memory(start, length)
    if type(m) is memoryview:
        return m
    return memoryview(m)


try:
    # For some prebuilt GDB, the python builtin module `struct` is not available
    import struct

    def read_u16(buffer, offset):
        """Read a 16-bit unsigned integer from a buffer"""
        if get_target_endianness() == LITTLE_ENDIAN:
            return struct.unpack_from("<H", buffer, offset)[0]
        else:
            return struct.unpack_from(">H", buffer, offset)[0]

    def read_u32(buffer, offset):
        """Read a 32-bit unsigned integer from a buffer"""
        if get_target_endianness() == LITTLE_ENDIAN:
            return struct.unpack_from("<I", buffer, offset)[0]
        else:
            return struct.unpack_from(">I", buffer, offset)[0]

    def read_u64(buffer, offset):
        """Read a 64-bit unsigned integer from a buffer"""
        if get_target_endianness() == LITTLE_ENDIAN:
            return struct.unpack_from("<Q", buffer, offset)[0]
        else:
            return struct.unpack_from(">Q", buffer, offset)[0]

except ModuleNotFoundError:

    def read_u16(buffer, offset):
        """Read a 16-bit unsigned integer from a buffer"""
        buffer_val = buffer[offset : offset + 2]
        value = [0, 0]

        if type(buffer_val[0]) is str:
            value[0] = ord(buffer_val[0])
            value[1] = ord(buffer_val[1])
        else:
            value[0] = buffer_val[0]
            value[1] = buffer_val[1]

        if get_target_endianness() == LITTLE_ENDIAN:
            return value[0] + (value[1] << 8)
        else:
            return value[1] + (value[0] << 8)

    def read_u32(buffer, offset):
        """Read a 32-bit unsigned integer from a buffer"""
        if get_target_endianness() == LITTLE_ENDIAN:
            return read_u16(buffer, offset) + (read_u16(buffer, offset + 2) << 16)
        else:
            return read_u16(buffer, offset + 2) + (read_u16(buffer, offset) << 16)

    def read_u64(buffer, offset):
        """Read a 64-bit unsigned integer from a buffer"""
        if get_target_endianness() == LITTLE_ENDIAN:
            return read_u32(buffer, offset) + (read_u32(buffer, offset + 4) << 32)
        else:
            return read_u32(buffer, offset + 4) + (read_u32(buffer, offset) << 32)


def read_uint(addr):
    buf = gdb.selected_inferior().read_memory(addr, 4)
    return int.from_bytes(buf, "little", signed=False)


def read_ulong(buffer, offset):
    """Read a long from a buffer"""
    if get_long_type().sizeof == 8:
        return read_u64(buffer, offset)
    else:
        return read_u32(buffer, offset)


def bswap(val, size):
    """Reverses the byte order in a gdb.Value or int value of size bytes"""
    return int.from_bytes(int(val).to_bytes(size, byteorder="little"), byteorder="big")


def swap16(val):
    return bswap(val, 2)


def swap32(val):
    return bswap(val, 4)


def swap64(val):
    return bswap(val, 8)


target_arch = None


def is_target_arch(arch, exact=False):
    """
    For non exact match, this function will
    return True if the target architecture contains
    keywords of an ARCH family. For example, x86 is
    contained in i386:x86_64.
    For exact match, this function will return True if
    the target architecture is exactly the same as ARCH.
    """
    if hasattr(gdb.Frame, "architecture"):
        archname = gdb.newest_frame().architecture().name()

        return arch in archname if not exact else arch == archname
    else:
        global target_arch
        if target_arch is None:
            target_arch = gdb.execute("show architecture", to_string=True)
            pattern = r'set to "(.*?)"\s*(\(currently (".*")\))?'
            match = re.search(pattern, target_arch)

            candidate = match.group(1)

            if candidate == "auto":
                target_arch = match.group(3)
            else:
                target_arch = candidate

        return arch in target_arch if not exact else arch == target_arch


# Kernel Specific Helper Functions

CONFIG_SMP_NCPUS = nitems(parse_and_eval("g_running_tasks"))


def is_target_smp():
    """Return Ture if the target use smp"""

    return CONFIG_SMP_NCPUS > 1


def get_ncpus():
    return CONFIG_SMP_NCPUS


# FIXME: support RISC-V/X86/ARM64 etc.
def in_interrupt_context(cpuid=0):
    frame = gdb.selected_frame()

    if is_target_arch("arm"):
        xpsr = int(frame.read_register("xpsr"))
        return xpsr & 0xF
    else:
        # TODO: figure out a more proper way to detect if
        # we are in an interrupt context
        g_current_regs = gdb_eval_or_none("g_current_regs")
        return not g_current_regs or not g_current_regs[cpuid]


# task
def get_register_byname(regname, tcb=None):
    frame = gdb.selected_frame()
    if not tcb:
        return int(frame.read_register(regname))

    if task_is_running(tcb):
        threads = gdb.selected_inferior().threads()
        if not threads:
            print("No threads found")
            return 0

        origin = gdb.selected_thread()

        # SMP online debugging is not currently supported by the debug tools.
        # However, in the case of QEMU, each simulated CPU runs in its own thread.
        # We can leverage this by inspecting QEMU threads in GDB to switch between CPUs,
        # and use GDB frames to access CPU registers.
        # This special case is handled below.
        if (
            check_inferior_valid()
            and "remote" in gdb.selected_inferior().connection.type.lower()
            and threads[0].details
            and "cpu" not in threads[0].details.lower()
        ):
            # We should use the TCB's pid to find the thread,
            # because gdbserver.py can parser the coredump
            thread = get_gdb_thread(tcb["pid"])
        else:
            # For SMP, we need to switch to the thread's CPU
            if is_target_smp() and len(threads) > 1:
                threads = sorted(threads, key=lambda t: t.num)
                thread = threads[tcb.cpu]
            else:
                # For single CPU, just use the first thread
                thread = threads[0]
        if thread and thread.is_valid():
            thread.switch()
            reg = int(gdb.selected_frame().read_register(regname))
            if origin and origin.is_valid():
                origin.switch()
                if frame and frame.is_valid():
                    frame.select()
            else:
                print("Warning: the original thread is not valid")
            return reg
        else:
            print(f"Thread is not valid, tcb: {tcb}")
            return 0

    # Ok, let's take it from the context in the given tcb
    tcbinfo = parse_and_eval("g_tcbinfo")
    for reg in ArrayIterator(tcbinfo.u.reginfo, tcbinfo.regs_num):
        if reg.name.string().lower() == regname.lower():
            xcpregs = tcb["xcp"]["regs"]
            if xcpregs.type.code != gdb.TYPE_CODE_PTR:
                # For sim, the xcpregs is an array, not pointer, thus need casting
                xcpregs = xcpregs.cast(lookup_type("char").pointer())
            value = gdb.Value(int(xcpregs) + reg.toffset)
            value = value.cast(lookup_type("uintptr_t").pointer())
            try:
                return int(value.dereference())
            except gdb.MemoryError:
                print(
                    f"Failed to read memory at {value}, tcb: {tcb['pid']}\n"
                    "Possible reason: The task is going to run soon, so tcb.xcp.regs is null"
                )
                return 0


def get_sp(tcb=None):
    # NuttX doesn't support unified register name for stack pointer
    arch = gdb.selected_inferior().architecture().name()
    regname = {
        "i386": "esp",
        "i386:x86": "esp",
        "i386:x86-64": "rsp",
    }.get(arch, "sp")

    return get_register_byname(regname, tcb)


def get_pc(tcb=None):
    # NuttX doesn't support unified register name for PC
    arch = gdb.selected_inferior().architecture().name()
    regname = {
        "i386": "eip",
        "i386:x86": "eip",
        "i386:x86-64": "rip",
    }.get(arch, "pc")
    return get_register_byname(regname, tcb)


def get_tcbs() -> List[Tcb]:
    # In case we have created/deleted tasks at runtime, the tcbs will change
    # so keep it as fresh as possible
    pidhash = parse_and_eval("g_pidhash")
    npidhash = parse_and_eval("g_npidhash")

    return [pidhash[i] for i in range(0, npidhash) if pidhash[i]]


def get_tcb(pid) -> Tcb:
    """get tcb from pid"""
    pidhash = parse_and_eval("g_pidhash")
    npidhash = parse_and_eval("g_npidhash")
    tcb = pidhash[pid & (npidhash - 1)]
    if not tcb or pid != tcb["pid"]:
        return None

    return tcb


def get_running_tcbs() -> List[Tcb]:
    """get running tcbs"""
    running_tasks = parse_and_eval("g_running_tasks")
    return [tcb for tcb in ArrayIterator(running_tasks) if tcb]


def get_tcb_type(tcb):
    """get tcb type"""
    if not tcb:
        return None
    try:
        mask = get_symbol_value("TCB_FLAG_TTYPE_MASK")
        if tcb["flags"] & mask == get_symbol_value("TCB_FLAG_TTYPE_PTHREAD"):
            return "PTHREAD"
        elif tcb["flags"] & mask == get_symbol_value("TCB_FLAG_TTYPE_KERNEL"):
            return "KTHREAD"
        elif tcb["flags"] & mask == get_symbol_value("TCB_FLAG_TTYPE_TASK"):
            return "TASK"
        return None
    except gdb.error as e:
        raise gdb.error(f"Failed to determine TCB type: {str(e)}") from e


def get_tid(tcb):
    """get tid from tcb"""
    if not tcb:
        return None
    try:
        return tcb["group"]["tg_pid"]
    except gdb.error:
        return None


def get_task_name(tcb_or_pid):
    if isinstance(tcb_or_pid, int):
        tcb = get_tcb(tcb_or_pid)
    else:
        tcb = tcb_or_pid

    if tcb is None:
        return ""

    try:
        name = tcb["name"].cast(gdb.lookup_type("char").pointer())
        return name.string()
    except gdb.error:
        return ""


def get_task_entry(tcb) -> Symbol:
    if not tcb:
        return None

    try:
        entry = tcb["entry"]["main"]
        return Symbol(entry)
    except gdb.error:
        return None


def task_is_running(tcb):
    return tcb["task_state"] == get_symbol_value("TSTATE_TASK_RUNNING")


def task_is_idle(tcb):
    return tcb["pid"] < CONFIG_SMP_NCPUS


# sem
def sem_is_mutex(sem):
    SEM_TYPE_MUTEX = 4
    return sem["flags"] & SEM_TYPE_MUTEX


def mutex_get_holder(mutex):
    NXSEM_MBLOCKING_BIT = 1 << 31
    return mutex["val"]["mholder"] & ~NXSEM_MBLOCKING_BIT


def switch_inferior(inferior):
    state = suppress_cli_notifications(True)

    if len(gdb.inferiors()) == 1:
        gdb.execute(
            f"add-inferior -exec {gdb.objfiles()[0].filename} -no-connection",
            to_string=True,
        )

    gdb.execute(f"inferior {inferior}", to_string=True)
    return state


def check_version():
    """Check the elf and memory version"""
    original_thread = None
    original_frame = None
    state = suppress_cli_notifications()
    if check_inferior_valid():
        original_thread = gdb.selected_thread()
        if original_thread and original_thread.is_valid():
            original_frame = gdb.selected_frame()
    switch_inferior(1)
    try:
        mem_version = gdb.execute("p g_version", to_string=True).split("=")[1]
    except gdb.error:
        gdb.write("No symbol g_version found in memory, skipping version check\n")
        suppress_cli_notifications(state)
        return

    switch_inferior(2)
    elf_version = gdb.execute("p g_version", to_string=True).split("=")[1]
    if mem_version != elf_version:
        gdb.write(f"\x1b[31;1mMemory version:{mem_version}")
        gdb.write(f"ELF version:   {elf_version}")
        gdb.write("Warning version not matched, please check!\x1b[m\n")
    else:
        gdb.write(f"Build version: {mem_version}\n")

    switch_inferior(1)  # Switch back
    if original_thread and original_thread.is_valid():
        original_thread.switch()
        if original_frame and original_frame.is_valid():
            original_frame.select()
    # Verify the ELF file version against the GDB tool version
    check_compatibility()
    suppress_cli_notifications(state)


def check_compatibility():
    """Check the elf and the GDB tool version"""
    from . import uname

    kernel_version = uname.kernel_version
    version_parts = kernel_version.split(" ")
    if len(version_parts) < 1:
        gdb.write(f"Invalid kernel_version format: {kernel_version}")
        return
    elf_version = version_parts[0]
    tool_verson = uname.tool_version

    if not elf_version.startswith(tool_verson):
        gdb.write(
            f"\x1b[31;1mWarning: ELF version {elf_version} "
            f"does not match GDB tool version {tool_verson}\x1b[m\n"
        )


def get_task_tls(tid, key):
    """get task tls from tid and key"""
    tcb = get_tcb(tid)
    if not tcb:
        return None

    try:
        stack_alloc_ptr = tcb["stack_alloc_ptr"].cast(
            lookup_type("struct tls_info_s").pointer()
        )
        tls_value = stack_alloc_ptr["tl_task"]["ta_telem"][int(key)]
        return tls_value.cast(lookup_type("uintptr_t").pointer())
    except gdb.error:
        return None


def get_thread_tls(pid, key):
    """get thread tls from pid and key"""
    tcb = get_tcb(pid)
    if not tcb:
        return None

    try:
        stack_alloc_ptr = tcb["stack_alloc_ptr"].cast(
            lookup_type("struct tls_info_s").pointer()
        )
        tls_value = stack_alloc_ptr["tl_elem"][int(key)]
        return tls_value.cast(lookup_type("uintptr_t").pointer())
    except gdb.error:
        return None


def get_task_argvstr(tcb: Tcb) -> List[str]:
    args = []
    try:
        TCB_FLAG_TTYPE_MASK = get_symbol_value("TCB_FLAG_TTYPE_MASK")
        TCB_FLAG_TTYPE_PTHREAD = get_symbol_value("TCB_FLAG_TTYPE_PTHREAD")

        if tcb.flags & TCB_FLAG_TTYPE_MASK == TCB_FLAG_TTYPE_PTHREAD:
            if tcb.type.code != gdb.TYPE_CODE_PTR:
                tcb = tcb.address
            ptcb = tcb.cast(lookup_type("struct pthread_entry_s").pointer())
            return ["", f"{tcb['entry']['main']}", f'{ptcb["arg"]}']

        tls_info_s = lookup_type("struct tls_info_s").pointer()
        tls = tcb.stack_alloc_ptr.cast(tls_info_s)
        argv = int(tcb.stack_alloc_ptr) + int(tls.tl_size)
        argv = gdb.Value(argv).cast(lookup_type("char").pointer().pointer())
        while argv.dereference():
            args.append(argv.dereference().string())
            argv += 1
    except gdb.error:
        pass

    return args


def gather_modules(dir=None) -> List[str]:
    dir = Path(dir).resolve() if dir else Path(__file__).parent
    modules = []

    for f in dir.rglob("*.py"):
        if f.name == "__init__.py":
            continue

        relative_path = f.relative_to(dir).with_suffix("")
        module_name = ".".join(relative_path.parts)
        modules.append(module_name)

    return modules


def gather_gdbcommands(modules=None, path=None) -> List[gdb.Command]:
    modules = modules or gather_modules(path)
    commands = []
    for m in modules:
        try:
            module = importlib.import_module(f"{__package__}.{m}")
            for c in module.__dict__.values():
                if isinstance(c, type) and issubclass(c, gdb.Command):
                    commands.append(c)
        except Exception:
            gdb.write(f"Ignore module {m}\n")
    return commands


def get_elf_md5():
    """Return the md5 checksum of the current ELF file"""
    file = gdb.objfiles()[0].filename
    with open(file, "rb") as f:
        hash = hashlib.md5(f.read()).hexdigest()
    return hash


def jsonify(obj, indent=None):
    if not obj:
        return "{}"

    def dumper(obj):
        try:
            return str(obj) if isinstance(obj, gdb.Value) else obj.toJSON()
        except Exception:
            try:
                return obj.__dict__
            except Exception as e:
                return f"<jsonify failed: {type(obj), {e}}>"

    return json.dumps(obj, default=dumper, indent=indent)


def enum(t: Union[str, gdb.Type], name=None):
    """Create python Enum class from C enum values
    Usage:

    in C:
    enum color_e {
        RED = 1,
        GREEN = 2,
    };

    in python:
    COLOR = utils.enum("enum color_e", "COLOR")
    print(COLOR.GREEN.value) # --> 2
    RED = COLOR(1)
    """
    if type(t) is str:
        t = lookup_type(t) or lookup_type("enum " + t)

    if t and t.code == gdb.TYPE_CODE_TYPEDEF:
        t = t.strip_typedefs()

    if not t or t.code != gdb.TYPE_CODE_ENUM:
        raise gdb.error(f"{t} is not an enum type")

    def commonprefix(m):
        "Given a list of pathnames, returns the longest common leading component"
        if not m:
            return ""
        s1 = min(m)
        s2 = max(m)
        for i, c in enumerate(s1):
            if c != s2[i]:
                return s1[:i]
        return s1

    # Remove the common prefix from names. This is a convention in python.
    # E.g. COLOR.RED, COLOR.GREEN instead of COLOR.COLOR_RED, COLOR.COLOR_GREEN

    prefix = commonprefix([f.name for f in t.fields()])

    names = {f.name[len(prefix) :]: f.enumval for f in t.fields()}

    if not name:
        if prefix:
            name = prefix[:-1] if prefix[-1] == "_" else prefix
        else:
            name = "Enum"

    return Enum(name, names)


class ArrayIterator:
    """An iterator for gdb array or pointer."""

    def __init__(self, array: gdb.Value, maxlen=None, reverse=False):
        type_code = array.type.code
        if type_code not in (gdb.TYPE_CODE_ARRAY, gdb.TYPE_CODE_PTR):
            raise gdb.error(f"Not an array: {array}, type: {array.type}")

        if type_code == gdb.TYPE_CODE_ARRAY:
            if (n := nitems(array)) > 0:
                maxlen = min(n, maxlen) if maxlen is not None else n

        if maxlen is None:
            raise gdb.error("Need to provide array length.")

        self.array = array
        self.maxlen = maxlen
        self.reverse = reverse
        self.index = maxlen - 1 if reverse else 0

    def __iter__(self):
        return self

    def __next__(self) -> gdb.Value:
        if (not self.reverse and self.index >= self.maxlen) or (
            self.reverse and self.index < 0
        ):
            raise StopIteration

        value = self.array[self.index]
        self.index = self.index - 1 if self.reverse else self.index + 1
        return value


class Hexdump(gdb.Command):
    """hexdump address/symbol <size>"""

    def __init__(self):
        super().__init__("hexdump", gdb.COMMAND_USER)

    @dont_repeat_decorator
    def invoke(self, args, from_tty):
        argv = args.split(" ")
        address = 0
        size = 0
        if argv[0] == "":
            gdb.write("Usage: hexdump address/symbol <size>\n")
            return

        if is_decimal(argv[0]) or is_hexadecimal(argv[0]):
            address = int(argv[0], 0)
            size = int(argv[1], 0)
        else:
            try:
                var = gdb.parse_and_eval(f"{argv[0]}")
                address = int(var.cast(long_type))
                size = int(argv[1]) if argv[1] else int(var.type.sizeof)
                gdb.write(f"{argv[0]} {hex(address)} {int(size)}\n")
            except Exception as e:
                gdb.write(f"Invalid {argv[0]}: {e}\n")

        hexdump(address, size)


PID0_REPLACE = 0x7FFFFFFF
MAX_FRAMES = 99


def get_gdb_thread_pid(thread: gdb.InferiorThread) -> int:
    return 0 if thread.ptid[1] == PID0_REPLACE else thread.ptid[1]


def get_gdb_thread(pid: int) -> Optional[gdb.InferiorThread]:
    for thread in gdb.selected_inferior().threads():
        if get_gdb_thread_pid(thread) == pid:
            return thread
    return None


def get_thread_frames(arg: Union[gdb.InferiorThread, int]) -> Union[List[gdb.Frame]]:
    thread = arg if not isinstance(arg, int) else get_gdb_thread(arg)
    if not thread:
        return []

    thread.switch()

    frames = []
    frame = gdb.newest_frame()

    while len(frames) < MAX_FRAMES and frame and frame.is_valid():
        frames.append(frame)
        frame = frame.older()

    return frames


def get_backtrace(arg: Union[gdb.InferiorThread, int]) -> List[int]:
    backtrace = []

    for frame in get_thread_frames(arg):
        backtrace.append(frame.pc())

    return backtrace


def get_frame_func_name(frame: gdb.Frame) -> str:
    function = frame.function()
    if function and function.is_valid():
        return function.name

    return ""


def get_frame_variables(frame: gdb.Frame) -> dict:
    varibles = {}
    try:
        block = frame.block()
        while block and block.is_valid():
            for symbol in block:
                if symbol.is_valid() and symbol.is_variable:
                    varibles[symbol.name] = symbol.value(frame)
            block = block.superblock
    except RuntimeError:
        pass

    return varibles
