#!/usr/bin/env python
#
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
#

import gdb
import re

import gdb.printing
from gdb.printing import RegexpCollectionPrettyPrinter


class TypedefRegexCollectionPrettyPrinter(RegexpCollectionPrettyPrinter):
    """Class for implementing a collection of pretty-printers, matching the
       type name to a regular expression.

       A pretty-printer in this collection will be used if the type of the
       value to be printed matches the printer's regular expression, or if
       the value is a pointer to and/or typedef to a type name that matches
       its regular expression.  The variations are tried in this order:

         1. the type name as known to the debugger (could be a 'typedef');
         2. the type after stripping off any number of layers of 'typedef';
         3. if it is a pointer, the pointed-to type;
         4. if it is a pointer, the pointed-to type minus some 'typedef's.

       In all cases, ignore 'const' and 'volatile' qualifiers.  When
       matching the pointed-to type, dereference the value or use 'None' if
       the value was a null pointer.

       This class is modeled on RegexpCollectionPrettyPrinter, which (in GDB
       7.3) matches on the base type's tag name and can't match a pointer
       type or any other type that doesn't have a tag name.
    """

    def __init__(self, name):
        super(TypedefRegexCollectionPrettyPrinter, self).__init__(name)

    def __call__(self, val):
        """Find and return an instantiation of a printer for VAL.
        """

        def lookup_type(type, val):
            """Return the first printer whose regular expression matches the
               name (tag name for struct/union/enum types) of TYPE, ignoring
               any 'const' or 'volatile' qualifiers.

               VAL is a gdb.Value, or may be None to indicate a dereferenced
               null pointer.  TYPE is the associated gdb.Type.
            """
            if type.code in [gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION,
                             gdb.TYPE_CODE_ENUM]:
                typename = type.tag
            else:
                typename = str(type.unqualified())
            for printer in self.subprinters:
                if printer.enabled and printer.compiled_re.search(typename):
                    return printer.gen_printer(val)

        def lookup_type_or_alias(type, val):
            """Return the first printer matching TYPE, or else if TYPE is a
               typedef then the first printer matching the aliased type.

               VAL is a gdb.Value, or may be None to indicate a dereferenced
               null pointer.  TYPE is the associated gdb.Type.
            """
            # First, look for a printer for the given (but unqualified) type.
            printer = lookup_type(type, val)
            if printer:
                return printer

            # If it's a typedef, look for a printer for the aliased type ...
            while type.code == gdb.TYPE_CODE_TYPEDEF:
                type = type.target()
                printer = lookup_type(type, val)
                if printer:
                    return printer

        # First, look for a printer for the given (but unqualified) type, or
        # its aliased type if it's a typedef.
        printer = lookup_type_or_alias(val.type, val)
        if printer:
            return printer

        # If it's a pointer, look for a printer for the pointed-to type.
        if val.type.code == gdb.TYPE_CODE_PTR:
            type = val.type.target()
            printer = lookup_type_or_alias(
                          type, val and val.dereference() or None)
            if printer:
                return printer

        # Cannot find a matching pretty printer in this collection.
        return None

class InferiorFunction:
    """A class whose instances are callable functions on the inferior
       process.
    """
    def __init__(self, function_name):
        self.function_name = function_name
        self.func = None

    def __call__(self, *args):
        if not self.func:
            self.func = gdb.parse_and_eval(self.function_name)
        return self.func(*args)

def children_as_map(children_iterator):
    """Convert an iteration of (key, value) pairs into the form required for
       a pretty-printer 'children' method when the display-hint is 'map'.
    """
    for k, v in children_iterator:
        yield 'key', k
        yield 'val', v


########################################################################

# Pretty-printing for APR library types.

# Some useful gdb.Type instances that can be initialized before any object
# files are loaded.
pvoidType = gdb.lookup_type('void').pointer()
cstringType = gdb.lookup_type('char').pointer()

