#!/bin/sh
# Python file written with a shell helper (mainly for windows).
# vim: set syntax=python ts=4 sts=4 sw=4 expandtab:
__SHELL_PROTECT=""" "
SCRIPT="$0"

for f in c:/Python3*
do
    [ -f "$f/python.exe" ] && PYTHON="$f/python.exe" && break
done > /dev/null 2>&1

[ -z "$PYTHON" ] && PYTHON=`which python3`
[ -z "$PYTHON" ] && PYTHON=`which python`

if [ -z "$PYTHON" ]
then
	echo "Cannot find python executable"
	exit 1
fi

export CONFIGURE_ROOT_DIR="`dirname $SCRIPT`"

"$PYTHON" "$0" "$@"

exit $?

__SHELL_PROTECT=" """
import os, sys

try:
    if sys.version_info[0] != 3:
        raise Exception()
except:
    sys.stderr.write("Invalid python version\n")
    sys.stderr.flush()
    sys.exit(1)

try:
    import coverage
    coverage.process_startup()
except:
    pass

import argparse
import os
import pipes
import re
import shutil
import stat
import subprocess
import sys
import cgitb

def cleanpath(p, **kwargs):
    p = os.path.normpath(p)
    if p.startswith('./'):
        p =  p[2:]
    if kwargs.get('replace_home'):
        p = os.path.join(
            '~',
            os.path.relpath(p, start=os.path.expanduser('~')),
        )
    return p.replace('\\', '/')

def cleanabspath(p, **kwargs):
    return cleanpath(os.path.abspath(p), **kwargs)

def cleanjoin(*args, **kwargs):
    return cleanpath(os.path.join(*args), **kwargs)

FATAL = "[ cfg ] FATAL"
ERROR = "[ cfg ] ERROR"
STATUS = "[ cfg ]"

def err(*args, **kwargs):
    kwargs['file'] = sys.stderr
    return print(*args, **kwargs)

def status(*args, **kwargs):
    return print(STATUS, *args, **kwargs)

def error(*args, **kwargs):
    return err(ERROR, *args, **kwargs)

def fatal(*args, **kwargs):
    try:
        err(FATAL, *args, **kwargs)
    finally:
        sys.exit(1)

def which(binary):
    paths = os.environ['PATH'].split(os.path.pathsep)
    for dir_ in paths:
        path = os.path.join(dir_, binary)
        if os.path.exists(path) and os.stat(path)[stat.ST_MODE] & stat.S_IXUSR:
            return path
    if sys.platform =='win32' and not binary.lower().endswith('.exe'):
        return which(binary + '.exe')
    return None

def cmd(cmd, stdin = b'', cwd = None, env=None):
    sys.stderr.flush()
    p = subprocess.Popen(cmd,
                         cwd = cwd,
                         stdin = subprocess.PIPE,
                         shell = False,
                         env = env)
    p.stdin.write(stdin)
    p.stdin.close()
    p.wait()
    if p.returncode != 0:
        raise Exception("Command failed")

DEBUG = True
VERBOSE = True
ROOT_DIR = None
HOME_URL = "http://hotgloupi.fr/configure.py.html"
TUP_HOME_URL = "http://gittup.org"
PROJECT_CONFIG_DIR_NAME = ".config"
PROJECT_CONFIG_FILENAME = "project.py"
TUP_INSTALL_DIR = None
TUP_GIT_URL = "git://github.com/gittup/tup.git"
TUP_WINDOWS_URL = "http://gittup.org/tup/win32/tup-latest.zip"
CONFIGURE_PY_GIT_URL = "git://github.com/hotgloupi/configure.py"
CONFIGURE_PY_GENERATORS = ['Tup', 'Makefile']

def self_install(project_config_dir, args):
    configure_py_install_dir = cleanjoin(project_config_dir, 'configure.py')
    status("Installing configure.py in", configure_py_install_dir)
    if not os.path.exists(configure_py_install_dir):
        os.makedirs(configure_py_install_dir)
        status("Cloning configure.py from", CONFIGURE_PY_GIT_URL)
        cmd(['git', 'clone', CONFIGURE_PY_GIT_URL, configure_py_install_dir])
    else:
        status("Updating configure.py")
        cmd(['git', 'pull'], cwd = configure_py_install_dir)

