#!/usr/bin/env python

import ConfigParser
import argparse
import os
import subprocess
import string
import sys
import urllib2
import warnings
import zipfile

root_path = os.path.abspath('.')
default_ini_file_path = root_path + '/cppdock.ini'

def parse_args_command(args):
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('command',      choices   = [
                                                        'init',
                                                        'build',
                                                        'install_dep',
                                                    ])
    return parser.parse_args(args)

def parse_args_init(args):
    parser = argparse.ArgumentParser(prog='cppdock init')
    parser.add_argument('-f', '--file', dest      = 'ini_file_path',
                                        default   = default_ini_file_path,
                                        help      = 'Specify path to ini file')
    return parser.parse_args(args)

def parse_args_build(args):
    parser = argparse.ArgumentParser(prog='cppdock build')
    parser.add_argument('-f', '--file', dest      = 'ini_file_path',
                                        default   = default_ini_file_path,
                                        help      = 'Specify path to ini file')

    parser.add_argument('--print-only', dest      = 'print_only',
                                        action    = 'store_true',
                                        help      = 'Print Dockerfile without building')

    group  = parser.add_argument_group('required arguments')
    group.add_argument('platform',      help      = 'Token for target platform')

    return parser.parse_args(args)

def parse_args_install_dep(args):
    parser = argparse.ArgumentParser(prog='cppdock install_dep')
    group  = parser.add_argument_group('required arguments')
    group.add_argument('platform',      help      = 'Token for target platform')
    group.add_argument('branch',        help      = 'SHA1 of git revision')
    group.add_argument('repo',          help      = 'Name of git repo (e.g. boostorg/hana)')
    return parser.parse_args(args)

def dispatch(command_args, cli_args):
    command = parse_args_command(command_args).command
    commands = {
        None:           lambda: None,
        'init':         (command_init,          parse_args_init),
        'build':        (command_build,         parse_args_build),
        'install_dep':  (command_install_dep,   parse_args_install_dep),
    }
    if command in commands:
        args = commands[command][1](cli_args)
        commands[command][0](args)

def command_init(args):
    load_config_file_with_args(args)

def command_build(args):
    config = load_config_file_with_args(args)
    #dockerfile = make_deps_dockerfile(config, args.platform)
    dockerfile = make_deps_dockerfile_stageless(config, args.platform)
    if args.print_only:
        print dockerfile
    else:
        make_dep_image(config, args.platform, dockerfile)

def command_install_dep(args):
    check_sysroot()
    url = make_archive_url(args.repo, args.branch)
    input = urllib2.urlopen(url)
    output = open('dep.zip', 'wr')
    buf = ''
    while True:
        buf = input.read(800)
        output.write(buf)
        output.flush()
        if (len(buf) == 0):
            break
    output.close()

    assert os.path.isfile('dep.zip')
    assert zipfile.is_zipfile('dep.zip')

    zipfile.ZipFile('dep.zip', 'r').extractall()
    recipe = find_recipe(args.platform, args.repo)
    cwd_dir = [x for x in os.listdir('.') if x.endswith(args.branch)][0]
    p = subprocess.Popen([recipe], cwd = cwd_dir)
    p.communicate('')
    if p.returncode != 0:
        print """

    FAILURE: Recipe returned error code {0}.
        """.format(p.returncode)
        sys.exit(1)

def install_deps_platform(platform, items):
    for i in range(len(items)):
        repo, branch = items[i]
        branch = branch[0:40]
        install_dep(i, platform, repo, branch)

def load_config_file_with_args(args):
    ini_file_path = args.ini_file_path
    return load_config_file(ini_file_path)

def load_config_file(ini_file_path):
    if not os.path.isfile(ini_file_path):
        print ini_file_path
        raise ValueError('Config file not found.')
    config = PrettyConfigParser()
    config.read(ini_file_path)
    for section in config.sections():
        if section == 'cppdock':
            continue
        for item in config.items(section):
            lock_item(config, section, item)
    write_config_file(config, ini_file_path)
    return config

def write_config_file(config, ini_file_path):
    try:
        config.write(open(ini_file_path, 'wb'))
    except:
        warnings.warn('WARNING: Unable to write to ini file')

def lock_item(config, section, item):
    repo, branch = item
    config.set(section, repo, get_current_sha_with_comment(item))

