#! /usr/bin/python

import os
import sys
import stat
import re
import errno

FILE_NOT_THERE=1
FILE_REGULAR=2
FILE_DIRECTORY=3
FILE_SYMLINK=4

## return tuple of (kind, mtime, size), or (FILE_NOT_THERE,0,0) if file not found
def stat_file(filename):
    try:
        s = os.stat(filename)
        kind = 0
        if stat.S_ISDIR(s.st_mode):
            kind = FILE_DIRECTORY
        elif stat.S_ISLNK(s.st_mode):
            kind = FILE_SYMLINK
        elif stat.S_ISREG(s.st_mode):
            kind = FILE_REGULAR
        else:
            raise Exception("Unknown file type (not a directory, regular, or link) %s" % filename)

        return (kind, s.st_mtime, s.st_size)
    except OSError, e:
        if e.errno == errno.ENOENT:
            return (FILE_NOT_THERE, 0, 0)
        else:
            raise

test_func_re = re.compile('gjstest_test_func_([a-zA-Z0-9_]+)')

def find_tests(base_filename, full_filename):
    f = open(full_filename)
    test_funcs = [] ## list of (path, funcname)

    found_expected_test_func = False
    in_tests = False
    for l in f.readlines():
        if 'GJS_BUILD_TESTS' in l:
            if '#if' in l:
                if in_tests:
                    raise Exception('GJS_BUILD_TESTS nested inside itself? ' + base_filename)
                else:
                    in_tests = True
            elif '#endif' in l:
                # GJS_BUILD_TESTS should have been in a comment post-endif
                if not in_tests:
                    raise Exception('#endif /* GJS_BUILD_TESTS */ found but no #if in ' + base_filename)
                else:
                    in_tests = False

        ## do the substring check before re match so we can plow
        ## through the file quickly and only do the more expensive
        ## match on relevant lines
        elif 'gjstest_test_func_' in l:
            match = test_func_re.search(l)
            if not match:
                raise Exception('line does not match test_func_re in ' + base_filename + ': ' + l)
            subname = match.group(1)
            funcname = 'gjstest_test_func_' + subname

            if not in_tests:
                raise Exception("Test func %s in %s not inside GJS_BUILD_TESTS" % (funcname, base_filename))

            ## check namespacing
            expected_subname = ''
            rest = base_filename
            while rest != '':
                (rest, last) = os.path.split(rest)
                if last != '':
                    last = last.replace('-', '_')
                    if expected_subname != '':
                        expected_subname = last + '_' + expected_subname
                    else:
                        (last, ext) = os.path.splitext(last)
                        expected_subname = last

            if not subname.startswith(expected_subname):
                raise Exception("Test funcs in '%s' should start with gjstest_test_func_%s" % (base_filename, expected_subname))

            test_path = '/' + subname.replace('_', '/')

            test_funcs.append((test_path, funcname))

    if in_tests:
        raise Exception('no #endif /* GJS_BUILD_TESTS */ found - comment with GJS_BUILD_TESTS in it is mandatory')

    ## return a tuple, so we can add other kinds of stuff to find later
    return (test_funcs)

output_dir = sys.argv[1]
input_files = sys.argv[2:]
top_srcdir = os.getenv('abs_top_srcdir')

out_header_name = os.path.join(output_dir, "gjstest.h")
out_impl_name = os.path.join(output_dir, "gjstest.c")

(out_header_kind, out_header_mtime, out_header_size) = stat_file(out_header_name)
(out_impl_kind, out_impl_mtime, out_impl_size) = stat_file(out_impl_name)

out_header_tmp_name = out_header_name + ".stamp"
out_impl_tmp_name = out_impl_name + ".stamp"

all_test_funcs = []

for filename in input_files:
    (test_funcs) = find_tests(filename, os.path.join(top_srcdir, filename))
    all_test_funcs = all_test_funcs + test_funcs

header_out = open(out_header_tmp_name, 'w')
impl_out = open(out_impl_tmp_name, 'w')

def write_generic_c_boilerplate(f):
    f.write('/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */\n')
    f.write('/* FILE AUTOGENERATED DO NOT EDIT */\n')
    f.write('\n')

write_generic_c_boilerplate(header_out)
write_generic_c_boilerplate(impl_out)

header_out.write('#ifndef __GJS_GJSTEST_GENERATED_H__\n')
header_out.write('#define __GJS_GJSTEST_GENERATED_H__\n\n')
header_out.write('#include <glib.h>\n\n')
header_out.write('G_BEGIN_DECLS\n\n')

header_out.write('void gjstest_add_all_tests(void);\n\n')

impl_out.write('#include <gjstest.h>\n\n')

impl_out.write('void\n')
impl_out.write('gjstest_add_all_tests(void)\n')
impl_out.write('{\n')

for (path, func) in all_test_funcs:
    impl_out.write('    g_test_add_func("%s", %s);\n' % (path, func))

    header_out.write('void %s(void);\n' % (func))

impl_out.write('}\n')

header_out.write('\n')
header_out.write('G_END_DECLS\n\n')
header_out.write('#endif  /* __GJS_GJSTEST_GENERATED_H__ */\n')

## close so we can stat and rename
impl_out.close()
header_out.close()

## rename only if changed, to avoid needless rebuilds.
## we use the size to decide if it changed... not really
## quite kosher, but will fail infrequently enough to
## not be annoying
(new_header_kind, new_header_mtime, new_header_size) = stat_file(out_header_tmp_name)
(new_impl_kind, new_impl_mtime, new_impl_size) = stat_file(out_impl_tmp_name)

if new_header_size != out_header_size or new_impl_size != out_impl_size:
    print "Replacing old %s and %s" % (out_header_name, out_impl_name)
    print "   %s  was %d bytes now %d" % (out_header_name, out_header_size, new_header_size)
    print "   %s  was %d bytes now %d" % (out_impl_name, out_impl_size, new_impl_size)
    os.rename(out_header_tmp_name, out_header_name)
    os.rename(out_impl_tmp_name, out_impl_name)
else:
    print "%s and %s appear to be unchanged, not updating" % (out_header_name, out_impl_name)
