"""
Functions for building libxgboost
"""

import logging
import os
import pathlib
import shutil
import subprocess
import sys
from platform import system
from typing import Optional

from .build_config import BuildConfiguration


def _lib_name() -> str:
    """Return platform dependent shared object name."""
    if system() in ["Linux", "OS400"] or system().upper().endswith("BSD"):
        name = "libxgboost.so"
    elif system() == "Darwin":
        name = "libxgboost.dylib"
    elif system() == "Windows":
        name = "xgboost.dll"
    else:
        raise NotImplementedError(f"System {system()} not supported")
    return name


def build_libxgboost(
    cpp_src_dir: pathlib.Path,
    build_dir: pathlib.Path,
    build_config: BuildConfiguration,
) -> pathlib.Path:
    """Build libxgboost in a temporary directory and obtain the path to built
    libxgboost.

    """
    logger = logging.getLogger("xgboost.packager.build_libxgboost")

    if not cpp_src_dir.is_dir():
        raise RuntimeError(f"Expected {cpp_src_dir} to be a directory")
    logger.info(
        "Building %s from the C++ source files in %s...", _lib_name(), str(cpp_src_dir)
    )

    def _build(*, generator: str) -> None:
        cmake_cmd = [
            "cmake",
            str(cpp_src_dir),
            generator,
            "-DKEEP_BUILD_ARTIFACTS_IN_BINARY_DIR=ON",
        ]
        cmake_cmd.extend(build_config.get_cmake_args())

        logger.info("CMake args: %s", str(cmake_cmd))
        subprocess.check_call(cmake_cmd, cwd=build_dir)

        if system() == "Windows":
            subprocess.check_call(
                ["cmake", "--build", ".", "--config", "Release"], cwd=build_dir
            )
        else:
            nproc = os.cpu_count()
            assert build_tool is not None
            subprocess.check_call([build_tool, f"-j{nproc}"], cwd=build_dir)

    if system() == "Windows":
        supported_generators = (
            "-GVisual Studio 17 2022",
            "-GVisual Studio 16 2019",
            "-GVisual Studio 15 2017",
            "-GMinGW Makefiles",
        )
        for generator in supported_generators:
            try:
                _build(generator=generator)
                logger.info(
                    "Successfully built %s using generator %s", _lib_name(), generator
                )
                break
            except subprocess.CalledProcessError as e:
                logger.info(
                    "Tried building with generator %s but failed with exception %s",
                    generator,
                    str(e),
                )
                # Empty build directory
                shutil.rmtree(build_dir)
                build_dir.mkdir()
        else:
            raise RuntimeError(
                "None of the supported generators produced a successful build!"
                f"Supported generators: {supported_generators}"
            )
    else:
        build_tool = "ninja" if shutil.which("ninja") else "make"
        generator = "-GNinja" if build_tool == "ninja" else "-GUnix Makefiles"
        try:
            _build(generator=generator)
        except subprocess.CalledProcessError as e:
            logger.info("Failed to build with OpenMP. Exception: %s", str(e))
            build_config.use_openmp = False
            _build(generator=generator)

    return build_dir / "lib" / _lib_name()


def locate_local_libxgboost(
    toplevel_dir: pathlib.Path,
    logger: logging.Logger,
) -> Optional[pathlib.Path]:
    """
    Locate libxgboost from the local project directory's lib/ subdirectory.
    """
    libxgboost = toplevel_dir.parent / "lib" / _lib_name()
    if libxgboost.exists():
        logger.info("Found %s at %s", libxgboost.name, str(libxgboost.parent))
        return libxgboost
    return None


def locate_or_build_libxgboost(
    toplevel_dir: pathlib.Path,
    build_dir: pathlib.Path,
    build_config: BuildConfiguration,
) -> pathlib.Path:
    """Locate libxgboost; if not exist, build it"""
    logger = logging.getLogger("xgboost.packager.locate_or_build_libxgboost")

    if build_config.use_system_libxgboost:
        # Find libxgboost from system prefix
        sys_prefix = pathlib.Path(sys.base_prefix)
        sys_prefix_candidates = [
            sys_prefix / "lib",
            # Paths possibly used on Windows
            sys_prefix / "bin",
            sys_prefix / "Library",
            sys_prefix / "Library" / "bin",
            sys_prefix / "Library" / "lib",
            sys_prefix / "Library" / "mingw-w64",
            sys_prefix / "Library" / "mingw-w64" / "bin",
            sys_prefix / "Library" / "mingw-w64" / "lib",
        ]
        sys_prefix_candidates = [
            p.expanduser().resolve() for p in sys_prefix_candidates
        ]
        for candidate_dir in sys_prefix_candidates:
            libtreelite_sys = candidate_dir / _lib_name()
            if libtreelite_sys.exists():
                logger.info("Using system XGBoost: %s", str(libtreelite_sys))
                return libtreelite_sys
        raise RuntimeError(
            f"use_system_libxgboost was specified but {_lib_name()} is "
            f"not found. Paths searched (in order): \n"
            + "\n".join([f"* {str(p)}" for p in sys_prefix_candidates])
        )

    libxgboost = locate_local_libxgboost(toplevel_dir, logger=logger)
    if libxgboost is not None:
        return libxgboost

    if toplevel_dir.joinpath("cpp_src").exists():
        # Source distribution; all C++ source files to be found in cpp_src/
        cpp_src_dir = toplevel_dir.joinpath("cpp_src")
    else:
        # Probably running "pip install ." from python-package/
        cpp_src_dir = toplevel_dir.parent
        if not cpp_src_dir.joinpath("CMakeLists.txt").exists():
            raise RuntimeError(f"Did not find CMakeLists.txt from {cpp_src_dir}")
    return build_libxgboost(cpp_src_dir, build_dir=build_dir, build_config=build_config)