def tup_install(root_dir, install_dir):
    from configure import platform
    if platform.IS_WINDOWS:
        tup_install_windows(install_dir)
    else:
        tup_install_git(root_dir, install_dir)
    from configure import tools
    print("Tup installed in", tools.which('tup'))


def tup_install_windows(dir):
    import urllib.request as r
    req = r.urlopen(TUP_WINDOWS_URL)
    if not os.path.exists(dir):
        os.makedirs(dir)
    tarball = os.path.join(dir, 'tup.zip')
    with open(tarball, 'wb') as f:
        while True:
            data = req.read(4096)
            if not data:
                break
            f.write(data)
    import zipfile
    with zipfile.ZipFile(tarball) as f:
        f.extractall(dir)


def tup_install_git(root_dir, dir):
    from configure import path
    status("Installing tup in", dir)
    if not path.exists(dir):
        os.makedirs(dir)
        status("Getting tup from", TUP_GIT_URL)
        cmd(['git', 'clone', TUP_GIT_URL, dir])
    else:
        status("Updating tup")
        cmd(['git', 'pull'], cwd = dir)

    tup_shell_bin = path.join(dir, "build", "tup")
    if not path.exists(dir, "build", "tup"):
        cmd(['sh', 'build.sh'], cwd=dir)
    else:
        status("Found shell version of tup at", tup_shell_bin)

    tup_dir = path.join(root_dir, '.tup')
    if os.path.exists(tup_dir):
        os.rename(tup_dir, tup_dir + '.bak')

    try:
        if not path.exists(dir, '.tup'):
            cmd(['./build/tup', 'init'], cwd=dir)
        cmd(['./build/tup', 'upd'], cwd=dir)
    finally:
        if path.exists(tup_dir + '.bak'):
            os.rename(tup_dir + '.bak', tup_dir)


def prepare_build(args, defines, exports, root_dir, project_config_dir):
    import configure # Should work at this point
    from configure.path import exists, join, absolute

    project = configure.Project(
        root_dir,
        project_config_dir,
        config_filename = PROJECT_CONFIG_FILENAME,
        new_project_env = exports,
    )

    env_build_dirs = list(
        d for d in project.env.get('BUILD_DIRECTORIES', [])
        if exists(d, '.configure.py_build')
    )
    configure.tools.debug("Found env build directories:", ' '.join(env_build_dirs))
    build_dirs = env_build_dirs
    if args.build_dir is not None:
        for build_dir in args.build_dir:
            tup_build_marker = join(build_dir, '.configure.py_build')
            if not exists(build_dir):
                os.makedirs(build_dir)
                with open(tup_build_marker, 'w') as f:
                    pass
            elif not exists(tup_build_marker):
                fatal('\n'.join([
                    "'%(build_dir)s' doest not seem to be a tup build directory:",
                    "\t* Remove this directory",
                    "\t* Touch the file %(tup_build_marker)s",
                ]) % locals())
            build_dirs.append(build_dir)
    else:
        build_dirs = env_build_dirs

    if args.dump_vars:
        status("Project variables:")
        for k, v in project.env.items():
            status("\t - %s = %s" % (k, v))

    if not build_dirs:
        fatal("No build directory specified on command line. (try -h switch)")
    configure.tools.verbose("Selected build directories:", build_dirs)

    project.env.build_directories = list(set(build_dirs + env_build_dirs))

    with project:
        for build_dir in build_dirs:
            with project.configure(build_dir, defines, args.generator) as build:
                if args.dump_vars:
                    status("Build variables for directory '%s':" % build_dir)
                    keys = sorted(build.env.keys())
                    for k in keys:
                        status("\t - %s = %s" % (k, build.env[k]))
                    continue

                if args.dump_build:
                    build.dump()
                else:
                    build.generate()

