############################################################################
#
# File Name: 		__init__.py
#
# Documentation:	http://docs.ftsuite.com/4ODS/StorageManager/__init__.py/html
#
"""
Manages data storage and transactions
WWW: http://4suite.org/4ODS         e-mail: support@4suite.org

Copyright (c) 2000-2001 Fourthought Inc, USA.   All Rights Reserved.
See  http://4suite.org/COPYRIGHT  for license and copyright information
"""

import sys

from Ft.Ods.StorageManager import Adapters
from Ft.Ods.StorageManager import GlobalCache
from Ft.Ods.StorageManager.Adapters import ClassCache
from Ft.Ods.StorageManager import Extent
from Ft.Ods.StorageManager.Adapters import Constants
from Ft.Ods.Exception import FtodsObjectNotFound, TransactionInProgress, TransactionNotInProgress, FtodsUnsupportedError, ObjectNameNotUnique, FtodsOperationBadDefinition
import Ft.Ods

import types
try:
    STRING_TYPES = [types.StringType, types.UnicodeType]
except:
    STRING_TYPES = [types.StringType]


LEGAL_NAME_CHARS = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_- /"

class StorageManager:
    TEST_MODE=0
    def __init__(self,factory,dbName):

        self._id = id(self)
        self._factory = factory
        self._open = 0
        self._hasBeenClosed = 0
        self._dbName = dbName
        

        #Create our adapter
        self._adapter = Adapters.GetAdapter()
        self._manager = Adapters.GetManager()
        self._gc = GlobalCache.GlobalCache()
        self._db = None

    #The transaction interface
    def begin(self):
        if self._open or self._hasBeenClosed:
            raise TransactionInProgress()
        self._open = 1
        self._4ods_clear()
        try:
            self._db = self._manager.connect(self._dbName)
            self._adapter.begin(self._db)
        except:
            self._factory._4ods_endTransaction(self._id)
            raise 
        

    def abort(self):
        if not self._open:
            raise TransactionNotInProgress()
        self._factory._4ods_endTransaction(self._id)
        self._adapter.abort(self._db)
        self._db = None
        self._open = 0
        self._hasBeenClosed = 1
        self._4ods_clean()

    def commit(self):
        if not self._open:
            raise TransactionNotInProgress()
        try:
            self._4ods_flush()
            self._open = 0
            self._hasBeenClosed = 1
            self._adapter.commit(self._db)
            self._db = None
            self._4ods_clean()
        finally:
            self._factory._4ods_endTransaction(self._id)
            
    def checkpoint(self):
        if not self._open:
            raise TransactionNotInProgress()
        self._4ods_flush()
        self._adapter.checkpoint(self._db)


        #Change out the TX on the deleted objects
        for objs in self._deadObjects.values():
            for o in objs.values():
                o._4ods_setTransaction(None)


        #Clean up the deleted
        self._deadObjects = {}
        self._newObjects = {}
        for t in Constants.g_allTypes:
            self._deadObjects[t] = {}
            self._newObjects[t] = {}

        #Clean up the literl class
        self._literalClassCache = {}
        
            

    def join(self):
        #FIXME Check to see if we need to do a leave
        if self._open:
            self._factory._4ods_registerThread(self)
        else:
            raise TransactionNotInProgress()

    def leave(self):
        if self._open:
            self._factory._4ods_unregisterThread(self)
        else:
            raise TransactionNotInProgress()


    ##################################
    #4ODS only interfaces
    ##################################


    #Called by the database to any outstanding transactions
    #When the database gets closed
    def _4ods_close(self):
        self._adapter.abort(self._db)
        self._db = None
        self._open = 0
        self._hasBeenClosed = 1
        self._4ods_clean()

    #Called to send the currently modified objects to disk
    def _4ods_flush(self):
        if not self._open:
            raise TransactionNotInProgress()


        keys = self._newObjects.keys()
        #Sort so that repos are done first
        keys.sort()

        #Have all of the blobs updated
        for (owner,name),data in self._newBlobs.items():
            bid = self._adapter.newBlob(self._db)
            owner._4ods_setBlobId(name,bid)
            self._adapter.writeBlob(self._db,bid,data)
        for bid,data in self._modifiedBlobs.items():
            self._adapter.writeBlob(self._db,bid,data)
        for bid in self._deadBlobs.keys():
            self._adapter.deleteBlob(self._db,bid)


        #New Writes
        for typ in keys:
##            if typ == Constants.Types.ROBJECT:
##                #Needed by some adapters
##                values = self._4ods_sortNewRepos(self._newObjects[Constants.Types.ROBJECT].values())
##            else:
            values = self._newObjects[typ].values()
            for obj in values:
                self.newWriteMapping[typ](self,obj)

        #Modified Writes
        for typ in keys:
            for id_,obj in self._modifiedObjects[typ].items():
                if obj._4ods_isModified():
                    self.writeMapping[typ](self,obj)

        #Dead Objects
        for typ in keys:
            for id_,obj in self._deadObjects[typ].items():
                self.deleteMapping[typ](self,obj)

        #Update bindings
        for name in self._deadBindings:
            self._adapter.unbind(self._db,name)
        for name in self._newBindings:
            self._adapter.bind(self._db,self._bindings[name]._4ods_getId(),name)

        #Have all extents update
        for name,ext in self._extents.items():
            ext._4ods_updateChanges(self._adapter,self._db)

    def _4ods_isOpen(self):
        return self._open


    def _4ods_getId(self):
        return self._id


    def _4ods_clear(self):

        self._literalClassCache = {}


        self._newObjects = {}
        self._modifiedObjects = {}
        self._deadObjects = {}

        for t in Constants.g_allTypes:
            self._newObjects[t] = {}
            self._modifiedObjects[t] = {}
            self._deadObjects[t] = {}

        self._extents = {}
        self._bindings = {}
        

        self._newBindings = []
        self._deadBindings = []


        self._newBlobs = {}
        self._deadBlobs = {}
        self._modifiedBlobs = {}

        self._operationCache = {}


    def _4ods_clean(self):
        #Should we Free the repo objects? Yes
        for ro in self._newObjects[Constants.Types.ROBJECT].values() + self._modifiedObjects[Constants.Types.ROBJECT].values() + self._deadObjects[Constants.Types.ROBJECT].values():
            Ft.Ods.FreePersistentObject(ro)
        self._4ods_clear()

    def _4ods_registerOperation(self,repoId,functionName,functionModule):
        if not self._open:
            raise TransactionNotInProgress()
        self._adapter.registerOperation(self._db,repoId,functionName,open(functionModule).read())

    def _4ods_getRegisteredOperation(self,repoId):
        if not self._open:
            raise TransactionNotInProgress()

        res = self._operationCache.get(repoId)
        if res:
            return res

        res = self._adapter.getOperation(self._db,repoId)
        if res is None:
            return None

        functionName,functionModule = res

        g = {}
        try:
            exec(functionModule,g)
        except:
            import traceback,cStringIO
            st = cStringIO.StringIO()
            traceback.print_exc(file=st)
            raise FtodsOperationBadDefinition(repoId,st.getvalue())
            
        self._operationCache[repoId] = g[functionName]
        return g[functionName]

    def _4ods_registerNewLiteralClass(self,typ,repoId,klass):
        if not self._open:
            raise TransactionNotInProgress()

        self._adapter.newPythonLiteralClass(self._db,typ,repoId,klass)


    def _4ods_registerNewPersistentObject(self,po):
        if not self._open:
            raise TransactionNotInProgress()

        #New ID
        self.newIdMapping[po._4ods_getOdsType()](self,po)
        self._newObjects[po._4ods_getOdsType()][po._4ods_getId()] = po

        #We need to update the extents of this object
        for name in po._extents:
            ext = self._4ods_lookup(name)
            ext._4ods_addObject(po)



    def _4ods_registerNewBlob(self,inst,name,data):
        if not self._open:
            raise TransactionNotInProgress()
        self._newBlobs[(inst,name)] = data
            

    def _4ods_registerModifiedPersistentObject(self,po):
        if not self._open:
            raise TransactionNotInProgress()
        self._modifiedObjects[po._4ods_getOdsType()][po._4ods_getId()] = po

    def _4ods_registerModifiedBlob(self,bid,data):
        if not self._open:
            raise TransactionNotInProgress()
        self._modifiedBlobs[bid] = data
        

    def _4ods_unregisterPersistentObject(self,po):
        if not self._open:
            raise TransactionNotInProgress()
        typ = po._4ods_getOdsType()
        id_ = po._4ods_getId()
        if self._modifiedObjects[typ].has_key(id_):
            del self._modifiedObjects[typ][id_]
        if self._deadObjects[typ].has_key(id_):
            del self._deadObjects[typ][id_]
        if self._newObjects[typ].has_key(id_):
            del self._newObjects[typ][id_]

    def _4ods_deletePersistentObject(self, po):
        if not self._open:
            raise TransactionNotInProgress()

        self._deadObjects[po._4ods_getOdsType()][po._4ods_getId()] = po


        #We need to update the extents of this object
        for name in po._extents:
            ext = self._4ods_lookup(name)
            ext._4ods_removeObject(po)
        if po._4ods_getOdsType() == Constants.Types.POBJECT:
            #Remove bindings
            for name in self._4ods_getObjectBindings(po):
                self._4ods_unbind(name)

    def _4ods_deleteBlob(self,inst,name,bid):
        if not self._open:
            raise TransactionNotInProgress()
        if not bid:
            if self._newBlobs.has_key((inst,name)):
                del self._newBlobs[(inst,name)]
        else:
            if self._modifiedBlobs.has_key(bid):
                del self._modifiedBlobs[bid]
            self._deadBlobs[bid] = 1
        

    def _4ods_createLiteralInstance(self,typ,repoId,data):
        if not self._open:
            raise TransactionNotInProgress()
        if not self._literalClassCache.has_key((typ,repoId)):
            klass = self._adapter.getPythonLiteralClass(self._db,typ,repoId)
            if not klass:
                raise FtodsObjectNotFound(repoId,typ)
            self._literalClassCache[(typ,repoId)] = klass

        modName,className = self._literalClassCache[(typ,repoId)]
        if sys.modules.has_key(modName):
            klass = getattr(sys.modules[modName],className)
        else:
            klass = getattr(__import__(modName,globals(),locals(),'*'),className)

        return apply(klass,(self._factory,data))


    def _4ods_getRepositoryObject(self,rid):
        inst,data = self.__getObject([Constants.Types.ROBJECT],rid,self._adapter.getRepositoryObject)
        if inst is not None: return inst
        return data[0](self._factory,data[1])

    def _4ods_getObject(self,oid):
        inst,data = self.__getObject([Constants.Types.POBJECT],oid,self._adapter.getObject)
        if inst is not None: return inst
        return data[0](self._factory,data[1])

    def _4ods_getCollection(self,cid):
        inst,data = self.__getObject([Constants.Types.LIST_COLLECTION,
                                      Constants.Types.SET_COLLECTION,
                                      Constants.Types.BAG_COLLECTION],cid,self._adapter.getCollection)
        if inst is not None: return inst
        return data[0](self._factory,cid,data[1],data[2],data[3])

    def _4ods_getDictionary(self,did):
        inst,data = self.__getObject([Constants.Types.DICTIONARY_COLLECTION],did,self._adapter.getDictionary)
        if inst is not None: return inst
        return data[0](self._factory,did,data[1],data[2],data[3],data[4],data[5])


    def _4ods_readBlob(self,bid):
        rt = self._adapter.readBlob(self._db,bid)
        if rt == None:
            raise FtodsObjectNotFound(bid,Constants.Types.BLOB)
        return rt


    def _4ods_lookup(self,name):
        if not self._open:
            raise TransactionNotInProgress()
        if self._extents.has_key(name):
            return self._extents[name]
        elif self._bindings.has_key(name):
            return self._bindings[name]
        elif name not in self._deadBindings or name in self._newBindings:
            #Get it from the adapter
            oid = self._adapter.lookup(self._db,name)
            if oid:
                obj = self._4ods_getObject(oid)
                self._bindings[name] = obj
                return obj
            ext = self._adapter.loadExtent(self._db,name)
            if ext is None:
                return None

            ext = Extent.Extent(name,self,ext[0],ext[1])
            self._extents[name] = ext
            return ext


    def _4ods_bind(self,object,name):
        if not self._open:
            raise TransactionNotInProgress()
        #Make sure the name does not have any illegal characters
        if not type(name) in STRING_TYPES:
            raise TypeError("Expected string, got %s" % type(name))
        if len(filter(lambda x, lc=LEGAL_NAME_CHARS:x not in lc,name)):
            raise ValueError("Name container illegal characters for an ODS bind.")

        if self._4ods_lookup(name):
            raise ObjectNameNotUnique(name)
        self._newBindings.append(name)
        self._bindings[name] = object

    def _4ods_unbind(self,name):
        if not self._open:
            raise TransactionNotInProgress()
        if self._bindings.has_key(name):
            del self._bindings[name]
        if name in self._newBindings:
            self._newBindings.remove(name)
        self._deadBindings.append(name)

    def _4ods_getAllBindings(self):
        if not self._open:
            raise TransactionNotInProgress()
        #Always go to the database (since we gotta anyways)
        bindings = self._adapter.getAllBindings(self._db) + self._newBindings
        for db in self._deadBindings:
            while db in bindings:
                bindings.remove(db)

        eNames = self._adapter.extentNames(self._db)

        newExts = filter(lambda x:x._new == 1,self._extents.values())
        deadExts = filter(lambda x:x._delete == 1,self._extents.values())

        eNames = eNames + newExts
        for de in deadExts:
            if de in self._newBindings:
                continue
            while de in eNames:
                eNames.remove(de)

        return bindings + eNames

    def _4ods_getObjectBindings(self,object):
        if not self._open:
            raise TransactionNotInProgress()

        bindings = self._adapter.getObjectBindings(self._db,object._4ods_getId())

        for name in self._newBindings:
            if object == self._bindings[name]:
                bindings.append(name)

        for db in self._deadBindings:
            if db in self._newBindings:
                continue
            while db in bindings:
                bindings.remove(db)
        return bindings
            
    def _4ods_isBound(self,object):
        if not self._open:
            raise TransactionNotInProgress()
        return self._adapter.lookup(self._db,object._4ods_getId()) != 0

    def _4ods_runQuery(self,cmd):
        raise FtodsUnsupportedError(feature= "OQL")
        

    def __getObject(self,types,oid,func):
        if not self._open:
            raise TransactionNotInProgress()

        for typ in types:
            if self._modifiedObjects[typ].has_key(oid):
                return self._modifiedObjects[typ][oid],None
            if self._newObjects[typ].has_key(oid):
                return self._newObjects[typ][oid],None

        for typ in types:
            data = self._gc.getObject(self._dbName,self._adapter,self._db,typ,oid)
            if not data:
                data = func(self._db,oid)
                if data is not None:
                    self._gc.registerObject(self._dbName,typ,oid,data)
                    return None,data
            else:
                return None,data
                
        raise FtodsObjectNotFound(oid,types and types[0] or None)






    #The callbacks that do stuff
    def _4ods_newRepoObjectId(self,ro):

        rid = None

        cid = self._adapter.getPythonClassId(self._db,ro.__class__)
        if cid is None:
            self._adapter.newPythonClass(self._db,ro.__class__)

        rid = self._adapter.newRepositoryObjectId(self._db,ro.meta_kind._v,ro.__class__,rid=rid)
        ro._4ods_setRepositoryId(rid)


    def _4ods_newCollectionId(self,col):
        
        kid = self._adapter.getPythonClassId(self._db,col.__class__)
        if kid is None:
            self._adapter.newPythonClass(self._db,col.__class__)

        cid = self._adapter.newCollection(self._db,col.__class__,col._collectionType,col._4ods_getSubType(),col._4ods_getSubRepositoryId())
        col._4ods_setCid(cid)

    def _4ods_newDictionaryId(self,dict):

        kid = self._adapter.getPythonClassId(self._db,dict.__class__)
        if kid is None:
            self._adapter.newPythonClass(self._db,dict.__class__)

        did = self._adapter.newDictionary(self._db,
                                          dict.__class__,
                                          dict._4ods_getKeyType(),
                                          dict._4ods_getKeyRepositoryId(),
                                          dict._4ods_getSubType(),
                                          dict._4ods_getSubRepositoryId())
        dict._4ods_setDid(did)


    def _4ods_newObjectId(self,o):
        #Give IDS to all new objects
        cid = self._adapter.getPythonClassId(self._db,o.__class__)
        if cid is None:
            self._adapter.newPythonClass(self._db,o.__class__)

        oid = self._adapter.newObjectId(self._db,o.__class__)
        o._4ods_setObjectId(oid)


    
    def _4ods_newRepoObjectWrite(self,ro):
        #Write a new repo objects

        #Create storage for all new repo objects
        if ro.meta_kind._v in [8,1]:
            #Create a new storage class
            self._adapter.newInterfaceStorage(self._db,ro)

        #See if it defines an extent
        if ro.meta_kind._v == 1:
            for extName in ro.extents:
                self._extents[extName] = Extent.Extent(extName,self,Constants.Types.POBJECT)


        self._adapter.writeRepositoryObject(self._db,ro.meta_kind._v,ro._4ods_getId(),ro._tupleTypes,ro._tupleNames,ro._4ods_getFullTuple())

    def _4ods_newObjectWrite(self,o):
        self._adapter.writeObject(self._db,o._4ods_getId(),o._typeId,o._tupleTypes,o._tupleNames,o._4ods_getFullTuple())

    def _4ods_newCollectionWrite(self,col):
        self._adapter.writeCollection(self._db,col._4ods_getId(),col._4ods_getChanges(),col._collectionType,col._4ods_getSubType())

    def _4ods_newDictionaryWrite(self,dict):
        self._adapter.writeDictionary(self._db,
                                      dict._4ods_getId(),
                                      dict._4ods_getChanges(),
                                      dict._4ods_getKeyType(),
                                      dict._4ods_getSubType())

        
    def _4ods_writeRepoObject(self,ro):
        #Write a new repo objects
        self._adapter.writeRepositoryObject(self._db,ro.meta_kind._v,ro._4ods_getId(),ro._tupleTypes,ro._tupleNames,ro._4ods_getModifiedTuple())

    _4ods_writeCollection = _4ods_newCollectionWrite
    _4ods_writeDictionary = _4ods_newDictionaryWrite

    def _4ods_writeObject(self,o):
        self._adapter.writeObject(self._db,o._4ods_getId(),o._typeId,o._tupleTypes,o._tupleNames,o._4ods_getModifiedTuple())


    def _4ods_deleteRepoObject(self,ro):
        #Write a new repo objects
        self._adapter.deleteRepositoryObject(self._db,ro._4ods_getId(),ro.meta_kind._v)
        #See if it defines an extent
        if ro.meta_kind._v == 1:
            for extName in ro.extents:
                ext = self._4ods_lookup(extName)
                ext.delete()

    def _4ods_deleteObject(self,o):
        #Write a new repo objects
        self._adapter.deleteObject(self._db,o._4ods_getId(),o.__class__)


    def _4ods_deleteCollection(self,col):
        self._adapter.deleteCollection(self._db,col._4ods_getId(),col._collectionType,col._4ods_getSubType())

    def _4ods_deleteDictionary(self,dict):
        self._adapter.deleteDictionary(self._db,dict._4ods_getId(),dict._4ods_getKeyType(),dict._4ods_getSubType())

    newIdMapping = {}
    newIdMapping[Constants.Types.ROBJECT] = _4ods_newRepoObjectId
    newIdMapping[Constants.Types.POBJECT] = _4ods_newObjectId
    newIdMapping[Constants.Types.LIST_COLLECTION] = _4ods_newCollectionId
    newIdMapping[Constants.Types.SET_COLLECTION] = _4ods_newCollectionId
    newIdMapping[Constants.Types.BAG_COLLECTION] = _4ods_newCollectionId
    newIdMapping[Constants.Types.DICTIONARY_COLLECTION] = _4ods_newDictionaryId

    newWriteMapping = {}
    newWriteMapping[Constants.Types.ROBJECT] = _4ods_newRepoObjectWrite
    newWriteMapping[Constants.Types.POBJECT] = _4ods_newObjectWrite
    newWriteMapping[Constants.Types.LIST_COLLECTION] = _4ods_newCollectionWrite
    newWriteMapping[Constants.Types.SET_COLLECTION] = _4ods_newCollectionWrite
    newWriteMapping[Constants.Types.BAG_COLLECTION] = _4ods_newCollectionWrite
    newWriteMapping[Constants.Types.DICTIONARY_COLLECTION] = _4ods_newDictionaryWrite

    writeMapping = {}
    writeMapping[Constants.Types.ROBJECT] = _4ods_writeRepoObject
    writeMapping[Constants.Types.POBJECT] = _4ods_writeObject
    writeMapping[Constants.Types.LIST_COLLECTION] = _4ods_writeCollection
    writeMapping[Constants.Types.SET_COLLECTION] = _4ods_writeCollection
    writeMapping[Constants.Types.BAG_COLLECTION] = _4ods_writeCollection
    writeMapping[Constants.Types.DICTIONARY_COLLECTION] = _4ods_writeDictionary

    deleteMapping = {}
    deleteMapping[Constants.Types.ROBJECT] = _4ods_deleteRepoObject
    deleteMapping[Constants.Types.POBJECT] = _4ods_deleteObject
    deleteMapping[Constants.Types.LIST_COLLECTION] = _4ods_deleteCollection
    deleteMapping[Constants.Types.SET_COLLECTION] = _4ods_deleteCollection
    deleteMapping[Constants.Types.BAG_COLLECTION] = _4ods_deleteCollection
    deleteMapping[Constants.Types.DICTIONARY_COLLECTION] = _4ods_deleteDictionary


##    def _4ods_sortNewRepos(self, repos):

##        final = []
##        cur = repos
##        still_going = []
##        for c in cur:
##            if c.meta_kind._v not in [1,8]:
##                final.append(c)
##            elif c.meta_kind._v == 8:
##                if len(c.inherits):
##                    still_going.append(c)
##                else:
##                    final.append(c)
##            elif c.meta_kind._v == 1:
##                if len(c.inherits):
##                    still_going.append(c)
##                elif c.extender:
##                    still_going.append(c)
##                else:
##                    final.append(c)
                    
##        lastLen = -1
##        while still_going:
##            cur = still_going
##            still_going = []
##            if len(cur) == lastLen:
##                #Complete pass with no changes
##                final = final + cur
##                break
##            lastLen = len(cur)
##            for ctr in range(len(cur)):
##                all_there = 1
##                c = cur[ctr]
##                for i in c.inherits:
##                    if i not in final:
##                        all_there = 0
##                if c.meta_kind._v == 1 and c.extender and c.extender not in final:
##                    all_there = 0
##                if all_there:
##                    final.append(c)
##                else:
##                    still_going.append(c)

##        return final

