import os
import sys
from pathlib import Path

from cx_Freeze.darwintools import DarwinFile

from dev_tools.build.freeze.cx.freezer import Freezer


class AbstractBundlingVerifier(object):
    # verify everything is loaded from the bundle

    def __init__(self, root_dir: Path, excludes):
        self.root_dir = root_dir
        self.excludes = excludes
        self.warnings = {}
        self.pulled_from_system = False

    def get_deps_dir(self) -> Path:
        raise NotImplementedError()

    def get_jre_jpeg_dir(self) -> Path:
        return self.get_deps_dir() / ("jre/bin" if sys.platform.startswith("win") else "jre/lib")

    def _get_bundle_required_dependencies(self, file, linker_warns):
        raise NotImplementedError()

    def _get_dependent_files(self, file, linker_warns):
        raise NotImplementedError()

    def run(self):
        self.check_jre_jpeg()
        self.check_other_than_deps()
        if self.pulled_from_system or self.warnings:
            msg = "  \n".join(str(kv) for kv in self.warnings.items())
            raise Exception(f"Libs not found or pulled from system\n  {msg}")

    def check_jre_jpeg(self):
        # FIXME: improve this hacky check for libjpeg...
        if len(items := [item for item in self.get_jre_jpeg_dir().iterdir() if item.name.find("jpeg") >= 0]) != 1:
            raise Exception(f"JRE libjpeg-so not found exactly: {items}")
        if deps := [dep for dep in self._get_dependent_files(items[0], {}) if os.path.basename(dep).find("jpeg") >= 0]:
            if len(deps) > 1:
                raise Exception(f"JRE libjpeg-dep not found exactly: {deps}")
            self.check_reference_bundling(Path(deps[0]), items[0])

    def check_other_than_deps(self):
        deps_dir = self.get_deps_dir()
        for root, _, file_names in os.walk(self.root_dir):
            try:
                # TODO: skipping deps, the JRE has zlib, X.org dependencies that are unnecessary it seems, so ignoring
                Path(root).relative_to(deps_dir)
                continue
            except ValueError:
                pass
            for file_name in file_names:
                file = os.path.join(root, file_name)
                for dep_file in self._get_bundle_required_dependencies(file, self.warnings):
                    self.check_reference_bundling(dep_file, file)

    def check_reference_bundling(self, dep_file: Path, by_file):
        try:
            dep_file.parent.relative_to(self.root_dir)
        except ValueError:
            self._handle_failed_reference_bundling(dep_file, by_file)

    def _handle_failed_reference_bundling(self, dep_file: Path, by_file):
        self.pulled_from_system = True
        print(f"The {dep_file} is pulled from system by {by_file}")


if sys.platform.startswith("win"):
    from cx_Freeze.util import GetDependentFiles, BindError

    class WinBundlingVerifier(AbstractBundlingVerifier):
        def __init__(self, root_dir: Path, excludes):
            AbstractBundlingVerifier.__init__(self, root_dir, [e.lower() for e in excludes])

        def get_deps_dir(self) -> Path:
            return self.root_dir / "deps"

        def _get_bundle_required_dependencies(self, file, linker_warns):
            """
            Windows is a little different here. Dependent files are looked up in: the directory where the app is
            loaded, the system-directory, the windows-directory, the current directory and lastly directories in
            the PATH env-var.
            This means from a python process we can't force dependent file lookup independent of the running process
            itself. Eg.: The Python39.dll will always be loaded/detected from the installed interpreter's path.
            So we need to set the PATH before checking and add exemptions to the check-handling.
            """
            paths = os.pathsep.join([str(self.root_dir), str(self.root_dir / "lib")])
            for dep_file in self._get_dependent_files(file, linker_warns, paths):
                if any(str(dep_file).lower().startswith(ex) for ex in self.excludes):
                    continue
                yield dep_file

        def _handle_failed_reference_bundling(self, dep_file: Path, by_file):
            if not (self.root_dir / dep_file.name).is_file():
                AbstractBundlingVerifier._handle_failed_reference_bundling(self, dep_file, by_file)

        def _get_dependent_files(self, file, linker_warns, paths=None):
            dependent_files = set()
            if str(file).rpartition(".")[-1].lower().endswith(("exe", "dll", "pyd")):
                origPath = os.environ["PATH"]
                if not paths:
                    paths = origPath + os.pathsep + os.pathsep.join(sys.path)
                os.environ["PATH"] = paths
                try:
                    files = GetDependentFiles(file)
                except BindError as exc:
                    linker_warns[str(exc)] = file
                else:
                    dependent_files = {Path(dep) for dep in files}
                os.environ["PATH"] = origPath
            return dependent_files

    BundlingVerifier = WinBundlingVerifier
elif sys.platform.startswith("linux"):
    from dev_tools.build.freeze.cx.freezer import Patchelf

    class LinuxBundlingVerifier(AbstractBundlingVerifier):
        def __init__(self, root_dir, excludes):
            AbstractBundlingVerifier.__init__(self, root_dir, excludes)
            self.patchelf = Patchelf()

        def get_deps_dir(self) -> Path:
            return self.root_dir / "deps"

        def _get_bundle_required_dependencies(self, file, linker_warns):
            for dep_file in self._get_dependent_files(file, linker_warns):
                if Freezer.GLIBC_RE.match(dep_file.name) or any(str(dep_file).startswith(ex) for ex in self.excludes):
                    continue
                yield dep_file

        def _get_dependent_files(self, file, linker_warns):
            return self.patchelf.get_dependent_files(file)

    BundlingVerifier = LinuxBundlingVerifier
elif sys.platform.startswith("darwin"):

    class DarwinBundlingVerifier(AbstractBundlingVerifier):
        def get_deps_dir(self) -> Path:
            # NOTE: This is at the distribution stage.
            return self.root_dir / "Contents/deps"

        def _get_bundle_required_dependencies(self, file, linker_warns):
            for dep_file in self._get_dependent_files(file, linker_warns):
                if any(str(dep_file).startswith(ex) for ex in self.excludes):
                    continue
                yield dep_file

        def _get_dependent_files(self, file, linker_warns):
            if not isinstance(file, DarwinFile):
                file = DarwinFile(file)
            dep_files = []
            if file.isMachO:
                for ref in file.getMachOReferenceList():
                    if not ref.isResolved():
                        linker_warns[ref.raw_path] = ref.source_file
                        continue
                    dep_files.append(ref.resolved_path)
            return dep_files

    BundlingVerifier = DarwinBundlingVerifier

else:
    raise NotImplementedError()
