# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

# mozilla/prettyprinters.py --- infrastructure for SpiderMonkey's auto-loaded pretty-printers.

import re

import gdb

# Decorators for declaring pretty-printers.
#
# In each case, the decoratee should be a SpiderMonkey-style pretty-printer
# factory, taking both a gdb.Value instance and a TypeCache instance as
# arguments; see TypeCache, below.


def check_for_reused_pretty_printer(fn):
    # Check that |fn| hasn't been registered as a pretty-printer under some
    # other name already. (The 'enabled' flags used by GDB's
    # 'enable/disable/info pretty-printer' commands are simply stored as
    # properties of the function objects themselves, so a single function
    # object can't carry the 'enabled' flags for two different printers.)
    if hasattr(fn, "enabled"):
        raise RuntimeError("pretty-printer function %r registered more than once" % fn)


# a dictionary mapping gdb.Type tags to pretty-printer functions.
printers_by_tag = {}

# A decorator: add the decoratee as a pretty-printer lookup function for types
# named |type_name|.


def pretty_printer(type_name):
    def add(fn):
        check_for_reused_pretty_printer(fn)
        add_to_subprinter_list(fn, type_name)
        printers_by_tag[type_name] = fn
        return fn

    return add


# a dictionary mapping gdb.Type tags to pretty-printer functions for pointers to
# that type.
ptr_printers_by_tag = {}

# A decorator: add the decoratee as a pretty-printer lookup function for
# pointers to types named |type_name|.


def ptr_pretty_printer(type_name):
    def add(fn):
        check_for_reused_pretty_printer(fn)
        add_to_subprinter_list(fn, "ptr-to-" + type_name)
        ptr_printers_by_tag[type_name] = fn
        return fn

    return add


# a dictionary mapping gdb.Type tags to pretty-printer functions for
# references to that type.
ref_printers_by_tag = {}

# A decorator: add the decoratee as a pretty-printer lookup function for
# references to instances of types named |type_name|.


def ref_pretty_printer(type_name):
    def add(fn):
        check_for_reused_pretty_printer(fn)
        add_to_subprinter_list(fn, "ref-to-" + type_name)
        ref_printers_by_tag[type_name] = fn
        return fn

    return add


# a dictionary mapping the template name portion of gdb.Type tags to
# pretty-printer functions for instantiations of that template.
template_printers_by_tag = {}

# A decorator: add the decoratee as a pretty-printer lookup function for
# instantiations of templates named |template_name|.


def template_pretty_printer(template_name):
    def add(fn):
        check_for_reused_pretty_printer(fn)
        add_to_subprinter_list(fn, "instantiations-of-" + template_name)
        template_printers_by_tag[template_name] = fn
        return fn

    return add


# A list of (REGEXP, PRINTER) pairs, such that if REGEXP (a RegexObject)
# matches the result of converting a gdb.Value's type to a string, then
# PRINTER is a pretty-printer lookup function that will probably like that
# value.
printers_by_regexp = []

# A decorator: add the decoratee as a pretty-printer factory for types
# that, when converted to a string, match |pattern|. Use |name| as the
# pretty-printer's name, when listing, enabling and disabling.


def pretty_printer_for_regexp(pattern, name):
    compiled = re.compile(pattern)

    def add(fn):
        check_for_reused_pretty_printer(fn)
        add_to_subprinter_list(fn, name)
        printers_by_regexp.append((compiled, fn))
        return fn

    return add


# Forget all pretty-printer lookup functions defined in the module name
# |module_name|, if any exist. Use this at the top of each pretty-printer
# module like this:
#
#   clear_module_printers(__name__)


def clear_module_printers(module_name):
    global printers_by_regexp

    # Remove all pretty-printers defined in the module named |module_name|
    # from d.
    def clear_dictionary(d):
        # Walk the dictionary, building a list of keys whose entries we
        # should remove. (It's not safe to delete entries from a dictionary
        # while we're iterating over it.)
        to_delete = []
        for k, v in d.items():
            if v.__module__ == module_name:
                to_delete.append(k)
                remove_from_subprinter_list(v)
        for k in to_delete:
            del d[k]

    clear_dictionary(printers_by_tag)
    clear_dictionary(ptr_printers_by_tag)
    clear_dictionary(ref_printers_by_tag)
    clear_dictionary(template_printers_by_tag)

    # Iterate over printers_by_regexp, deleting entries from the given module.
    new_list = []
    for p in printers_by_regexp:
        if p.__module__ == module_name:
            remove_from_subprinter_list(p)
        else:
            new_list.append(p)
    printers_by_regexp = new_list


