import os
import re
import sys
from pathlib import Path
from typing import Tuple, List, Union

from cx_Freeze.freezer import (
    WinFreezer as CxWinFreezer,
    LinuxFreezer as CxLinuxFreezer,
    DarwinFreezer as CxDarwinFreezer,
)
from cx_Freeze.patchelf import Patchelf as CxPatchelf


class FreezerMixin(object):
    def _pre_copy_hook(self, source: Path, target: Path) -> Tuple[Path, Path]:
        return Path(os.path.abspath(source)), Path(os.path.abspath(target))


if sys.platform.startswith("win"):

    class WinFreezer(FreezerMixin, CxWinFreezer):
        def __new__(cls, *args, **kwargs):
            return object.__new__(WinFreezer)

        def __init__(self, *args, **kwargs):
            CxWinFreezer.__init__(self, *args, **kwargs)

        def _post_copy_hook(
            self, source: Path, target: Path, copy_dependent_files: bool, include_mode: bool = False
        ) -> None:
            if copy_dependent_files and source not in self.finder.exclude_dependent_files:
                for dependent_file in CxWinFreezer._get_dependent_files(self, source):
                    target = Path(self.targetdir, os.path.basename(dependent_file))
                    if target.exists():
                        if target.read_bytes() != Path(dependent_file).read_bytes():
                            if dependent_file.as_posix().lower().endswith(
                                "/wx/vcruntime140.dll"
                            ) or dependent_file.as_posix().lower().endswith("/pyzmq.libs/msvcp140.dll"):
                                continue  # TODO: define this better
                            raise Exception(
                                f"Dependant files conflict!\n"
                                f"  Dependency source: {source}\n"
                                f"  Copy target path : {target}\n"
                                f"  New source path  : {dependent_file}"
                            )
                    else:
                        self._copy_file(dependent_file, target, copy_dependent_files)

    Freezer = WinFreezer

elif sys.platform.startswith("linux"):

    class Patchelf(CxPatchelf):
        def is_elf(self, path: Union[str, Path]) -> bool:
            if isinstance(path, str):
                path = Path(path)
            return CxPatchelf.is_elf(self, path.resolve())

    class LinuxFreezer(FreezerMixin, CxLinuxFreezer):

        GLIBC_RE = re.compile(
            r"^((ld|ld-linux-x86-64|libanl|libc|libcidn|libcrypt|libdl|libm|libnsl|libnss_compat|libnss_db|libnss_dns|"
            r"libnss_files|libnss_hesiod|libnss_nis|libnss_nisplus|libpthread|libresolv|librt|libthread_db|libutil)"
            r"(-\d+\.\d+)?\.so(\.\d+)?)$",
        )

        def __new__(cls, *args, **kwargs):
            return object.__new__(LinuxFreezer)

        def __init__(self, *args, **kwargs):
            CxLinuxFreezer.__init__(self, *args, **kwargs)
            self.patchelf = Patchelf()

        def _should_copy_file(self, path: Path) -> bool:
            if self.GLIBC_RE.match(path.name):
                return False
            return CxLinuxFreezer._should_copy_file(self, path)

        def _default_bin_path_excludes(self) -> List[str]:
            return []

        def _post_copy_hook(self, source: Path, target: Path, copy_dependent_files: bool, include_mode: bool = False):
            if copy_dependent_files and source not in self.finder.exclude_dependent_files:
                self._copy_patchelf(source, target)

        def _copy_patchelf(self, source: Path, target: Path):
            fix_rpath = set()
            for dependent_file in self._get_dependent_files(source):
                dependent_file = Path(dependent_file)
                if dependent_file.parent == source.parent or dependent_file.parent == target.parent:
                    dep_target = target.parent
                    fix_rpath.add("$ORIGIN")
                else:
                    dep_target = self.targetdir / "lib"
                    fix_rpath.add(
                        str(Path("$ORIGIN", "/".join([".."] * len(target.parent.relative_to(dep_target).parts))))
                    )
                dep_target = dep_target / dependent_file.name
                if dep_target.exists():
                    pass
                    # TODO: we can't check if the so is truly the same, because patchelf changes its content
                    # if dep_target.read_bytes() != dependent_file.read_bytes():
                    #     raise Exception(
                    #         f"Dependant files conflict!\n"
                    #         f"  Dependency source: {source}\n"
                    #         f"  Copy target path : {dep_target}\n"
                    #         f"  New source path  : {dependent_file}"
                    #     )
                else:
                    self._copy_file(dependent_file, dep_target, True)
            if fix_rpath:
                has_rpath = self.patchelf.get_rpath(target)
                rpath = ":".join(sorted(fix_rpath))
                if has_rpath != rpath:
                    self.patchelf.set_rpath(target, rpath)

        def _write_modules(self, filename, finder):
            CxLinuxFreezer._write_modules(self, filename, finder)
            # fix rpath of elf files that were copied as module data
            for dir_path, _, file_names in os.walk(self.targetdir / "lib"):
                dir_path = Path(dir_path)
                for file_name in file_names:
                    file_path = dir_path / file_name
                    if self.patchelf.is_elf(file_path):
                        self._copy_patchelf(file_path, file_path)

    Freezer = LinuxFreezer

elif sys.platform.startswith("darwin"):

    class DarwinFreezer(FreezerMixin, CxDarwinFreezer):
        def __new__(cls, *args, **kwargs):
            return object.__new__(DarwinFreezer)

        def __init__(self, *args, **kwargs):
            CxDarwinFreezer.__init__(self, *args, **kwargs)

    Freezer = DarwinFreezer

else:
    raise NotImplementedError()
