# Copyright (C) 2007-2008, ENPC - INRIA - EDF R&D
#     Author(s): Vivien Mallet
#
# This file is part of the air quality modeling system Polyphemus.
#
# Polyphemus is developed in the INRIA - ENPC joint project-team CLIME and in
# the ENPC - EDF R&D joint laboratory CEREA.
#
# Polyphemus 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 2 of the License, or (at your option)
# any later version.
#
# Polyphemus 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.
#
# For more information, visit the Polyphemus web site:
#      http://cerea.enpc.fr/polyphemus/

import distutils.sysconfig, os, glob

##############
# MAIN PATHS #
##############

# Path to Polyphemus.
try:
    polyphemus_path = create_variable("polyphemus_path", None)
    polyphemus_path = ARGUMENTS.get("polyphemus", polyphemus_path)
    polyphemus_path = os.path.abspath(polyphemus_path)
except:
    raise Exception, "\"polyphemus_path\" is not defined!"
if not os.path.isdir(polyphemus_path):
    raise Exception, "The Polyphemus path \"" + polyphemus_path \
          + "\" does not appear to be a valid path."

# Creates the include path list if needed.
include_path_list = to_string_list("include_path_list")

# Appends the other include paths.
include_path = to_string_list("include_path")
for path in include_path:
    if os.path.isdir(path):
        include_path_list.append(path)
    elif os.path.isdir(os.path.join(polyphemus_path, path)):
        include_path_list.append(os.path.join(polyphemus_path, path))
    else:
        raise Exception, "Unable to find the include directory \"" \
              + path + "\" (even in Polyphemus directory, \"" \
              + polyphemus_path + "\")."


########################
# COMPILERS AND LINKER #
########################

c_compiler = create_variable("c_compiler", None)
cpp_compiler = create_variable("cpp_compiler", None)
fortran_compiler = create_variable("fortran_compiler", None)
linker = create_variable("linker", "$CXX")


#############
# ARGUMENTS #
#############

add_argument("debug", ["0", "-1", "1", "2"])
add_argument("debug_cpp", [ARGUMENTS["debug"], "-1", "0", "1", "2"])
add_argument("debug_fortran",
             [ARGUMENTS["debug"], "-1", "0", "1", "2"])
add_argument("line", ["no", "yes"])
add_argument("mode_cpp", ["strict", "permissive"])
add_argument("mode_fortran", ["permissive", "strict"])
add_argument("_", ["1", "0", "2"])
add_argument("openmp", ["no", "yes"])
add_argument("mpi", ["no", "yes"])

flag_openmp = ARGUMENTS.get("flag_openmp", None)

# The compilers and the linker may be changed with command line options.
if ARGUMENTS["mpi"] == "yes":
    c_compiler = "mpicc"
    cpp_compiler = "mpiCC"
    fortran_compiler = "mpif90"
    linker = "mpiCC"

c_compiler = ARGUMENTS.get("c", c_compiler)
cpp_compiler = ARGUMENTS.get("cpp", cpp_compiler)
fortran_compiler = ARGUMENTS.get("fortran", fortran_compiler)
linker = ARGUMENTS.get("link", linker)

# C++-specific compilation options.
cpp_compilation_option = debug_flag("debug_cpp")
if ARGUMENTS["_"] == "1":
    cpp_compilation_option += " -DPOLYPHEMUS_SINGLE_UNDERSCORE"
elif ARGUMENTS["_"] == "2":
    cpp_compilation_option += " -DPOLYPHEMUS_DOUBLE_UNDERSCORE"

# Fortran-specific compilation options.
fortran_compilation_option = debug_flag("debug_fortran")
# Adds preprocessor to Fortran compilation.
fortran_compilation_option += " -cpp"

###############
# ENVIRONMENT #
###############

env = Environment(ENV = os.environ)
env.Replace(CONFIGURELOG = "#/.scons.log")
if os.environ.has_key("LD_LIBRARY_PATH"):
    env.Append(LIBPATH = os.environ["LD_LIBRARY_PATH"].split(":"))
