# Copyright 2023-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/>.

from abc import ABC, abstractmethod
from collections import defaultdict
from contextlib import contextmanager

import gdb
import gdb.printing

from .server import client_bool_capability
from .startup import DAPException, in_gdb_thread

# A list of all the variable references created during this pause.
all_variables = []


# When the inferior is re-started, we erase all variable references.
# See the section "Lifetime of Objects References" in the spec.
@in_gdb_thread
def clear_vars(event):
    global all_variables
    all_variables = []


gdb.events.cont.connect(clear_vars)


# A null context manager.  Python supplies one, starting in 3.7.
@contextmanager
def _null(**ignore):
    yield


@in_gdb_thread
def apply_format(value_format):
    """Temporarily apply the DAP ValueFormat.

    This returns a new context manager that applies the given DAP
    ValueFormat object globally, then restores gdb's state when finished."""
    if value_format is not None and "hex" in value_format and value_format["hex"]:
        return gdb.with_parameter("output-radix", 16)
    return _null()


class BaseReference(ABC):
    """Represent a variable or a scope.

    This class is just a base class, some methods must be implemented in
    subclasses.

    The 'ref' field can be used as the variablesReference in the protocol.
    """

    @in_gdb_thread
    def __init__(self, name):
        """Create a new variable reference with the given name.

        NAME is a string or None.  None means this does not have a
        name, e.g., the result of expression evaluation."""

        global all_variables
        all_variables.append(self)
        self.ref = len(all_variables)
        self.name = name
        self.reset_children()

    @in_gdb_thread
    def to_object(self):
        """Return a dictionary that describes this object for DAP.

        The resulting object is a starting point that can be filled in
        further.  See the Scope or Variable types in the spec"""
        result = {"variablesReference": self.ref if self.has_children() else 0}
        if self.name is not None:
            result["name"] = str(self.name)
        return result

    @abstractmethod
    def has_children(self):
        """Return True if this object has children."""
        return False

    def reset_children(self):
        """Reset any cached information about the children of this object."""
        # A list of all the children.  Each child is a BaseReference
        # of some kind.
        self.children = None
        # Map from the name of a child to a BaseReference.
        self.by_name = {}
        # Keep track of how many duplicates there are of a given name,
        # so that unique names can be generated.  Map from base name
        # to a count.
        self.name_counts = defaultdict(lambda: 1)

    @abstractmethod
    def fetch_one_child(self, index):
        """Fetch one child of this variable.

        INDEX is the index of the child to fetch.
        This should return a tuple of the form (NAME, VALUE), where
        NAME is the name of the variable, and VALUE is a gdb.Value."""
        return

    @abstractmethod
    def child_count(self):
        """Return the number of children of this variable."""
        return

    # Helper method to compute the final name for a child whose base
    # name is given.  Updates the name_counts map.  This is used to
    # handle shadowing -- in DAP, the adapter is responsible for
    # making sure that all the variables in a a given container have
    # unique names.  See
    # https://github.com/microsoft/debug-adapter-protocol/issues/141
    # and
    # https://github.com/microsoft/debug-adapter-protocol/issues/149
    def _compute_name(self, name):
        if name in self.by_name:
            self.name_counts[name] += 1
            # In theory there's no safe way to compute a name, because
            # a pretty-printer might already be generating names of
            # that form.  In practice I think we should not worry too
            # much.
            name = name + " #" + str(self.name_counts[name])
        return name

    @in_gdb_thread
    def fetch_children(self, start, count):
        """Fetch children of this variable.

        START is the starting index.
        COUNT is the number to return, with 0 meaning return all.
        Returns an iterable of some kind."""
        if count == 0:
            count = self.child_count()
        if self.children is None:
            self.children = [None] * self.child_count()
        for idx in range(start, start + count):
            if self.children[idx] is None:
                (name, value) = self.fetch_one_child(idx)
                name = self._compute_name(name)
                var = VariableReference(name, value)
                self.children[idx] = var
                self.by_name[name] = var
            yield self.children[idx]

    @in_gdb_thread
    def find_child_by_name(self, name):
        """Find a child of this variable, given its name.

        Returns the value of the child, or throws if not found."""
        # A lookup by name can only be done using names previously
        # provided to the client, so we can simply rely on the by-name
        # map here.
        if name in self.by_name:
            return self.by_name[name]
        raise DAPException("no variable named '" + name + "'")


class VariableReference(BaseReference):
    """Concrete subclass of BaseReference that handles gdb.Value."""

    def __init__(self, name, value, result_name="value"):
        """Initializer.

        NAME is the name of this reference, see superclass.
        VALUE is a gdb.Value that holds the value.
        RESULT_NAME can be used to change how the simple string result
        is emitted in the result dictionary."""
        super().__init__(name)
        self.result_name = result_name
        self.value = value
        self._update_value()

    # Internal method to update local data when the value changes.
    def _update_value(self):
        self.reset_children()
        self.printer = gdb.printing.make_visualizer(self.value)
        self.child_cache = None
        if self.has_children():
            self.count = -1
        else:
            self.count = None

    def assign(self, value):
        """Assign VALUE to this object and update."""
        self.value.assign(value)
        self._update_value()

    def has_children(self):
        return hasattr(self.printer, "children")

    def cache_children(self):
        if self.child_cache is None:
            # This discards all laziness.  This could be improved
            # slightly by lazily evaluating children, but because this
            # code also generally needs to know the number of
            # children, it probably wouldn't help much.  Note that
            # this is only needed with legacy (non-ValuePrinter)
            # printers.
            self.child_cache = list(self.printer.children())
        return self.child_cache

    def child_count(self):
        if self.count is None:
            return None
        if self.count == -1:
            num_children = None
            if isinstance(self.printer, gdb.ValuePrinter) and hasattr(
                self.printer, "num_children"
            ):
                num_children = self.printer.num_children()
            if num_children is None:
                num_children = len(self.cache_children())
            self.count = num_children
        return self.count

    def to_object(self):
        result = super().to_object()
        result[self.result_name] = str(self.printer.to_string())
        num_children = self.child_count()
        if num_children is not None:
            if (
                hasattr(self.printer, "display_hint")
                and self.printer.display_hint() == "array"
            ):
                result["indexedVariables"] = num_children
            else:
                result["namedVariables"] = num_children
        if client_bool_capability("supportsMemoryReferences"):
            # https://github.com/microsoft/debug-adapter-protocol/issues/414
            # changed DAP to allow memory references for any of the
            # variable response requests, and to lift the restriction
            # to pointer-to-function from Variable.
            if self.value.type.strip_typedefs().code == gdb.TYPE_CODE_PTR:
                result["memoryReference"] = hex(int(self.value))
        if client_bool_capability("supportsVariableType"):
            result["type"] = str(self.value.type)
        return result

    @in_gdb_thread
    def fetch_one_child(self, idx):
        if isinstance(self.printer, gdb.ValuePrinter) and hasattr(
            self.printer, "child"
        ):
            (name, val) = self.printer.child(idx)
        else:
            (name, val) = self.cache_children()[idx]
        # A pretty-printer can return something other than a
        # gdb.Value, but it must be convertible.
        if not isinstance(val, gdb.Value):
            val = gdb.Value(val)
        return (name, val)


@in_gdb_thread
def find_variable(ref):
    """Given a variable reference, return the corresponding variable object."""
    global all_variables
    # Variable references are offset by 1.
    ref = ref - 1
    if ref < 0 or ref > len(all_variables):
        raise DAPException("invalid variablesReference")
    return all_variables[ref]
