#!/usr/bin/python3

# Copyright (C) 2013 Brandon Invergo <brandon@invergo.net>
#
# This file is part of pyconfigure.
#
# pyconfigure is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyconfigure is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyconfigure.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import print_function
import os
import stat
import sys
import getopt
import shutil
import subprocess
import warnings


DATADIR = "/usr/share/pyconfigure"


# os.mkdir throws an OSError in Python 2.x when the target already
# exists, while it throws a FileExistsError in Python 3.x.
try:
    raise FileExistsError
except:
    FileExistsError = OSError


def safe_copy(src, dst, overwrite=False):
    if not overwrite and os.path.exists(dst):
        warnings.warn("{0} exists. Skipping.".format(dst))
    else:
        shutil.copy(src, dst)


def parse_pkg_info(pkg_info):
    """Parse a PKG-INFO file to get all the relevant package meta-data.
    The file should be in PKG-INFO format version 1.2"""

    # Fields that can only appear once in the file
    singles = ["Name", "Version", "Summary", "Keywords", "Home-page",
               "Download-URL", "Author", "Author-email", "Maintainer",
               "Maintainer-email", "License", "Requires-Python", 
               "Description", "Metadata-Version"]
    # Fields that can appear more than once
    multis = ["Platform", "Supported-Platform", "Classifier", "Requires-Dist",
              "Provides-Dist", "Obsoletes-Dist", "Requires-External",
              "Project-URL"]
    pkg_meta = {}
    for key in singles:
        pkg_meta[key] = ""
    for key in multis:
        pkg_meta[key] = []
    with open(pkg_info) as h:
        lines = h.readlines()
    for line in lines:
        if line[0:8] == "       |":
            if key is None or key == "":
                sys.exit("*** Error: Folded field that doesn't belong to any key")
            pkg_meta[key] = '\n'.join([pkg_meta[key], line[8:]])
            continue
        key, sep, val = line.partition(':')
        val = val.strip()
        if key == "Metadata-Version" and float(val) < 1.2:
            sys.exit("*** Error: PKG-INFO metadata format version 1.2 or greater \
is required")
        elif key in singles:
            if pkg_meta[key] == "":
                pkg_meta[key] = val
            else:
                print("==> Warning: multiple entries for metadata field \
'{0}'".format(key))
        elif key in multis:
            pkg_meta[key].append(val)
        else:
            print("==> Warning: unknown metadata field '{0}'".format(key))
            continue
    if "Project-URL" in pkg_meta and len(pkg_meta["Project-URL"]) > 0:
        pkg_meta["URL"] = pkg_meta["Project-URL"][0]
    else:
        pkg_meta["URL"] = ""
    for key in multis:
        if key in pkg_meta and len(pkg_meta[key]) > 0:
            pkg_meta["{0}-list".format(key)] = "',\n\t\t'".join(pkg_meta[key])
        else:
            pkg_meta["{0}-list".format(key)] = ""
    return pkg_meta


def subst_meta(in_file, out_file, pkg_meta):
    """Substitute the package's metadata into the template files.  Substitutions
    occur where one of the metadata keys appears in the file wrapped in \[ and \]
    (i.e. \[URL\] -> {http://myproject.org})"""

    with open(in_file) as h:
        lines = h.readlines()
    with open(out_file, 'w') as h:
        for line in lines:
            line_fmt = line.format(**pkg_meta)
            line_fmt = line_fmt.replace("\\[", "{")
            line_fmt = line_fmt.replace("\\]", "}")
            h.write(line_fmt)
    
    
def copy_aux_files(output):
    """Copy auxiliary install scripts to the destination directory."""
    
    bootstrap = os.path.join(DATADIR, "bootstrap.sh")
    install_sh = os.path.join(DATADIR, "install-sh")
    print("Copying bootstrap.sh")
    shutil.copy(bootstrap, output)
    os.chmod(os.path.join(output, "bootstrap.sh"),
                          stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | \
             stat.S_IROTH | stat.S_IXOTH)
    print("Copying install-sh")
    shutil.copy(install_sh, output)
    os.chmod(os.path.join(output, "install-sh"),
                          stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | \
             stat.S_IROTH | stat.S_IXOTH)
    

def copy_macros(output):
    """Copy the Autoconf macros to the destination directory."""
    
    macros = os.path.join(DATADIR, "m4", "python.m4")
    macro_dir = os.path.join(output, "m4")
    print("Copying Python M4 macros")
    try:
        os.mkdir(macro_dir)
    except FileExistsError:
        pass
    shutil.copy(macros, macro_dir)


def gen_configure(pkg_meta, output, overwrite):
    """Generate the configure script. Copy the source files into the project
    directory and then run autoreconf to generate the configure script"""
    
    config_src = os.path.join(DATADIR, "configure.ac")
    config_dst = os.path.join(output, "configure.ac")
    print("Generating configure.ac")
    if not overwrite and os.path.exists(config_dst):
        warnings.warn("configure.ac exists. Skipping.")
    else:
        subst_meta(config_src, config_dst, pkg_meta)
    print("Generating configure")
    subprocess.call(["autoreconf", "-fvi", output])


