#! /usr/bin/python
# That line was just so programs like gvim or emacs will understand that this is Python code!  Don't
# make this file executable.

###############################################################################
#                                                                             #
# Copyright (C) 2006-2013 Edward d'Auvergne                                   #
#                                                                             #
# This file is part of the program relax (http://www.nmr-relax.com).          #
#                                                                             #
# This program 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.                                         #
#                                                                             #
# This program 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 relax; if not, write to the Free Software                        #
#                                                                             #
###############################################################################


# Import statements.
from os import getcwd, path, remove, rmdir, sep, walk
import platform
from re import search
from shutil import rmtree
from subprocess import PIPE, Popen
import sys

# Scons modules.
from scons.distrib import package, gpg_sign
from scons.install import install, uninstall
from scons.manuals import clean_manual_files, compile_api_manual_html, compile_user_manual_html, compile_user_manual_pdf, fetch_docstrings, version_file
from SCons.Script import ARGUMENTS
import SCons.Util

# relax version file.
from version import version



########################
# Paths and file names #
########################

# The operating system.
SYSTEM = platform.uname()[0]

# The machine type.
MACH = platform.uname()[4]

# Symbolic link flag.
SYMLINK_FLAG = 1

# GNU/Linux.
if SYSTEM == 'Linux':
    # System specific string.
    SYS = 'GNU-Linux'

    # Linux installation path.
    INSTALL_PATH = '/usr/local'


# MS Windows.
elif SYSTEM == 'Windows' or SYSTEM == 'Microsoft':
    # Set the system to 'Windows' no matter what.
    SYSTEM = 'Windows'

    # Architecture.
    arch = platform.architecture()[0]

    # 32 bit.
    if arch == '32bit':
        SYS = 'Win32'

    # 64 bit.
    elif arch == '64bit':
        SYS = 'Win64'

    # Unknown.
    else:
        SYS = 'Win'

    # Windows installation path.
    INSTALL_PATH = 'C:\\'

    # No symlinks!
    SYMLINK_FLAG = 0


# Mac OS X.
elif SYSTEM == 'Darwin':
    # System specific string.
    SYS = SYSTEM

    # Mac OS X installation path.
    INSTALL_PATH = sys.prefix + sep + 'local'


# All other operating systems.
else:
    # System specific string.
    SYS = SYSTEM

    # Installation path.
    INSTALL_PATH = sys.prefix + sep + 'local'



# Installation.
###############

# Relax installation directory.
RELAX_PATH = INSTALL_PATH + sep + 'relax'

# Installation path for binaries.
BIN_PATH = INSTALL_PATH + sep + 'bin'

# Symbolic link installation path.
SYMLINK = BIN_PATH + sep + 'relax'



# The distribution files.
#########################

if SYSTEM == 'Windows':
    BIN_FILE = 'relax-' + version + '.' + SYS
    SRC_FILE = 'relax-' + version + '.src'
    DIST_TYPE = 'zip'
elif SYSTEM == 'Darwin':
    BIN_FILE = 'relax-' + version + '.' + SYS
    SRC_FILE = 'relax-' + version + '.src'
    DIST_TYPE = 'dmg'

else:
    BIN_FILE = 'relax-' + version + '.' + SYS + '.' + MACH
    SRC_FILE = 'relax-' + version + '.src'
    DIST_TYPE = 'tar'


# GPG key.
##########

GPG_KEY = ARGUMENTS.get('key')


# Documentation.
################

# Documentation directory.
DOCS_DIR = 'docs' + sep

# LaTeX directory.
LATEX_DIR = 'docs' + sep + 'latex' + sep