def parse_args():
    class StoreBuildDirsAndDefines(argparse.Action):
        def __call__(self, parser, ns, values, option_string = None):
            for attr in ['build_variable', 'build_dir']:
                if not hasattr(ns, attr) or getattr(ns, attr) is None:
                    setattr(ns, attr, [])
            for i, v in enumerate(values):
                if '=' in v:
                    getattr(ns, 'build_variable').append(v)
                else:
                    getattr(ns, 'build_dir').append(v)

    parser = argparse.ArgumentParser(
        description = "Configure your project's builds"
    )
    parser.add_argument(
        '-i', '--init',
        action = 'store',
        help = 'Initialize the given directory as a configure.py project (default to current directory)',
        default = False,
        nargs = '?',
    )
    parser.add_argument(
        'build_dir',
        action = StoreBuildDirsAndDefines,
        help = "Build directories to configure",
        nargs = '*',
    )
    parser.add_argument(
        'build_variable',
        action = StoreBuildDirsAndDefines,
        help = "Build specific variables",
        nargs = '*',
    )
    parser.add_argument(
        '-D', '--define',
        dest = 'project_variable',
        action = 'append',
        help = "Project variables",
    )
    parser.add_argument(
        '-U', '--undef',
        action = 'append',
        help = "Remove a variable from selected builds and from the project",
        default = [],
    )
    parser.add_argument(
        '-v', '--verbose',
        action = 'store_true',
        help = "verbose mode"
    )
    parser.add_argument(
        '-d', '--debug',
        action = 'store_true',
        help = "debug mode"
    )
    parser.add_argument(
        '--dump-vars',
        action = 'store_true',
        help = "dump variables"
    )
    parser.add_argument(
        '--dump-build',
        action = 'store_true',
        help = "dump commands that would be executed"
    )
    parser.add_argument(
        '--install',
        action = 'store_true',
        help = "install when needed"
    )
    parser.add_argument(
        '--self-install',
        action = 'store_true',
        help = "install (or update) configure.py"
    )
    parser.add_argument(
        '--tup-install',
        action = 'store_true',
        help = "install (or update) tup"
    )
    parser.add_argument(
        '--generator', '-G',
        default = None,
        help = "Generate build rules for another build system",
        choices = CONFIGURE_PY_GENERATORS
    )
    return parser, parser.parse_args()

def parse_cmdline_variables(args):
    res = {}
    if not args:
        return res
    for arg in args:
        arg = arg.strip()
        if '=' not in arg:
            fatal("'=' not found in define: use %s=true to define a boolean variable" % arg)
        parts = arg.split('=')
        k = parts[0].strip()
        v = '='.join(parts[1:]).strip()
        op = '='
        for p in ['+', ':']:
            if k.endswith(p):
                op = p + '='
                k = k[:-1]
                break
        if v.lower() in ['1', 'true']:
            v = True
        elif v.lower() in ['0', 'false']:
            v = False
        elif v.startswith('['):
            if not v.endswith(']'):
                fatal("Missing ']' when defining %s" % k)
            v = [e.strip() for e in v[1:-1].split(',')]
        res[k] = v
        #{
        #    'op': op,
        #    'value': v,
        #}
    return res


def is_root_dir(dir):
    """True when dir is a configure.py project root directory."""
    return os.path.isfile(
        os.path.join(dir, PROJECT_CONFIG_DIR_NAME, PROJECT_CONFIG_FILENAME)
    )

def find_root_dir(args):
    """Retreive the project root directory.

    if the `init` flag has been given, the we use the specified directory.
    Otherwise we check first from the current directory to the upmost
    directory. Finally, we check in the configure script location.
    """
    if args.init is not False:
        if args.init is None:
            return os.getcwd()
        return args.init

    dir = os.path.abspath(os.getcwd())
    while True:
        if is_root_dir(dir):
            return dir
        up = os.path.abspath(os.path.join(dir, '..'))
        if up == dir:
            break
        dir = up

    dir = os.path.abspath(os.path.dirname(__file__))
    if is_root_dir(dir):
        return dir