def get_current_sha_with_comment(item):
    repo, branch = item
    if len(branch) >= 40:
        return branch
    else:
        branch = normalize_branch_name(branch)
        lines = subprocess.check_output(['git', 'ls-remote', normalize_repo_name(repo)]).splitlines()
        for line in lines:
            if line.endswith(branch):
                return line[0:40] + ' # ' + (item[1] or 'HEAD')
        raise ValueError("Unable to get SHA from remote git repository")

def normalize_repo_name(name):
    return "https://github.com/" + name;

def normalize_branch_name(name):
    if len(name) == 0 or name == 'HEAD':
        return 'HEAD'
    elif name.startswith('refs/'):
        return name
    else:
        return "refs/heads/" + name

def is_sha(name):
    return len(name) == 40

class PrettyConfigParser(ConfigParser.RawConfigParser):
    def write(self, fp):
        max_key_length = get_max_key_length(self)
        if self._defaults:
            fp.write("[%s]\n" % DEFAULTSECT)
            for (key, value) in self._defaults.items():
                fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
            fp.write("\n")
        for section in self._sections:
            fp.write("[%s]\n" % section)
            for (key, value) in self._sections[section].items():
                if key == "__name__":
                    continue
                if (value is not None) or (self._optcre == self.OPTCRE):
                    equal_left_padding = ' ' * (max_key_length - len(key))
                    key = " = ".join((key + equal_left_padding, str(value).replace('\n', '\n\t')))

                fp.write("%s\n" % (key))
            fp.write("\n")

def get_max_key_length(config):
    length = 0
    for section in config.sections():
        for item in config.items(section):
            len_ = len(item[0])
            if (len_ > length):
                length = len_
    return length

def make_archive_url(repo, branch):
    return 'https://github.com/{0}/archive/{1}.zip'.format(repo, branch)

def find_recipe(platform, repo):
    repo = repo.replace('/', '-')
    local_recipe_path = '/opt/cppdock_recipes'
    builtin_recipe_path = os.path.expanduser('~') + '/.cppdock_recipes'
    repo_with_platform = repo + '-' + platform
    xs = [
        local_recipe_path   + '/' + repo_with_platform,
        local_recipe_path   + '/' + repo,
        local_recipe_path   + '/default',
        builtin_recipe_path + '/' + repo_with_platform,
        builtin_recipe_path + '/' + repo,
        builtin_recipe_path + '/default',
    ]
    for x in xs:
        if os.path.isfile(x):
            return x
    raise ValueError('Unable to find cppdock recipe: ' + repo_with_platform)

def check_sysroot():
    if not os.path.isdir('/opt/sysroot/'):
        raise ValueError('Sysroot directory is missing: /opt/sysroot/')

def get_arg(args, i):
    next(iter(args[i:i+1]), None)

def get_config_option(config, name):
    defaults = {
        'project': None,
        'compiler_linux_x64':     'ricejasonf/emscripten_fastcomp:1.37.19',
        'compiler_emscripten':    'ricejasonf/emscripten_fastcomp:1.37.19',
        'compiler_tvossimulator': 'ricejasonf/emscripten_fastcomp:1.37.19',
        'platform_linux_x64':     'ricejasonf/cppdock:linux_x64',
        'platform_emscripten':    'ricejasonf/cppdock:emscripten',
        'platform_tvossimulator': 'ricejasonf/cppdock:tvossimulator',
    }
    value = None
    try:
        value = config.get('cppdock', name)
    except:
        if name in defaults:
            value = defaults[name]
    if value == None:
        raise ValueError('Config option has no default for "{0}"'.format(name))
    return value

def make_dep_image(config, platform, dockerfile):
    tag_name = "{0}_build:{1}".format(
        get_config_option(config, 'project'),
        platform
    )
    p = subprocess.Popen(['docker', 'build', '--tag=' + tag_name, '--file=-', '.'], stdin=subprocess.PIPE)
    out, err = p.communicate(dockerfile)
    if p.returncode == 0:
        print """

    Finished building {0}.
        """.format(tag_name)
    else:
        print """

    FAILURE: Build of {0} FAILED.
        """.format(tag_name)
        sys.exit(1)

