# ===----------------------------------------------------------------------===//
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ===----------------------------------------------------------------------===//

import importlib
import locale
import os
import platform
import re
import subprocess
import sys

from libcudacxx.util import executeCommand


class DefaultTargetInfo(object):
    def __init__(self, full_config):
        self.full_config = full_config

    def platform(self):
        return sys.platform.lower().strip()

    def add_locale_features(self, features):
        self.full_config.lit_config.warning(
            "No locales entry for target_system: %s" % self.platform()
        )

    def add_cxx_compile_flags(self, flags):
        pass

    def add_cxx_link_flags(self, flags):
        pass

    def configure_env(self, env):
        pass

    def allow_cxxabi_link(self):
        return True

    def add_sanitizer_features(self, sanitizer_type, features):
        pass

    def use_lit_shell_default(self):
        return False


def test_locale(loc):
    assert loc is not None
    default_locale = locale.setlocale(locale.LC_ALL)
    try:
        locale.setlocale(locale.LC_ALL, loc)
        return True
    except locale.Error:
        return False
    finally:
        locale.setlocale(locale.LC_ALL, default_locale)


def add_common_locales(features, lit_config, is_windows=False):
    # A list of locales needed by the test-suite.
    # The list uses the canonical name for the locale used in the test-suite
    # TODO: On Linux ISO8859 *may* needs to hyphenated.
    locales = [
        ("en_US.UTF-8", "English_United States.1252"),
        ("fr_FR.UTF-8", "French_France.1252"),
        ("ru_RU.UTF-8", "Russian_Russia.1251"),
        ("zh_CN.UTF-8", "Chinese_China.936"),
        ("fr_CA.ISO8859-1", "French_Canada.1252"),
        ("cs_CZ.ISO8859-2", "Czech_Czech Republic.1250"),
    ]
    for loc_id, windows_loc_name in locales:
        loc_name = windows_loc_name if is_windows else loc_id
        if test_locale(loc_name):
            features.add("locale.{0}".format(loc_id))
        else:
            lit_config.warning(
                "The locale {0} is not supported by "
                "your platform. Some tests will be "
                "unsupported.".format(loc_name)
            )


class DarwinLocalTI(DefaultTargetInfo):
    def __init__(self, full_config):
        super(DarwinLocalTI, self).__init__(full_config)

    def is_host_macosx(self):
        name = subprocess.check_output(["sw_vers", "-productName"]).strip()
        return name == "Mac OS X"

    def get_macosx_version(self):
        assert self.is_host_macosx()
        version = subprocess.check_output(["sw_vers", "-productVersion"]).strip()
        version = re.sub(r"([0-9]+\.[0-9]+)(\..*)?", r"\1", version)
        return version

    def get_sdk_version(self, name):
        assert self.is_host_macosx()
        cmd = ["xcrun", "--sdk", name, "--show-sdk-path"]
        try:
            out = subprocess.check_output(cmd).strip()
        except OSError:
            pass

        if not out:
            self.full_config.lit_config.fatal("cannot infer sdk version with: %r" % cmd)

        return re.sub(r".*/[^0-9]+([0-9.]+)\.sdk", r"\1", out)

    def get_platform(self):
        platform = self.full_config.get_lit_conf("platform")
        if platform:
            platform = re.sub(r"([^0-9]+)([0-9\.]*)", r"\1-\2", platform)
            name, version = tuple(platform.split("-", 1))
        else:
            name = "macosx"
            version = None

        if version:
            return (False, name, version)

        # Infer the version, either from the SDK or the system itself.  For
        # macosx, ignore the SDK version; what matters is what's at
        # /usr/lib/libc++.dylib.
        if name == "macosx":
            version = self.get_macosx_version()
        else:
            version = self.get_sdk_version(name)
        return (True, name, version)

    def add_locale_features(self, features):
        add_common_locales(features, self.full_config.lit_config)

    def add_cxx_compile_flags(self, flags):
        if self.full_config.use_deployment:
            _, name, _ = self.full_config.config.deployment
            cmd = ["xcrun", "--sdk", name, "--show-sdk-path"]
        else:
            cmd = ["xcrun", "--show-sdk-path"]
        out, err, exit_code = executeCommand(cmd)
        if exit_code != 0:
            self.full_config.lit_config.warning(
                "Could not determine macOS SDK path! stderr was " + err
            )
        if exit_code == 0 and out:
            sdk_path = out.strip()
            self.full_config.lit_config.note("using SDKROOT: %r" % sdk_path)
            assert isinstance(sdk_path, str)
            flags += ["-isysroot", sdk_path]

    def add_cxx_link_flags(self, flags):
        flags += ["-lSystem"]

    def configure_env(self, env):
        library_paths = []
        # Configure the library path for libc++
        if self.full_config.cxx_runtime_root:
            library_paths += [self.full_config.cxx_runtime_root]
        elif self.full_config.use_system_cxx_lib:
            if os.path.isdir(str(self.full_config.use_system_cxx_lib)):
                library_paths += [self.full_config.use_system_cxx_lib]

        # Configure the abi library path
        if self.full_config.abi_library_root:
            library_paths += [self.full_config.abi_library_root]
        if library_paths:
            env["DYLD_LIBRARY_PATH"] = ":".join(library_paths)

    def allow_cxxabi_link(self):
        # Don't link libc++abi explicitly on OS X because the symbols
        # should be available in libc++ directly.
        return False


