# pylint: disable=wrong-import-position,wrong-import-order,import-outside-toplevel
# This file is part of QLKNN-fortran
# You should have received the QLKNN-fortran LICENSE in the root of the project
# f2py compilation inspired by numpy setup.py
import ast
import logging
import os
import site
import subprocess
import textwrap

import sys

# Import other stdlib packages
from itertools import chain
from pathlib import Path

# Use setuptools to build packages
import setuptools
from setuptools import find_packages

# Import distutils after setuptools
from distutils.version import LooseVersion as V

setuptools_version = setuptools.__version__

# Python supported version checks. Keep right after stdlib imports to ensure we
# get a sensible error for older Python versions
if sys.version_info[:2] < (3, 6):
    raise RuntimeError("Python version >= 3.6 required.")
python_requires = ">=3.6"


# Check setuptools version before continuing for legacy builds
if V(setuptools_version) < V("42"):
    raise RuntimeError(
        "Setuptools version outdated. Found"
        f" {V(setuptools_version)} need at least {V('42')}"
    )

# Workaround for https://github.com/pypa/pip/issues/7953
# Cannot install into user site directory with editable source
site.ENABLE_USER_SITE = "--user" in sys.argv[1:]

# Put setup needed variables in env-like syntax
CLASSIFIERS = """\
Intended Audience :: Science/Research
Natural Language :: English
Programming Language :: Python :: 3
Topic :: Utilities
"""

package_name = "qlknnfort"
F90WRAP_MODULE = "qlknn_f90wrap"
AUTHOR = "Karel van de Plassche"
AUTHOR_EMAIL = "k.l.vandeplassche@differ.nl"
SOURCE_URL = "https://gitlab.com/qualikiz-group/QLKNN-fortran"


# Collect env-specific settings
this_file = Path(__file__)
this_dir = this_file.parent.resolve()


def check_submodules():
    """verify that the submodules are checked out and clean
    use `git submodule update --init`; on failure
    """
    # From Numpy
    if not os.path.exists(".git"):
        return
    with open(".gitmodules") as f:
        for line in f:
            if "path" in line:
                p = line.split("=")[-1].strip()
                if not os.path.exists(p):
                    raise ValueError("Submodule {} missing".format(p))

    proc = subprocess.Popen(["git", "submodule", "status"], stdout=subprocess.PIPE)
    status, _ = proc.communicate()
    status = status.decode("ascii", "replace")
    for line in status.splitlines():
        if line.startswith("-") or line.startswith("+"):
            raise ValueError("Submodule not clean: {}".format(line))


