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

import gdb
import re

"""GDB commands for working with xmethods."""


def validate_xm_regexp(part_name, regexp):
    try:
        return re.compile(regexp)
    except SyntaxError:
        raise SyntaxError("Invalid %s regexp: %s", part_name, regexp)


def parse_xm_command_args(arg):
    """Parses the arguments passed to a xmethod command.

    Arguments:
        arg: The argument string passed to a xmethod command.

    Returns:
        A 3-tuple: (<locus matching regular expression>,
                    <matcher matching regular expression>,
                    <name matching regular experession>)
    """
    argv = gdb.string_to_argv(arg)
    argc = len(argv)
    if argc > 2:
        raise SyntaxError("Too many arguments to command.")
    locus_regexp = ""
    matcher_name_regexp = ""
    xm_name_regexp = None
    if argc >= 1:
        locus_regexp = argv[0]
    if argc == 2:
        parts = argv[1].split(";", 1)
        matcher_name_regexp = parts[0]
        if len(parts) > 1:
            xm_name_regexp = parts[1]
    if xm_name_regexp:
        name_re = validate_xm_regexp("xmethod name", xm_name_regexp)
    else:
        name_re = None
    return (
        validate_xm_regexp("locus", locus_regexp),
        validate_xm_regexp("matcher name", matcher_name_regexp),
        name_re,
    )


def get_global_method_matchers(locus_re, matcher_re):
    """Returns a dict of matching globally registered xmethods.

    Arguments:
        locus_re: Even though only globally registered xmethods are
                  looked up, they will be looked up only if 'global' matches
                  LOCUS_RE.
        matcher_re: The regular expression matching the names of xmethods.

    Returns:
        A dict of matching globally registered xmethod matchers.  The only
        key in the dict will be 'global'.
    """
    locus_str = "global"
    xm_dict = {locus_str: []}
    if locus_re.match("global"):
        xm_dict[locus_str].extend([m for m in gdb.xmethods if matcher_re.match(m.name)])
    return xm_dict


def get_method_matchers_in_loci(loci, locus_re, matcher_re):
    """Returns a dict of matching registered xmethods in the LOCI.

    Arguments:
        loci: The list of loci to lookup matching xmethods in.
        locus_re: If a locus is an objfile, then xmethod matchers will be
                  looked up in it only if its filename matches the regular
                  expression LOCUS_RE.  If a locus is the current progspace,
                  then xmethod matchers will be looked up in it only if the
                  string "progspace" matches LOCUS_RE.
        matcher_re: The regular expression to match the xmethod matcher
                    names.

    Returns:
        A dict of matching xmethod matchers.  The keys of the dict are the
        filenames of the loci the xmethod matchers belong to.
    """
    xm_dict = {}
    for locus in loci:
        if isinstance(locus, gdb.Progspace):
            if not locus_re.match("progspace"):
                continue
            locus_type = "progspace"
        else:
            if not locus_re.match(locus.filename):
                continue
            locus_type = "objfile"
        locus_str = "%s %s" % (locus_type, locus.filename)
        xm_dict[locus_str] = [m for m in locus.xmethods if matcher_re.match(m.name)]
    return xm_dict


def print_xm_info(xm_dict, name_re):
    """Print a dictionary of xmethods."""

    def get_status_string(m):
        if not m.enabled:
            return " [disabled]"
        else:
            return ""

    if not xm_dict:
        return
    for locus_str in xm_dict:
        if not xm_dict[locus_str]:
            continue
        print("Xmethods in %s:" % locus_str)
        for matcher in xm_dict[locus_str]:
            print("  %s%s" % (matcher.name, get_status_string(matcher)))
            if not matcher.methods:
                continue
            for m in matcher.methods:
                if name_re is None or name_re.match(m.name):
                    print("    %s%s" % (m.name, get_status_string(m)))


def set_xm_status1(xm_dict, name_re, status):
    """Set the status (enabled/disabled) of a dictionary of xmethods."""
    for locus_str, matchers in xm_dict.items():
        for matcher in matchers:
            if not name_re:
                # If the name regex is missing, then set the status of the
                # matcher and move on.
                matcher.enabled = status
                continue
            if not matcher.methods:
                # The methods attribute could be None.  Move on.
                continue
            for m in matcher.methods:
                if name_re.match(m.name):
                    m.enabled = status