# Our subprinters array. The 'subprinters' attributes of all lookup
# functions returned by lookup_for_objfile point to this array instance,
# which we mutate as subprinters are added and removed.
subprinters = []

# Set up the 'name' and 'enabled' attributes on |subprinter|, and add it to our
# list of all SpiderMonkey subprinters.


def add_to_subprinter_list(subprinter, name):
    subprinter.name = name
    subprinter.enabled = True
    subprinters.append(subprinter)


# Remove |subprinter| from our list of all SpiderMonkey subprinters.


def remove_from_subprinter_list(subprinter):
    subprinters.remove(subprinter)


# An exception class meaning, "This objfile has no SpiderMonkey in it."


class NotSpiderMonkeyObjfileError(TypeError):
    pass


# TypeCache: a cache for frequently used information about an objfile.
#
# When a new SpiderMonkey objfile is loaded, we construct an instance of
# this class for it. Then, whenever we construct a pretty-printer for some
# gdb.Value, we also pass, as a second argument, the TypeCache for the
# objfile to which that value's type belongs.
#
# if objfile doesn't seem to have SpiderMonkey code in it, the constructor
# raises NotSpiderMonkeyObjfileError.
#
# Pretty-printer modules may add attributes to this to hold their own
# cached values. Such attributes should be named mod_NAME, where the module
# is named mozilla.NAME; for example, mozilla.JSObject should store its
# metadata in the TypeCache's mod_JSObject attribute.


class TypeCache:
    def __init__(self, objfile):
        self.objfile = objfile

        # Unfortunately, the Python interface doesn't allow us to specify
        # the objfile in whose scope lookups should occur. But simply
        # knowing that we need to lookup the types afresh is probably
        # enough.
        self.void_t = gdb.lookup_type("void")
        self.void_ptr_t = self.void_t.pointer()
        self.uintptr_t = gdb.lookup_type("uintptr_t")
        try:
            self.JSString_ptr_t = gdb.lookup_type("JSString").pointer()
            self.StringFlags_ptr_t = gdb.lookup_type("js::StringFlags").pointer()
            self.JSSymbol_ptr_t = gdb.lookup_type("JS::Symbol").pointer()
            self.JSObject_ptr_t = gdb.lookup_type("JSObject").pointer()
        except gdb.error:
            raise NotSpiderMonkeyObjfileError

        self.mod_GCCellPtr = None
        self.mod_Interpreter = None
        self.mod_JSObject = None
        self.mod_JSOp = None
        self.mod_JS_Value = None
        self.mod_ExecutableAllocator = None
        self.mod_IonGraph = None
        self.mod_StringFlags = None


# Yield a series of all the types that |t| implements, by following typedefs
# and iterating over base classes. Specifically:
# - |t| itself is the first value yielded.
# - If we yield a typedef, we later yield its definition.
# - If we yield a type with base classes, we later yield those base classes.
# - If we yield a type with some base classes that are typedefs,
#   we yield all the type's base classes before following the typedefs.
#
# This is a hokey attempt to order the implemented types by meaningfulness when
# pretty-printed. Perhaps it is entirely misguided, and we should actually
# collect all applicable pretty-printers, and then use some ordering on the
# pretty-printers themselves.
#
# We may yield a type more than once (say, if it appears more than once in the
# class hierarchy).


def implemented_types(t):
    # Yield all types that follow |t|.
    def followers(t):
        if t.code == gdb.TYPE_CODE_TYPEDEF:
            yield t.target()
            for t2 in followers(t.target()):
                yield t2
        elif is_struct_or_union(t):
            base_classes = []
            for f in t.fields():
                if f.is_base_class:
                    yield f.type
                    base_classes.append(f.type)
            for b in base_classes:
                for t2 in followers(b):
                    yield t2

    yield t
    yield from followers(t)


template_regexp = re.compile(r"([\w_:]+)<")


def is_struct_or_union(t):
    return t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION)


def is_struct_or_union_or_enum(t):
    return t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION, gdb.TYPE_CODE_ENUM)


# Construct and return a pretty-printer lookup function for objfile, or
# return None if the objfile doesn't contain SpiderMonkey code
# (specifically, definitions for SpiderMonkey types).


