#!/usr/bin/env python3

# If the code below looks horrible and unpythonic, do not panic.
#
# It is.
#
# This is a manual conversion from the original Perl script to
# Python. Improvements are welcome.

import os, sys, re, argparse

output_stream = sys.stdout

def write_output(output):
    global output_stream
    print(output, file=output_stream)

version = '@GLIB_VERSION@'

# glib-mkenums.py
# Information about the current enumeration
flags = None # Is enumeration a bitmask?
option_underscore_name = '' # Overriden underscore variant of the enum name
                            # for example to fix the cases we don't get the
                            # mixed-case -> underscorized transform right.
option_lowercase_name = ''  # DEPRECATED.  A lower case name to use as part
                            # of the *_get_type() function, instead of the
                            # one that we guess. For instance, when an enum
                            # uses abnormal capitalization and we can not
                            # guess where to put the underscores.
seenbitshift = 0        # Have we seen bitshift operators?
enum_prefix = None        # Prefix for this enumeration
enumname = ''            # Name for this enumeration
enumshort = ''           # $enumname without prefix
enumname_prefix = ''       # prefix of $enumname
enumindex = 0        # Global enum counter
firstenum = 1        # Is this the first enumeration per file?
entries = []            # [ name, val ] for each entry
sandbox = None      # sandbox for safe evaluation of expressions

output = ''            # Filename to write result into

def parse_trigraph(opts):
    result = {}

    for opt in re.split(r'\s*,\s*', opts):
        opt = re.sub(r'^\s*', '', opt)
        opt = re.sub(r'\s*$', '', opt)
        m = re.search(r'(\w+)(?:=(.+))?', opt)
        assert(m is not None)
        groups = m.groups()
        key = groups[0]
        if len(groups) > 1:
            val = groups[1]
        else:
            val = 1
        result[key] = val
    return result

def parse_entries(file, file_name):
    global entries, enumindex, enumname, seenbitshift, flags
    looking_for_name = False

    for line in file:
        # read lines until we have no open comments
        while re.search(r'/\*([^*]|\*(?!/))*$', line):
            line = file.readline()

        # strip comments w/o options
        line = re.sub(r'''/\*(?!<)
            ([^*]+|\*(?!/))*
           \*/''', '', line, flags=re.X)

        line = line.rstrip()

        # skip empty lines
        if len(line.strip()) == 0:
            continue

        if looking_for_name:
            m = re.match('\s*(\w+)', line)
            if m:
                enumname = m.group(1);
                return True

        # Handle include files
        m = re.match(r'\#include\s*<([^>]*)>', line)
        if m:
            newfilename = os.path.join("..", m.group(1))
            newfile = open(newfilename)

            if not parse_entries(newfile, newfilename):
                return False
            else:
                continue

        m = re.match(r'\s*\}\s*(\w+)', line)
        if m:
            enumname = m.group(1)
            enumindex+=1;
            return 1;

        m = re.match(r'\s*\}', line)
        if m:
            enumindex+=1;
            looking_for_name = True
            continue

        m = re.match(r'''\s*
              (\w+)\s*                   # name
              (?:=(                      # value
                   \s*\w+\s*\(.*\)\s*       # macro with multiple args
                   |                        # OR
                   (?:[^,/]|/(?!\*))*       # anything but a comma or comment
                  ))?,?\s*
              (?:/\*<                    # options
                (([^*]|\*(?!/))*)
               >\s*\*/)?,?
              \s*$''', line, flags=re.X)
        if m:
            groups = m.groups()
            name = groups[0]
            value = None
            options = None
            if len(groups) > 1:
                value = groups[1]
            if len(groups) > 2:
                options = groups[2]
            if flags is None and value is not None and '<<' in value:
                seenbitshift = 1

            if options is not None:
                options = parse_trigraph(options)
                if options['skip'] is not None:
                    entries.append((name, value, options['nick']));
            else:
                entries.append((name, value))
        elif re.match('s*\#', line):
            pass
        else:
            sys.exit("Failed to parse %s." % file_name)
    return False

def print_version():
    print("glib-mkenums version glib-" + version)
    print("glib-mkenums comes with ABSOLUTELY NO WARRANTY.")
    print("You may redistribute copies of glib-mkenums under the terms of")
    print("the GNU General Public License which can be found in the")
    print("GLib source package. Sources, examples and contact")
    print("information are available at http://www.gtk.org")
    sys.exit(0)