if os.environ.has_key("LIBRARY_PATH"):
    env.Append(LIBPATH = os.environ["LIBRARY_PATH"].split(":"))
if os.environ.has_key("CPATH"):
    env.Append(CPPPATH = os.environ["CPATH"].split(":"))
if os.environ.has_key("CPLUS_INCLUDE_PATH"):
    env.Append(CPPPATH = os.environ["CPLUS_INCLUDE_PATH"].split(":"))

# Compilers.
if c_compiler is not None:
    env.Replace(CC = c_compiler)
if cpp_compiler is not None:
    env.Replace(CXX = cpp_compiler)
if fortran_compiler is not None:
    env.Replace(F77 = fortran_compiler)
    env.Replace(F90 = fortran_compiler)
    env.Replace(F95 = fortran_compiler)
    env.Replace(FORTRAN = fortran_compiler)

# In case of GNU compilers, a few options are added.
if "g++" in env["CXX"] and ARGUMENTS["mode_cpp"] == "strict":
    cpp_compilation_option += " -Wall -ansi -pedantic -Wno-unused" \
        + " -Wno-parentheses"
    # For latest GCC versions.
    s, o = commands.getstatusoutput(env["CXX"] + " -dumpversion")
    if s == 0:
        version = [int(x) for x in o.split('.')]
        if version >= [3, 4]:
            cpp_compilation_option += " -Wextra"
        if version >= [4, 3, 2]:
            cpp_compilation_option += " -Wno-empty-body"
if ("g77" in env["FORTRAN"] or "gfortran" in env["FORTRAN"]) \
       and ARGUMENTS["mode_fortran"] == "strict":
    fortran_compilation_option += " -Wall -pedantic"
if "g77" in env["FORTRAN"]:
    fortran_compilation_option += " -fno-second-underscore"

if ARGUMENTS["openmp"] == "yes":
    if "g++" in env["CXX"]:
        cpp_compilation_option += " -fopenmp " \
                                  "-DPOLYPHEMUS_PARALLEL_WITH_OPENMP"
    elif "icpc" in env["CXX"]:
        cpp_compilation_option += " -openmp -DPOLYPHEMUS_PARALLEL_WITH_OPENMP"
    elif ARGUMENTS.has_key("flag_openmp"):
        cpp_compilation_option += " " + ARGUMENTS["flag_openmp"] \
                                  + " -DPOLYPHEMUS_PARALLEL_WITH_OPENMP"
    else:
        print "[WARNING]: No openMP parallelization. Please use the option " \
              "flag_openmp to indicate the openMP compiling option to your " \
              "C++ compiler."
    cpp_compilation_option += " -DBZ_THREADSAFE"

    if "gfortran" in env["FORTRAN"]:
        fortran_compilation_option += " -fopenmp"
    elif "ifort" in env["FORTRAN"]:
        fortran_compilation_option += " -openmp"
    elif ARGUMENTS.has_key("flag_openmp"):
        fortran_compilation_option += " " + ARGUMENTS["flag_openmp"] \
                                      + " -DPOLYPHEMUS_PARALLEL_WITH_OPENMP"
    else:
        print "[WARNING]: No openMP parallelization. Please use the option" \
              " flag_openmp to indicate the openMP compiling option " \
              "suitable to your FORTRAN compiler."

if ARGUMENTS["mpi"] == "yes":
    cpp_compilation_option += " -DPOLYPHEMUS_PARALLEL_WITH_MPI"
    fortran_compilation_option += " -DPOLYPHEMUS_PARALLEL_WITH_MPI"

# For enabling FastJX when it has been installed.
fastjx_file = os.path.join(polyphemus_path, "include/fastJX/fastJX.f")
if os.path.isfile(fastjx_file):
    cpp_compilation_option += " -DPOLYPHEMUS_FASTJX"