def gen_makefile(pkg_meta, output, prefer_make, overwrite):
    """Generate the Makefile. If the --prefer-make option was passed, copy the
    Makefile that contains the installation logic written in Make, otherwise
    copy the Makefile that acts as a wrapper around Distutils"""
    
    if prefer_make:
        makefile_src = os.path.join(DATADIR, "Makefile.in.make")
    else:
        makefile_src = os.path.join(DATADIR, "Makefile.in.distutils")
    makefile_dst = os.path.join(output, "Makefile.in")
    print("Generating Makefile.in")
    safe_copy(makefile_src, makefile_dst, overwrite)


def gen_distutils(pkg_meta, output, prefer_make, target, overwrite):
    """Generate the setup.py.in script. If the --prefer-make option was passed,
    copy the setup.py.in that wraps the Makefile, otherwise script that uses
    'target'. 'target' is a number representing which Python-based distribution
    to use. 0 -> distutils """

    if prefer_make:
        setup_py_src = os.path.join(DATADIR, "setup.py.in.make")
    else:
        setup_py_file = {0: "setup.py.in.distutils"}.get(target)
        if setup_py_file is None:
            setup_py_file = "setup.py.in.distutils"
        setup_py_src = os.path.join(DATADIR, setup_py_file)
    setup_py_dst = os.path.join(output, "setup.py.in")
    print("Generating setup.py.in")
    if not prefer_make:
        if not overwrite and os.path.exists(setup_py_dst):
            warnings.warn("setup.py.in exists. Skipping.")
        else:
            subst_meta(setup_py_src, setup_py_dst, pkg_meta)
    else:
        safe_copy(setup_py_src, setup_py_dst, overwrite)


def print_usage():
    print("""Usage: pyconf [OPTIONS] [TARGET] PKG-INFO
Generate `configure' and installation scripts for a Python program

OPTIONS
    -o, --output=DIR    directory to move the generated files
                        (default ".")
    -m, --prefer-make   prefer using Make for performing installation 
                        logic, instead of the Python-based TARGET
    --macros-only       copy the pyconfigure M4 macros to the output
                        directory and exit
    --no-make           do not generate a Makefile
    --overwrite         overwrite existing files
    --help              show this information and exit
    --version           output version information and exit
TARGETS:
    distutils (default)

By default, the generated `Makefile' will be a wrapper around the
TARGET-based script for Python (i.e. distutils' `setup.py' script).
If `--prefer-make' is specified, the TARGET-based Python script will
be a wrapper around `Makefile'.

For more information on the PKG-INFO file format, see PEP 345.
<http://www.python.org/dev/peps/pep-0345/>

Report bugs to: bug-pyconfigure@gnu.org
pyconfigure home page: <http://www.gnu.org/software/pyconfigure/>
General help using GNU software: <http://www.gnu.org/get/help/>""")


def print_version():
    print("""pyconf (GNU pyconfigure)  0.2.3
Copyright (C) 2013 Brandon Invergo
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.""")


if __name__ == "__main__":
    long_args = ["help", "version", "output", "prefer-make", "no-make",
                 "overwrite", "macros-only"]
    try:
        opts, args = getopt.gnu_getopt(sys.argv[1:], "hvo:m", long_args)
    except getopt.GetoptError as err:
        print(str(err))
        print_usage()
        sys.exit(2)
    output = os.getcwd()
    prefer_make = False
    no_make = False
    macros_only = False
    overwrite = False
    for o, a in opts:
        if o in ["-h", "--help"]:
            print_usage()
            sys.exit()
        elif o in ["-v", "--version"]:
            print_version()
            sys.exit()
        elif o in ["-o", "--output"]:
            output = a
        elif o in ["-m", "--prefer-make"]:
            prefer_make = True
        elif o == "--no-make":
            no_make = True
            prefer_make = False
        elif o == "--macros-only":
            macros_only = True
        elif o == "--overwrite":
            overwrite = True
        else:
            print("Warning: unhandled option")
    if len(args) == 0:
        print_usage()
        sys.exit(2)
    elif len(args) == 1:
        target = 0
        pkg_info = args[0]
    else:
        # do the following with an eye to the future of handling more
        # Python packaging systems. Right now, only distutils is supported.
        # Choose the target number based on the target selected by the user
        # i.e. distutils -> 0
        target = {"distutils":0}.get(args[0])
        if target is None:
            print("Error: invalid target {0}".format(args[0]))
            sys.exit(2)
        pkg_info = args[1]
    if not os.path.isfile(pkg_info):
        print("Error: PKG-INFO file does not exist")
        sys.exit(2)
    print("Running pyconfigure in {0}".format(output))
    copy_macros(output)
    if macros_only:
        sys.exit(0)
    print("Parsing metadata...")
    pkg_meta = parse_pkg_info(pkg_info)
    copy_aux_files(output)
    gen_configure(pkg_meta, output, overwrite)
    if not no_make:
        gen_makefile(pkg_meta, output, prefer_make, overwrite)
    # also with an eye to the future. This will be a list of gen_TARGET functions
    # and the function is chosen according to the index number 'target'. Since
    # only distutils is supported, the list has one element, gen_distutils, and
    # only target 0 works
    [gen_distutils][target](pkg_meta, output, prefer_make, target, overwrite)
    sys.exit(0)