help_epilog = '''Production text substitutions:
  @EnumName@            PrefixTheXEnum
  @enum_name@           prefix_the_xenum
  @ENUMNAME@            PREFIX_THE_XENUM
  @ENUMSHORT@           THE_XENUM
  @ENUMPREFIX@          PREFIX
  @VALUENAME@           PREFIX_THE_XVALUE
  @valuenick@           the-xvalue
  @valuenum@            the integer value (limited support, Since: 2.26)
  @type@                either enum or flags
  @Type@                either Enum or Flags
  @TYPE@                either ENUM or FLAGS
  @filename@            name of current input file
  @basename@            base name of the current input file (Since: 2.22)
'''


# production variables:
idprefix = ""    # "G", "Gtk", etc
symprefix = ""   # "g", "gtk", etc, if not just lc($idprefix)
fhead = ""   # output file header
fprod = ""   # per input file production
ftail = ""   # output file trailer
eprod = ""   # per enum text (produced prior to value itarations)
vhead = ""   # value header, produced before iterating over enum values
vprod = ""   # value text, produced for each enum value
vtail = ""   # value tail, produced after iterating over enum values
comment_tmpl = ""   # comment template

def read_template_file(file):
    global idprefix, symprefix, fhead, fprod, ftail, eprod, vhead, vprod, vtail, comment_tmpl
    tmpl = {'file-header': fhead,
            'file-production': fprod,
            'file-tail': ftail,
            'enumeration-production': eprod,
            'value-header': vhead,
            'value-production': vprod,
            'value-tail': vtail,
            'comment': comment_tmpl,
    }
    in_ = 'junk'

    ifile = open(file)
    for line in ifile:
        m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line)
        if m:
            if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl:
                in_ = m.group(2)
                continue
            elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl:
                in_ = 'junk'
                continue
            else:
                sys.exit("Malformed template file " + file);

        if in_ != 'junk':
            tmpl[in_] += line

    if in_ != 'junk':
        sys.exit("Malformed template file " + file)

    fhead = tmpl['file-header']
    fprod = tmpl['file-production']
    ftail = tmpl['file-tail']
    eprod = tmpl['enumeration-production']
    vhead = tmpl['value-header']
    vprod = tmpl['value-production']
    vtail = tmpl['value-tail']
    comment_tmpl = tmpl['comment']

    # default to C-style comments
    if comment_tmpl == "":
        comment_tmpl = "/* @comment@ */"

if len(sys.argv) == 1:
    usage()

parser = argparse.ArgumentParser(epilog=help_epilog,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)

parser.add_argument('--identifier-prefix', default='', dest='idprefix',
                    help='Identifier prefix')
parser.add_argument('--symbol-prefix', default='', dest='symprefix',
                    help='symbol-prefix')
parser.add_argument('--fhead', default='', dest='fhead',
                    help='Output file header')
parser.add_argument('--ftail', default='', dest='ftail',
                    help='Per input file production')
parser.add_argument('--eprod', default='', dest='eprod',
                    help='Per enum text (produced prior to value iterations)')
parser.add_argument('--vhead', default='', dest='vhead',
                    help='Value header, produced before iterating over enum values')
parser.add_argument('--vprod', default='', dest='vprod',
                    help='Value text, produced for each enum value.')
parser.add_argument('--vtail', default='', dest='vtail',
                    help='Value tail, produced after iterating over enum values')
parser.add_argument('--comments', default='', dest='comment_tmpl',
                    help='Comment structure')
parser.add_argument('--template', default='', dest='template',
                    help='Template file')
parser.add_argument('--output', default=None, dest='output')
parser.add_argument('--version', '-v', default=False, action='store_true', dest='version',
                    help='Print version informations')
parser.add_argument('args', nargs='*')

options = parser.parse_args()

if options.version:
    print_version()

if options.template != '':
    read_template_file(options.template)

idprefix += options.idprefix
symprefix += options.symprefix
fhead += options.fhead
ftail += options.ftail
eprod += options.eprod
vhead += options.vhead
vprod += options.vprod
vtail += options.vtail
if options.comment_tmpl != '':
    comment_tmpl = options.comment_tmpl
output = options.output

if output is not None:
    (out_dir, out_fn) = os.path.split(options.output)
    out_suffix = '_' + os.path.splitext(out_fn)[1]
    if out_dir == '':
        out_dir = '.'
    tmpfile = tempfile.NamedTemporaryFile(dir=out_dir, delete=False)
    output_stream = tmpfile
else:
    tmpfile = None

# put auto-generation comment
comment = comment_tmpl.replace('@comment@', 'Generated data (by glib-mkenums)')
write_output("\n" + comment + '\n')

def replace_specials(prod):
    prod = prod.replace(r'\\a', r'\a')
    prod = prod.replace(r'\\b', r'\b')
    prod = prod.replace(r'\\t', r'\t')
    prod = prod.replace(r'\\n', r'\n')
    prod = prod.replace(r'\\f', r'\f')
    prod = prod.replace(r'\\r', r'\r')
    prod = prod.rstrip()
    return prod

