#!/usr/bin/python
# vim: set ts=4 sw=4 et :
import re, glob, time, sys, os, pwd

# To find modules in directory it was symlinked to.
sys.path.insert(0, os.path.dirname(sys.argv[0]))

# Default syntax map
types = {
    'CONSTANT':      'Constant',
    'DEFINE':        'Constant',
    'MACRO':         'Macro',
    'FUNCTION':      'Function',
    'VARIABLE':      'Identifier',
    'STRUCT':        'Type',
    'ENUM':          'Type',
    'UNION':         'Type',
    'TYPEDEF':       'Type',
    'USER_FUNCTION': 'Type'
}

# Default options
options = {
    'filter_regexp': r'^_',
    'override': {},
}

progname = sys.argv[0]
args = sys.argv[1:]
if len(args) != 1 and len(args) != 2:
    print """\
Usage: %s [config.py] FOO-decl.txt >OUTPUT

Config-file options:
  filter_regexp     Exclude matching symbols (default: r'^_')
  override          Declaration override map (like {'NULL': 'Constant', ... })
                    nonexistent symbols are inserted
                    symbols mapped to None or empty type are removed
  types             Type highligting override map (like {'Enum': 'Keyword'})

Gtk-doc declaration types (case doesn't matter):
  Constant, Macro, Function, Struct, Enum, Union, Typedef, User_Function,
  Variable
Extra symbol types:
  Define (parameterless macro)
Default map:\
""" % progname
    maxlen = max(len(x) for x in types.keys())
    for t in sorted(types.keys()):
        tt = t.title()
        print '  %*s -> %s' % (maxlen, tt, types[t])
    sys.exit(0)


# Config file
if len(args) == 2:
    xoptions = {}
    execfile(args[0], xoptions)
    options.update(xoptions)
    del args[0]

if options.has_key('types'):
    types.update(dict([(k.upper(), v) for k, v in options['types'].items()]))

normalize = lambda x: x.title().replace('_', '')

find_decl = re.compile(r'<(?P<type>' + r'|'.join(types.keys()) + r')>\n'
                       + r'<NAME>(?P<ident>\w+)</NAME>\n'
                       + r'(?P<body>.*?)'
                       + r'</(?P=type)>\n',
                       re.S).finditer
enum_values = re.compile(r'^\s+(?P<ident>[A-Z][A-Z0-9_]+)\b', re.M).finditer
is_param_macro = re.compile(r'^\s*#\s*define\s+\w+\(', re.M).search
get_ident_macro = re.compile(r'^\s*#\s*define\s+\w+\s+'
                             + r'(?P<ident>[A-Za-z_]\w+)\s*$', re.M).search
ignore_symbol = re.compile(options['filter_regexp']).search

# Override symbols, they are created if they don't exist, if they map to None,
# they are removed.
def override(decls, overrides):
    for ident, typ in overrides.items():
        if typ:
            decls[ident] = typ.upper()
        elif ident in decls:
            del decls[ident]

# Read FOO-decl.txt, updating the declaration dictionary and identifier types
# decls is a map indexed by gtk-doc symbol type and holding maps that assign
#       each symbol its kind (nothing, Deprecated)
# indirect is a map from parameterless macros to the symbols they expand to,
#           this is later used to change the symbol type from macro to its
#           actual role
def read_decl_txt(filename, decls, indirect, deprecated):
    fh = file(filename, 'r')
    text = fh.read()
    fh.close()
    for d in find_decl(text):
        ident, typ, body = (d.group(x) for x in ('ident', 'type', 'body'))
        if ignore_symbol(ident):
            continue

        if body.find('<DEPRECATED/>') > -1:
            deprecated.add(ident)

        # Change parameterless macros to constants.  If they resolve to an
        # identifier, remember them for symbol type remapping.
        if typ == 'MACRO' and not is_param_macro(body):
            typ = 'DEFINE'
            m = get_ident_macro(body)
            if m:
                indirect[ident] = m.group('ident')

        # Add the identifier to the declaration list.  Store strings directly,
        # unless there's more than type for the symbol, then change it to a
        # type list.
        if ident in decls:
            if type(decls[ident]) == type(typ):
                if decls[ident] != typ:
                    decls[ident] = (decls[ident], typ)
            else:
                if typ not in decls[ident]:
                    decls[ident] += (typ,)
        else:
            decls[ident] = typ

        # Find enum values and make them constants
        if typ == 'ENUM':
            for m in enum_values(body):
                decls[m.group('ident')] = 'CONSTANT'

# Kill macros if the same symbol also exists as a regular symbol of another
# type (this fixes things like g_file_test()).
def resolve_macros(decls, indirect):
    for macro in indirect.keys():
        # No other meaning
        d = decls[macro]
        if type(d) == type(macro):
            continue
        # Multiple types, remove the macro meaning.
        d = [x for x in d if x != 'DEFINE']
        if len(d) == 1:
            decls[macro] = d[0]
        else:
            decls[macro] = tuple(d)

def find_conflicts(decls):
    for ident in decls.keys():
        d = decls[ident]
        if type(d) == type(ident):
            continue
        # If there's MACRO in the mix, try to remove it if it improves things.
        d = tuple(x for x in d if x != 'MACRO')
        if len(d) == 1:
            decls[ident] = d[0]
            continue
        # A hard conflict
        sys.stderr.write('%s has multiple types: %s; '
                         'keeping %s.\n'
                         % (ident, ', '.join(d), d[0]))
        # First came, first served
        decls[ident] = d[0]

decls = {}
indirect = {}
deprecated = set()
read_decl_txt(args[0], decls, indirect, deprecated)
resolve_macros(decls, indirect)
override(decls, options['override'])
find_conflicts(decls)

for typ in sorted(set(decls.values())):
    normalsym = ' '.join(x for x in decls
                         if decls[x] == typ and x not in deprecated)
    deprecatedsym = ' '.join(x for x in decls
                             if decls[x] == typ and x in deprecated)
    typ = normalize(typ)
    if normalsym:
        print typ, normalsym
    if deprecatedsym:
        print (typ + 'Deprecated'), deprecatedsym
