# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

from spack import *


class Dealii(CMakePackage, CudaPackage):
    """C++ software library providing well-documented tools to build finite
    element codes for a broad variety of PDEs."""

    homepage = "https://www.dealii.org"
    url      = "https://github.com/dealii/dealii/releases/download/v8.4.1/dealii-8.4.1.tar.gz"
    git      = "https://github.com/dealii/dealii.git"

    maintainers = ['davydden', 'jppelteret']

    # Don't add RPATHs to this package for the full build DAG.
    # only add for immediate deps.
    transitive_rpaths = False

    version('develop', branch='master')
    version('9.1.1', sha256='fc5b483f7fe58dfeb52d05054011280f115498e337af3e085bf272fd1fd81276')
    version('9.1.0', sha256='5b070112403f8afbb72345c1bb24d2a38d11ce58891217e353aab97957a04600')
    version('9.0.1', sha256='df2f0d666f2224be07e3741c0e8e02132fd67ea4579cd16a2429f7416146ee64')
    version('9.0.0', sha256='c918dc5c1a31d62f6eea7b524dcc81c6d00b3c378d4ed6965a708ab548944f08')
    version('8.5.1', sha256='d33e812c21a51f7e5e3d3e6af86aec343155650b611d61c1891fbc3cabce09ae')
    version('8.5.0', sha256='e6913ff6f184d16bc2598c1ba31f879535b72b6dff043e15aef048043ff1d779')
    version('8.4.2', sha256='ec7c00fadc9d298d1a0d16c08fb26818868410a9622c59ba624096872f3058e4')
    version('8.4.1', sha256='00a0e92d069cdafd216816f1aff460f7dbd48744b0d9e0da193287ebf7d6b3ad')
    version('8.4.0', sha256='36a20e097a03f17b557e11aad1400af8c6252d25f7feca40b611d5fc16d71990')
    version('8.3.0', sha256='4ddf72632eb501e1c814e299f32fc04fd680d6fda9daff58be4209e400e41779')
    version('8.2.1', sha256='d75674e45fe63cd9fa294460fe45228904d51a68f744dbb99cd7b60720f3b2a0')
    version('8.1.0', sha256='d666bbda2a17b41b80221d7029468246f2658051b8c00d9c5907cd6434c4df99')

    variant('mpi',      default=True,  description='Compile with MPI')
    variant('assimp',   default=True,
            description='Compile with Assimp')
    variant('arpack',   default=True,
            description='Compile with Arpack and PArpack (only with MPI)')
    variant('adol-c',   default=True,
            description='Compile with Adol-c')
    variant('doc',      default=False,
            description='Compile with documentation')
    variant('ginkgo',   default=True, description='Compile with Ginkgo')
    variant('gmsh',     default=True,  description='Compile with GMSH')
    variant('gsl',      default=True,  description='Compile with GSL')
    variant('hdf5',     default=True,
            description='Compile with HDF5 (only with MPI)')
    variant('metis',    default=True,  description='Compile with Metis')
    variant('muparser', default=True,  description='Compile with muParser')
    variant('nanoflann', default=True, description='Compile with Nanoflann')
    variant('netcdf',   default=True,
            description='Compile with Netcdf (only with MPI)')
    variant('oce',      default=True,  description='Compile with OCE')
    variant('p4est',    default=True,
            description='Compile with P4est (only with MPI)')
    variant('petsc',    default=True,
            description='Compile with Petsc (only with MPI)')
    variant('scalapack', default=True,
            description='Compile with ScaLAPACK (only with MPI)')
    variant('sundials', default=True,
            description='Compile with Sundials')
    variant('slepc',    default=True,
            description='Compile with Slepc (only with Petsc and MPI)')
    variant('symengine', default=True,
            description='Compile with SymEngine')
    variant('trilinos', default=True,
            description='Compile with Trilinos (only with MPI)')
    variant('python',   default=False,
            description='Compile with Python bindings')
    variant('int64',    default=False,
            description='Compile with 64 bit indices support')
    variant('optflags', default=False,
            description='Compile using additional optimization flags')
    variant('build_type', default='DebugRelease',
            description='The build type to build',
            values=('Debug', 'Release', 'DebugRelease'))

    # required dependencies, light version
    depends_on('blas')
    # Boost 1.58 is blacklisted, require at least 1.59, see
    # https://github.com/dealii/dealii/issues/1591
    # There are issues with 1.65.1 and 1.65.0:
    # https://github.com/dealii/dealii/issues/5262
    # we take the patch from https://github.com/boostorg/serialization/pull/79
    # more precisely its variation https://github.com/dealii/dealii/pull/5572#issuecomment-349742019
    # 1.68.0 has issues with serialization https://github.com/dealii/dealii/issues/7074
    # adopt https://github.com/boostorg/serialization/pull/105 as a fix
    depends_on('boost@1.59.0:1.63,1.65.1,1.67.0:+thread+system+serialization+iostreams',
               patches=[patch('boost_1.65.1_singleton.patch',
                              level=1,
                              when='@1.65.1'),
                        patch('boost_1.68.0.patch',
                              level=1,
                              when='@1.68.0'),
                       ],
               when='~python')
    depends_on('boost@1.59.0:1.63,1.65.1,1.67.0:+thread+system+serialization+iostreams+python',
               patches=[patch('boost_1.65.1_singleton.patch',
                              level=1,
                              when='@1.65.1'),
                        patch('boost_1.68.0.patch',
                              level=1,
                              when='@1.68.0'),
                       ],
               when='+python')
    # bzip2 is not needed since 9.0
    depends_on('bzip2', when='@:8.99')
    depends_on('lapack')
    depends_on('suite-sparse')
    depends_on('tbb')
    depends_on('zlib')

    # optional dependencies
    depends_on('mpi',              when='+mpi')
    depends_on('adol-c@2.6.4:',    when='@9.0:+adol-c')
    depends_on('arpack-ng+mpi',    when='+arpack+mpi')
    depends_on('assimp',           when='@9.0:+assimp')
    depends_on('doxygen+graphviz', when='+doc')
    depends_on('graphviz',         when='+doc')
    depends_on('ginkgo',           when='@9.1:+ginkgo')
    depends_on('gmsh+tetgen+netgen+oce', when='@9.0:+gmsh', type=('build', 'run'))
    depends_on('gsl',              when='@8.5.0:+gsl')
    # FIXME: next line fixes concretization with petsc
    depends_on('hdf5+mpi+hl+fortran', when='+hdf5+mpi+petsc')
    depends_on('hdf5+mpi+hl', when='+hdf5+mpi~petsc')
    depends_on('cuda@8:',          when='+cuda')
    depends_on('cmake@3.9:',       when='+cuda', type='build')
    # older version of deal.II do not build with Cmake 3.10, see
    # https://github.com/dealii/dealii/issues/5510
    depends_on('cmake@:3.9.99',    when='@:8.99', type='build')
    # FIXME: concretizer bug. The two lines mimic what comes from PETSc
    # but we should not need it
    depends_on('metis@5:+int64',   when='+metis+int64')
    depends_on('metis@5:~int64',   when='+metis~int64')
    depends_on('muparser', when='+muparser')
    depends_on('nanoflann',        when='@9.0:+nanoflann')
    depends_on('netcdf+mpi',       when='+netcdf+mpi')
    depends_on('netcdf-cxx',       when='+netcdf+mpi')
    depends_on('oce',              when='+oce')
    depends_on('p4est',            when='+p4est+mpi')
    depends_on('petsc+mpi~int64',  when='+petsc+mpi~int64')
    depends_on('petsc+mpi+int64',  when='+petsc+mpi+int64')
    depends_on('petsc@:3.6.4',     when='@:8.4.1+petsc+mpi')
    depends_on('python',           when='@8.5.0:+python')
    depends_on('scalapack',        when='@9.0:+scalapack')
    depends_on('slepc',            when='+slepc+petsc+mpi')
    depends_on('slepc@:3.6.3',     when='@:8.4.1+slepc+petsc+mpi')
    depends_on('slepc~arpack',     when='+slepc+petsc+mpi+int64')
    depends_on('sundials@:3~pthread', when='@9.0:+sundials')
    # Both Trilinos and SymEngine bundle the Teuchos RCP library.
    # This leads to conflicts between macros defined in the included
    # headers when they are not compiled in the same mode.
    # See https://github.com/symengine/symengine/issues/1516
    # FIXME: uncomment when the following is fixed
    # https://github.com/spack/spack/issues/11160
    # depends_on("symengine@0.4: build_type=Release", when="@9.1:+symengine+trilinos^trilinos~debug")  # NOQA: ignore=E501
    # depends_on("symengine@0.4: build_type=Debug", when="@9.1:+symengine+trilinos^trilinos+debug")  # NOQA: ignore=E501
    depends_on('symengine@0.4:', when='@9.1:+symengine')
    # do not require +rol to make concretization of xsdk possible
    depends_on('trilinos+amesos+aztec+epetra+ifpack+ml+muelu+sacado+teuchos',       when='+trilinos+mpi~int64~cuda')
    depends_on('trilinos+amesos+aztec+epetra+ifpack+ml+muelu+sacado+teuchos~hypre', when='+trilinos+mpi+int64~cuda')
    # FIXME: temporary disable Tpetra when using CUDA due to
    # namespace "Kokkos::Impl" has no member "cuda_abort"
    depends_on('trilinos@master+amesos+aztec+epetra+ifpack+ml+muelu+rol+sacado+teuchos~amesos2~ifpack2~intrepid2~kokkos~tpetra~zoltan2',       when='+trilinos+mpi~int64+cuda')
    depends_on('trilinos@master+amesos+aztec+epetra+ifpack+ml+muelu+rol+sacado+teuchos~hypre~amesos2~ifpack2~intrepid2~kokkos~tpetra~zoltan2', when='+trilinos+mpi+int64+cuda')

    # Explicitly provide a destructor in BlockVector,
    # otherwise deal.II may fail to build with Intel compilers.
    patch('https://github.com/dealii/dealii/commit/a89d90f9993ee9ad39e492af466b3595c06c3e25.patch',
          sha256='4282b32e96f2f5d376eb34f3fddcc4615fcd99b40004cca784eb874288d1b31c',
          when='@9.0.1')

    # https://github.com/dealii/dealii/pull/7935
    patch('https://github.com/dealii/dealii/commit/f8de8c5c28c715717bf8a086e94f071e0fe9deab.patch',
          sha256='61f217744b70f352965be265d2f06e8c1276685e2944ca0a88b7297dd55755da',
          when='@9.0.1 ^boost@1.70.0:')

    # check that the combination of variants makes sense
    # 64-bit BLAS:
    for p in ['openblas', 'intel-mkl', 'intel-parallel-studio+mkl']:
        conflicts('^{0}+ilp64'.format(p), when='@:8.5.1',
                  msg='64bit BLAS is only supported from 9.0.0')

    # interfaces added in 9.0.0:
    for p in ['assimp', 'gmsh', 'nanoflann', 'scalapack', 'sundials',
              'adol-c']:
        conflicts('+{0}'.format(p), when='@:8.5.1',
                  msg='The interface to {0} is supported from version 9.0.0 '
                      'onwards. Please explicitly disable this variant '
                      'via ~{0}'.format(p))

    # interfaces added in 9.1.0:
    for p in ['ginkgo', 'symengine']:
        conflicts('+{0}'.format(p), when='@:9.0',
                  msg='The interface to {0} is supported from version 9.1.0 '
                      'onwards. Please explicitly disable this variant '
                      'via ~{0}'.format(p))

    conflicts('+slepc', when='~petsc',
              msg='It is not possible to enable slepc interfaces '
                  'without petsc.')

    conflicts('+adol-c', when='^trilinos+chaco',
              msg='symbol clash between the ADOL-C library and '
                  'Trilinos SEACAS Chaco.')

    # interfaces added in 8.5.0:
    for p in ['gsl', 'python']:
        conflicts('+{0}'.format(p), when='@:8.4.2',
                  msg='The interface to {0} is supported from version 8.5.0 '
                      'onwards. Please explicitly disable this variant '
                      'via ~{0}'.format(p))

    # MPI requirements:
    for p in ['arpack', 'hdf5', 'netcdf', 'p4est', 'petsc', 'scalapack',
              'slepc', 'trilinos']:
        conflicts('+{0}'.format(p), when='~mpi',
                  msg='To enable {0} it is necessary to build deal.II with '
                      'MPI support enabled.'.format(p))

    def cmake_args(self):
        spec = self.spec
        options = []
        # release flags
        cxx_flags_release = []
        # debug and release flags
        cxx_flags = []

        lapack_blas_libs = spec['lapack'].libs + spec['blas'].libs
        lapack_blas_headers = spec['lapack'].headers + spec['blas'].headers
        options.extend([
            '-DDEAL_II_COMPONENT_EXAMPLES=ON',
            '-DDEAL_II_WITH_THREADS:BOOL=ON',
            '-DBOOST_DIR=%s' % spec['boost'].prefix,
            # CMake's FindBlas/Lapack may pickup system's blas/lapack instead
            # of Spack's. Be more specific to avoid this.
            # Note that both lapack and blas are provided in -DLAPACK_XYZ.
            '-DLAPACK_FOUND=true',
            '-DLAPACK_INCLUDE_DIRS=%s' % ';'.join(
                lapack_blas_headers.directories),
            '-DLAPACK_LIBRARIES=%s' % lapack_blas_libs.joined(';'),
            '-DUMFPACK_DIR=%s' % spec['suite-sparse'].prefix,
            '-DZLIB_DIR=%s' % spec['zlib'].prefix,
            '-DDEAL_II_ALLOW_BUNDLED=OFF'
        ])

        if (spec.satisfies('^intel-parallel-studio+tbb')):
            # deal.II/cmake will have hard time picking up TBB from Intel.
            tbb_ver = '.'.join(('%s' % spec['tbb'].version).split('.')[1:])
            options.extend([
                '-DTBB_FOUND=true',
                '-DTBB_VERSION=%s' % tbb_ver,
                '-DTBB_INCLUDE_DIRS=%s' % ';'.join(
                    spec['tbb'].headers.directories),
                '-DTBB_LIBRARIES=%s' % spec['tbb'].libs.joined(';')
            ])
        else:
            options.append('-DTBB_DIR=%s' % spec['tbb'].prefix)

        if (spec.satisfies('^openblas+ilp64') or
            spec.satisfies('^intel-mkl+ilp64') or
            spec.satisfies('^intel-parallel-studio+mkl+ilp64')):
            options.append('-DLAPACK_WITH_64BIT_BLAS_INDICES=ON')

        if spec.satisfies('@:8.99'):
            options.extend([
                # Cmake may still pick up system's bzip2, fix this:
                '-DBZIP2_FOUND=true',
                '-DBZIP2_INCLUDE_DIRS=%s' % spec['bzip2'].prefix.include,
                '-DBZIP2_LIBRARIES=%s' % spec['bzip2'].libs.joined(';')
            ])

        # Set recommended flags for maximum (matrix-free) performance, see
        # https://groups.google.com/forum/?fromgroups#!topic/dealii/3Yjy8CBIrgU
        if spec.satisfies('%gcc'):
            cxx_flags_release.extend(['-O3'])
        elif spec.satisfies('%intel'):
            cxx_flags_release.extend(['-O3'])
        elif spec.satisfies('%clang'):
            cxx_flags_release.extend(['-O3', '-ffp-contract=fast'])

        # Python bindings
        if spec.satisfies('@8.5.0:'):
            options.extend([
                '-DDEAL_II_COMPONENT_PYTHON_BINDINGS=%s' %
                ('ON' if '+python' in spec else 'OFF')
            ])
            if '+python' in spec:
                python_exe = spec['python'].command.path
                python_library = spec['python'].libs[0]
                python_include = spec['python'].headers.directories[0]
                options.extend([
                    '-DPYTHON_EXECUTABLE=%s' % python_exe,
                    '-DPYTHON_INCLUDE_DIR=%s' % python_include,
                    '-DPYTHON_LIBRARY=%s' % python_library
                ])

        # Set directory structure:
        if spec.satisfies('@:8.2.1'):
            options.extend(['-DDEAL_II_COMPONENT_COMPAT_FILES=OFF'])
        else:
            options.extend([
                '-DDEAL_II_EXAMPLES_RELDIR=share/deal.II/examples',
                '-DDEAL_II_DOCREADME_RELDIR=share/deal.II/',
                '-DDEAL_II_DOCHTML_RELDIR=share/deal.II/doc'
            ])

        # CUDA
        if '+cuda' in spec:
            options.append(
                '-DDEAL_II_WITH_CUDA=ON'
            )
            if not spec.satisfies('^cuda@9:'):
                options.append('-DDEAL_II_WITH_CXX14=OFF')
            cuda_arch = spec.variants['cuda_arch'].value
            if cuda_arch != 'none':
                if len(cuda_arch) > 1:
                    raise InstallError(
                        'deal.II only supports compilation for a single GPU!'
                    )
                flags = '-arch=sm_{0}'.format(cuda_arch[0])
                # FIXME: there are some compiler errors in dealii
                # with: flags = ' '.join(self.cuda_flags(cuda_arch))
                # Stick with -arch=sm_xy for now.
                options.append(
                    '-DDEAL_II_CUDA_FLAGS={0}'.format(flags)
                )
        else:
            options.extend([
                '-DDEAL_II_WITH_CUDA=OFF',
            ])

        # MPI
        if '+mpi' in spec:
            options.extend([
                '-DDEAL_II_WITH_MPI:BOOL=ON',
                '-DCMAKE_C_COMPILER=%s' % spec['mpi'].mpicc,
                '-DCMAKE_CXX_COMPILER=%s' % spec['mpi'].mpicxx,
                '-DCMAKE_Fortran_COMPILER=%s' % spec['mpi'].mpifc,
            ])
        else:
            options.extend([
                '-DDEAL_II_WITH_MPI:BOOL=OFF',
            ])

        # Optional dependencies for which library names are the same as CMake
        # variables:
        for library in (
                'gsl', 'hdf5', 'p4est', 'petsc', 'slepc', 'trilinos', 'metis',
                'sundials', 'nanoflann', 'assimp', 'gmsh', 'muparser',
                'symengine', 'ginkgo'):
            if ('+' + library) in spec:
                options.extend([
                    '-D%s_DIR=%s' % (library.upper(), spec[library].prefix),
                    '-DDEAL_II_WITH_%s:BOOL=ON' % library.upper()
                ])
            else:
                options.extend([
                    '-DDEAL_II_WITH_%s:BOOL=OFF' % library.upper()
                ])

        # adol-c
        if '+adol-c' in spec:
            options.extend([
                '-DADOLC_DIR=%s' % spec['adol-c'].prefix,
                '-DDEAL_II_WITH_ADOLC=ON'
            ])
        else:
            options.extend([
                '-DDEAL_II_WITH_ADOLC=OFF'
            ])

        # doxygen
        options.extend([
            '-DDEAL_II_COMPONENT_DOCUMENTATION=%s' %
            ('ON' if '+doc' in spec else 'OFF'),
        ])

        # arpack
        if '+arpack' in spec and '+mpi' in spec:
            options.extend([
                '-DARPACK_DIR=%s' % spec['arpack-ng'].prefix,
                '-DDEAL_II_WITH_ARPACK=ON',
                '-DDEAL_II_ARPACK_WITH_PARPACK=ON'
            ])
        else:
            options.extend([
                '-DDEAL_II_WITH_ARPACK=OFF'
            ])

        # since Netcdf is spread among two, need to do it by hand:
        if '+netcdf' in spec and '+mpi' in spec:
            netcdf = spec['netcdf-cxx'].libs + spec['netcdf'].libs
            options.extend([
                '-DNETCDF_FOUND=true',
                '-DNETCDF_LIBRARIES=%s' % netcdf.joined(';'),
                '-DNETCDF_INCLUDE_DIRS=%s;%s' % (
                    spec['netcdf-cxx'].prefix.include,
                    spec['netcdf'].prefix.include),
            ])
        else:
            options.extend([
                '-DDEAL_II_WITH_NETCDF=OFF'
            ])

        if '+scalapack' in spec:
            scalapack = spec['scalapack'].libs
            options.extend([
                '-DSCALAPACK_FOUND=true',
                '-DSCALAPACK_INCLUDE_DIRS=%s' % (
                    spec['scalapack'].prefix.include),
                '-DSCALAPACK_LIBRARIES=%s' % scalapack.joined(';'),
                '-DDEAL_II_WITH_SCALAPACK=ON'
            ])
        else:
            options.extend([
                '-DDEAL_II_WITH_SCALAPACK=OFF'
            ])

        # Open Cascade
        if '+oce' in spec:
            options.extend([
                '-DOPENCASCADE_DIR=%s' % spec['oce'].prefix,
                '-DDEAL_II_WITH_OPENCASCADE=ON'
            ])
        else:
            options.extend([
                '-DDEAL_II_WITH_OPENCASCADE=OFF'
            ])

        # 64 bit indices
        options.extend([
            '-DDEAL_II_WITH_64BIT_INDICES=%s' % ('+int64' in spec)
        ])

        # collect CXX flags:
        if len(cxx_flags_release) > 0 and '+optflags' in spec:
            options.extend([
                '-DCMAKE_CXX_FLAGS_RELEASE:STRING=%s' % (
                    ' '.join(cxx_flags_release)),
                '-DCMAKE_CXX_FLAGS:STRING=%s' % (
                    ' '.join(cxx_flags))
            ])

        return options

    def setup_environment(self, spack_env, run_env):
        run_env.set('DEAL_II_DIR', self.prefix)