def generate_f2py_wrapper():
    """ Generate the f90wrap .f90 files for f2py ingestion

    Uses the TUBS makefile for a specific compiler target. No code is compiled
    for this step, so TUBS settings are irrelevant as long as the
    ``f90wrap_files`` target can be found.

    Returns:
        tuple containing paths needed for compiler later
    """
    import f90wrap  # Check if we can import f90wrap
    import numpy  # Check if we can import numpy (for f2py)

    env = os.environ.copy()

    # QLKNN is build using the TUBS build system using a Makefile
    # This will handle caching for us
    tubs_env_vars = {
        "BUILD": "release",
        "TOOLCHAIN": "gcc",
        "TUBSCFG_MPI": "0",
        "TUBSCFG_MKL": "0",
        "VERBOSE": "1",
        "TARGET_ENV": "default",
    }
    env.update(tubs_env_vars)
    env["F90WRAP_MODULE"] = F90WRAP_MODULE
    outp = subprocess.run(
        "make f90wrap_files",
        shell=True,
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    lines = outp.stdout.decode("UTF-8").split("\n")
    for line in lines:
        if line.startswith("PROJECT_SUB_QLKNN_SRCS="):
            srcs: list = line.split("=")[1].split(" ")
        elif line.startswith("BAKEDMODDEST="):
            moddest: str = line.split("=")[1]
        elif line.startswith("BAKEDBUILDDIR="):
            builddir: str = line.split("=")[1]
        elif line.startswith("PROJECT_SUB_QLKNN_OBJS="):
            qlknn_objs: list = line.split("=")[1].split(" ")

    gen_files = []
    for src in srcs:
        name = Path(src).name
        gen_name = f"f90wrap_{name}"
        gen_file = Path(package_name) / gen_name
        if not gen_file.exists():
            raise RuntimeError("Generation of {gen_name} failed")
        gen_files.append(str(gen_file))

    # We should now have a Python wrapper and autogenerated f90wrap_* files
    # in the package directory
    return gen_files, moddest, qlknn_objs


def check_lfs():
    """ Check if LFS is pulled.

    Raises:
        RuntimeError if the LFS files are not pulled
    """
    hyper_data = this_dir / "data/qlknn-hyper-namelists"
    test_file = hyper_data / "net_efiitg_gb.nml"
    if not test_file.exists():
        raise RuntimeError(f"Namelist file {test_file} does not exist. Did LFS fail?")
    with test_file.open() as f_:
        line = f_.readline()
        if line != "&sizes\n":
            raise RuntimeError(
                f"Namelist file {test_file} does not start with sizes. Did LFS fail?"
            )


def get_setup_reqs():
    """ Get list of pep-440 package specifications required to run setup.py"""
    # Legacy setuptools support, e.g. `python setup.py something`
    # See [PEP-0517](https://www.python.org/dev/peps/pep-0517/) and
    # [setuptools docs](https://setuptools.readthedocs.io/en/latest/userguide/quickstart.html#basic-use)
    # Avoid using the toml library here, it's not part of stdlib
    pyproject: list = open("pyproject.toml").readlines()
    requires_line: str = [line for line in pyproject if "requires =" in line][0]
    requires: str = requires_line.split("=", 1)[1]
    setup_requires: list = ast.literal_eval(requires.strip())
    return setup_requires


def get_txt_reqs():
    """ Get list of pep-440 package specifications specified in *.txt files"""
    reqs = {}
    for req in ["core", "develop"]:
        reqs[req]: list = open(this_dir / f"requirements_{req}.txt").readlines()
    return reqs


def get_optional_reqs():
    """ Get list of pep-440 package specifications required for extras"""
    reqs = get_txt_reqs()
    del reqs["core"]
    ## collect all optional dependencies in a "all" target
    reqs["all"] = list(chain(*reqs.values()))
    return reqs


def get_install_reqs():
    """ Get list of pep-440 package specifications mandatory during runtime"""
    reqs = get_txt_reqs()
    return reqs["core"]


def setup_package():
    """ Set up and call Numpy's setup() needed for setuptools to build dists and install """

    with open(os.path.join(this_dir, "README.md"), encoding="utf-8") as file:
        long_description = file.read()

    metadata = dict(
        name=package_name,
        description="Pythonic wrappers for the IMAS Access Layer",
        long_description=long_description,
        long_description_content_type="text/markdown",
        url=SOURCE_URL,
        author=AUTHOR,
        author_email=AUTHOR_EMAIL,
        maintainer=AUTHOR,
        maintainer_email=AUTHOR_EMAIL,
        classifiers=[_f for _f in CLASSIFIERS.split("\n") if _f],
        packages=find_packages(),
        # Include files specified by MANIFEST.in
        include_package_data=True,
        package_data={package_name: ["data/qlknn-hyper-namelists/*"]},
        # No pyproject.toml for --no-build-installation. Use setup.py instead
        use_scm_version={
            "write_to": package_name + "/version.py",
            "write_to_template": '__version__ = "{version}"',
            "relative_to": this_file,
            # For tarball installs without metadata (e.g. .git repository)
            "version_scheme": "guess-next-dev",
            "local_scheme": "no-local-version",
            "fallback_version": os.getenv(package_name.upper(), "0.0.0"),
        },
        python_requires=python_requires,
        # Duplicate from pyproject.toml for older setuptools
        setup_requires=get_setup_reqs(),
        install_requires=get_install_reqs(),
        extras_require=get_optional_reqs(),
        project_urls={
            "Bug Tracker": "https://gitlab.com/qualikiz-group/QLKNN-fortran/-/issues",
            "Documentation": "https://qualikiz-group.gitlab.io/QLKNN-fortran/",
            "Source Code": SOURCE_URL,
        },
        license="L-GPL",
        test_suite="pytest",
        zip_safe=False,  # You need disk access
    )

    if "--force" in sys.argv:
        run_build = True
        sys.argv.remove("--force")
    else:
        # Raise errors for unsupported commands, improve help output, etc.
        run_build = parse_setuppy_commands()
    run_build = True  # Always build for now. TODO: Implement pre-build package

    check_submodules()

    # Get and check QLKNN namelist files
    old_path = os.getcwd()
    os.chdir(this_dir)
    if run_build:
        subprocess.run("git lfs install", shell=True, check=True)
        subprocess.run("git lfs pull", shell=True, check=True)
    check_lfs()

    # Generate f90wrap files and create f2py Extension
    if run_build:
        wrapper_sources, mod_dest, qlknn_objs = generate_f2py_wrapper()
    else:
        os.chdir(old_path)
        raise NotImplementedError("QLKNN-fortran generate_f2py_wrapper was not ran.")
    os.chdir(old_path)

    from numpy.distutils.extension import Extension

    ext = Extension(
        name="_" + F90WRAP_MODULE,
        sources=wrapper_sources,
        include_dirs=[mod_dest],
        extra_objects=qlknn_objs,
    )
    metadata["ext_modules"] = [ext]

    from numpy.distutils.core import setup
    setup(**metadata)
    return


def parse_setuppy_commands():
    """Check the commands and respond appropriately.  Disable broken commands.

    Return a boolean value for whether or not to run the build or not (avoid
    parsing Cython and template files if False).
    """
    args = sys.argv[1:]

    if not args:
        # User forgot to give an argument probably, let setuptools handle that.
        return True

    info_commands = [
        "--help-commands",
        "--name",
        "--version",
        "-V",
        "--fullname",
        "--author",
        "--author-email",
        "--maintainer",
        "--maintainer-email",
        "--contact",
        "--contact-email",
        "--url",
        "--license",
        "--description",
        "--long-description",
        "--platforms",
        "--classifiers",
        "--keywords",
        "--provides",
        "--requires",
        "--obsoletes",
        "version",
    ]

    for command in info_commands:
        if command in args:
            return False

    good_commands = (
        "develop",
        "sdist",
        "build",
        "build_ext",  # Enhanced by numpy
        "build_clib",  # Enhanced by numpy
        "bdist_wheel",
        "build_src",
    )

    for command in good_commands:
        if command in args:
            return True

    # The following commands are supported, but we need to show more
    # useful messages to the user
    if "install" in args:
        print(
            textwrap.dedent(
                """
            Note: if you need reliable uninstall behavior, then install
            with pip instead of using `setup.py install`:

              - `pip install .`       (from a git repo or downloaded source
                                       release)
              - `pip install numpy`   (last NumPy release on PyPi)

            """
            )
        )
        return True

    if "--help" in args or "-h" in sys.argv[1]:
        print(
            textwrap.dedent(
                """
                """
            )
        )
        return False

    # The following commands aren't supported.  They can only be executed when
    # the user explicitly adds a --force command-line argument.
    bad_commands = dict(
        bdist="`setup.py bdist` is not supported",
    )

    for command in ("bdist",):
        bad_commands[command] = "`setup.py %s` is not supported" % command

    for command in bad_commands.keys():
        if command in args:
            print(
                textwrap.dedent(bad_commands[command])
                + "\nAdd `--force` to your command to use it anyway if you "
                "must (unsupported).\n"
            )
            sys.exit(1)

    # Commands that do more than print info, but also don't need Cython and
    # template parsing.
    other_commands = ["egg_info", "install_egg_info", "rotate", "dist_info"]
    for command in other_commands:
        if command in args:
            return False

    # If we got here, we didn't detect what setup.py command was given
    raise RuntimeError("Unrecognized setuptools command: {}".format(args))


if __name__ == "__main__":
    setup_package()
