#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Install proceedure

:author:       Michael Mulich
:copyright:    2010 by Penn State University
:organization: WebLion Group, Penn State University
:license:      GPL, see LICENSE for more detail
"""
import os
import sys
import shutil
from optparse import OptionParser
from subprocess import Popen, PIPE
from tempfile import mkdtemp

assert sys.version_info >= (2, 7), "Python >= 2.7 is required"

__version__ = '0.1.0'
HERE = os.path.abspath(os.path.dirname(__file__))
RUN_LOCATION = os.path.abspath(os.curdir)
EASY_INSTALL_CMD = "from setuptools.command.easy_install import main; main()"
SCRIPT_ENTRY_POINT_GROUP = 'console_scripts'

# REQUIRES Setuptools (or Distribute)
is_using_distribute = False
try:
    import pkg_resources
    if hasattr(pkg_resources, '_distribute'):
        is_using_distribute = True
except ImportError:
    raise RuntimeError("This installation script requires Setuptools "
                       "to be installed prior to the build. Please install "
                       "Setuptools or Distribute before continuing.")
try:
    if not is_using_distribute:
        pkg_resources.require('Setuptools>=0.6c11')
except pkg_resources.VersionConflict:
    dist = pkg_resources.get_distribution('Setuptools')
    raise RuntimeError("This installation script requires a version of "
                       "Setuptools that is greater than or equal to "
                       "0.6c11. Please upgrade the Setuptools "
                       "distribution before continuing. You currently "
                       "have version %s installed" % dist.version)

# Which variant of Setuptools is being used
# Addresses https://weblion.psu.edu/trac/weblion/ticket/2363
if is_using_distribute:
    setuptools_variant = 'distribute'
else:
    setuptools_variant = 'setuptools'


def init_configs(in_location, out_location, config_vars, ext='.in'):
    """Initialize the configuration files. Use variable substitution on files
    ending with the defined extension (the ext keyword argument).

    Returns a list of configuration files."""
    files = []
    for file in os.listdir(in_location):
        if file.startswith('.'):
            # VC files (e.g. .svn and .hg)
            continue
        file_path = os.path.join(in_location, file)
        if file.endswith(ext):
            out_file_path = os.path.join(out_location, file_path.rstrip(ext))
            with open(file_path, 'r') as f_in:
                with open(out_file_path, 'w') as f_out:
                    f_out.write(f_in.read() % config_vars)
            file_path = out_file_path
        files.append(file_path)

def install_dist(src_loc, base_install_dir='.', base_build_dir='.'):
    """Installs a distribution using Setuptool's easy_install command.
    Returns a path location where the distribution has been installed."""
    # Define some useful information that we are going to use later in
    # the process to figure out the name and version
    from pip.req import InstallRequirement
    req = InstallRequirement(os.path.basename(src_loc), None,
                             source_dir=src_loc)
    common_args = [sys.executable, '-c', EASY_INSTALL_CMD, '-mZUNxd']

    # Install the dist to a temporary location
    tmp = mkdtemp(dir=base_build_dir)
    args = common_args + [tmp, src_loc]
    install_process = Popen(args, stdout=PIPE, stderr=PIPE, env=os.environ)
    stdout, stderr = install_process.communicate()
    if not install_process.returncode == 0:
        print("ERROR while building \"%s\"." % req.name)
        print("We were attempting to run \"%s\"." % ' '.join(args))
        indent_print = lambda s: '\n'.join(['  ' + l for l in s.split('\n')])
        print("Output before the error:")
        print(indent_print(stdout))
        print("Error message:")
        print(indent_print(stderr))
        sys.exit(1)

    # Move the built distribution to an install location
    built_dist_loc = os.path.join(tmp, os.listdir(tmp)[0])
    install_dist_dirname = "%s-%s.egg" % (req.name, req.pkg_info()['version'])
    install_dist_loc = os.path.join(base_install_dir, install_dist_dirname)
    shutil.copytree(built_dist_loc, install_dist_loc)
    shutil.rmtree(tmp)

    # Create a Distribution object from install
    metadata_dirname = 'EGG-INFO'
    metadata = pkg_resources.PathMetadata(install_dist_loc,
                                          os.path.join(install_dist_loc,
                                                       metadata_dirname))
    dist = pkg_resources.Distribution(location=install_dist_loc,
                                      metadata=metadata,
                                      project_name=req.name,
                                      version=req.pkg_info()['version'])

    return dist

def gen_scripts(dist, install_dir):
    """Generate the distribution's scripts."""
    gen_script_cmd = [sys.executable, os.path.join(HERE, 'gen_script.py')]
    common_args = ['-d', install_dir, '-e', os.path.abspath(sys.executable)]
    scripts = []
    for name in pkg_resources.get_entry_map(dist, SCRIPT_ENTRY_POINT_GROUP):
        entry_point = dist.get_entry_info(SCRIPT_ENTRY_POINT_GROUP, name)
        args = common_args + [entry_point.module_name,
                              '.'.join(entry_point.attrs),
                              name]
        gen_script_process = Popen(gen_script_cmd + args)
        gen_script_process.wait()
        assert gen_script_process.returncode == 0, args
        scripts.append(name)
    return scripts

