# This file is part of Rubber and thus covered by the GPL
# (c) Emmanuel Beffara, 2002--2006
"""
Mechanisms to dynamically load extra modules to help the LaTeX compilation.
All the modules must be derived from the TexModule class.
"""
import importlib.machinery
import importlib.util

from os.path import *
from dbtexmf.dblatex.grubber.msg import _, msg

import sys

class TexModule (object):
    """
    This is the base class for modules. Each module should define a class
    named 'Module' that derives from this one. The default implementation
    provides all required methods with no effects.
    """
    def __init__ (self, env, dict):
        """
        The constructor receives two arguments: 'env' is the compiling
        environment, 'dict' is a dictionary that describes the command that
        caused the module to load.
        """

    def pre_compile (self):
        """
        This method is called before the first LaTeX compilation. It is
        supposed to build any file that LaTeX would require to compile the
        document correctly. The method must return true on failure.
        """
        return 0

    def post_compile (self):
        """
        This method is called after each LaTeX compilation. It is supposed to
        process the compilation results and possibly request a new
        compilation. The method must return true on failure.
        """
        return 0

    def last_compile (self):
        """
        This method is called after the last LaTeX compilation.
        It is supposed to terminate the compilation for its specific needs.
        The method must return true on failure.
        """
        return 0

    def clean (self):
        """
        This method is called when cleaning the compiled files. It is supposed
        to remove all the files that this modules generates.
        """

    def command (self, cmd, args):
        """
        This is called when a directive for the module is found in the source.
        The method can raise 'AttributeError' when the directive does not
        exist and 'TypeError' if the syntax is wrong. By default, when called
        with argument "foo" it calls the method "do_foo" if it exists, and
        fails otherwise.
        """
        getattr(self, "do_" + cmd)(*args)

    def get_errors (self):
        """
        This is called if something has failed during an operation performed
        by this module. The method returns a generator with items of the same
        form as in LaTeXDep.get_errors.
        """
        if None:
            yield None


class Plugins (object):
    """
    This class gathers operations related to the management of external Python
    modules. Modules are requested through the `register' method, and
    they are searched for first in the current directory, then in the
    (possibly) specified Python package (using Python's path).
    """
    def __init__ (self, path=None):
        """
        Initialize the module set, possibly setting a path name in which
        modules will be searched for.
        """
        self.modules = {}
        if not path:
            self.path = [dirname(__file__)]
            sys.path.append(self.path[0])
        else:
            self.path = path

    def __getitem__ (self, name):
        """
        Return the module object of the given name.
        """
        return self.modules[name]

    def register (self, name):
        """
        Attempt to register a module with the specified name. If an
        appropriate module is found, load it and store it in the object's
        dictionary. Return 0 if no module was found, 1 if a module was found
        and loaded, and 2 if the module was found but already loaded.
        """
        if name in self.modules:
            return 2
        spec = importlib.machinery.PathFinder.find_spec(name, [""])
        if not spec:
            if not self.path:
                return 0
            spec = importlib.machinery.PathFinder.find_spec(name, self.path)
        if not spec:
            return 0
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        sys.modules[name] = module
        self.modules[name] = module
        return 1

    def clear(self):
        """
        Empty the module table, unregistering every module registered. No
        modules are unloaded, however, but this has no other effect than
        speeding the registration if the modules are loaded again.
        """
        self.modules.clear()


class Modules (Plugins):
    """
    This class gathers all operations related to the management of modules.
    The modules are    searched for first in the current directory, then as
    scripts in the 'modules' directory in the program's data directort, then
    as a Python module in the package `rubber.latex'.
    """
    def __init__ (self, env):
        #Plugins.__init__(self, rubber.rules.latex.__path__)
        Plugins.__init__(self)
        self.env = env
        self.objects = {}
        self.commands = {}

    def __getitem__ (self, name):
        """
        Return the module object of the given name.
        """
        return self.objects[name]

    def __contains__(self, name):
        """
        Check if a given module is loaded.
        """
        return name in self.objects

    def register (self, name, dict={}):
        """
        Attempt to register a package with the specified name. If a module is
        found, create an object from the module's class called `Module',
        passing it the environment and `dict' as arguments, and execute all
        delayed commands for this module. The dictionary describes the
        command that caused the registration.
        """
        if name in self:
            msg.debug(_("module %s already registered") % name)
            return 2

        # First look for a script

        moddir = ""
        mod = None
        for path in "", join(moddir, "modules"):
            file = join(path, name + ".rub")
            if exists(file):
                mod = ScriptModule(self.env, file)
                msg.log(_("script module %s registered") % name)
                break

        # Then look for a Python module

        if not mod:
            if Plugins.register(self, name) == 0:
                msg.debug(_("no support found for %s") % name)
                return 0
            mod = self.modules[name].Module(self.env, dict)
            msg.log(_("built-in module %s registered") % name)

        # Run any delayed commands.

        if name in self.commands:
            for (cmd, args, vars) in self.commands[name]:
                msg.push_pos(vars)
                try:
                    mod.command(cmd, args)
                except AttributeError:
                    msg.warn(_("unknown directive '%s.%s'") % (name, cmd))
                except TypeError:
                    msg.warn(_("wrong syntax for '%s.%s'") % (name, cmd))
                msg.pop_pos()
            del self.commands[name]

        self.objects[name] = mod
        return 1

    def clear (self):
        """
        Unregister all modules.
        """
        Plugins.clear(self)
        self.objects = {}
        self.commands = {}

    def command (self, mod, cmd, args):
        """
        Send a command to a particular module. If this module is not loaded,
        store the command so that it will be sent when the module is register.
        """
        if mod in self.objects:
            self.objects[mod].command(cmd, args)
        else:
            if mod not in self.commands:
                self.commands[mod] = []
            self.commands[mod].append((cmd, args, self.env.vars.copy()))

