# Unwinder commands.
# Copyright 2015-2018 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


def validate_regexp(exp, idstring):
    try:
        return re.compile(exp)
    except SyntaxError:
        raise SyntaxError("Invalid %s regexp: %s." % (idstring, exp))


def parse_unwinder_command_args(arg):
    """Internal utility to parse unwinder command argv.

    Arguments:
        arg: The arguments to the command. The format is:
             [locus-regexp [name-regexp]]

    Returns:
        A 2-tuple of compiled regular expressions.

    Raises:
        SyntaxError: an error processing ARG
    """

    argv = gdb.string_to_argv(arg)
    argc = len(argv)
    if argc > 2:
        raise SyntaxError("Too many arguments.")
    locus_regexp = ""
    name_regexp = ""
    if argc >= 1:
        locus_regexp = argv[0]
        if argc >= 2:
            name_regexp = argv[1]
    return (validate_regexp(locus_regexp, "locus"),
            validate_regexp(name_regexp, "unwinder"))


class InfoUnwinder(gdb.Command):
    """GDB command to list unwinders.

    Usage: info unwinder [locus-regexp [name-regexp]]

    LOCUS-REGEXP is a regular expression matching the location of the
    unwinder.  If it is omitted, all registered unwinders from all
    loci are listed.  A locus can be 'global', 'progspace' to list
    the unwinders from the current progspace, or a regular expression
    matching filenames of objfiles.

    NAME-REGEXP is a regular expression to filter unwinder names.  If
    this omitted for a specified locus, then all registered unwinders
    in the locus are listed.
    """

    def __init__(self):
        super(InfoUnwinder, self).__init__("info unwinder",
                                            gdb.COMMAND_STACK)

    def list_unwinders(self, title, unwinders, name_re):
        """Lists the unwinders whose name matches regexp.

        Arguments:
            title: The line to print before the list.
            unwinders: The list of the unwinders.
            name_re: unwinder name filter.
        """
        if not unwinders:
            return
        print(title)
        for unwinder in unwinders:
            if name_re.match(unwinder.name):
                print("  %s%s" % (unwinder.name,
                                  "" if unwinder.enabled else " [disabled]"))

    def invoke(self, arg, from_tty):
        locus_re, name_re = parse_unwinder_command_args(arg)
        if locus_re.match("global"):
            self.list_unwinders("Global:", gdb.frame_unwinders,
                                name_re)
        if locus_re.match("progspace"):
            cp = gdb.current_progspace()
            self.list_unwinders("Progspace %s:" % cp.filename,
                                cp.frame_unwinders, name_re)
        for objfile in gdb.objfiles():
            if locus_re.match(objfile.filename):
                self.list_unwinders("Objfile %s:" % objfile.filename,
                                    objfile.frame_unwinders, name_re)


def do_enable_unwinder1(unwinders, name_re, flag):
    """Enable/disable unwinders whose names match given regex.

    Arguments:
        unwinders: The list of unwinders.
        name_re: Unwinder name filter.
        flag: Enable/disable.

    Returns:
        The number of unwinders affected.
    """
    total = 0
    for unwinder in unwinders:
        if name_re.match(unwinder.name):
            unwinder.enabled = flag
            total += 1
    return total


def do_enable_unwinder(arg, flag):
    """Enable/disable unwinder(s)."""
    (locus_re, name_re) = parse_unwinder_command_args(arg)
    total = 0
    if locus_re.match("global"):
        total += do_enable_unwinder1(gdb.frame_unwinders, name_re, flag)
    if locus_re.match("progspace"):
        total += do_enable_unwinder1(gdb.current_progspace().frame_unwinders,
                                     name_re, flag)
    for objfile in gdb.objfiles():
        if locus_re.match(objfile.filename):
            total += do_enable_unwinder1(objfile.frame_unwinders, name_re,
                                         flag)
    if total > 0:
        gdb.invalidate_cached_frames()
    print("%d unwinder%s %s" % (total, "" if total == 1 else "s",
                                "enabled" if flag else "disabled"))


class EnableUnwinder(gdb.Command):
    """GDB command to enable unwinders.

    Usage: enable unwinder [locus-regexp [name-regexp]]

    LOCUS-REGEXP is a regular expression specifying the unwinders to
    enable.  It can 'global', 'progspace', or the name of an objfile
    within that progspace.

    NAME_REGEXP is a regular expression to filter unwinder names.  If
    this omitted for a specified locus, then all registered unwinders
    in the locus are affected.

    """

    def __init__(self):
        super(EnableUnwinder, self).__init__("enable unwinder",
                                             gdb.COMMAND_STACK)

    def invoke(self, arg, from_tty):
        """GDB calls this to perform the command."""
        do_enable_unwinder(arg, True)


class DisableUnwinder(gdb.Command):
    """GDB command to disable the specified unwinder.

    Usage: disable unwinder [locus-regexp [name-regexp]]

    LOCUS-REGEXP is a regular expression specifying the unwinders to
    disable.  It can 'global', 'progspace', or the name of an objfile
    within that progspace.

    NAME_REGEXP is a regular expression to filter unwinder names.  If
    this omitted for a specified locus, then all registered unwinders
    in the locus are affected.

    """

    def __init__(self):
        super(DisableUnwinder, self).__init__("disable unwinder",
                                              gdb.COMMAND_STACK)

    def invoke(self, arg, from_tty):
        """GDB calls this to perform the command."""
        do_enable_unwinder(arg, False)


def register_unwinder_commands():
    """Installs the unwinder commands."""
    InfoUnwinder()
    EnableUnwinder()
    DisableUnwinder()


register_unwinder_commands()
