# Copyright 2022-2024 Free Software Foundation, Inc.

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# This is deprecated in 3.9, but required in older versions.
from typing import Optional

import gdb

from .frames import dap_frame_generator
from .modules import module_id
from .scopes import symbol_value
from .server import capability, export_line, request
from .sources import make_source
from .startup import in_gdb_thread
from .state import set_thread
from .typecheck import type_check
from .varref import apply_format


# Helper function to compute parameter information for a stack frame.
@in_gdb_thread
def _compute_parameters(frame, stack_format):
    arg_iter = frame.frame_args()
    if arg_iter is None:
        return ""
    result = []
    for arg in arg_iter:
        desc = []
        name, val = symbol_value(arg, frame)
        # We don't try to use any particular language's syntax for the
        # output here.
        if stack_format["parameterTypes"]:
            desc.append("[" + str(val.type) + "]")
        if stack_format["parameterNames"]:
            desc.append(name)
            # If both the name and the value are requested, insert an
            # '=' for clarity.
            if stack_format["parameterValues"]:
                desc.append("=")
        if stack_format["parameterValues"]:
            desc.append(val.format_string(summary=True))
        result.append(" ".join(desc))
    return ", ".join(result)


# Helper function to compute a stack trace.
@in_gdb_thread
def _backtrace(thread_id, levels, startFrame, stack_format):
    with apply_format(stack_format):
        set_thread(thread_id)
        frames = []
        frame_iter = dap_frame_generator(startFrame, levels, stack_format["includeAll"])
        for frame_id, current_frame in frame_iter:
            pc = current_frame.address()
            # The stack frame format affects the name, so we build it up
            # piecemeal and assign it at the end.
            name = current_frame.function()
            # The meaning of StackFrameFormat.parameters was clarified
            # in https://github.com/microsoft/debug-adapter-protocol/issues/411.
            if stack_format["parameters"] and (
                stack_format["parameterTypes"]
                or stack_format["parameterNames"]
                or stack_format["parameterValues"]
            ):
                name += "(" + _compute_parameters(current_frame, stack_format) + ")"
            newframe = {
                "id": frame_id,
                # This must always be supplied, but we will set it
                # correctly later if that is possible.
                "line": 0,
                # GDB doesn't support columns.
                "column": 0,
                "instructionPointerReference": hex(pc),
            }
            line = current_frame.line()
            if line is not None:
                newframe["line"] = export_line(line)
                if stack_format["line"]:
                    # Unclear whether export_line should be called
                    # here, but since it's just for users we pick the
                    # gdb representation.
                    name += ", line " + str(line)
            objfile = gdb.current_progspace().objfile_for_address(pc)
            if objfile is not None:
                newframe["moduleId"] = module_id(objfile)
                if stack_format["module"]:
                    name += ", module " + objfile.username
            filename = current_frame.filename()
            if filename is not None:
                newframe["source"] = make_source(filename)
            newframe["name"] = name
            frames.append(newframe)
        # Note that we do not calculate totalFrames here.  Its absence
        # tells the client that it may simply ask for frames until a
        # response yields fewer frames than requested.
        return {
            "stackFrames": frames,
        }


# A helper function that checks the types of the elements of a
# StackFrameFormat, and converts this to a dict where all the members
# are set.  This simplifies the implementation code a bit.
@type_check
def check_stack_frame(
    *,
    # Note that StackFrameFormat extends ValueFormat, which is why
    # "hex" appears here.
    hex: Optional[bool] = False,
    parameters: Optional[bool] = False,
    parameterTypes: Optional[bool] = False,
    parameterNames: Optional[bool] = False,
    parameterValues: Optional[bool] = False,
    line: Optional[bool] = False,
    module: Optional[bool] = False,
    includeAll: Optional[bool] = False,
    **rest
):
    return {
        "hex": hex,
        "parameters": parameters,
        "parameterTypes": parameterTypes,
        "parameterNames": parameterNames,
        "parameterValues": parameterValues,
        "line": line,
        "module": module,
        "includeAll": includeAll,
    }


@request("stackTrace")
@capability("supportsDelayedStackTraceLoading")
def stacktrace(
    *, levels: int = 0, startFrame: int = 0, threadId: int, format=None, **extra
):
    # It's simpler if the format is always set.
    if format is None:
        format = {}
    format = check_stack_frame(**format)
    return _backtrace(threadId, levels, startFrame, format)