def make_deps_dockerfile(config, platform):
    cppdock_image  = get_config_option(config, 'platform_' + platform)
    deps = get_deps_from_config(config, platform)
    deps_stages = string.join([make_deps_stage(cppdock_image, x, y, z) for x, y, z in deps])
    deps_imports = string.join([make_deps_import(x, y) for x, y, z in deps])

    return """
{0}
FROM {1}
{2}
""".format(deps_stages, cppdock_image, deps_imports)

def make_deps_dockerfile_stageless(config, platform):
    cppdock_image  = get_config_option(config, 'platform_' + platform)
    deps = get_deps_from_config(config, platform)
    deps_stages = string.join([make_install_dep(x, y, z) for x, y, z in deps])

    return """
FROM {0} as build_all
{1}

FROM {2}

    COPY --from=build_all /opt/install /opt/sysroot
""".format(cppdock_image, deps_stages, cppdock_image)

def make_deps_stage(cppdock_image, platform, branch, repo):
    copy_recipes_term = ""
    if os.path.isdir(root_path + '/cppdock_recipes'):
        copy_recipes_term = "COPY cppdock_recipes /opt/cppdock_recipes/" 

    return """
FROM {0} as build_{2}_{1}
    {4}
    WORKDIR /usr/local/src
    RUN cppdock install_dep {1} {2} {3}
""".format(cppdock_image, platform, branch, repo, copy_recipes_term)

def make_install_dep(platform, branch, repo):
    return """
    RUN cppdock install_dep {0} {1} {2}""".format(platform, branch, repo)

def make_deps_import(platform, branch):
    return """
    COPY --from=build_{1}_{0} /opt/install/ /opt/sysroot""".format(platform, branch)

def get_deps_from_config(config, platform):
    if not config.has_section(platform):
        raise ValueError('Platform is not specified in cppdock ini file')
    return [(platform, item[1][0:40], item[0]) for item in config.items(platform)]

#
#def get_dep_dir_name(repo, branch):
#    return '{0}-{1}'.format(repo.split('/').pop(), branch)
        

dispatch(os.sys.argv[1:2], os.sys.argv[2:])

eda4a90199834f8c22afe9672eb06dce6080de06
#!/usr/bin/env python

import ConfigParser
import argparse
import os
import subprocess
import string
import sys
import urllib2
import warnings
import zipfile

root_path = os.path.abspath('.')
default_ini_file_path = root_path + '/cppdock.ini'

def parse_args_command(args):
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('command',      choices   = [
                                                        'init',
                                                        'build',
                                                        'install_dep',
                                                    ])
    return parser.parse_args(args)

def parse_args_init(args):
    parser = argparse.ArgumentParser(prog='cppdock init')
    parser.add_argument('-f', '--file', dest      = 'ini_file_path',
                                        default   = default_ini_file_path,
                                        help      = 'Specify path to ini file')
    return parser.parse_args(args)

def parse_args_build(args):
    parser = argparse.ArgumentParser(prog='cppdock build')
    parser.add_argument('-f', '--file', dest      = 'ini_file_path',
                                        default   = default_ini_file_path,
                                        help      = 'Specify path to ini file')

    parser.add_argument('--print-only', dest      = 'print_only',
                                        action    = 'store_true',
                                        help      = 'Print Dockerfile without building')

    group  = parser.add_argument_group('required arguments')
    group.add_argument('platform',      help      = 'Token for target platform')

    return parser.parse_args(args)

def parse_args_install_dep(args):
    parser = argparse.ArgumentParser(prog='cppdock install_dep')
    group  = parser.add_argument_group('required arguments')
    group.add_argument('platform',      help      = 'Token for target platform')
    group.add_argument('branch',        help      = 'SHA1 of git revision')
    group.add_argument('repo',          help      = 'Name of git repo (e.g. boostorg/hana)')
    return parser.parse_args(args)

def dispatch(command_args, cli_args):
    command = parse_args_command(command_args).command
    commands = {
        None:           lambda: None,
        'init':         (command_init,          parse_args_init),
        'build':        (command_build,         parse_args_build),
        'install_dep':  (command_install_dep,   parse_args_install_dep),
    }
    if command in commands:
        args = commands[command][1](cli_args)
        commands[command][0](args)

def command_init(args):
    load_config_file_with_args(args)