# Some functions that resolve to calls into the inferior process.
apr_hash_count = InferiorFunction('apr_hash_count')
apr_hash_first = InferiorFunction('apr_hash_first')
apr_hash_next = InferiorFunction('apr_hash_next')
svn__apr_hash_index_key = InferiorFunction('svn__apr_hash_index_key')
svn__apr_hash_index_val = InferiorFunction('svn__apr_hash_index_val')

def children_of_apr_hash(hash_p, value_type=None):
    """Iterate over an 'apr_hash_t *' GDB value, in the way required for a
       pretty-printer 'children' method when the display-hint is 'map'.
       Cast the value pointers to VALUE_TYPE, or return values as '...' if
       VALUE_TYPE is None.
    """
    hi = apr_hash_first(0, hash_p)
    while (hi):
        k = svn__apr_hash_index_key(hi).reinterpret_cast(cstringType)
        if value_type:
            val = svn__apr_hash_index_val(hi).reinterpret_cast(value_type)
        else:
            val = '...'
        try:
            key = k.string()
        except:
            key = '<unreadable>'
        yield key, val
        hi = apr_hash_next(hi)

class AprHashPrinter:
    """for 'apr_hash_t' of 'char *' keys and unknown values"""
    def __init__(self, val):
        if val:
            self.hash_p = val.address
        else:
            self.hash_p = val

    def to_string(self):
        """Return a string to be displayed before children are displayed, or
           return None if we don't want any such.
        """
        if not self.hash_p:
            return 'NULL'
        return 'hash of ' + str(apr_hash_count(self.hash_p)) + ' items'

    def children(self):
        if not self.hash_p:
            return []
        return children_as_map(children_of_apr_hash(self.hash_p))

    def display_hint(self):
        return 'map'

def children_of_apr_array(array, value_type):
    """Iterate over an 'apr_array_header_t' GDB value, in the way required for
       a pretty-printer 'children' method when the display-hint is 'array'.
       Cast the values to VALUE_TYPE.
    """
    nelts = int(array['nelts'])
    elts = array['elts'].reinterpret_cast(value_type.pointer())
    for i in range(nelts):
        yield str(i), elts[i]

class AprArrayPrinter:
    """for 'apr_array_header_t' of unknown elements"""
    def __init__(self, val):
        self.array = val

    def to_string(self):
        if not self.array:
            return 'NULL'
        nelts = self.array['nelts']
        return 'array of ' + str(int(nelts)) + ' items'

    def children(self):
        # We can't display the children as we don't know their type.
        return []

    def display_hint(self):
        return 'array'

########################################################################

# Pretty-printing for Subversion libsvn_subr types.

class SvnBooleanPrinter:
    """for svn_boolean_t"""
    def __init__(self, val):
        self.val = val

    def to_string(self):
        if self.val is None:
            return '(NULL)'
        if self.val:
            return 'TRUE'
        else:
            return 'FALSE'

class SvnStringPrinter:
    """for svn_string_t"""
    def __init__(self, val):
        self.val = val

    def to_string(self):
        if not self.val:
            return 'NULL'

        data = self.val['data']
        len = int(self.val['len'])
        return data.string(length=len)

    def display_hint(self):
        if self.val:
            return 'string'

class SvnMergeRangePrinter:
    """for svn_merge_range_t"""
    def __init__(self, val):
        self.val = val

    def to_string(self):
        if not self.val:
            return 'NULL'

        r = self.val
        start = int(r['start'])
        end = int(r['end'])
        if start >= 0 and start < end:
            if start + 1 == end:
                rs = str(end)
            else:
                rs = str(start + 1) + '-' + str(end)
        elif end >= 0 and end < start:
            if start == end + 1:
                rs = '-' + str(start)
            else:
                rs = str(start) + '-' + str(end + 1)
        else:
            rs = '(INVALID: s=%d, e=%d)' % (start, end)
        if not r['inheritable']:
            rs += '*'
        return rs

    def display_hint(self):
        if self.val:
            return 'string'