def store_abs_path(option, opt_str, value, parser):
    setattr(parser.values, option.dest, os.path.abspath(value))

def separation_parser(option, opt_str, value, parser,
                      sep_char, should_lower=False):
    new_value = [should_lower and v.lower() or v
                 for v in value.split(sep_char)
                 if v]
    setattr(parser.values, option.dest, tuple(new_value))

parser = OptionParser()
parser.add_option('-b', '--build-dir', dest='build_dir',
                  metavar='BUILDDIR',
                  type='string', nargs=1,
                  action='callback', callback=store_abs_path,
                  default=os.path.join(RUN_LOCATION, 'build'),
                  help="Build directory")
parser.add_option('-s', '--source-dir', dest='source_dir',
                  metavar='SOURCEDIR',
                  type='string', nargs=1,
                  action='callback', callback=store_abs_path,
                  default=os.path.join(RUN_LOCATION, 'source'),
                  help="Source directory")
parser.add_option('-c', '--config-dir', dest='config_dir',
                  metavar='CONFIGDIR',
                  type='string', nargs=1,
                  action='callback', callback=store_abs_path,
                  default=os.path.join(RUN_LOCATION, 'configuration'),
                  help="Configuration directory")
parser.add_option('-f', '--final-dir', dest='final_dir',
                  metavar='FINALDIR',
                  help="The libraries final install location.")
parser.add_option('-e', '--exclude', dest='exclusions',
                  metavar='DIST_NAME',
                  type='string', nargs=1,
                  action='callback',
                  callback=separation_parser, callback_args=(':', True),
                  default=tuple(),
                  # Unfortunately, we have to do a colon semparated list.
                  # This is due to optparse inablity to handle n+/- args and
                  # spaces are valid in distribution names.
                  help="Distributions to exclude from the build. A colon "
                       "separated string. "
                       "(Setuptools variants are automatically excluded "
                       "from the build.)")
parser.add_option('--name', dest='name',
                  metavar='NAME',
                  type='string', nargs=1,
                  default='unknown',
                  help="The name of the project being built.")

def main():
    (options, args) = parser.parse_args()

    # Initialize the build locations.
    project_name = options.name
    build_area = options.build_dir
    source_area = options.source_dir
    config_area = options.config_dir
    if not os.path.exists(build_area):
        os.mkdir(build_area)

    # Define the build variables.
    in_file_vars = {'HERE': RUN_LOCATION}
    in_file_vars.update(os.environ)
    in_file_vars['setuptools_variant'] = setuptools_variant
    prefix = 'dist'
    dist_exclusions = list(options.exclusions)
    dist_exclusions.extend([setuptools_variant, 'pip'])
    # Define a working set to keep track of what distribution have been built.
    # NOTE: We don't actually use the working set yet, but it might be
    # handy in the future.
    working_set = pkg_resources.WorkingSet([]) # the empty list is important!

    # Write out the .in configuration files.
    configs = init_configs(config_area, build_area, in_file_vars)

    # Define locations where the results should go.
    install_dir = os.path.join(build_area, '%s' % prefix)
    script_dir = os.path.join(build_area, '%s-scripts' % prefix)
    pth_file_dir = os.path.join(build_area, '%s-pth' % prefix)
    tmp_install_area = os.path.join(build_area, 'tmp_installs')
    # Ensure these directories have been created.
    for dir in (install_dir, script_dir, pth_file_dir, tmp_install_area):
        if not os.path.exists(dir):
            os.mkdir(dir)

    # Build the distributions and generate their scripts.
    for dist_dir in os.listdir(source_area):
        if dist_dir.startswith('.') or dist_dir.lower() in dist_exclusions:
            # (e.g. .svn and .hg)
            continue
        src_loc = os.path.join(source_area, dist_dir)
        # logging ->"Installing %s..." % dist_dir)
        dist = install_dist(src_loc,
                            base_install_dir=install_dir,
                            base_build_dir=tmp_install_area)
        # Generate the scripts
        scripts = gen_scripts(dist, script_dir)
        # logging -> "Installing %s's scripts: %s" % (dist.project_name, ', '.join(scripts))
        working_set.add(dist)

    # Build the PTH file.
    pth_filename = '%s.pth' % project_name
    lib_final_dir = install_dir
    if options.final_dir is not None:
        lib_final_dir = options.final_dir
    with open(os.path.join(pth_file_dir, pth_filename), 'w') as pth:
        final_locs = []
        for dist in working_set:
            dist_dirname = os.path.basename(dist.location)
            final_locs.append(os.path.join(lib_final_dir, dist_dirname))
        pth.writelines('\n'.join(final_locs))

if __name__ == '__main__':
    main()