def command_build(args):
    config = load_config_file_with_args(args)
    #dockerfile = make_deps_dockerfile(config, args.platform)
    dockerfile = make_deps_dockerfile_stageless(config, args.platform)
    if args.print_only:
        print dockerfile
    else:
        make_dep_image(config, args.platform, dockerfile)

def command_install_dep(args):
    check_sysroot()
    url = make_archive_url(args.repo, args.branch)
    input = urllib2.urlopen(url)
    output = open('dep.zip', 'wr')
    buf = ''
    while True:
        buf = input.read(800)
        output.write(buf)
        output.flush()
        if (len(buf) == 0):
            break
    output.close()

    assert os.path.isfile('dep.zip')
    assert zipfile.is_zipfile('dep.zip')

    zipfile.ZipFile('dep.zip', 'r').extractall()
    recipe = find_recipe(args.platform, args.repo)
    cwd_dir = [x for x in os.listdir('.') if x.endswith(args.branch)][0]
    p = subprocess.Popen([recipe], cwd = cwd_dir)
    p.communicate('')
    if p.returncode != 0:
        print """

    FAILURE: Recipe returned error code {0}.
        """.format(p.returncode)
        sys.exit(1)

def install_deps_platform(platform, items):
    for i in range(len(items)):
        repo, branch = items[i]
        branch = branch[0:40]
        install_dep(i, platform, repo, branch)

def load_config_file_with_args(args):
    ini_file_path = args.ini_file_path
    return load_config_file(ini_file_path)

def load_config_file(ini_file_path):
    if not os.path.isfile(ini_file_path):
        print ini_file_path
        raise ValueError('Config file not found.')
    config = PrettyConfigParser()
    config.read(ini_file_path)
    for section in config.sections():
        if section == 'cppdock':
            continue
        for item in config.items(section):
            lock_item(config, section, item)
    write_config_file(config, ini_file_path)
    return config

def write_config_file(config, ini_file_path):
    try:
        config.write(open(ini_file_path, 'wb'))
    except:
        warnings.warn('WARNING: Unable to write to ini file')

def lock_item(config, section, item):
    repo, branch = item
    config.set(section, repo, get_current_sha_with_comment(item))

def get_current_sha_with_comment(item):
    repo, branch = item
    if len(branch) >= 40:
        return branch
    else:
        branch = normalize_branch_name(branch)
        lines = subprocess.check_output(['git', 'ls-remote', normalize_repo_name(repo)]).splitlines()
        for line in lines:
            if line.endswith(branch):
                return line[0:40] + ' # ' + (item[1] or 'HEAD')
        raise ValueError("Unable to get SHA from remote git repository")

def normalize_repo_name(name):
    return "https://github.com/" + name;

def normalize_branch_name(name):
    if len(name) == 0 or name == 'HEAD':
        return 'HEAD'
    elif name.startswith('refs/'):
        return name
    else:
        return "refs/heads/" + name

def is_sha(name):
    return len(name) == 40

class PrettyConfigParser(ConfigParser.RawConfigParser):
    def write(self, fp):
        max_key_length = get_max_key_length(self)
        if self._defaults:
            fp.write("[%s]\n" % DEFAULTSECT)
            for (key, value) in self._defaults.items():
                fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
            fp.write("\n")
        for section in self._sections:
            fp.write("[%s]\n" % section)
            for (key, value) in self._sections[section].items():
                if key == "__name__":
                    continue
                if (value is not None) or (self._optcre == self.OPTCRE):
                    equal_left_padding = ' ' * (max_key_length - len(key))
                    key = " = ".join((key + equal_left_padding, str(value).replace('\n', '\n\t')))

                fp.write("%s\n" % (key))
            fp.write("\n")

def get_max_key_length(config):
    length = 0
    for section in config.sections():
        for item in config.items(section):
            len_ = len(item[0])
            if (len_ > length):
                length = len_
    return length

def make_archive_url(repo, branch):
    return 'https://github.com/{0}/archive/{1}.zip'.format(repo, branch)