class SvnRangelistPrinter:
    """for svn_rangelist_t"""
    def __init__(self, val):
        self.array = val
        self.svn_merge_range_t = gdb.lookup_type('svn_merge_range_t')

    def to_string(self):
        if not self.array:
            return 'NULL'

        s = ''
        for key, val in children_of_apr_array(self.array,
                                              self.svn_merge_range_t.pointer()):
            if s:
                s += ','
            s += SvnMergeRangePrinter(val).to_string()
        return s

    def display_hint(self):
        if self.array:
            return 'string'

class SvnMergeinfoPrinter:
    """for svn_mergeinfo_t"""
    def __init__(self, val):
        self.hash_p = val
        self.svn_rangelist_t = gdb.lookup_type('svn_rangelist_t')

    def to_string(self):
        if self.hash_p == 0:
            return 'NULL'

        s = ''
        for key, val in children_of_apr_hash(self.hash_p,
                                             self.svn_rangelist_t.pointer()):
            if s:
                s += '; '
            s += key + ':' + SvnRangelistPrinter(val).to_string()
        return '{ ' + s + ' }'

class SvnMergeinfoCatalogPrinter:
    """for svn_mergeinfo_catalog_t"""
    def __init__(self, val):
        self.hash_p = val
        self.svn_mergeinfo_t = gdb.lookup_type('svn_mergeinfo_t')

    def to_string(self):
        if self.hash_p == 0:
            return 'NULL'

        s = ''
        for key, val in children_of_apr_hash(self.hash_p,
                                             self.svn_mergeinfo_t):
            if s:
                s += ',\n  '
            s += "'" + key + "': " + SvnMergeinfoPrinter(val).to_string()
        return '{ ' + s + ' }'

########################################################################

# Pretty-printing for Subversion libsvn_client types.

class SvnPathrevPrinter:
    """for svn_client__pathrev_t"""
    def __init__(self, val):
        self.val = val

    def to_string(self):
        if not self.val:
            return 'NULL'

        rev = int(self.val['rev'])
        url = self.val['url'].string()
        repos_root_url = self.val['repos_root_url'].string()
        relpath = url[len(repos_root_url):]
        return "%s@%d" % (relpath, rev)

    def display_hint(self):
        if self.val:
            return 'string'


########################################################################

libapr_printer = None
libsvn_printer = None

def build_libsvn_printers():
    """Construct the pretty-printer objects."""

    global libapr_printer, libsvn_printer

    libapr_printer = TypedefRegexCollectionPrettyPrinter("libapr")
    libapr_printer.add_printer('apr_hash_t', r'^apr_hash_t$',
                               AprHashPrinter)
    libapr_printer.add_printer('apr_array_header_t', r'^apr_array_header_t$',
                               AprArrayPrinter)

    libsvn_printer = TypedefRegexCollectionPrettyPrinter("libsvn")
    libsvn_printer.add_printer('svn_boolean_t', r'^svn_boolean_t$',
                               SvnBooleanPrinter)
    libsvn_printer.add_printer('svn_string_t', r'^svn_string_t$',
                               SvnStringPrinter)
    libsvn_printer.add_printer('svn_client__pathrev_t', r'^svn_client__pathrev_t$',
                               SvnPathrevPrinter)
    libsvn_printer.add_printer('svn_merge_range_t', r'^svn_merge_range_t$',
                               SvnMergeRangePrinter)
    libsvn_printer.add_printer('svn_rangelist_t', r'^svn_rangelist_t$',
                               SvnRangelistPrinter)
    libsvn_printer.add_printer('svn_mergeinfo_t', r'^svn_mergeinfo_t$',
                               SvnMergeinfoPrinter)
    libsvn_printer.add_printer('svn_mergeinfo_catalog_t', r'^svn_mergeinfo_catalog_t$',
                               SvnMergeinfoCatalogPrinter)


def register_libsvn_printers(obj):
    """Register the pretty-printers for the object file OBJ."""

    global libapr_printer, libsvn_printer

    # Printers registered later take precedence.
    gdb.printing.register_pretty_printer(obj, libapr_printer)
    gdb.printing.register_pretty_printer(obj, libsvn_printer)


# Construct the pretty-printer objects, once, at GDB start-up time when this
# Python module is loaded.  (Registration happens later, once per object
# file.)
build_libsvn_printers()