class FreeBSDLocalTI(DefaultTargetInfo):
    def __init__(self, full_config):
        super(FreeBSDLocalTI, self).__init__(full_config)

    def add_locale_features(self, features):
        add_common_locales(features, self.full_config.lit_config)

    def add_cxx_link_flags(self, flags):
        flags += ["-lc", "-lm", "-lpthread", "-lgcc_s", "-lcxxrt"]


class NetBSDLocalTI(DefaultTargetInfo):
    def __init__(self, full_config):
        super(NetBSDLocalTI, self).__init__(full_config)

    def add_locale_features(self, features):
        add_common_locales(features, self.full_config.lit_config)

    def add_cxx_link_flags(self, flags):
        flags += ["-lc", "-lm", "-lpthread", "-lgcc_s", "-lc++abi", "-lunwind"]


class LinuxLocalTI(DefaultTargetInfo):
    def __init__(self, full_config):
        super(LinuxLocalTI, self).__init__(full_config)

    def platform(self):
        return "linux"

    def add_locale_features(self, features):
        add_common_locales(features, self.full_config.lit_config)

    def add_cxx_compile_flags(self, flags):
        flags += [
            "-D__STDC_FORMAT_MACROS",
            "-D__STDC_LIMIT_MACROS",
            "-D__STDC_CONSTANT_MACROS",
        ]

    def add_cxx_link_flags(self, flags):
        enable_threads = (
            "libcpp-has-no-threads" not in self.full_config.config.available_features
        )
        llvm_unwinder = self.full_config.get_lit_bool("llvm_unwinder", False)
        shared_libcxx = self.full_config.get_lit_bool("enable_shared", True)
        flags += ["-lm"]
        if not llvm_unwinder:
            flags += ["-lgcc_s", "-lgcc"]
        if enable_threads:
            flags += ["-lpthread"]
            if not shared_libcxx:
                flags += ["-lrt"]
        flags += ["-lc"]
        if llvm_unwinder:
            flags += ["-lunwind", "-ldl"]
        else:
            flags += ["-lgcc_s"]
        builtins_lib = self.full_config.get_lit_conf("builtins_library")
        if builtins_lib:
            flags += [builtins_lib]
        else:
            flags += ["-lgcc"]
        use_libatomic = self.full_config.get_lit_bool("use_libatomic", False)
        if use_libatomic:
            flags += ["-latomic"]
        san = self.full_config.get_lit_conf("use_sanitizer", "").strip()
        if san:
            # The libraries and their order are taken from the
            # linkSanitizerRuntimeDeps function in
            # clang/lib/Driver/Tools.cpp
            flags += ["-lpthread", "-lrt", "-lm", "-ldl"]


class WindowsLocalTI(DefaultTargetInfo):
    def __init__(self, full_config):
        super(WindowsLocalTI, self).__init__(full_config)

    def add_locale_features(self, features):
        add_common_locales(features, self.full_config.lit_config, is_windows=True)

    def use_lit_shell_default(self):
        # Default to the internal shell on Windows, as bash on Windows is
        # usually very slow.
        return True


def make_target_info(full_config):
    default = "libcudacxx.test.target_info.LocalTI"
    info_str = full_config.get_lit_conf("target_info", default)
    if info_str != default:
        mod_path, _, info = info_str.rpartition(".")
        mod = importlib.import_module(mod_path)
        target_info = getattr(mod, info)(full_config)
        full_config.lit_config.note("inferred target_info as: %r" % info_str)
        return target_info
    target_system = platform.system()
    if target_system == "Darwin":
        return DarwinLocalTI(full_config)
    if target_system == "FreeBSD":
        return FreeBSDLocalTI(full_config)
    if target_system == "NetBSD":
        return NetBSDLocalTI(full_config)
    if target_system == "Linux":
        return LinuxLocalTI(full_config)
    if target_system == "Windows":
        return WindowsLocalTI(full_config)
    return DefaultTargetInfo(full_config)
