#     Copyright 2021, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#
""" Standard plug-in to tell Nuitka about DLLs needed for standalone imports.

When DLLs are imported, we cannot see this and need to be told that. This
encodes the knowledge we have for various modules. Feel free to add to this
and submit patches to make it more complete.
"""

import os
import re
import sys

from nuitka.Options import isStandaloneMode
from nuitka.plugins.PluginBase import NuitkaPluginBase
from nuitka.utils.FileOperations import listDir
from nuitka.utils.SharedLibraries import getPyWin32Dir
from nuitka.utils.Utils import isLinux, isWin32Windows
from nuitka.utils.Yaml import parsePackageYaml


class NuitkaPluginDllFiles(NuitkaPluginBase):
    plugin_name = "dll-files"

    def __init__(self):
        self.config = parsePackageYaml(__package__, "dll-files.yml")

    @staticmethod
    def isAlwaysEnabled():
        return True

    @staticmethod
    def isRelevant():
        return isStandaloneMode()

    def _handleDllConfig(self, dll_config, full_name, count):
        config_found = False

        if "include_from_code" in dll_config:
            config_found = True

            setup_codes = dll_config.get("setup_code")
            dll_filename_code = dll_config.get("dll_filename_code")
            dest_path = dll_config.get("dest_path")

            dll_filename = self.queryRuntimeInformationMultiple(
                "%s_%s" % (full_name.asString().replace(".", "_"), count),
                setup_codes=setup_codes,
                values=(("dll_filename", dll_filename_code),),
            ).dll_filename

            module_filename = self.locateModule(full_name)

            yield self.makeDllEntryPoint(
                source_path=dll_filename,
                dest_path=os.path.join(
                    dest_path,
                    os.path.relpath(dll_filename, os.path.dirname(module_filename)),
                ),
                package_name=full_name,
            )

        if "include_from_filenames" in dll_config:
            config_found = True

            module_filename = self.locateModule(full_name)

            dll_dir = dll_config.get("dll_dir", ".")
            dll_dir = os.path.normpath(os.path.join(module_filename, dll_dir))

            dest_path = dll_config.get("dest_path")

            # TODO: Rather than using listDir, we should have a function that
            # gives all DLLs below a folder.
            for pattern in dll_config.get("patterns"):
                pattern = pattern + r"\.(?:dll|so|dylib)"
                regexp = re.compile(pattern, re.IGNORECASE)

                for dll_filename, filename in listDir(dll_dir):
                    if not regexp.match(filename):
                        continue
                    yield self.makeDllEntryPoint(
                        source_path=dll_filename,
                        dest_path=os.path.join(
                            dest_path,
                            filename,
                        ),
                        package_name=full_name,
                    )

        if not config_found:
            self.sysexit(
                "Unsupported config for module '%s' encountered." % full_name.asString()
            )

    def _handleDllConfigs(self, config, full_name):
        dll_configs = config.get("dlls")

        if dll_configs:
            if type(dll_configs) is not list or not dll_configs:
                self.sysexit(
                    "Error, requiring list below 'dlls' entry for '%s' entry."
                    % full_name
                )

            found = 0

            for count, dll_config in enumerate(dll_configs, start=1):
                for dll_entry_point in self._handleDllConfig(
                    dll_config=dll_config, full_name=full_name, count=count
                ):
                    yield dll_entry_point
                    found += 1

            self.reportFileCount(full_name, found)

    def getExtraDlls(self, module):
        full_name = module.getFullName()

        # Checking for config, but also allowing fall through for cases that have to
        # have some code still here.
        config = self.config.get(full_name)
        if config:
            for dll_entry_point in self._handleDllConfigs(
                config=config, full_name=full_name
            ):
                yield dll_entry_point

        # TODO: This is legacy code, ideally moved to yaml config over time.
        if full_name == "uuid" and isLinux():
            uuid_dll_path = self.locateDLL("uuid")

            if uuid_dll_path is not None:
                yield self.makeDllEntryPoint(
                    uuid_dll_path, os.path.basename(uuid_dll_path), None
                )
        elif full_name == "iptc" and isLinux():
            import iptc.util  # pylint: disable=I0021,import-error

            xtwrapper_dll = iptc.util.find_library("xtwrapper")[0]
            xtwrapper_dll_path = xtwrapper_dll._name  # pylint: disable=protected-access

            yield self.makeDllEntryPoint(
                xtwrapper_dll_path, os.path.basename(xtwrapper_dll_path), None
            )
        elif full_name == "coincurve._libsecp256k1" and isWin32Windows():
            yield self.makeDllEntryPoint(
                os.path.join(module.getCompileTimeDirectory(), "libsecp256k1.dll"),
                os.path.join(full_name.getPackageName(), "libsecp256k1.dll"),
                full_name.getPackageName(),
            )
        # TODO: This should be its own plugin.
        elif (
            full_name
            in (
                "pythoncom",
                "win32api",
                "win32clipboard",
                "win32console",
                "win32cred",
                "win32crypt",
                "win32event",
                "win32evtlog",
                "win32file",
                "win32gui",
                "win32help",
                "win32inet",
                "win32job",
                "win32lz",
                "win32net",
                "win32pdh",
                "win32pipe",
                "win32print",
                "win32process",
                "win32profile",
                "win32ras",
                "win32security",
                "win32service",
                "win32trace",
                "win32transaction",
                "win32ts",
                "win32wnet",
            )
            and isWin32Windows()
        ):
            pywin_dir = getPyWin32Dir()

            if pywin_dir is not None:
                for dll_name in "pythoncom", "pywintypes":

                    pythoncom_filename = "%s%d%d.dll" % (
                        dll_name,
                        sys.version_info[0],
                        sys.version_info[1],
                    )
                    pythoncom_dll_path = os.path.join(pywin_dir, pythoncom_filename)

                    if os.path.exists(pythoncom_dll_path):
                        yield self.makeDllEntryPoint(
                            pythoncom_dll_path, pythoncom_filename, None
                        )