def lookup_for_objfile(objfile):
    # Create a type cache for this objfile.
    try:
        cache = TypeCache(objfile)
    except NotSpiderMonkeyObjfileError:
        if gdb.parameter("verbose"):
            gdb.write(
                "objfile '%s' has no SpiderMonkey code; not registering pretty-printers\n"
                % (objfile.filename,)
            )
        return None

    # Return a pretty-printer for |value|, if we have one. This is the lookup
    # function object we place in each gdb.Objfile's pretty-printers list, so it
    # carries |name|, |enabled|, and |subprinters| attributes.
    def lookup(value):
        # If |table| has a pretty-printer for |tag|, apply it to |value|.
        def check_table(table, tag):
            if tag in table:
                f = table[tag]
                if f.enabled:
                    return f(value, cache)
            return None

        def check_table_by_type_name(table, t):
            if t.code == gdb.TYPE_CODE_TYPEDEF:
                return check_table(table, str(t))
            elif is_struct_or_union_or_enum(t) and t.tag:
                return check_table(table, t.tag)
            else:
                return None

        for t in implemented_types(value.type):
            if t.code == gdb.TYPE_CODE_PTR:
                for t2 in implemented_types(t.target()):
                    p = check_table_by_type_name(ptr_printers_by_tag, t2)
                    if p:
                        return p
            elif t.code == gdb.TYPE_CODE_REF:
                for t2 in implemented_types(t.target()):
                    p = check_table_by_type_name(ref_printers_by_tag, t2)
                    if p:
                        return p
            else:
                p = check_table_by_type_name(printers_by_tag, t)
                if p:
                    return p
                if is_struct_or_union(t) and t.tag:
                    m = template_regexp.match(t.tag)
                    if m:
                        p = check_table(template_printers_by_tag, m.group(1))
                        if p:
                            return p

        # Failing that, look for a printer in printers_by_regexp. We have
        # to scan the whole list, so regexp printers should be used
        # sparingly.
        s = str(value.type)
        for r, f in printers_by_regexp:
            if f.enabled:
                m = r.match(s)
                if m:
                    p = f(value, cache)
                    if p:
                        return p

        # No luck.
        return None

    # Give |lookup| the attributes expected of a pretty-printer with
    # subprinters, for enabling and disabling.
    lookup.name = "SpiderMonkey"
    lookup.enabled = True
    lookup.subprinters = subprinters

    return lookup


# A base class for pretty-printers for pointer values that handles null
# pointers, by declining to construct a pretty-printer for them at all.
# Derived classes may simply assume that self.value is non-null.
#
# To help share code, this class can also be used with reference types.
#
# This class provides the following methods, which subclasses are free to
# override:
#
# __init__(self, value, cache): Save value and cache as properties by those names
#     on the instance.
#
# to_string(self): format the type's name and address, as GDB would, and then
#     call a 'summary' method (which the subclass must define) to produce a
#     description of the referent.
#
#     Note that pretty-printers returning a 'string' display hint must not use
#     this default 'to_string' method, as GDB will take everything it returns,
#     including the type name and address, as string contents.


class Pointer:
    def __new__(cls, value, cache):
        # Don't try to provide pretty-printers for NULL pointers.
        if value.type.strip_typedefs().code == gdb.TYPE_CODE_PTR and value == 0:
            return None
        return super().__new__(cls)

    def __init__(self, value, cache):
        self.value = value
        self.cache = cache

    def to_string(self):
        # See comment above.
        assert not hasattr(self, "display_hint") or self.display_hint() != "string"
        concrete_type = self.value.type.strip_typedefs()
        if concrete_type.code == gdb.TYPE_CODE_PTR:
            address = self.value.cast(self.cache.void_ptr_t)
        elif concrete_type.code == gdb.TYPE_CODE_REF:
            address = "@" + str(self.value.address.cast(self.cache.void_ptr_t))
        else:
            assert not "mozilla.prettyprinters.Pointer applied to bad value type"
        try:
            summary = self.summary()
        except gdb.MemoryError as r:
            summary = str(r)
        v = "(%s) %s %s" % (self.value.type, address, summary)
        return v

    def summary(self):
        raise NotImplementedError


field_enum_value = None

# Given |t|, a gdb.Type instance representing an enum type, return the
# numeric value of the enum value named |name|.
#
# Pre-2012-4-18 versions of GDB store the value of an enum member on the
# gdb.Field's 'bitpos' attribute; later versions store it on the 'enumval'
# attribute. This function retrieves the value from either.


def enum_value(t, name):
    global field_enum_value
    f = t[name]
    # Monkey-patching is a-okay in polyfills! Just because.
    if not field_enum_value:
        if hasattr(f, "enumval"):

            def field_enum_value(f):
                return f.enumval

        else:

            def field_enum_value(f):
                return f.bitpos

    return field_enum_value(f)
