#     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 avoid bloat at compile time.

Nuitka hard codes stupid monkey patching normally not needed here and avoids
that to be done and causing massive degradations.

* cffi importing setuptools is not needed, workaround that with
  --noinclude-setuptools-mode=nofollow if warned about including it.

  Setuptools includes massive amounts of build tools which use other
  things optionally.

"""

import pkgutil

from nuitka.containers.odict import OrderedDict
from nuitka.Errors import NuitkaForbiddenImportEncounter
from nuitka.plugins.PluginBase import NuitkaPluginBase
from nuitka.utils.ModuleNames import ModuleName
from nuitka.utils.Yaml import parseYaml


class NuitkaPluginAntiBloat(NuitkaPluginBase):
    plugin_name = "anti-bloat"
    plugin_desc = "Patch stupid imports out of common library modules source code."

    def __init__(
        self, noinclude_setuptools_mode, noinclude_pytest_mode, custom_choices
    ):
        self.config = parseYaml(
            pkgutil.get_data("nuitka.plugins.standard", "anti-bloat.yml")
        )

        self.handled_modules = OrderedDict()

        if noinclude_setuptools_mode != "allow":
            self.handled_modules["setuptools"] = noinclude_setuptools_mode

        if noinclude_pytest_mode != "allow":
            self.handled_modules["pytest"] = noinclude_pytest_mode

        for custom_choice in custom_choices:
            if ":" not in custom_choice:
                self.sysexit(
                    "Error, malformed value  '%s' for '--noinclude-custom-mode' used."
                    % custom_choice
                )

            module_name, mode = custom_choice.rsplit(":", 1)

            if mode not in ("error", "warning", "nofollow", "allow"):
                self.sysexit(
                    "Error, illegal mode given '%s' in '--noinclude-custom-mode=%s'"
                    % (mode, custom_choice)
                )

            self.handled_modules[ModuleName(module_name)] = mode

    @classmethod
    def addPluginCommandLineOptions(cls, group):
        group.add_option(
            "--noinclude-setuptools-mode",
            action="store",
            dest="noinclude_setuptools_mode",
            choices=("error", "warning", "nofollow", "allow"),
            default="warning",
            help="""\
What to do if a setuptools import is encountered. This can be big with
dependencies, and should definitely be avoided.""",
        )

        group.add_option(
            "--noinclude-pytest-mode",
            action="store",
            dest="noinclude_pytest_mode",
            choices=("error", "warning", "nofollow", "allow"),
            default="warning",
            help="""\
What to do if a pytest import is encountered. This can be big with
dependencies, and should definitely be avoided.""",
        )

        group.add_option(
            "--noinclude-custom-mode",
            action="append",
            dest="custom_choices",
            default=[],
            help="""\
What to do if a specific import is encountered. Format is module name,
which can and should be a top level package and then one choice, "error",
"warning", "nofollow", e.g. PyQt5:error.""",
        )

    def onModuleSourceCode(self, module_name, source_code):
        config = self.config.get(module_name)

        if not config:
            return source_code

        description = config.get("description", "description not given")

        self.info(
            "Handling module '%s' for: %s." % (module_name.asString(), description)
        )

        context = {}
        context_code = config.get("context", "")
        if type(context_code) in (tuple, list):
            context_code = "\n".join(context_code)

        # We trust the yaml files, pylint: disable=eval-used,exec-used
        exec(context_code, context)

        for replace_src, replace_code in config.get("replacements", {}).items():
            replace_dst = eval(replace_code, context)

            source_code = source_code.replace(replace_src, replace_dst)

        return source_code

    def onModuleEncounter(self, module_filename, module_name, module_kind):
        for handled_module_name, mode in self.handled_modules.items():
            if module_name.hasNamespace(handled_module_name):
                # Make sure the compilation abrts.
                if mode == "error":
                    raise NuitkaForbiddenImportEncounter(module_name)

                # Either issue a warning, or pretend the module doesn't exist for standalone or
                # at least will not be included.
                if mode == "warning":
                    self.warning("Unwanted import of '%s' encountered." % module_name)
                elif mode == "nofollow":
                    self.info(
                        "Forcing import of '%s' to not be followed." % module_name
                    )
                    return (
                        False,
                        "user requested to not follow '%s' import" % module_name,
                    )

        # Do not provide an opinion about it.
        return None