# Other flags may be available.
flag_compiler = create_flag_variable("flag_compiler")
if flag_compiler:
    cpp_compilation_option += " " + flag_compiler
    fortran_compilation_option += " " + flag_compiler

flag_cpp = create_flag_variable("flag_cpp")
if flag_cpp:
    cpp_compilation_option += " " + flag_cpp

flag_fortran = create_flag_variable("flag_fortran")
if flag_fortran != "":
    fortran_compilation_option += " " + flag_fortran

# Linker.
env.Replace(LINK = linker)

# Libraries.
python_version = distutils.sysconfig.get_python_version()
def CheckLibPython(conf):
    conf.CheckLib("python" + python_version)

def CheckLibLua(conf):
    lua_name = ['lua5.1', 'lua-5.1', 'lua']
    for lua in lua_name:
        if not conf.CheckLibWithHeader(lua, 'lua.h', 'c'):
            if env.WhereIs('pkg-config'):
                # BSD and Mac requires 'lua-5.1' instead of 'lua5.1'.
                try:
                    # Mutes 'stderr' in a portable way to avoid confuse the user.
                    stderr = sys.stderr
                    sys.stderr = open(os.devnull, 'w')
                    print 'pkg-config --cflags --libs ' + lua
                    env.ParseConfig('pkg-config --cflags --libs ' + lua)
                    break
                except:
                    continue
                finally:
                    sys.stderr.close()
                    sys.stderr = stderr
    else:
        print 'Did not find Lua, should bundle it...'

custom_checklib = {
                   "python" : CheckLibPython,
                   "lua" : CheckLibLua
                   }

library_path = to_string_list("library_path")
for path in library_path:
    if os.path.isdir(path):
        env.Append(LIBPATH = [path])
    elif os.path.isdir(os.path.join(polyphemus_path, path)):
        env.Append(LIBPATH = [os.path.join(polyphemus_path, path)])
    else:
        raise Exception, "Unable to find the library directory \"" \
              + path + "\" (even in Polyphemus directory, \"" \
              + polyphemus_path + "\")."
# Checks which libraries are available.
library_list = to_string_list("library_list")
library_list += Split(ARGUMENTS.get("library_list", ""))

for library in ["m", "stdc++", "blas", "atlas", "lapack", "g2c", "gfortran",
                "gslcblas", "blitz", "newran", "netcdf", "netcdf_c++"]:
    if library not in library_list:
        library_list += [library]
conf = Configure(env)

for library in library_list:
    if custom_checklib.has_key(library):
        custom_checklib[library](conf)
    else:
        conf.CheckLib(library)

# Additional link flags.
flag_link = create_flag_variable("flag_link")

if ARGUMENTS["openmp"] == "yes":
    if "g++" in env["CXX"]:
        flag_link += " -fopenmp"
    elif "icpc" in env["CXX"]:
        flag_link += " -openmp"
    elif ARGUMENTS.has_key("flag_openmp"):
        flag_link += " " + ARGUMENTS["flag_openmp"]
    else:
        print "[WARNING]: No openMP parallelization. Please, use the " \
              "options flag_openmp to add the appropriate openMP linking " \
              "option."
env.Replace(LINKFLAGS = flag_link)

# Includes.
if "python" in library_list:
    include_path_list.append(distutils.sysconfig.get_python_inc())
env.Append(CPPPATH = include_path_list)
env.Append(F77PATH = include_path_list)
env.Append(FORTRANPATH = include_path_list)


if ARGUMENTS["line"] == "no":
    env.Replace(CCCOMSTR = "[C] $SOURCE")
    env.Replace(CXXCOMSTR = "[C++] $SOURCE")
    env.Replace(F77COMSTR = "[F77] $SOURCE")
    env.Replace(F77PPCOMSTR = "[F77-PP] $SOURCE")
    env.Replace(F90COMSTR = "[F90] $SOURCE")
    env.Replace(FORTRANCOMSTR = "[FORTRAN] $SOURCE")
    env.Replace(FORTRANPPCOMSTR = "[FORTRAN-PP] $SOURCE")
    env.Replace(LINKCOMSTR = "[Linking] $TARGET")

