import glob
import os
import subprocess
import sys
import uuid

import packaging.version
from setuptools import Extension, setup
from setuptools.command.bdist_egg import bdist_egg
from setuptools.command.build_ext import build_ext
from setuptools.command.sdist import sdist


ROOT = os.path.realpath(os.path.dirname(__file__))
METATENSOR_CORE = os.path.realpath(os.path.join(ROOT, "..", "metatensor-core"))

METATENSOR_TORCH = os.path.join(ROOT, "..", "..", "metatensor-torch")


class cmake_ext(build_ext):
    """Build the native library using cmake"""

    def run(self):
        import torch

        import metatensor

        source_dir = METATENSOR_TORCH
        build_dir = os.path.join(ROOT, "build", "cmake-build")
        install_dir = os.path.join(os.path.realpath(self.build_lib), "metatensor/torch")

        os.makedirs(build_dir, exist_ok=True)

        # Tell CMake where to find metatensor & torch
        cmake_prefix_path = [
            metatensor.utils.cmake_prefix_path,
            torch.utils.cmake_prefix_path,
        ]

        cmake_options = [
            "-DCMAKE_BUILD_TYPE=Release",
            f"-DCMAKE_INSTALL_PREFIX={install_dir}",
            f"-DCMAKE_PREFIX_PATH={';'.join(cmake_prefix_path)}",
        ]

        # ==================================================================== #
        # HACK: Torch cmake build system has a hard time finding CuDNN, so we
        # help it by pointing it to the right files

        # First try using the `nvidia.cudnn` package (dependency of torch on PyPI)
        try:
            import nvidia.cudnn

            cudnn_root = os.path.dirname(nvidia.cudnn.__file__)
        except ImportError:
            # Otherwise try to find CuDNN inside PyTorch itself
            cudnn_root = os.path.join(torch.utils.cmake_prefix_path, "..", "..")

            cudnn_version = os.path.join(cudnn_root, "include", "cudnn_version.h")
            if not os.path.exists(cudnn_version):
                # create a minimal cudnn_version.h (with a made-up version),
                # because it is not bundled together with the CuDNN shared
                # library in PyTorch conda distribution, see
                # https://github.com/pytorch/pytorch/issues/47743
                with open(cudnn_version, "w") as fd:
                    fd.write("#define CUDNN_MAJOR 8\n")
                    fd.write("#define CUDNN_MINOR 5\n")
                    fd.write("#define CUDNN_PATCHLEVEL 0\n")

        cmake_options.append(f"-DCUDNN_INCLUDE_DIR={cudnn_root}/include")
        cmake_options.append(f"-DCUDNN_LIBRARY={cudnn_root}/lib")
        # do not warn if the two variables above aren't used
        cmake_options.append("--no-warn-unused-cli")

        # end of HACK
        # ==================================================================== #

        subprocess.run(
            ["cmake", source_dir, *cmake_options],
            cwd=build_dir,
            check=True,
        )
        subprocess.run(
            [
                "cmake",
                "--build",
                build_dir,
                "--config",
                "Release",
                "--target",
                "install",
            ],
            check=True,
        )

        with open(os.path.join(install_dir, "_build_versions.py"), "w") as fd:
            fd.write("# Autogenerated file, do not edit\n\n\n")
            # Store the version of torch used to build the extension, to give a
            # nice error message to the user when trying to load the extension
            # with an older torch version installed
            fd.write(
                "# version of torch used when compiling this package\n"
                f"BUILD_TORCH_VERSION = '{torch.__version__}'\n\n"
            )

            # same for metatensor-core
            fd.write(
                "# version of metatensor-core used when compiling this package\n"
                f"BUILD_METATENSOR_CORE_VERSION = '{metatensor.__version__}'\n"
            )


class bdist_egg_disabled(bdist_egg):
    """Disabled version of bdist_egg

    Prevents setup.py install performing setuptools' default easy_install,
    which it should never ever do.
    """

    def run(self):
        sys.exit(
            "Aborting implicit building of eggs.\nUse `pip install .` or "
            "`python -m build --wheel . && pip install dist/metatensor_torch-*.whl` "
            "to install from source."
        )


class sdist_git_version(sdist):
    """
    Create a sdist with an additional generated file containing the extra
    version from git.
    """

    def run(self):
        with open("n_commits_since_last_tag", "w") as fd:
            fd.write(str(n_commits_since_last_tag()))

        # run original sdist
        super().run()

        os.unlink("n_commits_since_last_tag")