def set_xm_status(arg, status):
    """Set the status (enabled/disabled) of xmethods matching ARG.
    This is a helper function for enable/disable commands.  ARG is the
    argument string passed to the commands.
    """
    locus_re, matcher_re, name_re = parse_xm_command_args(arg)
    set_xm_status1(get_global_method_matchers(locus_re, matcher_re), name_re, status)
    set_xm_status1(
        get_method_matchers_in_loci([gdb.current_progspace()], locus_re, matcher_re),
        name_re,
        status,
    )
    set_xm_status1(
        get_method_matchers_in_loci(gdb.objfiles(), locus_re, matcher_re),
        name_re,
        status,
    )


class InfoXMethod(gdb.Command):
    """GDB command to list registered xmethod matchers.

    Usage: info xmethod [LOCUS-REGEXP [NAME-REGEXP]]

    LOCUS-REGEXP is a regular expression matching the location of the
    xmethod matchers.  If it is omitted, all registered xmethod matchers
    from all loci are listed.  A locus could be 'global', a regular expression
    matching the current program space's filename, or a regular expression
    matching filenames of objfiles.  Locus could be 'progspace' to specify that
    only xmethods from the current progspace should be listed.

    NAME-REGEXP is a regular expression matching the names of xmethod
    matchers.  If this omitted for a specified locus, then all registered
    xmethods in the locus are listed.  To list only a certain xmethods
    managed by a single matcher, the name regexp can be specified as
    matcher-name-regexp;xmethod-name-regexp."""

    def __init__(self):
        super(InfoXMethod, self).__init__("info xmethod", gdb.COMMAND_DATA)

    def invoke(self, arg, from_tty):
        locus_re, matcher_re, name_re = parse_xm_command_args(arg)
        print_xm_info(get_global_method_matchers(locus_re, matcher_re), name_re)
        print_xm_info(
            get_method_matchers_in_loci(
                [gdb.current_progspace()], locus_re, matcher_re
            ),
            name_re,
        )
        print_xm_info(
            get_method_matchers_in_loci(gdb.objfiles(), locus_re, matcher_re), name_re
        )


class EnableXMethod(gdb.Command):
    """GDB command to enable a specified (group of) xmethod(s).

    Usage: enable xmethod [LOCUS-REGEXP [NAME-REGEXP]]

    LOCUS-REGEXP is a regular expression matching the location of the
    xmethod matchers.  If it is omitted, all registered xmethods matchers
    from all loci are enabled.  A locus could be 'global', a regular expression
    matching the current program space's filename, or a regular expression
    matching filenames of objfiles.  Locus could be 'progspace' to specify that
    only xmethods from the current progspace should be enabled.

    NAME-REGEXP is a regular expression matching the names of xmethods
    within a given locus.  If this omitted for a specified locus, then all
    registered xmethod matchers in the locus are enabled.  To enable only
    a certain xmethods managed by a single matcher, the name regexp can be
    specified as matcher-name-regexp;xmethod-name-regexp."""

    def __init__(self):
        super(EnableXMethod, self).__init__("enable xmethod", gdb.COMMAND_DATA)

    def invoke(self, arg, from_tty):
        set_xm_status(arg, True)


class DisableXMethod(gdb.Command):
    """GDB command to disable a specified (group of) xmethod(s).

    Usage: disable xmethod [LOCUS-REGEXP [NAME-REGEXP]]

    LOCUS-REGEXP is a regular expression matching the location of the
    xmethod matchers.  If it is omitted, all registered xmethod matchers
    from all loci are disabled.  A locus could be 'global', a regular
    expression matching the current program space's filename, or a regular
    expression filenames of objfiles. Locus could be 'progspace' to specify
    that only xmethods from the current progspace should be disabled.

    NAME-REGEXP is a regular expression matching the names of xmethods
    within a given locus.  If this omitted for a specified locus, then all
    registered xmethod matchers in the locus are disabled.  To disable
    only a certain xmethods managed by a single matcher, the name regexp
    can be specified as matcher-name-regexp;xmethod-name-regexp."""

    def __init__(self):
        super(DisableXMethod, self).__init__("disable xmethod", gdb.COMMAND_DATA)

    def invoke(self, arg, from_tty):
        set_xm_status(arg, False)


def register_xmethod_commands():
    """Installs the xmethod commands."""
    InfoXMethod()
    EnableXMethod()
    DisableXMethod()


register_xmethod_commands()
