##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Persistent Module."""

__metaclass__ = type

from zope.interface import implements

from persistent import Persistent
from persistent.cPersistence import GHOST
from zodbcode.interfaces import IPersistentModuleManager
from zodbcode.interfaces \
     import IPersistentModuleImportRegistry, IPersistentModuleUpdateRegistry
from zodbcode.patch import NameFinder, convert

# builtins are explicitly assigned when a module is unpickled
import __builtin__

# Modules aren't picklable by default, but we'd like them to be
# pickled just like classes (by name).
import copy_reg

def _pickle_module(mod):
    return _unpickle_module, (mod.__name__,)

def _unpickle_module(modname):
    mod = __import__(modname)
    if "." in modname:
        parts = modname.split(".")[1:]
        for part in parts:
            mod = getattr(mod, part)
    return mod

copy_reg.pickle(type(copy_reg), _pickle_module, _unpickle_module)

# XXX Is this comment still relevant?
#
# There seems to be something seriously wrong with a module pickle
# that contains objects pickled via save_global().  These objects are
# pickled using references to the module.  It appears that unpickling the
# object in the module causes the persistence machinery to fail.
#
# My suspicion is that the assignment to po_state before trying to
# load the state confuses things.  The first call to setstate attempts
# to reference an attribute of the module.  That getattr() fails because
# the module is not a ghost, but does have any empty dict.  Since
# that getattr() fails, its state can't be unpickled.
#
# Not sure what to do about this.

class PersistentModule(Persistent):

    def __init__(self, name):
        self.__name__ = name

    def __repr__(self):
        return "<%s %s>" % (self.__class__.__name__, self.__name__)

    # XXX need getattr &c. hooks to update _p_changed?
    # XXX what about code that modifies __dict__ directly?
    # XXX one example is a function that rebinds a global

    def __getstate__(self):
        d = self.__dict__.copy()
        try:
            del d["__builtins__"]
        except KeyError:
            pass
        return d

    def __setstate__(self, state):
        state["__builtins__"] = __builtin__
        self.__dict__.update(state)

class PersistentPackage(PersistentModule):
    # XXX Is it okay that these packages don't have __path__?

    # A PersistentPackage can exist in a registry without a manager.
    # It only gets a manager if someone creates an __init__ module for
    # the package.

    def __init__(self, name):
        self.__name__ = name

__persistent_module_registry__ = "__persistent_module_registry__"

def newModule(registry, name, source):
    """Return a manager object for a newly created module."""
    mgr = PersistentModuleManager(registry)
    mgr.new(name, source)
    return mgr


def compileModule(module, registry, source):
    # Try to prevent compilation errors from files without trailing newlines.
    if source and source[-1] != "\n":
        source += "\n"
    module._p_changed = True
    moddict = module.__dict__
    old_names = NameFinder(module)
    moddict[__persistent_module_registry__] = registry
    # XXX need to be able to replace sys.std{in,out,err} at this point
    exec source in moddict
    # XXX and restore them here.
    del moddict[__persistent_module_registry__]
    new_names = NameFinder(module)
    replacements = new_names.replacements(old_names)
    convert(module, replacements)

class PersistentModuleManager(Persistent):

    implements(IPersistentModuleManager)

    def __init__(self, registry):
        self._registry = registry
        self._module = None
        self.name = None
        self.source = None

    def new(self, name, source):
        """Return a new module from a name and source text."""
        if self._module is not None:
            raise ValueError("module already exists")
        if "." in name:
            parent = self._new_package(name)
        else:
            parent = None
            self._module = PersistentModule(name)
        try:
            self._registry.setModule(name, self._module)
        except ValueError:
            self._module = None
            raise
        self.name = name
        try:
            self.update(source)
        except:
            self._registry.delModule(name)
            raise
        if parent is not None:
            modname = name.split(".")[-1]
            setattr(parent, modname, self._module)

    def update(self, source):
        # Try to prevent compilation errors from files without trailing
        # newlines.
        compileModule(self._module, self._registry, source)
        self.source = source

    def remove(self):
        self._registry.delModule(self._module.__name__)
        self._module = None

    def _new_package(self, name):
        parent = self._get_parent(name)
        modname = name.split(".")[-1]
        if modname == "__init__":
            self._module = parent
            return None
        else:
            self._module = PersistentModule(name)
            return parent

    def _get_parent(self, name):
        # If a module is being created in a package, automatically
        # create parent packages that do no already exist.
        parts = name.split(".")[:-1]
        parent = None
        for i in range(len(parts)):
            if parts[i] == "__init__":
                raise ValueError("__init__ can not be a package")
            pname = ".".join(parts[:i+1])
            package = self._registry.findModule(pname)
            if package is None:
                package = PersistentPackage(pname)
                self._registry.setModule(pname, package)
                if parent is not None:
                    setattr(parent, parts[i], package)
            elif not isinstance(package, PersistentPackage):
                raise ValueError("%s is module" % pname)
            parent = package
        return parent