def n_commits_since_last_tag():
    """
    If git is available and we are building from a checkout, get the number of commits
    since the last tag. Otherwise, this always returns 0.
    """
    script = os.path.join(ROOT, "..", "..", "scripts", "n-commits-since-last-tag.py")
    assert os.path.exists(script)

    TAG_PREFIX = "metatensor-torch-v"
    output = subprocess.run(
        [sys.executable, script, TAG_PREFIX],
        stderr=subprocess.PIPE,
        stdout=subprocess.PIPE,
        check=True,
        encoding="utf8",
    )

    if output.stderr:
        print(output.stderr, file=sys.stderr)
        return 0
    else:
        return int(output.stdout)


def create_version_number(version):
    version = packaging.version.parse(version)

    if os.path.exists("n_commits_since_last_tag"):
        # we are building from a sdist, without git available, but the git
        # version was recorded in the `n_commits_since_last_tag` file
        with open("n_commits_since_last_tag") as fd:
            n_commits = int(fd.read().strip())
    else:
        n_commits = n_commits_since_last_tag()

    if n_commits != 0:
        # if we have commits since the last tag, this mean we are in a pre-release of
        # the next version. So we increase either the minor version number or the
        # release candidate number (if we are closing up on a release)
        if version.pre is not None:
            assert version.pre[0] == "rc"
            pre = ("rc", version.pre[1] + 1)
            release = version.release
        else:
            major, minor, patch = version.release
            release = (major, minor + 1, 0)
            pre = None

        # this is using a private API which is intended to become public soon:
        # https://github.com/pypa/packaging/pull/698. In the mean time we'll
        # use this
        version._version = version._version._replace(release=release)
        version._version = version._version._replace(pre=pre)
        version._version = version._version._replace(dev=("dev", n_commits))

    return str(version)


if __name__ == "__main__":
    if not os.path.exists(METATENSOR_TORCH):
        # we are building from a sdist, which should include metatensor-core Rust
        # sources as a tarball
        tarballs = glob.glob(os.path.join(ROOT, "metatensor-torch-*.tar.gz"))

        if not len(tarballs) == 1:
            raise RuntimeError(
                "expected a single 'metatensor-torch-*.tar.gz' file containing "
                "metatensor-torch Rust sources. remove all files and re-run "
                "scripts/package-torch.sh"
            )

        METATENSOR_TORCH = os.path.realpath(tarballs[0])
        subprocess.run(
            ["cmake", "-E", "tar", "xf", METATENSOR_TORCH],
            cwd=ROOT,
            check=True,
        )

        METATENSOR_TORCH = ".".join(METATENSOR_TORCH.split(".")[:-2])

    with open(os.path.join(METATENSOR_TORCH, "VERSION")) as fd:
        METATENSOR_TORCH_VERSION = fd.read().strip()

    with open(os.path.join(ROOT, "AUTHORS")) as fd:
        authors = fd.read().splitlines()

    if authors[0].startswith(".."):
        # handle "raw" symlink files (on Windows or from full repo tarball)
        with open(os.path.join(ROOT, authors[0])) as fd:
            authors = fd.read().splitlines()

    install_requires = ["torch >= 1.11"]

    # when packaging a release, we should never use local dependencies
    METATENSOR_NO_LOCAL_DEPS = os.environ.get("METATENSOR_NO_LOCAL_DEPS", "0") == "1"
    if not METATENSOR_NO_LOCAL_DEPS and os.path.exists(METATENSOR_CORE):
        # we are building from a git checkout

        # add a random uuid to the file url to prevent pip from using a cached
        # wheel for metatensor-core, and force it to re-build from scratch
        uuid = uuid.uuid4()
        install_requires.append(f"metatensor-core @ file://{METATENSOR_CORE}?{uuid}")
    else:
        # we are building from a sdist/installing from a wheel
        install_requires.append("metatensor-core >=0.1.0-rc2,<0.2.0")

    setup(
        version=create_version_number(METATENSOR_TORCH_VERSION),
        author=", ".join(authors),
        install_requires=install_requires,
        ext_modules=[
            Extension(name="metatensor_torch", sources=[]),
        ],
        cmdclass={
            "build_ext": cmake_ext,
            "bdist_egg": bdist_egg if "bdist_egg" in sys.argv else bdist_egg_disabled,
            "sdist": sdist_git_version,
        },
        package_data={
            "metatensor-torch": [
                "metatensor/torch/bin/*",
                "metatensor/torch/lib/*",
                "metatensor/torch/include/*",
            ]
        },
    )
