import glob
import hashlib
import os
import shutil

from conan import ConanFile
from conan.errors import ConanInvalidConfiguration
from conan.tools.apple import is_apple_os
from conan.tools.build import cross_building, stdcpp_library, check_min_cppstd
from conan.tools.env import Environment, VirtualBuildEnv
from conan.tools.files import apply_conandata_patches, copy, export_conandata_patches, get, mkdir, rename, replace_in_file, rm, rmdir, save
from conan.tools.gnu import Autotools, AutotoolsToolchain
from conan.tools.layout import basic_layout
from conan.tools.microsoft import check_min_vs, is_msvc, unix_path
from conan.tools.scm import Version

required_conan_version = ">=1.57.0"


class ICUConan(ConanFile):
    name = "icu"
    homepage = "http://site.icu-project.org"
    license = "ICU"
    description = "ICU is a mature, widely used set of C/C++ and Java libraries " \
                  "providing Unicode and Globalization support for software applications."
    url = "https://github.com/conan-io/conan-center-index"
    topics = ("icu4c", "i see you", "unicode")
    package_type = "library"
    settings = "os", "arch", "compiler", "build_type"
    options = {
        "shared": [True, False],
        "fPIC": [True, False],
        "data_packaging": ["files", "archive", "library", "static"],
        "with_dyload": [True, False],
        "dat_package_file": [None, "ANY"],
        "with_icuio": [True, False],
        "with_extras": [True, False],
    }
    default_options = {
        "shared": False,
        "fPIC": True,
        "data_packaging": "archive",
        "with_dyload": True,
        "dat_package_file": None,
        "with_icuio": True,
        "with_extras": False,
    }

    @property
    def _min_cppstd(self):
        return 17

    @property
    def _compilers_minimum_version(self):
        return {
            "gcc": "8",
            "clang": "7",
            "apple-clang": "12",
            "Visual Studio": "16",
            "msvc": "192",
        }

    @property
    def _settings_build(self):
        return getattr(self, "settings_build", self.settings)

    @property
    def _enable_icu_tools(self):
        return self.settings.os not in ["iOS", "tvOS", "watchOS", "Emscripten"]

    def export_sources(self):
        export_conandata_patches(self)

    def config_options(self):
        if self.settings.os == "Windows":
            del self.options.fPIC
            del self.options.data_packaging

    def configure(self):
        if self.options.shared:
            self.options.rm_safe("fPIC")
        if Version(self.version) >= "74.1":
            self.license = "Unicode-3.0"

    def validate(self):
        if self.options.dat_package_file:
            if not os.path.exists(str(self.options.dat_package_file)):
                raise ConanInvalidConfiguration("Non-existent dat_package_file specified")
        if Version(self.version) >= "75.1":
            if self.settings.compiler.cppstd:
                check_min_cppstd(self, self._min_cppstd)
            minimum_version = self._compilers_minimum_version.get(str(self.settings.compiler), False)
            if minimum_version and Version(self.settings.compiler.version) < minimum_version:
                raise ConanInvalidConfiguration(
                    f"{self.ref} requires C++{self._min_cppstd}, which your compiler does not support."
                )

    def layout(self):
        basic_layout(self, src_folder="src")

    @staticmethod
    def _sha256sum(file_path):
        m = hashlib.sha256()
        with open(file_path, "rb") as fh:
            for data in iter(lambda: fh.read(8192), b""):
                m.update(data)
        return m.hexdigest()

    def package_id(self):
        if self.info.options.dat_package_file:
            self.info.options.dat_package_file = self._sha256sum(str(self.info.options.dat_package_file))

    def build_requirements(self):
        if self._settings_build.os == "Windows":
            self.win_bash = True
            if not self.conf.get("tools.microsoft.bash:path", check_type=str):
                self.tool_requires("msys2/cci.latest")

        if cross_building(self) and hasattr(self, "settings_build"):
            self.tool_requires(str(self.ref))

    def source(self):
        get(self, **self.conan_data["sources"][self.version], strip_root=True)

    def generate(self):
        env = VirtualBuildEnv(self)
        env.generate()

        tc = AutotoolsToolchain(self)
        if check_min_vs(self, "180", raise_invalid=False):
            tc.extra_cflags.append("-FS")
            tc.extra_cxxflags.append("-FS")
        if Version(self.version) >= "75.1" and not self.settings.compiler.cppstd and is_msvc(self):
            tc.extra_cxxflags.append(f"-std:c++{self._min_cppstd}")
        if not self.options.shared:
            tc.extra_defines.append("U_STATIC_IMPLEMENTATION")
        if is_apple_os(self):
            tc.extra_defines.append("_DARWIN_C_SOURCE")
        if self.settings.os == "Linux":
            # $ survives bash (sourcing conanautotoolstoolchain.sh) once, then Make's own
            # $(LDFLAGS) expansion once more, so it needs escaping for both: \\ + \$ + \$
            tc.extra_ldflags.append(r"-Wl,-rpath,\\\$\$ORIGIN")
        yes_no = lambda v: "yes" if v else "no"
        tc.configure_args.extend([
            "--datarootdir=${prefix}/lib", # do not use share
            f"--enable-release={yes_no(self.settings.build_type != 'Debug')}",
            f"--enable-debug={yes_no(self.settings.build_type == 'Debug')}",
            f"--enable-dyload={yes_no(self.options.with_dyload)}",
            f"--enable-extras={yes_no(self.options.with_extras)}",
            f"--enable-icuio={yes_no(self.options.with_icuio)}",
            "--disable-layoutex",
            "--disable-layout",
            f"--enable-tools={yes_no(self._enable_icu_tools)}",
            "--disable-tests",
            "--disable-samples",
        ])
        if cross_building(self):
            base_path = unix_path(self, self.dependencies.build["icu"].package_folder)
            tc.configure_args.append(f"--with-cross-build={base_path}")
            if self.settings.os in ["iOS", "tvOS", "watchOS"]:
                # ICU build scripts interpret all Apple platforms as 'darwin'.
                # Since this can coincide with the `build` triple, we need to tweak
                # the build triple to avoid the collision and ensure the scripts
                # know we are cross-building.
                host_triplet = f"{str(self.settings.arch)}-apple-darwin"
                build_triplet = f"{str(self._settings_build.arch)}-apple"
                tc.update_configure_args({"--host": host_triplet,
                                          "--build": build_triplet})
        else:
            arch64 = ["x86_64", "sparcv9", "ppc64", "ppc64le", "armv8", "armv8.3", "mips64", "riscv64"]
            bits = "64" if self.settings.arch in arch64 else "32"
            tc.configure_args.append(f"--with-library-bits={bits}")
        if self.settings.os != "Windows":
            # http://userguide.icu-project.org/icudata
            # This is the only directly supported behavior on Windows builds.
            tc.configure_args.append(f"--with-data-packaging={self.options.data_packaging}")
        tc.generate()

        if is_msvc(self):
            env = Environment()
            env.define("CC", "cl -nologo")
            env.define("CXX", "cl -nologo")
            if cross_building(self):
                env.define("icu_cv_host_frag", "mh-msys-msvc")
            env.vars(self).save_script("conanbuild_icu_msvc")

    def _patch_sources(self):
        apply_conandata_patches(self)

        replace_in_file(
                self,
                os.path.join(self.source_folder, "source", "configure"),
                "if test -z \"$PYTHON\"",
                "if true",
        )

        if self._settings_build.os == "Windows":
            # https://unicode-org.atlassian.net/projects/ICU/issues/ICU-20545
            makeconv_cpp = os.path.join(self.source_folder, "source", "tools", "makeconv", "makeconv.cpp")
            replace_in_file(self, makeconv_cpp,
                            "pathBuf.appendPathPart(arg, localError);",
                            "pathBuf.append(\"/\", localError); pathBuf.append(arg, localError);")

        # relocatable shared libs on macOS
        mh_darwin = os.path.join(self.source_folder, "source", "config", "mh-darwin")
        replace_in_file(self, mh_darwin, "-install_name $(libdir)/$(notdir", "-install_name @rpath/$(notdir")
        replace_in_file(self,
            mh_darwin,
            "-install_name $(notdir $(MIDDLE_SO_TARGET)) $(PKGDATA_TRAILING_SPACE)",
            "-install_name @rpath/$(notdir $(MIDDLE_SO_TARGET))",
        )

        # workaround for https://unicode-org.atlassian.net/browse/ICU-20531
        mkdir(self, os.path.join(self.build_folder, "data", "out", "tmp"))

        # workaround for "No rule to make target 'out/tmp/dirs.timestamp'"
        save(self, os.path.join(self.build_folder, "data", "out", "tmp", "dirs.timestamp"), "")

    def build(self):
        self._patch_sources()

        if self.options.dat_package_file:
            dat_package_file = glob.glob(os.path.join(self.source_folder, "source", "data", "in", "*.dat"))
            if dat_package_file:
                shutil.copy(str(self.options.dat_package_file), dat_package_file[0])

        autotools = Autotools(self)
        autotools.configure(build_script_folder=os.path.join(self.source_folder, "source"))
        autotools.make()

    @property
    def _data_filename(self):
        vtag = Version(self.version).major
        arch = self.settings.get_safe("arch")
        suffix = "b" if arch in {"ppc32", "ppc64",
                                 "sparc", "sparcv9",
                                 "s390", "s390x",
                                 "mips", "mips64"} else "l"
        return f"icudt{vtag}{suffix}.dat"

    @property
    def _data_path(self):
        data_dir_name = "icu"
        if self.settings.os == "Windows" and self.settings.build_type == "Debug":
            data_dir_name += "d"
        data_dir = os.path.join(self.package_folder, "lib", data_dir_name, str(self.version))
        return os.path.join(data_dir, self._data_filename)

    def package(self):
        copy(self, "LICENSE", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses"))
        autotools = Autotools(self)
        autotools.install()

        dll_files = glob.glob(os.path.join(self.package_folder, "lib", "*.dll"))
        if dll_files:
            bin_dir = os.path.join(self.package_folder, "bin")
            mkdir(self, bin_dir)
            for dll in dll_files:
                dll_name = os.path.basename(dll)
                rm(self, dll_name, bin_dir)
                rename(self, src=dll, dst=os.path.join(bin_dir, dll_name))

        if self.settings.os != "Windows" and self.options.data_packaging in ["files", "archive"]:
            mkdir(self, os.path.join(self.package_folder, "res"))
            rename(self, src=self._data_path, dst=os.path.join(self.package_folder, "res", self._data_filename))

        # Copy some files required for cross-compiling
        config_dir = os.path.join(self.package_folder, "config")
        copy(self, "icucross.mk", src=os.path.join(self.build_folder, "config"), dst=config_dir)
        copy(self, "icucross.inc", src=os.path.join(self.build_folder, "config"), dst=config_dir)

        rmdir(self, os.path.join(self.package_folder, "lib", "icu"))
        rmdir(self, os.path.join(self.package_folder, "lib", "man"))
        rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig"))
        rmdir(self, os.path.join(self.package_folder, "share"))

    def package_info(self):
        self.cpp_info.set_property("cmake_find_mode", "both")
        self.cpp_info.set_property("cmake_file_name", "ICU")

        prefix = "s" if self.settings.os == "Windows" and not self.options.shared else ""
        suffix = "d" if self.settings.os == "Windows" and self.settings.build_type == "Debug" else ""

        # icudata
        self.cpp_info.components["icu-data"].set_property("cmake_target_name", "ICU::data")
        icudata_libname = "icudt" if self.settings.os == "Windows" else "icudata"
        self.cpp_info.components["icu-data"].libs = [f"{prefix}{icudata_libname}{suffix}"]
        if not self.options.shared:
            self.cpp_info.components["icu-data"].defines.append("U_STATIC_IMPLEMENTATION")
            # icu uses c++, so add the c++ runtime
            libcxx = stdcpp_library(self)
            if libcxx:
                self.cpp_info.components["icu-data"].system_libs.append(libcxx)

        # Alias of data CMake component
        self.cpp_info.components["icu-data-alias"].set_property("cmake_target_name", "ICU::dt")
        self.cpp_info.components["icu-data-alias"].requires = ["icu-data"]

        # icuuc
        self.cpp_info.components["icu-uc"].set_property("cmake_target_name", "ICU::uc")
        self.cpp_info.components["icu-uc"].set_property("pkg_config_name", "icu-uc")
        self.cpp_info.components["icu-uc"].libs = [f"{prefix}icuuc{suffix}"]
        self.cpp_info.components["icu-uc"].requires = ["icu-data"]
        if self.settings.os in ["Linux", "FreeBSD"]:
            self.cpp_info.components["icu-uc"].system_libs = ["m", "pthread"]
            if self.options.with_dyload:
                self.cpp_info.components["icu-uc"].system_libs.append("dl")
        elif self.settings.os == "Windows":
            self.cpp_info.components["icu-uc"].system_libs = ["advapi32"]

        # icui18n
        self.cpp_info.components["icu-i18n"].set_property("cmake_target_name", "ICU::i18n")
        self.cpp_info.components["icu-i18n"].set_property("pkg_config_name", "icu-i18n")
        icui18n_libname = "icuin" if self.settings.os == "Windows" else "icui18n"
        self.cpp_info.components["icu-i18n"].libs = [f"{prefix}{icui18n_libname}{suffix}"]
        self.cpp_info.components["icu-i18n"].requires = ["icu-uc"]
        if self.settings.os in ["Linux", "FreeBSD"]:
            self.cpp_info.components["icu-i18n"].system_libs = ["m"]

        # Alias of i18n CMake component
        self.cpp_info.components["icu-i18n-alias"].set_property("cmake_target_name", "ICU::in")
        self.cpp_info.components["icu-i18n-alias"].requires = ["icu-i18n"]

        # icuio
        if self.options.with_icuio:
            self.cpp_info.components["icu-io"].set_property("cmake_target_name", "ICU::io")
            self.cpp_info.components["icu-io"].set_property("pkg_config_name", "icu-io")
            self.cpp_info.components["icu-io"].libs = [f"{prefix}icuio{suffix}"]
            self.cpp_info.components["icu-io"].requires = ["icu-i18n", "icu-uc"]

        if self.settings.os != "Windows" and self.options.data_packaging in ["files", "archive"]:
            self.cpp_info.components["icu-data"].resdirs = ["res"]
            data_path = os.path.join(self.package_folder, "res", self._data_filename).replace("\\", "/")
            self.runenv_info.prepend_path("ICU_DATA", data_path)
            if self._enable_icu_tools or self.options.with_extras:
                self.buildenv_info.prepend_path("ICU_DATA", data_path)

        if self._enable_icu_tools:
            # icutu
            self.cpp_info.components["icu-tu"].set_property("cmake_target_name", "ICU::tu")
            self.cpp_info.components["icu-tu"].libs = [f"{prefix}icutu{suffix}"]
            self.cpp_info.components["icu-tu"].requires = ["icu-i18n", "icu-uc"]
            if self.settings.os in ["Linux", "FreeBSD"]:
                self.cpp_info.components["icu-tu"].system_libs = ["pthread"]

            # icutest
            self.cpp_info.components["icu-test"].set_property("cmake_target_name", "ICU::test")
            self.cpp_info.components["icu-test"].libs = [f"{prefix}icutest{suffix}"]
            self.cpp_info.components["icu-test"].requires = ["icu-tu", "icu-uc"]

        # TODO: to remove after conan v2
        self.cpp_info.names["cmake_find_package"] = "ICU"
        self.cpp_info.names["cmake_find_package_multi"] = "ICU"
        self.cpp_info.components["icu-data"].names["cmake_find_package"] = "data"
        self.cpp_info.components["icu-data"].names["cmake_find_package_multi"] = "data"
        self.cpp_info.components["icu-data-alias"].names["cmake_find_package"] = "dt"
        self.cpp_info.components["icu-data-alias"].names["cmake_find_package_multi"] = "dt"
        self.cpp_info.components["icu-uc"].names["cmake_find_package"] = "uc"
        self.cpp_info.components["icu-uc"].names["cmake_find_package_multi"] = "uc"
        self.cpp_info.components["icu-i18n"].names["cmake_find_package"] = "i18n"
        self.cpp_info.components["icu-i18n"].names["cmake_find_package_multi"] = "i18n"
        self.cpp_info.components["icu-i18n-alias"].names["cmake_find_package"] = "in"
        self.cpp_info.components["icu-i18n-alias"].names["cmake_find_package_multi"] = "in"
        if self.options.with_icuio:
            self.cpp_info.components["icu-io"].names["cmake_find_package"] = "io"
            self.cpp_info.components["icu-io"].names["cmake_find_package_multi"] = "io"
        if self.settings.os != "Windows" and self.options.data_packaging in ["files", "archive"]:
            self.env_info.ICU_DATA.append(data_path)
        if self._enable_icu_tools:
            self.cpp_info.components["icu-tu"].names["cmake_find_package"] = "tu"
            self.cpp_info.components["icu-tu"].names["cmake_find_package_multi"] = "tu"
            self.cpp_info.components["icu-test"].names["cmake_find_package"] = "test"
            self.cpp_info.components["icu-test"].names["cmake_find_package_multi"] = "test"
        if self._enable_icu_tools or self.options.with_extras:
            self.env_info.PATH.append(os.path.join(self.package_folder, "bin"))
