# Twisted, the Framework of Your Internet
# Copyright (C) 2001-2002 Matthew W. Lefkowitz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# -*- test-case-name: twisted.test.test_popsicle -*-


"""Abstract Repository classes for Popsicle.
"""

# System Imports
import os
import weakref

# Twisted Imports
from twisted.internet import defer
from twisted.persisted.styles import instance

# Sibling Imports
from twisted.popsicle.freezer import theFreezer, ISaver, PersistentReference, ref

class OIDNotFound(Exception):
    pass

class Repository:
    """A data storage that can be loaded from.  A repository of objects.

    I am an abstract class.  Subclass me and implement 'loadOID' and 'saveOID'

    Note that 'Repository' implementations are really open-ended.  This
    implementation strategy is suggested, though, and it is strongly suggested
    that authors of new repositories implement at least the caching shown here.

    However, facilities such as ad-hoc querying are omitted from this
    interface, and should be provided by e.g. an SQL Repository implementation.

    A repository is both a saver and a collection of PersistentReferences.
    """

    __implements__ = ISaver

    _lastOID = 0

    def saveOID(self, oid, obj):
        """
        Return a Deferred which will fire True when the object is saved.
        """
        raise NotImplementedError()


    def __init__(self):
        """Initialize me (set up cache).
        """
        self._cache = weakref.WeakValueDictionary()
        self._revCache = weakref.WeakKeyDictionary()
        self._pRefs = weakref.WeakValueDictionary()

    def __getstate__(self):
        dct = self.__dict__.copy()
        del dct['_cache']
        del dct['_revCache']
        del dct['_pRefs']
        return dct

    def __setstate__(self, dct):
        self.__dict__ = dct
        self._cache = weakref.WeakValueDictionary()
        self._revCache = weakref.WeakKeyDictionary()
        self._pRefs = weakref.WeakValueDictionary()

    def load(self, oid):
        """Load an object from cache or by OID. Return a Deferred.

        This method should be called by external objects looking for a
        'starting point' into the repository.
        """
        oid = str(oid)
        if self._pRefs.has_key(oid):
            pRef = self._pRefs[oid]
        else:
            pRef = PersistentReference(str(oid), self, None)
        return pRef()

    def loadNow(self, oid):
        """External API for synchronously loading stuff.

        This should ONLY BE USED by code that is doing the actual
        loading/saving of structured objects.  Application code should always
        make calls through PersistentReference.__call__, otherwise it will not
        work on some back-ends.
        """
        oid = str(oid)
        if self._cache.get(oid):
            return self._cache[oid]
        # this code is copied and subtly changed from _cbLoadedOID.  I wish I
        # could have found a better way to do it, but -- expect bugs here!
        pRef = PersistentReference(oid, self, None)
        pRef.deferred = defer.Deferred()
        try:
            try:
                obj = self.loadOIDNow(oid)
            except:
                pRef.deferred.errback()
                raise
            else:
                theFreezer.setPersistentReference(obj, pRef)
                theFreezer.addSaver(obj, self)
                pRef.deferred.callback(obj)
                self.cache(oid, obj)
                return obj
        finally:
            del pRef.deferred

    def loadOIDNow(self, oid):
        """
        Implement me if you want to implement synchronous loading.
        """
        raise NotImplementedError()

    def loadOID(self, oid):
        """Implement me to return a Deferred if you want to implement asynchronous loading.
        """
        return defer.execute(self.loadOIDNow, oid)

    def createOID(self, oid, klass):
        """Create an instance with an oid and cache it.  This is useful during loading.
        """
        i = instance(klass)
        self.cache(oid, i, 0)
        return i

    def cleaned(self):
        """The freezer finished cleaning, and some of my objects were cleaned.
        """

    def loadRef(self, pRef):
        """
        Synonymous with ref.__call__().
        """
        oid = pRef.oid
        obj = self._cache.get(oid)
        if obj is not None:
            return defer.succeed(obj)
        elif self._pRefs.has_key(oid):
            # have a persistent ref, but no object
            return pRef.deferred
        else:
            # have no persistent ref
            d = defer.Deferred()
            self._pRefs[oid] = pRef
            pRef.deferred = d
            d2 = self.loadOID(oid)
            d2.addCallback(self._cbLoadedOID, oid, pRef)
            return d


    def _cbLoadedOID(self, result, oid, pref):
        theFreezer.setPersistentReference(result, pref)
        theFreezer.addSaver(result, self)
        self.cache(oid, result)
        pref.deferred.callback(result)
        del pref.deferred
        return result


    def generateOID(self, obj):
        """Generate an OID synchronously.

        Necessary for some types of persistence, but 
        """
        self._lastOID += 1
        return self._lastOID

    def cache(self, oid, obj, finished=1):
        """Weakly cache an object for the given OID.

        This means I own it, so also register it with the Freezer as such.
        """
        self._cache[oid] = obj
        self._revCache[obj] = oid

    def getOID(self, obj):
        if self._revCache.has_key(obj):
            return self._revCache[obj]
        else:
            # TODO: if OID generation really needs to be async...
            return ref(obj).acquireOID(self)

    def save(self, obj):
        """
        Save an object...

        If this is the first time I am saving this particular object, I need to
        locate a new unique ID for it.
        """
        theFreezer._savingRepo = self
        try:
            oid = self.getOID(obj)
            val = self.saveOID(oid, obj)
            self.cache(oid, obj)
            return val
        finally:
            theFreezer._savingRepo = None


class DirectoryRepository(Repository):
    def __init__(self, dirname):
        if not os.path.isdir(dirname):
            os.mkdir(dirname)
            fn = os.path.join(dirname,".popsiqnum")
        Repository.__init__(self)
        self.dirname = dirname

    def generateOID(self, obj):
        fn = os.path.join(self.dirname,".popsiqnum")
        try:
            seq = str(int(open(fn).read()) + 1)
        except IOError:
            seq = '0'
        open(fn,'w').write(seq)
        return seq