def main():
    from os.path import exists, join, abspath, dirname
    parser, args = parse_args()
    DEBUG = args.debug
    VERBOSE = args.verbose
    root_dir = find_root_dir(args)
    if root_dir is None:
        fatal(
            '\n'.join([
                "No configure.py project found, you can:",
                "\t* Move to the project's directory",
                "\t* Create a new project with --init",
            ])
        )
    project_config_dir = cleanjoin(root_dir, PROJECT_CONFIG_DIR_NAME)
    tup_install_dir = cleanjoin(project_config_dir, 'tup')

    if DEBUG:
        status("root directory set to", root_dir)
        cgitb.enable(format = 'text')


    # Add this one to use with configure with configure.py
    sys.path.insert(0, join(abspath(dirname(__file__)), '..', 'src'))

    # In case we were auto-installed
    sys.path.insert(0, join(project_config_dir,'configure.py/src'))

    # XXX This one should be removed (Allowing imports in project files ?)
    sys.path.insert(0, project_config_dir)

    have_configure = False
    try:
        import configure
        have_configure = True
    except ImportError: pass

    if args.self_install or (args.install and not have_configure):
        self_install(project_config_dir, args)
        try:
            import imp
            file_, pathname, descr = imp.find_module("configure", [join(project_config_dir,'configure.py/src')])
            configure = imp.load_module("configure", file_, pathname, descr)
        except Exception as e:
            fatal("Sorry, configure installation failed for some reason:", e)

    try:
        import configure

        # configure.py will use these functions to log
        configure.tools.status = status
        configure.tools.error = error
        configure.tools.fatal = fatal

        configure.tools.DEBUG = DEBUG
        configure.tools.VERBOSE = VERBOSE or DEBUG
    except ImportError as e:
        if DEBUG is True:
            raise e

        fatal(
            '\n'.join([
                "Cannot find configure.py module, your options are:",
                "\t* Just use the --self-install flag (installed in %(config_dir)s/configure.py)",
                "\t* Add it as a submodule: `git submodule add %(git_url)s %(config_dir)s/configure.py`",
                "\t* Install it somewhere (see %(home_url)s)",
            ]) % {
                'config_dir': cleanabspath(project_config_dir, replace_home=True),
                'home_url': HOME_URL,
                'git_url': CONFIGURE_PY_GIT_URL,
            }
        )

    configure.tools.PATH.insert(0, configure.path.absolute(tup_install_dir))

    if args.tup_install or (args.install and not configure.tools.which('tup')):
        tup_install(root_dir, tup_install_dir)

    if 'Tup' == args.generator and not configure.tools.which('tup'):
        fatal(
            '\n'.join([
                "Cannot find tup binary, your options are:",
                "\t* Just use the --tup-install flag (installed in %(config_dir)s/tup)",
                "\t* Install it somewhere (see %(home_url)s)",
            ]) % {
                'config_dir': cleanabspath(project_config_dir, replace_home=True),
                'home_url': TUP_HOME_URL
            }
        )

    try:
        defines = parse_cmdline_variables(args.build_variable)
        exports = parse_cmdline_variables(args.project_variable)

        if args.init is not False:
            configure.Project.initialize(
                directory = root_dir,
                config_directory = PROJECT_CONFIG_DIR_NAME,
                config_filename = PROJECT_CONFIG_FILENAME
            )
            status('Project successfully initialized')
            status(
                "Please edit %s and re-run the configure script" % configure.path.join(
                    project_config_dir,
                    PROJECT_CONFIG_FILENAME,
                    replace_home = True,
                )
            )
        else:
            prepare_build(args, defines, exports, root_dir, project_config_dir)

    except configure.VariableNotFound as e:
        fatal('\n'.join([
            "Couldn't find any variable '%s', do one of the following:" % e.name,
            "\t* Export it with: `%s=something ./configure`" % e.name,
            "\t* Define it with: `./configure -D%s=something`" % e.name,
            "\t* Define it in your project config file (%s)" % cleanjoin(project_config_dir, PROJECT_CONFIG_FILENAME),
        ]))


if __name__ == '__main__':
    main()