class PersistentModuleImporter:
    """An import hook that loads persistent modules.

    The importer cooperates with other objects to make sure imports of
    persistent modules work correctly.  The default importer depends
    on finding a persistent module registry in the globals passed to
    __import__().  It looks for the name __persistent_module_registry__.
    A PersistentModuleManager places its registry in the globals used
    to exec module source.

    It is important that the registry be activated before it is used
    to handle imports.  If a ghost registry is used for importing, a
    circular import occurs.  The second import occurs when the
    machinery searches for the class of the registry.  It will re-use
    the registry and fail, because the registry will be marked as
    changed but not yet have its state loaded.
    XXX There ought to be a way to deal with this.
    """

    # The import hook doesn't use sys.modules, because Zope might want
    # to have placeful registries.  That is, a particular module might
    # execute in a context where there is more than one persistent
    # module registry active.  In this case, it isn't safe to use
    # sys.modules, because each registry could have a different binding
    # for a particular name.

    _saved_import = None

    def install(self):
        if self._saved_import is not None:
            raise TypeError("Already installed!")
        self._saved_import = __builtin__.__import__
        __builtin__.__import__ = self.__import__

    def uninstall(self):
        if self._saved_import is None:
            raise TypeError("Not installed!")
        __builtin__.__import__ = self._saved_import
        self._saved_import = None

    def _import(self, registry, name, parent, fromlist):
        mod = None
        if parent is not None:
            fullname = "%s.%s" % (parent, name)
            mod = registry.findModule(fullname)
            if mod is None:
                parent = None
        if mod is None: # no parent or didn't find in parent
            mod = registry.findModule(name)
        if mod is None:
            return None
        if fromlist:
            if isinstance(mod, PersistentPackage):
                self._import_fromlist(registry, mod, fromlist)
            return mod
        else:
            i = name.find(".")
            if i == -1:
                return mod
            name = name[:i]
            if parent:
                name = "%s.%s" % (parent, name)
            top = registry.findModule(name)
            assert top is not None, "No package for module %s" % name
            return top

    def _import_fromlist(self, registry, mod, fromlist):
        for name in fromlist:
            if not hasattr(mod, name):
                fullname = "%s.%s" % (mod.__name__, name)
                self._import(registry, fullname, None, [])

    def __import__(self, name, globals={}, locals={}, fromlist=[]):
        registry = globals.get(__persistent_module_registry__)
        if registry is not None:
            mod = self._import(registry, name, self._get_parent(globals),
                               fromlist)
            if mod is not None:
                return mod
        return self._saved_import(name, globals, locals, fromlist)

    def _get_parent(self, globals):
        name = globals.get("__name__")
        if name is None or "." not in name:
            return None
        i = name.rfind(".")
        return name[:i]

class PersistentModuleRegistry(Persistent):
    """A collection of persistent modules.

    The registry is similar in purpose to sys.modules.  A persistent
    module manager stores its modules in a registry, and the importer
    looks for them there.
    """

    implements(IPersistentModuleImportRegistry,
               IPersistentModuleUpdateRegistry)

    def __init__(self):
        self._modules = {}

    def findModule(self, name):
        assert self._p_changed is not None
        return self._modules.get(name)

    def setModule(self, name, module):
        if name in self._modules:
            # The name is already in use.
            # XXX should raise a better error
            raise ValueError(name)
        self._p_changed = True
        self._modules[name] = module

    def delModule(self, name):
        self._p_changed = True
        del self._modules[name]

    def modules(self):
        """Return a list of the modules in the registry."""
        return self._modules.keys()
    
class ManagedRegistry(PersistentModuleRegistry):
    """A collection of persistent modules and their managers.

    An extension of the persistent module registry that also collects
    the managers.  For persistent modules to be useful, the managers
    must be stored in the database.  This registry stores managers
    as well as their modules, so that all objects related to the modules
    in the registry are reachable from the registry.
    """

    def __init__(self):
        super(ManagedRegistry, self).__init__()
        self._mgrs = {}

    def newModule(self, name, source):
        mgr = PersistentModuleManager(self)
        mgr.new(name, source)
        self._p_changed = True
        self._mgrs[name] = mgr

    def updateModule(self, name, source):
        self._mgrs[name].update(source)

    def removeModule(self, name):
        self._mgrs[name].remove()
        self._p_changed = True
        del self._mgrs[name]