class Main:
    def __init__(self):
        """Initialise the main building targets.

        This function sets up the Scons build Environments, sets custom Builders, sets the build
        targets, and sets the build dependancies.
        """

        # Initialisation.
        #################

        # Set the help message.
        self.help()


        # C module compilation.
        #######################

        # Setup the rules for building the relaxation curve fitting C modules (and set it as the default).
        self.relax_fit()
        Default(self.relax_fit_object)



        # Program installation.
        #######################

        # Install target.
        install_env = Environment(BUILDERS={'install' : Builder(action=install)},
                                  BIN_PATH=BIN_PATH,
                                  INSTALL_PATH=INSTALL_PATH,
                                  RELAX_PATH=RELAX_PATH,
                                  SYMLINK=SYMLINK,
                                  SYMLINK_FLAG=SYMLINK_FLAG)
        install_env.install(target='install', source=None)

        # Uninstall target.
        uninstall_env = Environment(BUILDERS={'uninstall' : Builder(action=uninstall)},
                                  BIN_PATH=BIN_PATH,
                                  INSTALL_PATH=INSTALL_PATH,
                                  RELAX_PATH=RELAX_PATH,
                                  SYMLINK=SYMLINK,
                                  SYMLINK_FLAG=SYMLINK_FLAG)
        uninstall_env.uninstall(target='uninstall', source=None)



        # Distribution packages.
        ########################

        # Target for creating the binary distribution file.
        binary_dist_env = Environment(BUILDERS={'dummy' : Builder(action=self.dummy)})
        binary_dist_env.dummy(target='binary_dist', source=None)
        binary_dist_env.Depends('binary_dist', 'version_check')           # First check the program version number.
        binary_dist_env.Depends('binary_dist', self.relax_fit_object)     # Compile the C code.
        binary_dist_env.Depends('binary_dist', 'manual_clean_nodeps')     # Clean up the temporary manual files.
        binary_dist_env.Depends('binary_dist', 'clean')                   # Then clean up all other temporary files.
        binary_dist_env.Depends('binary_dist', 'package_bin')             # Package the binary distribution.
        binary_dist_env.Depends('binary_dist', 'gpg_bin')                 # GPG sign the binary distribution file.

        # Target for creating the source distribution file.
        source_dist_env = Environment(BUILDERS={'dummy' : Builder(action=self.dummy)})
        source_dist_env.dummy(target='source_dist', source=None)
        source_dist_env.Depends('source_dist', 'version_check')           # First check the program version number.
        source_dist_env.Depends('source_dist', 'manual_clean_nodeps')     # Clean up the temporary manual files.
        source_dist_env.Depends('source_dist', 'clean_all')               # Then clean up the sources.
        source_dist_env.Depends('source_dist', 'package_src')             # Package the source distribution.
        source_dist_env.Depends('source_dist', 'gpg_src')                 # GPG sign the source distribution file.

        # Target for packaging the binary distribution.
        package_bin_env = Environment(BUILDERS={'archive' : Builder(action=package)},
                                      DIST_FILE=BIN_FILE,
                                      DIST_TYPE=DIST_TYPE)
        package_bin_env.archive(target='package_bin', source=None)
        package_bin_env.Depends('package_bin', 'version_check')     # Check the program version number first.

        # Target for packaging the source distribution.
        package_src_env = Environment(BUILDERS={'archive' : Builder(action=package)},
                                      DIST_FILE=SRC_FILE,
                                      DIST_TYPE='ALL')
        package_src_env.archive(target='package_src', source=None)
        package_src_env.Depends('package_src', 'version_check')     # Check the program version number first.

        # Target for creating a GPG signature of the binary distribution file.
        gpg_bin_env = Environment(BUILDERS={'sign' : Builder(action=gpg_sign)},
                                  DIST_FILE=BIN_FILE,
                                  DIST_TYPE=DIST_TYPE,
                                  GPG_KEY=GPG_KEY)
        gpg_bin_env.sign(target='gpg_bin', source=None)
        gpg_bin_env.Depends('gpg_bin', 'version_check')     # Check the program version number before signing.

        # Target for creating a GPG signature of the source distribution file.
        gpg_src_env = Environment(BUILDERS={'sign' : Builder(action=gpg_sign)},
                                  DIST_FILE=SRC_FILE,
                                  DIST_TYPE='ALL',
                                  GPG_KEY=GPG_KEY)
        gpg_src_env.sign(target='gpg_src', source=None)
        gpg_src_env.Depends('gpg_src', 'version_check')     # Check the program version number before signing.



        # relax version checking.
        #########################

        # relax version number checking target.
        version_check_env = Environment(BUILDERS={'check' : Builder(action=self.test_version)})
        version_check_env.check(target='version_check', source=None)



        # Cleaning up.
        ##############

        # Clean target.
        clean_all_env = Environment(BUILDERS={'clean' : Builder(action=self.clean_all_files)})
        clean_all_env.clean(target='clean_all', source=None)
        clean_all_env.Depends('clean_all', 'clean')            # Run the 'clean' target.
        clean_all_env.Depends('clean_all', 'manual_clean')     # Run the 'manual_clean' target.

        # Target for removing temporary files.
        clean_env = Environment(BUILDERS={'clean' : Builder(action=self.clean_files)})
        clean_env.Depends('clean', 'manual_clean')
        clean_env.clean(target='clean', source=None)



        # relax manuals.
        ################

        # Create the manual build environment.
        manual_env = Environment(DOCS_DIR=DOCS_DIR,
                                 LATEX_DIR=LATEX_DIR,
                                 SYSTEM=SYSTEM)


        # Set up the builder for the standard manual targets (using the self.dummy function).
        manual_env.Append(BUILDERS={'Manual' : Builder(action=self.dummy)})

        # Target for creating the user manual (PDF version).
        manual_env.Manual(target='user_manual_pdf', source=None)
        manual_env.Depends('user_manual_pdf', 'manual_clean')
        manual_env.Depends('user_manual_pdf', 'manual_version_file')
        manual_env.Depends('user_manual_pdf', 'fetch_docstrings')
        manual_env.Depends('user_manual_pdf', 'compile_user_manual_pdf')

        # Target for creating the user manual (PDF version, without fetching the docstrings).
        manual_env.Manual(target='user_manual_pdf_nofetch', source=None)
        manual_env.Depends('user_manual_pdf_nofetch', 'manual_clean')
        manual_env.Depends('user_manual_pdf_nofetch', 'manual_version_file')
        manual_env.Depends('user_manual_pdf_nofetch', 'compile_user_manual_pdf')

        # Target for creating the user manual (HTML version).
        manual_env.Manual(target='user_manual_html', source=None)
        manual_env.Depends('user_manual_html', 'manual_clean')
        manual_env.Depends('user_manual_html', 'manual_version_file')
        manual_env.Depends('user_manual_html', 'fetch_docstrings')
        manual_env.Depends('user_manual_html', 'compile_user_manual_html')

        # Target for creating the user manual (HTML version, without fetching the docstrings).
        manual_env.Manual(target='user_manual_html_nofetch', source=None)
        manual_env.Depends('user_manual_html_nofetch', 'manual_clean')
        manual_env.Depends('user_manual_html_nofetch', 'manual_version_file')
        manual_env.Depends('user_manual_html_nofetch', 'compile_user_manual_html')

        # Target for creating the API documentation manual (HTML version).
        manual_env.Manual(target='api_manual_html', source=None)
        manual_env.Depends('api_manual_html', 'manual_clean')
        manual_env.Depends('api_manual_html', 'compile_api_manual_html')


        # Target for creating relax version number LaTeX file.
        manual_env.Append(BUILDERS={'Version' : Builder(action=version_file)})
        manual_env.Version(target='manual_version_file', source=None)

        # Target for fetching the docstrings.
        manual_env.Append(BUILDERS={'Fetch' : Builder(action=fetch_docstrings)})
        manual_env.Fetch(target='fetch_docstrings', source=None)

        # Target for compiling the PDF version of the user manual from the LaTeX sources.
        manual_env.Append(BUILDERS={'CompileUserManualPDF' : Builder(action=compile_user_manual_pdf)})
        manual_env.CompileUserManualPDF(target='compile_user_manual_pdf', source=None)

        # Target for compiling the HTML version of the user manual from the LaTeX sources.
        manual_env.Append(BUILDERS={'CompileUserManualHTML' : Builder(action=compile_user_manual_html)})
        manual_env.CompileUserManualHTML(target='compile_user_manual_html', source=None)

        # Target for compiling the HTML version of the API documentation manual using Epydoc.
        manual_env.Append(BUILDERS={'CompileAPIManualHTML' : Builder(action=compile_api_manual_html)})
        manual_env.CompileAPIManualHTML(target='compile_api_manual_html', source=None)

        # Clean target.
        manual_env.Append(BUILDERS={'Clean' : Builder(action=clean_manual_files)})
        manual_env.Clean(target='manual_clean', source=None)

        # Clean target (with no manual environments dependent on it).
        manual_env.Append(BUILDERS={'Clean' : Builder(action=clean_manual_files)})
        manual_env.Clean(target='manual_clean_nodeps', source=None)


    def clean_all_files(self, target, source, env):
        """Builder action for cleaning up."""

        # Print out.
        print
        print("#########################")
        print("# Cleaning up all files #")
        print("#########################\n\n")

        # Extensions of files to remove.
        temp_extns = ['so', 'sconsign', 'dll', 'pyd']

        # Print out.
        print("\nRemoving the files ending in " + `temp_extns` + ".\n")

        # Walk through the directories.
        for root, dirs, files in walk(getcwd()):
            # Loop over the files in the current directory.
            for file in files:
                # Loop over the extensions.
                for ext in temp_extns:
                    if search('\.' + ext + '$', file):
                        remove(path.join(root, file))

        # Remove build directories.
        if path.isdir('build'):
            print("Removing the build directory 'build' used to create a Mac OS X app.")
            rmtree('build')
        if path.isdir('dist'):
            print("Removing the distribution directory 'dist' used to create a Mac OS X app.")
            rmtree('dist')

        # Final printout.
        print("\n\n\n")


    def clean_files(self, target, source, env):
        """Builder action for removing temporary files."""

        # Print out.
        print
        print("###############################")
        print("# Cleaning up temporary files #")
        print("###############################\n\n")

        # Extensions of temporary files.
        temp_extns = ['pyc', 'pyo', 'bak', 'o', 'os', 'obj', 'exp', 'lib']

        # Print out.
        print("\nRemoving the files ending in " + `temp_extns` + ".\n")

        # Walk through the directories.
        for root, dirs, files in walk(getcwd()):
            # Loop over the files in the current directory.
            for file in files:
                # Loop over the extensions.
                for ext in temp_extns:
                    if search('\.' + ext + '$', file):
                        remove(path.join(root, file))

        # Remove relax save state files.
        print("Removing temporary relax save state files (of the form relax_state_xxxxxxxx_xxxxxx.bz2).\n")
        for root, dirs, files in walk(getcwd()):
            # Loop over the files in the current directory.
            for file in files:
                if search('^relax_state_.*.bz2', file):
                    remove(path.join(root, file))

        # Remove the Python 3 __pycache__ directories.
        print("Removing the Python 3 __pycache__ directories.\n")
        for root, dirs, files in walk(getcwd()):
            # Loop over the directories.
            for dir in dirs:
                if search('__pycache__', dir):
                    rmdir(path.join(root, dir))


        # Final printout.
        print("\n\n\n")


    def det_arch(self):
        """Nasty hack to make Scons behave properly with cross-compilation on Mac OS X!

        @return:    The list of CPU architects to cross compile.
        @rtype:     list of str
        """

        # The list of archectures to try.
        archs = ['i386', 'ppc', 'x86_64']
        allowed = []

        # Loop over each arch and test it.
        for arch in archs:
            # Run gcc.
            pipe = Popen('gcc -arch %s' % arch, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=False)

            # Loop over the error lines.
            for line in pipe.stderr.readlines():
                # Successful arch.
                if search("no input files", line):
                    allowed.append(arch)

        # Return the list.
        return allowed


    def dummy(self, target, source, env):
        """Dummy function which returns zero."""

        return 0


    def help(self):
        """The help message."""

        # Intro.
        string = '\nHelp for using Scons to build the various components of relax.\n\n'

        # Usage message.
        string = string + 'usage: scons [target]\n'

        # No target.
        string = string + '\nNo target:\n'
        string = string + '  %-25s\n' % ('compile the C modules')

        # Standard targets.
        string = string + '\nStandard targets:\n'
        string = string + '  %-25s%-40s\n' % ('install', 'install relax')
        string = string + '  %-25s%-40s\n' % ('uninstall', 'uninstall relax')
        string = string + '  %-25s%-40s\n' % ('binary_dist', 'create the binary distribution packages')
        string = string + '  %-25s%-40s\n' % ('source_dist', 'create the source distribution packages')
        string = string + '  %-25s%-40s\n' % ('clean', 'remove the temporary files')
        string = string + '  %-25s%-40s\n' % ('clean_all', 'remove the compiled and temporary files')
        string = string + '  %-25s%-40s\n' % ('user_manual_pdf', 'create the user manual (PDF version)')
        string = string + '  %-25s%-40s\n' % ('user_manual_pdf_nofetch', 'create the user manual (PDF version, without fetching the docstrings)')
        string = string + '  %-25s%-40s\n' % ('user_manual_html', 'create the user manual (HTML version)')
        string = string + '  %-25s%-40s\n' % ('user_manual_html_nofetch', 'create the user manual (HTML version, without fetching the docstrings)')
        string = string + '  %-25s%-40s\n' % ('api_manual_html', 'create the API documentation manual (HTML version)')

        # Specific targets.
        string = string + '\nSpecific targets:\n'
        string = string + '  %-25s%-40s\n' % ('package_bin', 'package the binary distribution')
        string = string + '  %-25s%-40s\n' % ('package_src', 'package the source distribution')
        string = string + '  %-25s%-40s\n' % ('gpg_bin', 'GPG sign the binary distribution file')
        string = string + '  %-25s%-40s\n' % ('gpg_src', 'GPG sign the source distribution file')
        string = string + '  %-25s%-40s\n' % ('version_check', 'check the relax version number')
        string = string + '  %-25s%-40s\n' % ('manual_version_file', 'create the relax version number LaTeX file')
        string = string + '  %-25s%-40s\n' % ('fetch_docstrings', 'fetch and LaTeX format the docstrings')
        string = string + '  %-25s%-40s\n' % ('compile_user_manual_pdf', 'compile the PDF version of the user manual from the LaTeX sources')
        string = string + '  %-25s%-40s\n' % ('compile_user_manual_html', 'compile the HTML version of the user manual from the LaTeX sources')
        string = string + '  %-25s%-40s\n' % ('compile_api_manual_html', 'compile the HTML version of the API documentation manual using Epydoc')
        string = string + '  %-25s%-40s\n' % ('manual_clean', 'remove the temporary manual files')
        string = string + '  %-25s%-40s\n' % ('manual_clean_nodeps', 'remove the temporary manual files (with no manual environments dependent on it)')

        # Set the help message.
        Help(string)


    def relax_fit(self):
        """Function for setting up scons for building the relaxation curve fitting C modules."""

        # The directory.
        dir = 'target_functions'

        # File names.
        files = ['c_chi2.c',
                 'exponential.c',
                 'relax_fit.c']

        # Construct the python include path (for Python.h).
        py_include_minpath = sys.prefix + path.sep + 'include'
        py_include_fullpath = py_include_minpath + path.sep + 'python' + `sys.version_info[0]` + '.' + `sys.version_info[1]`

        # Construct the python bin path.
        py_bin_minpath = sys.prefix + path.sep + 'bin'
        py_bin_fullpath = py_bin_minpath + path.sep + 'python' + `sys.version_info[0]` + '.' + `sys.version_info[1]`

        # Relaxation curve fitting build environment.
        env = Environment()

        # Determine the build architectures.
        archs = self.det_arch()

        # C flags.
        if SYSTEM == 'Windows':
            cflags = '/nologo /I\"' + py_include_minpath + '\"'
        else:
            cflags = '-I' + py_include_fullpath
        if env['PLATFORM'] == 'darwin':
            for arch in archs:
                cflags += ' -arch %s' % arch

        # Python library path.
        libpath = ''
        if SYSTEM == 'Windows':
            libpath = sys.prefix + path.sep + 'libs'

        # Add the python library path to the environment.
        env.Append(LIBPATH = libpath)

        # Catch Mac OS X and send the correct command line options to the linker (these may become redundant as SCons improves).
        if env['PLATFORM'] == 'darwin':
            # The flags.
            lnflags = [
                '-bundle',
                '-bundle_loader', py_bin_fullpath,
                '-dynamic',
                '-undefined', 'dynamic_lookup'
            ]

            # Force all architectures.
            for arch in archs:
                lnflags.append('-arch')
                lnflags.append(arch)

            # Set up the environment.
            env.Append(LINKFLAGS = lnflags)
            env['SHLINKFLAGS'] = SCons.Util.CLVar('$LINKFLAGS')

        # Shared library prefix and suffix.
        prefix = ''
        suffix = '.so'
        if SYSTEM == 'Windows':
            suffix = '.pyd'

        # Loop over the relaxation curve fitting files.
        nodes = []
        for file in files:
            nodes.append(env.SharedObject(dir + path.sep + file, CCFLAGS=cflags))

        # Build the relaxation curve fitting module.
        self.relax_fit_object = env.SharedLibrary(target=dir + path.sep + 'relax_fit', source=nodes, SHLIBPREFIX=prefix, SHLIBSUFFIX=suffix)

        # Print out string returning function.
        def print_string(target, source, env):
            string = "\n\n\n\n"
            string = string + "###########################\n"
            string = string + "# Compiling the C modules #\n"
            string = string + "###########################\n\n\n"
            string = string + "Building the relaxation curve fitting module " + `str(self.relax_fit_object[0])` + "\n"
            return string

        # Add the printout as an action to take before constructing the first object.
        env.AddPreAction(nodes[0], Action(self.dummy, print_string))


    def test_version(self, target, source, env):
        """Builder action for testing that the program version number has been set."""

        # Print out.
        print
        print("#######################################")
        print("# Checking the program version number #")
        print("#######################################\n\n")
        print("Version number: " + version + "\n")

        # Test.
        if version == "repository checkout":
            sys.stderr.write("The program version number has not been set.\n\n")
            sys.exit()

        # Final printout.
        print("\n\n\n")


# Execute the main class.
if __name__ in ['__main__', 'SCons.Script']:
    Main()
