#!/usr/bin/env python
import os, sys, re, subprocess, optparse
from distutils.spawn import find_executable

# ctags regexes
c_module = r'/^[ \t]*module[ \t]+([_a-zA-Z0-9$]+)/\1/'
c_enum   = r'/^[ \t]*enum[ \t]+([_a-zA-Z0-9$]+)/\1/'
c_class  = r'/^[ \t]*(extern[ \t]+)?class[ \t]+([_a-zA-Z0-9$]+)/\2/'
c_record = r'/^[ \t]*(extern[ \t]+)?record[ \t]+([_a-zA-Z0-9$]+)/\2/'
c_func   = r'/^[ \t]*((export [_a-zA-Z0-9$]+|inline|extern)[ \t]+)?(proc|iter)[ \t]+((param|ref)[ \t]+)?([_a-zA-Z0-9]+\.)?([_a-zA-Z0-9$]+)/\7/'

ctags_opts = [
    '--langdef=Chapel',
    '--langmap=Chapel:.chpl',
    '--regex-chapel='+c_module+'m,modules,module names/',
    '--regex-chapel='+c_enum+'g,enums,enumeration names/',
    '--regex-chapel='+c_class+'c,classes,class definitions/',
    '--regex-chapel='+c_record+'r,records,record definitions/',
    '--regex-chapel='+c_func+'f,functions,function definitions/',
]

# etags regexes use escaped () and | for capturing groups :(
# same as above otherwise
e_module = r'/^[ \t]*module[ \t]+\([_a-zA-Z0-9$]+\)/\1/'
e_enum   = r'/^[ \t]*enum[ \t]+\([_a-zA-Z0-9$]+\)/\1/'
e_class  = r'/^[ \t]*\(extern[ \t]+\)?class[ \t]+\([_a-zA-Z0-9$]+\)/\2/'
e_record = r'/^[ \t]*\(extern[ \t]+\)?record[ \t]+\([_a-zA-Z0-9$]+\)/\2/'
e_func   = r'/^[ \t]*\(\(export [_a-zA-Z0-9$]+\|inline\|extern\)[ \t]+\)?\(proc\|iter\)[ \t]+\(\(param\|ref\)[ \t]+\)?\([_a-zA-Z0-9]+\.\)?\([_a-zA-Z0-9$]+\)/\7/'

etags_opts = [
    '-o', 'TAGS',
    '--language=none',
    '--regex='+e_module,
    '--regex='+e_enum,
    '--regex='+e_class,
    '--regex='+e_record,
    '--regex='+e_func,
]


def check_ex_ctags():
    if not find_executable('ctags'):
        return False

    process = subprocess.Popen(['ctags', '--version'],
                                stdout=subprocess.PIPE)
    out, err = process.communicate()
    if sys.version_info[0] >= 3 and not isinstance(out, str):
        out = str(out, 'utf-8')

    return bool(out and ('Exuberant Ctags' in out) and ('+regex' in out))


def collect_chpl_files(file_list, recurse):
    result = []
    for x in file_list:
        if os.path.isfile(x) and os.path.splitext(x)[1] == '.chpl':
            result.append(x)
        elif recurse:
            for root, dirs, files in os.walk(x):
                for f in files:
                    if os.path.splitext(f)[1] == '.chpl':
                        result.append(os.path.join(root, f))
                # skip hidden
                dirs[:] = [d for d in dirs if d[0] != '.']
    # strip off any leading "./"s from the path to work around a bug in ctags
    # (which was fixed ~5 years ago) that results in mangled output
    return [p[2:] if p.startswith("./") else p for p in result]


def _main():
    parser = optparse.OptionParser(usage="usage: %prog {-e | -v} [-r] [DIR | FILE...]",
        epilog="If neither -e or -v are set, a TAGS file will be generated by "
               "default. If CHPL_EDITOR or EDITOR are set to vi(m) a tags file "
               "will be created instead.")
    parser.add_option('-e', '--emacs', dest='emacs', action='store_true',
                      default=False,
                      help="Generate a TAGS file.")
    parser.add_option('-v', '--vi', dest='vi', action='store_true',
                      default=False,
                      help="Generate a tags file.")
    parser.add_option('-r', '--recurse', dest='recurse', action='store_true',
                      default=False,
                      help="Recurse into directories provided to find .chpl files.")
    (options, args) = parser.parse_args()

    if options.vi and options.emacs:
        sys.stderr.write("chpltags: Can't specify both -e and -v\n")
        sys.exit(1)

    if not (options.vi or options.emacs):
        editor = os.environ.get('CHPL_EDITOR', '')
        if not editor:
            editor = os.environ.get('EDITOR', '')
        if editor.startswith('vi'):
            options.vi = True
        else:
            options.emacs = True

    files = collect_chpl_files(args, options.recurse)
    if not files:
        sys.stderr.write("chpltags: No files specified.\n")
        sys.exit(1)

    have_ex_ctags = check_ex_ctags()

    global ctags_opts, etags_opts
    if have_ex_ctags:
        if options.emacs:
            ctags_opts = ['-e'] + ctags_opts
        process = subprocess.Popen(['ctags'] + ctags_opts + files,
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE)
        process.communicate()
        sys.exit(process.returncode)
    elif options.emacs:
        if not find_executable('etags'):
            sys.stderr.write("chpltags: Must have either Exuberant Ctags "
                             "with +regex or etags installed to create "
                             "Chapel tags.\n")
            sys.exit(1)
        process = subprocess.Popen(['etags'] + etags_opts + files,
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE)
        process.communicate()
        sys.exit(process.returncode)
    elif options.vi:
        sys.stderr.write("chpltags: Must have Exuberant Ctags installed with "
                         "+regex to create Chapel tags.\n")
        sys.exit(1)


if __name__ == '__main__':
    _main()