if len(fhead) > 0:
    prod = fhead;
    base = os.path.basename(options.args[0])

    prod = prod.replace('@filename@', options.args[0])
    prod = prod.replace('@basename@', base)
    prod = replace_specials(prod)
    write_output(prod)

def process_file(curfilename):
    global entries, flags, seenbitshift, enum_prefix
    firstenum = True
    curfile = open(curfilename)
    for line in curfile:
        # read lines until we have no open comments
        while re.search(r'/\*([^*]|\*(?!/))*$', line):
            line = curfile.readline()

        # strip comments w/o options
        line = re.sub(r'''/\*(?!<)
           ([^*]+|\*(?!/))*
           \*/''', '', line)

        # ignore forward declarations
        if re.match(r'\s*typedef\s+enum.*;/;', line):
            continue

        m = re.match(r'''\s*typedef\s+enum\s*
               ({)?\s*
               (?:/\*<
                 (([^*]|\*(?!/))*)
                >\s*\*/)?
               \s*({)?''', line, flags=re.X)
        if m:
            groups = m.groups()
            if len(groups) >= 2 and groups[2] is not None:
                options = parse_trigraph(groups[2]);
                if 'skip' in options:
                    continue
                enum_prefix = options['prefix']
                flags = options['flags']
                option_lowercase_name = options['lowercase_name']
                option_underscore_name = options['underscore_name']
            else:
                enum_prefix = None
                flags = None
                option_lowercase_name = None
                option_underscore_name = None

            if option_lowercase_name is not None:
                if option_underscore_name is not None:
                    print("$0: $ARGV:$.: lowercase_name overriden with underscore_name", file=sys.stderr)
                    option_lowercase_name = None
                else:
                    print("$0: $ARGV:$.: lowercase_name is deprecated, use underscore_name", file=sys.stderr)

            # Didn't have trailing '{' look on next lines
            if groups[0] is None and (len(groups) < 5 or groups[4] is None):
                while True:
                    line = curfile.readline()
                    if re.match(r'\s*\{', line):
                        break

            seenbitshift = 0;
            entries = [];

            # Now parse the entries
            parse_entries(curfile, curfilename);

            # figure out if this was a flags or enums enumeration
            if flags is None:
                flags = seenbitshift

            # Autogenerate a prefix
            if enum_prefix is None:
                for entry in entries:
                    if len(entry) < 3 or entry[2] is None:
                        name = entry[0];
                        if enum_prefix is not None:
                            enum_prefix = os.path.commonprefix([name, enum_prefix])
                        else:
                            enum_prefix = name;
                if enum_prefix is None:
                    enum_prefix = ""
                else:
                    # Trim so that it ends in an underscore
                    enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix)
            else:
                # canonicalize user defined prefixes
                enum_prefix = enum_prefix.upper()
                enum_prefix = enum_prefix.replace('-', '_')
                enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_')

            fixed_entries = []
            for e in entries:
                name = e[0]
                num = e[1]
                if len(e) < 3 or e[2] is None:
                    nick = name = re.sub(r'^' + enum_prefix, '', name)
                    nick = nick.replace('_', '-').lower()
                    e = (name, num, nick)
                fixed_entries.append(e)
            entries = fixed_entries

            # Spit out the output
            if option_underscore_name is not None:
                enumlong = option_underscore_name.upper()
                enumsym = option_underscore_name.lower()
                enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong)

                enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong)
            elif symprefix == '' and idprefix == '':
                # enumname is e.g. GMatchType
                enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname)

                enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname)
                enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
                enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
                enumshort = enumshort.upper()

                enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper()

                enumlong = enspace.upper() + "_" + enumshort
                enumsym = enspace.lower() + "_" + enumshort.lower()

                if option_lowercase_name is not None:
                    enumsym = option_lowercase_name;
            else:
                enumshort = enumname;
                if idprefix:
                    enumshort = re.sub(r'^' + idprefix, '', enumshort)
                else:
                    enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort)

                enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
                enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
                enumshort = enumshort.upper();

                if symprefix is not None:
                    enumname_prefix = symprefix.upper()
                else:
                    enumname_prefix = idprefix.upper()

                enumlong = enumname_prefix + "_" + enumshort
                enumsym = enumlong.lower()

            if firstenum:
                firstenum = False

                if len(fprod) > 0:
                    prod = fprod;
                    base = os.path.basename(curfilename);

                    prod = prod.replace('@filename@', curfilename)
                    prod = prod.replace('@basename@', base)
                    prod = replace_specials(prod)

                    write_output(prod)

            if len(eprod) > 0:
                prod = eprod;

                prod = prod.replace('@enum_name@', enumsym)
                prod = prod.replace('@EnumName@', enumname)
                prod = prod.replace('@ENUMSHORT@', enumshort)
                prod = prod.replace('@ENUMNAME@', enumlong)
                prod = prod.replace('@ENUMPREFIX@', enumname_prefix)
                if flags:
                    prod =~ prod.replace('@type@', 'flags')
                else:
                    prod = prod.replace('@type@', 'enum')
                if flags:
                    prod = prod.replace('@Type@', 'Flags')
                else:
                    prod = prod.replace('@Type@', 'Enum')
                if flags:
                    prod = prod.replace('@TYPE@', 'FLAGS')
                else:
                    prod = prod.replace('@TYPE@', 'ENUM')
                prod = replace_specials(prod)
                write_output(prod)

            if len(vhead) > 0:
                prod = vhead;
                prod = prod.replace('@enum_name@', enumsym)
                prod = prod.replace('@EnumName@', enumname)
                prod = prod.replace('@ENUMSHORT@', enumshort)
                prod = prod.replace('@ENUMNAME@', enumlong)
                prod = prod.replace('@ENUMPREFIX@', enumname_prefix)
                if flags:
                    prod = prod.replace('@type@', 'flags')
                else:
                    prod = prod.replace('@type@', 'enum')
                if flags:
                    prod = prod.replace('@Type@', 'Flags')
                else:
                    prod = prod.replace('@Type@', 'Enum')
                if flags:
                    prod = prod.replace('@TYPE@', 'FLAGS')
                else:
                    prod = prod.replace('@TYPE@', 'ENUM')
                prod = replace_specials(prod)
                write_output(prod)

            if len(vprod) > 0:
                prod = vprod;
                next_num = 0

                prod = replace_specials(prod)
                for name, num, nick in entries:
                    tmp_prod = prod

                    if '@valuenum@' in prod:
                        # only attempt to eval the value if it is requested
                        # this prevents us from throwing errors otherwise
                        if num is not None:
                            # use sandboxed evaluation as a reasonable
                            # approximation to C constant folding
                            inum = eval(num, {}, {});

                            # make sure it parsed to an integer
                            if not isinstance(inum, int):
                                sys.exit("Unable to parse enum value '%s'" % num)
                            num = inum
                        else:
                            num = next_num

                        tmp_prod = tmp_prod.replace('@valuenum@', str(num))
                        next_num = int(num) + 1

                    tmp_prod = tmp_prod.replace('@VALUENAME@', name)
                    tmp_prod = tmp_prod.replace('@valuenick@', nick)
                    if flags:
                        tmp_prod = tmp_prod.replace('@type@', 'flags')
                    else:
                        tmp_prod = tmp_prod.replace('@type@', 'enum')
                    if flags:
                        tmp_prod = tmp_prod.replace('@Type@', 'Flags')
                    else:
                        tmp_prod = tmp_prod.replace('@Type@', 'Enum')
                    if flags:
                        tmp_prod = tmp_prod.replace('@TYPE@', 'FLAGS')
                    else:
                        tmp_prod = tmp_prod.replace('@TYPE@', 'ENUM')
                    tmp_prod = tmp_prod.rstrip()

                    write_output(tmp_prod)

            if len(vtail) > 0:
                prod = vtail
                prod = prod.replace('@enum_name@', enumsym)
                prod = prod.replace('@EnumName@', enumname)
                prod = prod.replace('@ENUMSHORT@', enumshort)
                prod = prod.replace('@ENUMNAME@', enumlong)
                prod = prod.replace('@ENUMPREFIX@', enumname_prefix)
                if flags:
                    prod = prod.replace('@type@', 'flags')
                else:
                    prod = prod.replace('@type@', 'enum')
                if flags:
                    prod = prod.replace('@Type@', 'Flags')
                else:
                    prod = prod.replace('@Type@', 'Enum')
                if flags:
                    prod = prod.replace('@TYPE@', 'FLAGS')
                else:
                    prod = prod.replace('@TYPE@', 'ENUM')
                prod = replace_specials(prod)
                write_output(prod)

for fname in options.args:
    process_file(fname)

if len(ftail) > 0:
    prod = ftail
    base = os.path.basename(options.args[-1]) # FIXME, wrong

    prod = prod.replace('@filename@', 'ARGV') # wrong too
    prod = prod.replace('@basename@', base)
    prod = replace_specials(prod)
    write_output(prod)

# put auto-generation comment
comment = comment_tmpl;
comment = comment.replace('@comment@', 'Generated data ends here')
write_output("\n" + comment + "\n")

if tmpfile is not None:
    tmpfilename = tmpfile.name
    tmpfile.close()
    os.unlink(options.output)
    os.rename(tmpfilename, options.output)