env.Replace(CCFLAGS = cpp_compilation_option)
env.Replace(F77FLAGS = fortran_compilation_option)
env.Replace(FORTRANFLAGS = fortran_compilation_option)
env.Replace(F90FLAGS = fortran_compilation_option)

################
# C++ PROGRAMS #
################

# The targets to be built.
target_list = to_string_list("target_list")
if not target_list:
    target_list = glob.glob("*.cpp")

# In case there is a list of targets to be excluded.
exclude_target = to_string_list("exclude_target")
for filename in target_list[:]:
    if filename in exclude_target or filename[:-4] in exclude_target:
        target_list.remove(filename)


def spack_dependency(path):
    species_file = glob.glob(os.path.join(path, "*.species")) + \
                   glob.glob(os.path.join(path, "species"))
    reactions_file = glob.glob(os.path.join(path, "*.reactions")) + \
                     glob.glob(os.path.join(path, "reactions"))
    spack_config = glob.glob(os.path.join(path, "spack_config"))
    if spack_config:
        spack_config = spack_config[0]

    if not species_file or not reactions_file:
        if spack_config:
            raise Exception, "In \"" + path + "\":\n"\
                             "Spack needs a '.species' file "\
                             "and a '.reactions' file to operate"
        return []
    if len(species_file) > 1:
        if spack_config:
            raise Exception, "In \"" + path + "\":\n"\
                             "Several species file given to Spack, "\
                             "only one is expected"
        return []
    if len(reactions_file) > 1:
        if spack_config:
            raise Exception, "In \"" + path + "\":\n"\
                             "Several reactions file given to Spack, "\
                             "only one is expected"
        return []
    if not spack_config:
            raise Exception, "In \"" + path + "\":\n"\
                    "There is a species file and reactions file, "\
                    "but Spack needs a 'spack_config' file to operate"

    spack_fortran_output = []
    for filename in ["dimensions.f", "dratedc.f", "fexchem.f",
                     "jacdchemdc.f", "kinetic.f", "rates.f"]:
        spack_fortran_output.append(os.path.join(path, filename))

    spack_config_output = [os.path.join(path, "species.spack.dat")]

    spack_input = [os.path.basename(p) for p in species_file + reactions_file ]

    spack_generator = os.path.join(polyphemus_path,
                                   "include/spack/src/generate_chem_files.sh")

    # "generate_chem_files.sh" has a weakness:
    # It removes and regenerates all the files each time.
    # But SCons is able to run it just for updating one of the output
    # file, **while trying to build the other output files in parallel**.
    # => the compilation can then fail, duh! Just rerun scons to make it build.
    env.Command(spack_fortran_output + spack_config_output,
                species_file + reactions_file,
                "cd " + path + " && " + \
                " ".join([spack_generator] + spack_input))

    # Returns Fortran files not generated yet and that would therefore
    # not be included as a dependency.
    return [ f for f in spack_fortran_output if not os.path.exists(f) ]


# Dependency list.
dependency_list = to_string_list("dependency_list")
for path in include_path_list:
    dependency_list += glob.glob(os.path.join(path, "*.f"))
    dependency_list += glob.glob(os.path.join(path, "*.F"))
    dependency_list += glob.glob(os.path.join(path, "*.f90"))
    dependency_list += glob.glob(os.path.join(path, "*.F90"))
    dependency_list += glob.glob(os.path.join(path, "*.C"))
    dependency_list += glob.glob(os.path.join(path, "*.c"))
    dependency_list += spack_dependency(path)

# In case there is a list of dependencies to be excluded.
exclude_dependency = to_string_list("exclude_dependency")
for expression in exclude_dependency + target_list:
    for dependency in dependency_list[:]:
        if re.search(expression, dependency) is not None:
            dependency_list.remove(dependency)