def find_recipe(platform, repo):
    repo = repo.replace('/', '-')
    local_recipe_path = '/opt/cppdock_recipes'
    builtin_recipe_path = os.path.expanduser('~') + '/.cppdock_recipes'
    repo_with_platform = repo + '-' + platform
    xs = [
        local_recipe_path   + '/' + repo_with_platform,
        local_recipe_path   + '/' + repo,
        local_recipe_path   + '/default',
        builtin_recipe_path + '/' + repo_with_platform,
        builtin_recipe_path + '/' + repo,
        builtin_recipe_path + '/default',
    ]
    for x in xs:
        if os.path.isfile(x):
            return x
    raise ValueError('Unable to find cppdock recipe: ' + repo_with_platform)

def check_sysroot():
    if not os.path.isdir('/opt/sysroot/'):
        raise ValueError('Sysroot directory is missing: /opt/sysroot/')

def get_arg(args, i):
    next(iter(args[i:i+1]), None)

def get_config_option(config, name):
    defaults = {
        'project': None,
        'compiler_linux_x64':     'ricejasonf/emscripten_fastcomp:1.37.19',
        'compiler_emscripten':    'ricejasonf/emscripten_fastcomp:1.37.19',
        'compiler_tvossimulator': 'ricejasonf/emscripten_fastcomp:1.37.19',
        'platform_linux_x64':     'ricejasonf/cppdock:linux_x64',
        'platform_emscripten':    'ricejasonf/cppdock:emscripten',
        'platform_tvossimulator': 'ricejasonf/cppdock:tvossimulator',
    }
    value = None
    try:
        value = config.get('cppdock', name)
    except:
        if name in defaults:
            value = defaults[name]
    if value == None:
        raise ValueError('Config option has no default for "{0}"'.format(name))
    return value

def make_dep_image(config, platform, dockerfile):
    tag_name = "{0}_build:{1}".format(
        get_config_option(config, 'project'),
        platform
    )
    p = subprocess.Popen(['docker', 'build', '--tag=' + tag_name, '--file=-', '.'], stdin=subprocess.PIPE)
    out, err = p.communicate(dockerfile)
    if p.returncode == 0:
        print """

    Finished building {0}.
        """.format(tag_name)
    else:
        print """

    FAILURE: Build of {0} FAILED.
        """.format(tag_name)
        sys.exit(1)

def make_deps_dockerfile(config, platform):
    cppdock_image  = get_config_option(config, 'platform_' + platform)
    deps = get_deps_from_config(config, platform)
    deps_stages = string.join([make_deps_stage(cppdock_image, x, y, z) for x, y, z in deps])
    deps_imports = string.join([make_deps_import(x, y) for x, y, z in deps])

    return """
{0}
FROM {1}
{2}
""".format(deps_stages, cppdock_image, deps_imports)

def make_deps_dockerfile_stageless(config, platform):
    cppdock_image  = get_config_option(config, 'platform_' + platform)
    deps = get_deps_from_config(config, platform)
    deps_stages = string.join([make_install_dep(x, y, z) for x, y, z in deps])

    return """
FROM {0} as build_all
{1}

FROM {2}

    COPY --from=build_all /opt/install /opt/sysroot
""".format(cppdock_image, deps_stages, cppdock_image)

def make_deps_stage(cppdock_image, platform, branch, repo):
    copy_recipes_term = ""
    if os.path.isdir(root_path + '/cppdock_recipes'):
        copy_recipes_term = "COPY cppdock_recipes /opt/cppdock_recipes/" 

    return """
FROM {0} as build_{2}_{1}
    {4}
    WORKDIR /usr/local/src
    RUN cppdock install_dep {1} {2} {3}
""".format(cppdock_image, platform, branch, repo, copy_recipes_term)

def make_install_dep(platform, branch, repo):
    return """
    RUN cppdock install_dep {0} {1} {2}""".format(platform, branch, repo)

def make_deps_import(platform, branch):
    return """
    COPY --from=build_{1}_{0} /opt/install/ /opt/sysroot""".format(platform, branch)

def get_deps_from_config(config, platform):
    if not config.has_section(platform):
        raise ValueError('Platform is not specified in cppdock ini file')
    return [(platform, item[1][0:40], item[0]) for item in config.items(platform)]

#
#def get_dep_dir_name(repo, branch):
#    return '{0}-{1}'.format(repo.split('/').pop(), branch)
        

dispatch(os.sys.argv[1:2], os.sys.argv[2:])
