# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

import os
import shutil
import sys

from spack_repo.builtin_mock.build_systems.cmake import CMakePackage

import spack.llnl.util.tty as tty
from spack.package import *


class Hdf5(CMakePackage):
    """HDF5 is a data model, library, and file format for storing and managing
    data. It supports an unlimited variety of datatypes, and is designed for
    flexible and efficient I/O and for high volume and complex data.
    """

    homepage = "https://portal.hdfgroup.org"
    url = "https://support.hdfgroup.org/ftp/HDF5/releases/hdf5-1.10/hdf5-1.10.7/src/hdf5-1.10.7.tar.gz"
    list_url = "https://support.hdfgroup.org/ftp/HDF5/releases"
    list_depth = 3
    git = "https://github.com/HDFGroup/hdf5.git"
    maintainers(
        "lrknox",
        "brtnfld",
        "byrnHDF",
        "ChristopherHogan",
        "epourmal",
        "gheber",
        "hyoklee",
        "lkurz",
        "soumagne",
    )

    test_requires_compiler = True

    # The 'develop' version is renamed so that we could uninstall (or patch) it
    # without affecting other develop version.
    version("develop-1.13", branch="develop")
    version("develop-1.12", branch="hdf5_1_12")
    version("develop-1.10", branch="hdf5_1_10")
    version("develop-1.8", branch="hdf5_1_8")

    version("1.12.1", sha256="79c66ff67e666665369396e9c90b32e238e501f345afd2234186bfb8331081ca")
    version("1.12.0", sha256="a62dcb276658cb78e6795dd29bf926ed7a9bc4edf6e77025cd2c689a8f97c17a")
    # HDF5 1.12 broke API compatibility, so we currently prefer the latest
    # 1.10 release.  packages that want later versions of HDF5 should specify,
    # e.g., depends_on("hdf5@1.12:") to get 1.12 or higher.
    version(
        "1.10.7",
        sha256="7a1a0a54371275ce2dfc5cd093775bb025c365846512961e7e5ceaecb437ef15",
        preferred=True,
    )
    version("1.10.6", sha256="5f9a3ee85db4ea1d3b1fa9159352aebc2af72732fc2f58c96a3f0768dba0e9aa")
    version("1.10.5", sha256="6d4ce8bf902a97b050f6f491f4268634e252a63dadd6656a1a9be5b7b7726fa8")
    version("1.10.4", sha256="8f60dc4dd6ab5fcd23c750d1dc5bca3d0453bdce5c8cdaf0a4a61a9d1122adb2")
    version("1.10.3", sha256="b600d7c914cfa80ae127cd1a1539981213fee9994ac22ebec9e3845e951d9b39")
    version("1.10.2", sha256="bfec1be8c366965a99812cf02ddc97e4b708c1754fccba5414d4adccdc073866")
    version("1.10.1", sha256="048a9d149fb99aaa1680a712963f5a78e9c43b588d0e79d55e06760ec377c172")
    version(
        "1.10.0-patch1", sha256="6e78cfe32a10e6e0629393cdfddf6cfa536571efdaf85f08e35326e1b4e9eff0"
    )
    version("1.10.0", sha256="81f6201aba5c30dced5dcd62f5d5477a2790fd5850e02ac514ca8bf3e2bb375a")

    version("1.8.22", sha256="8406d96d9355ef8961d2739fb8fd5474ad4cdf52f3cfac657733defd9709bfaa")
    version("1.8.21", sha256="87d8c82eba5cf766d97cd06c054f4639c1049c4adeaa3a79f77f8bd374f80f37")
    version("1.8.19", sha256="a4335849f19fae88c264fd0df046bc321a78c536b2548fc508627a790564dc38")
    version("1.8.18", sha256="cdb195ad8d9e6782acf24b2488061289f615628c2ccda8457b0a0c3fb7a8a063")
    version("1.8.17", sha256="d9cda297ee76ade9881c4208987939250d397bae6252d0ccb66fa7d24d67e263")
    version("1.8.16", sha256="ed17178abd9928a7237f30370189ba767b9e39e0db45917c2ac4665eb9cb4771")
    version("1.8.15", sha256="4e963216b7d32469596bc1321a8c3f6e0c278dcbbdb7be6414c63c081b34c275")
    version("1.8.14", sha256="1dbefeeef7f591897c632b2b090db96bb8d35ad035beaa36bc39cb2bc67e0639")
    version("1.8.13", sha256="82f6b38eec103b4fccfbf14892786e0c27a8135d3252d8601cf5bf20066d38c1")
    version("1.8.12", sha256="b5cccea850096962b5fd9e96f22c4f47d2379224bb41130d9bc038bb6c37dfcb")
    version("1.8.10", sha256="4813b79c5fb8701a625b9924b8203bc7154a77f9b826ad4e034144b4056a160a")

    variant("shared", default=True, description="Builds a shared version of the library")

    variant("hl", default=False, description="Enable the high-level library")
    variant("cxx", default=False, description="Enable C++ support")
    variant("fortran", default=False, description="Enable Fortran support")
    variant("java", default=False, description="Enable Java support")
    variant("threadsafe", default=False, description="Enable thread-safe capabilities")
    variant("tools", default=True, description="Enable building tools")
    variant("mpi", default=True, description="Enable MPI support")
    variant("szip", default=False, description="Enable szip support")
    # Build HDF5 with API compatibility.
    variant(
        "api",
        default="default",
        description="Choose api compatibility for earlier version",
        values=("default", "v114", "v112", "v110", "v18", "v16"),
        multi=False,
    )

    depends_on("cmake@3.12:", type="build")

    depends_on("mpi", when="+mpi")
    depends_on("java", type=("build", "run"), when="+java")
    # numactl does not currently build on darwin
    if sys.platform != "darwin":
        depends_on("numactl", when="+mpi+fortran")
    depends_on("szip", when="+szip")
    depends_on("zlib-api")

    # The compiler wrappers (h5cc, h5fc, etc.) run 'pkg-config'.
    depends_on("pkgconfig", type="run")

    conflicts("api=v114", when="@1.6.0:1.12", msg="v114 is not compatible with this release")
    conflicts("api=v112", when="@1.6.0:1.10", msg="v112 is not compatible with this release")
    conflicts("api=v110", when="@1.6.0:1.8", msg="v110 is not compatible with this release")
    conflicts("api=v18", when="@1.6.0:1.6", msg="v18 is not compatible with this release")

    # The Java wrappers and associated libhdf5_java library
    # were first available in 1.10
    conflicts("+java", when="@:1.9")
    # The Java wrappers cannot be built without shared libs.
    conflicts("+java", when="~shared")

    # There are several officially unsupported combinations of the features:
    # 1. Thread safety is not guaranteed via high-level C-API but in some cases
    #    it works.
    # conflicts('+threadsafe+hl')

    # 2. Thread safety is not guaranteed via Fortran (CXX) API, but it's
    #    possible for a dependency tree to contain a package that uses Fortran
    #    (CXX) API in a single thread and another one that uses low-level C-API
    #    in multiple threads. To allow for such scenarios, we don't specify the
    #    following conflicts.
    # conflicts('+threadsafe+cxx')
    # conflicts('+threadsafe+fortran')

    # 3. Parallel features are not supported via CXX API, but for the reasons
    #    described in #2 we allow for such combination.
    # conflicts('+mpi+cxx')

    # There are known build failures with intel@18.0.1. This issue is
    # discussed and patch is provided at
    # https://software.intel.com/en-us/forums/intel-fortran-compiler-for-linux-and-mac-os-x/topic/747951.
    patch("h5f90global-mult-obj-same-equivalence-same-common-block.patch", when="@1.10.1%intel@18")

    # Turn line comments into block comments to conform with pre-C99 language
    # standards. Versions of hdf5 after 1.8.10 don't require this patch,
    # either because they conform to pre-C99 or neglect to ask for pre-C99
    # language standards from their compiler. The hdf5 build system adds
    # the -ansi cflag (run 'man gcc' for info on -ansi) for some versions
    # of some compilers (see hdf5-1.8.10/config/gnu-flags). The hdf5 build
    # system does not provide an option to disable -ansi, but since the
    # pre-C99 code is restricted to just five lines of line comments in
    # three src files, this patch accomplishes the simple task of patching the
    # three src files and leaves the hdf5 build system alone.
    patch("pre-c99-comments.patch", when="@1.8.10")

    # There are build errors with GCC 8, see
    # https://forum.hdfgroup.org/t/1-10-2-h5detect-compile-error-gcc-8-1-0-on-centos-7-2-solved/4441
    patch(
        "https://salsa.debian.org/debian-gis-team/hdf5/raw/bf94804af5f80f662cad80a5527535b3c6537df6/debian/patches/gcc-8.patch",
        sha256="57cee5ff1992b4098eda079815c36fc2da9b10e00a9056df054f2384c4fc7523",
        when="@1.10.2%gcc@8:",
    )

    # Disable MPI C++ interface when C++ is disabled, otherwise downstream
    # libraries fail to link; see https://github.com/spack/spack/issues/12586
    patch(
        "h5public-skip-mpicxx.patch",
        when="@1.8.10:1.8.21,1.10.0:1.10.5+mpi~cxx",
        sha256="b61e2f058964ad85be6ee5ecea10080bf79e73f83ff88d1fa4b602d00209da9c",
    )

    # Fixes BOZ literal constant error when compiled with GCC 10.
    # The issue is described here: https://github.com/spack/spack/issues/18625
    patch(
        "hdf5_1.8_gcc10.patch",
        when="@:1.8.21",
        sha256="0e20187cda3980a4fdff410da92358b63de7ebef2df1d7a425371af78e50f666",
    )

    # The argument 'buf_size' of the C function 'h5fget_file_image_c' is
    # declared as intent(in) though it is modified by the invocation. As a
    # result, aggressive compilers such as Fujitsu's may do a wrong
    # optimization to cause an error.
    def patch(self):
        filter_file(
            "INTEGER(SIZE_T), INTENT(IN) :: buf_size",
            "INTEGER(SIZE_T), INTENT(OUT) :: buf_size",
            "fortran/src/H5Fff.F90",
            string=True,
            ignore_absent=True,
        )
        filter_file(
            "INTEGER(SIZE_T), INTENT(IN) :: buf_size",
            "INTEGER(SIZE_T), INTENT(OUT) :: buf_size",
            "fortran/src/H5Fff_F03.f90",
            string=True,
            ignore_absent=True,
        )

    # The parallel compiler wrappers (i.e. h5pcc, h5pfc, etc.) reference MPI
    # compiler wrappers and do not need to be changed.
    filter_compiler_wrappers(
        "h5cc", "h5hlcc", "h5fc", "h5hlfc", "h5c++", "h5hlc++", relative_root="bin"
    )

    def url_for_version(self, version):
        url = (
            "https://support.hdfgroup.org/ftp/HDF5/releases/hdf5-{0}/hdf5-{1}/src/hdf5-{1}.tar.gz"
        )
        return url.format(version.up_to(2), version)

    def flag_handler(self, name, flags):
        cmake_flags = []

        if name == "cflags":
            if self.spec.satisfies("%gcc") or self.spec.satisfies("%clang"):
                # Quiet warnings/errors about implicit declaration of functions
                # in C99:
                cmake_flags.append("-Wno-implicit-function-declaration")
                # Note that this flag will cause an error if building %nvhpc.
            if self.spec.satisfies("@:1.8.12~shared"):
                # More recent versions set CMAKE_POSITION_INDEPENDENT_CODE to
                # True and build with PIC flags.
                cmake_flags.append(self.compiler.cc_pic_flag)
        elif name == "cxxflags":
            if self.spec.satisfies("@:1.8.12+cxx~shared"):
                cmake_flags.append(self.compiler.cxx_pic_flag)
        elif name == "fflags":
            if self.spec.satisfies("@:1.8.12+fortran~shared"):
                cmake_flags.append(self.compiler.fc_pic_flag)
        elif name == "ldlibs":
            if "+fortran %fj" in self.spec:
                cmake_flags.extend(["-lfj90i", "-lfj90f", "-lfjsrcinfo", "-lelf"])

        return flags, None, (cmake_flags or None)

    @property
    def libs(self):
        """HDF5 can be queried for the following parameters:

        - "hl": high-level interface
        - "cxx": C++ APIs
        - "fortran": Fortran APIs
        - "java": Java APIs

        :return: list of matching libraries
        """
        query_parameters = self.spec.last_query.extra_parameters

        shared = "+shared" in self.spec

        # This map contains a translation from query_parameters
        # to the libraries needed
        query2libraries = {
            tuple(): ["libhdf5"],
            ("cxx", "fortran", "hl", "java"): [
                # When installed with Autotools, the basename of the real
                # library file implementing the High-level Fortran interface is
                # 'libhdf5hl_fortran'. Starting versions 1.8.22, 1.10.5 and
                # 1.12.0, the Autotools installation also produces a symbolic
                # link 'libhdf5_hl_fortran.<so/a>' to
                # 'libhdf5hl_fortran.<so/a>'. Note that in the case of the
                # dynamic library, the latter is a symlink to the real sonamed
                # file 'libhdf5_fortran.so.<abi-version>'. This means that all
                # dynamically linked executables/libraries of the dependent
                # packages need 'libhdf5_fortran.so.<abi-version>' with the same
                # DT_SONAME entry. However, the CMake installation (at least
                # starting version 1.8.10) does not produce it. Instead, the
                # basename of the library file is 'libhdf5_hl_fortran'. Which
                # means that switching to CMake requires rebuilding of all
                # dependant packages that use the High-level Fortran interface.
                # Therefore, we do not try to preserve backward compatibility
                # with Autotools installations by creating symlinks. The only
                # packages that could benefit from it would be those that
                # hardcode the library name in their building systems. Such
                # packages should simply be patched.
                "libhdf5_hl_fortran",
                "libhdf5_hl_f90cstub",
                "libhdf5_hl_cpp",
                "libhdf5_hl",
                "libhdf5_fortran",
                "libhdf5_f90cstub",
                "libhdf5_java",
                "libhdf5",
            ],
            ("cxx", "hl"): ["libhdf5_hl_cpp", "libhdf5_hl", "libhdf5"],
            ("fortran", "hl"): [
                "libhdf5_hl_fortran",
                "libhdf5_hl_f90cstub",
                "libhdf5_hl",
                "libhdf5_fortran",
                "libhdf5_f90cstub",
                "libhdf5",
            ],
            ("hl",): ["libhdf5_hl", "libhdf5"],
            ("cxx", "fortran"): ["libhdf5_fortran", "libhdf5_f90cstub", "libhdf5_cpp", "libhdf5"],
            ("cxx",): ["libhdf5_cpp", "libhdf5"],
            ("fortran",): ["libhdf5_fortran", "libhdf5_f90cstub", "libhdf5"],
            ("java",): ["libhdf5_java", "libhdf5"],
        }

        # Turn the query into the appropriate key
        key = tuple(sorted(query_parameters))
        libraries = query2libraries[key]

        return find_libraries(libraries, root=self.prefix, shared=shared, recursive=True)

    @when("@:1.8.21,1.10.0:1.10.5+szip")
    def setup_build_environment(self, env: EnvironmentModifications) -> None:
        env.set("SZIP_INSTALL", self.spec["szip"].prefix)

    @run_before("cmake")
    def fortran_check(self):
        if "+fortran" in self.spec and not self.compiler.fc:
            msg = "cannot build a Fortran variant without a Fortran compiler"
            raise RuntimeError(msg)

    def cmake_args(self):
        spec = self.spec

        if spec.satisfies("@:1.8.15+shared"):
            tty.warn("hdf5@:1.8.15+shared does not produce static libraries")

        args = [
            # Always enable this option. This does not actually enable any
            # features: it only *allows* the user to specify certain
            # combinations of other arguments.
            self.define("ALLOW_UNSUPPORTED", True),
            # Speed-up the building by skipping the examples:
            self.define("HDF5_BUILD_EXAMPLES", False),
            self.define(
                "BUILD_TESTING",
                self.run_tests or
                # Version 1.8.22 fails to build the tools when shared libraries
                # are enabled but the tests are disabled.
                spec.satisfies("@1.8.22+shared+tools"),
            ),
            self.define("HDF5_ENABLE_Z_LIB_SUPPORT", True),
            self.define_from_variant("HDF5_ENABLE_SZIP_SUPPORT", "szip"),
            self.define_from_variant("HDF5_ENABLE_SZIP_ENCODING", "szip"),
            self.define_from_variant("BUILD_SHARED_LIBS", "shared"),
            self.define("ONLY_SHARED_LIBS", False),
            self.define_from_variant("HDF5_ENABLE_PARALLEL", "mpi"),
            self.define_from_variant("HDF5_ENABLE_THREADSAFE", "threadsafe"),
            self.define_from_variant("HDF5_BUILD_HL_LIB", "hl"),
            self.define_from_variant("HDF5_BUILD_CPP_LIB", "cxx"),
            self.define_from_variant("HDF5_BUILD_FORTRAN", "fortran"),
            self.define_from_variant("HDF5_BUILD_JAVA", "java"),
            self.define_from_variant("HDF5_BUILD_TOOLS", "tools"),
        ]

        api = spec.variants["api"].value
        if api != "default":
            args.append(self.define("DEFAULT_API_VERSION", api))

        if "+mpi" in spec:
            args.append(self.define("CMAKE_C_COMPILER", spec["mpi"].mpicc))

            if "+cxx" in self.spec:
                args.append(self.define("CMAKE_CXX_COMPILER", spec["mpi"].mpicxx))

            if "+fortran" in self.spec:
                args.append(self.define("CMAKE_Fortran_COMPILER", spec["mpi"].mpifc))

        return args

    @run_after("install")
    def ensure_parallel_compiler_wrappers(self):
        # When installed with Autotools and starting at least version 1.8.10,
        # the package produces C compiler wrapper called either 'h5cc' (when MPI
        # support is disabled) or 'h5pcc' (when MPI support is enabled). The
        # CMake installation produces the wrapper called 'h5cc' (regardless of
        # whether MPI support is enabled) only starting versions 1.8.21, 1.10.2
        # and 1.12.0. The current develop versions also produce 'h5pcc' when MPI
        # support is enabled and the file is identical to 'h5cc'. Here, we make
        # sure that 'h5pcc' is available when MPI support is enabled (only for
        # versions that generate 'h5cc').
        if self.spec.satisfies("@1.8.21:1.8.22,1.10.2:1.10.7,1.12.0+mpi"):
            with working_dir(self.prefix.bin):
                # No try/except here, fix the condition above instead:
                symlink("h5cc", "h5pcc")

        # The same as for 'h5pcc'. However, the CMake installation produces the
        # Fortran compiler wrapper called 'h5fc' only starting versions 1.8.22,
        # 1.10.6 and 1.12.0. The current develop versions do not produce 'h5pfc'
        # at all. Here, we make sure that 'h5pfc' is available when Fortran and
        # MPI support are enabled (only for versions that generate 'h5fc').
        if self.spec.satisfies(
            "@1.8.22:1.8," "1.10.6:1.10," "1.12.0:1.12," "develop:" "+fortran+mpi"
        ):
            with working_dir(self.prefix.bin):
                # No try/except here, fix the condition above instead:
                symlink("h5fc", "h5pfc")

    @run_after("install")
    def fix_package_config(self):
        # We need to fix the pkg-config files, which are also used by the
        # compiler wrappers. The files are created starting versions 1.8.21,
        # 1.10.2 and 1.12.0. However, they are broken (except for the version
        # 1.8.22): the files are named <name>-<version>.pc but reference <name>
        # packages. This was fixed in the develop versions at some point: the
        # files started referencing <name>-<version> packages but got broken
        # again: the files got names <name>.pc but references had not been
        # updated accordingly. Another issue, which we address here, is that
        # some Linux distributions install pkg-config files named hdf5.pc and we
        # want to override them. Therefore, the following solution makes sure
        # that each <name>-<version>.pc file is symlinked by <name>.pc and all
        # references to <name>-<version> packages in the original files are
        # replaced with references to <name> packages.
        pc_files = find(self.prefix.lib.pkgconfig, "hdf5*.pc", recursive=False)

        if not pc_files:
            # This also tells us that the pkgconfig directory does not exist.
            return

        # Replace versioned references in all pkg-config files:
        filter_file(
            r"(Requires(?:\.private)?:.*)(hdf5[^\s,]*)(?:-[^\s,]*)(.*)",
            r"\1\2\3",
            *pc_files,
            backup=False,
        )

        # Create non-versioned symlinks to the versioned pkg-config files:
        with working_dir(self.prefix.lib.pkgconfig):
            for f in pc_files:
                src_filename = os.path.basename(f)
                version_sep_idx = src_filename.find("-")
                if version_sep_idx > -1:
                    tgt_filename = src_filename[:version_sep_idx] + ".pc"
                    if not os.path.exists(tgt_filename):
                        symlink(src_filename, tgt_filename)

    @run_after("install")
    @on_package_attributes(run_tests=True)
    def check_install(self):
        self._check_install()

    def _check_install(self):
        # Build and run a small program to test the installed HDF5 library
        spec = self.spec
        print("Checking HDF5 installation...")
        checkdir = "spack-check"
        with working_dir(checkdir, create=True):
            source = r"""
#include <hdf5.h>
#include <assert.h>
#include <stdio.h>
int main(int argc, char **argv) {
  unsigned majnum, minnum, relnum;
  herr_t herr = H5get_libversion(&majnum, &minnum, &relnum);
  assert(!herr);
  printf("HDF5 version %d.%d.%d %u.%u.%u\n", H5_VERS_MAJOR, H5_VERS_MINOR,
         H5_VERS_RELEASE, majnum, minnum, relnum);
  return 0;
}
"""
            expected = """\
HDF5 version {version} {version}
""".format(
                version=str(spec.version.up_to(3))
            )
            with open("check.c", "w", encoding="utf-8") as f:
                f.write(source)
            if "+mpi" in spec:
                cc = Executable(spec["mpi"].mpicc)
            else:
                cc = Executable(self.compiler.cc)
            cc(*(["-c", "check.c"] + spec["hdf5"].headers.cpp_flags.split()))
            cc(*(["-o", "check", "check.o"] + spec["hdf5"].libs.ld_flags.split()))
            try:
                check = Executable("./check")
                output = check(output=str)
            except ProcessError:
                output = ""
            success = output == expected
            if not success:
                print("Produced output does not match expected output.")
                print("Expected output:")
                print("-" * 80)
                print(expected)
                print("-" * 80)
                print("Produced output:")
                print("-" * 80)
                print(output)
                print("-" * 80)
                raise RuntimeError("HDF5 install check failed")
        shutil.rmtree(checkdir)

    def _test_check_versions(self):
        """Perform version checks on selected installed package binaries."""
        spec_vers_str = "Version {0}".format(self.spec.version)

        exes = [
            "h5copy",
            "h5diff",
            "h5dump",
            "h5format_convert",
            "h5ls",
            "h5mkgrp",
            "h5repack",
            "h5stat",
            "h5unjam",
        ]
        use_short_opt = ["h52gif", "h5repart", "h5unjam"]
        for exe in exes:
            reason = "test: ensuring version of {0} is {1}".format(exe, spec_vers_str)
            option = "-V" if exe in use_short_opt else "--version"
            self.run_test(
                exe, option, spec_vers_str, installed=True, purpose=reason, skip_missing=True
            )

    def _test_example(self):
        """This test performs copy, dump, and diff on an example hdf5 file."""
        test_data_dir = self.test_suite.current_test_data_dir

        filename = "spack.h5"
        h5_file = test_data_dir.join(filename)

        reason = "test: ensuring h5dump produces expected output"
        expected = get_escaped_text_output(test_data_dir.join("dump.out"))
        self.run_test(
            "h5dump",
            filename,
            expected,
            installed=True,
            purpose=reason,
            skip_missing=True,
            work_dir=test_data_dir,
        )

        reason = "test: ensuring h5copy runs"
        options = ["-i", h5_file, "-s", "Spack", "-o", "test.h5", "-d", "Spack"]
        self.run_test(
            "h5copy", options, [], installed=True, purpose=reason, skip_missing=True, work_dir="."
        )

        reason = "test: ensuring h5diff shows no differences between orig and" " copy"
        self.run_test(
            "h5diff",
            [h5_file, "test.h5"],
            [],
            installed=True,
            purpose=reason,
            skip_missing=True,
            work_dir=".",
        )

    def test(self):
        """Perform smoke tests on the installed package."""
        # Simple version check tests on known binaries
        self._test_check_versions()

        # Run sequence of commands on an hdf5 file
        self._test_example()

        # Run existing install check
        self._check_install()
