"""DistributedObject module: contains the DistributedObject class"""

from direct.showbase.PandaObject import *
from direct.directnotify.DirectNotifyGlobal import *
from PyDatagram import PyDatagram
from PyDatagramIterator import PyDatagramIterator

# Values for DistributedObject.activeState

ESNew          = 1
ESDeleted      = 2
ESDisabling    = 3
ESDisabled     = 4  # values here and lower are considered "disabled"
ESGenerating   = 5  # values here and greater are considered "generated"
ESGenerated    = 6

class DistributedObject(PandaObject):
    """Distributed Object class:"""
    notify = directNotify.newCategory("DistributedObject")

    # A few objects will set neverDisable to 1... Examples are
    # localToon, and anything that lives in the UberZone. This
    # keeps them from being disabled when you change zones,
    # even to the quiet zone.
    neverDisable = 0

    def __init__(self, cr):
        assert self.notify.debugStateCall(self)
        try:
            self.DistributedObject_initialized
        except:
            self.DistributedObject_initialized = 1
            self.cr = cr
            if wantOtpServer:
                # Location stores the parentId, zoneId of this object
                self.__location = (None, None)

            # Most DistributedObjects are simple and require no real
            # effort to load.  Some, particularly actors, may take
            # some significant time to load; these we can optimize by
            # caching them when they go away instead of necessarily
            # deleting them.  The object should set cacheable to 1 if
            # it needs to be optimized in this way.
            self.setCacheable(0)

            # This count tells whether the object can be deleted right away,
            # or not.
            self.delayDeleteCount = 0
            # This flag tells whether a delete has been requested on this
            # object.
            self.deleteImminent = 0

            # Keep track of our state as a distributed object.  This
            # is only trustworthy if the inheriting class properly
            # calls up the chain for disable() and generate().
            self.activeState = ESNew

            # These are used by getCallbackContext() and doCallbackContext().
            self.__nextContext = 0
            self.__callbacks = {}

            # This is used by doneBarrier().
            self.__barrierContext = None

            #zone of the distributed object, default to 0
            self.zone = 0

    def setNeverDisable(self, bool):
        assert((bool == 1) or (bool == 0))
        self.neverDisable = bool

    def getNeverDisable(self):
        return self.neverDisable

    def setCacheable(self, bool):
        assert((bool == 1) or (bool == 0))
        self.cacheable = bool
        return None

    def getCacheable(self):
        return self.cacheable

    def deleteOrDelay(self):
        if self.delayDeleteCount > 0:
            self.deleteImminent = 1
        else:
            self.disableAnnounceAndDelete()

    def delayDelete(self, flag):
        # Flag should be 0 or 1, meaning increment or decrement count
        # Also see DelayDelete.py

        if (flag == 1):
            self.delayDeleteCount += 1
        elif (flag == 0):
            self.delayDeleteCount -= 1
        else:
            self.notify.error("Invalid flag passed to delayDelete: " + str(flag))

        if (self.delayDeleteCount < 0):
            self.notify.error("Somebody decremented delayDelete for doId %s without incrementing"
                              % (self.doId))
        elif (self.delayDeleteCount == 0):
            assert(self.notify.debug("delayDeleteCount for doId %s now 0"
                                     % (self.doId)))
            if self.deleteImminent:
                assert(self.notify.debug("delayDeleteCount for doId %s -- deleteImminent"
                                         % (self.doId)))
                self.disableAnnounceAndDelete()
        else:
            self.notify.debug("delayDeleteCount for doId %s now %s"
                              % (self.doId, self.delayDeleteCount))

        # Return the count just for kicks
        return self.delayDeleteCount

    def disableAnnounceAndDelete(self):
        self.disableAndAnnounce()
        self.delete()

    def disableAndAnnounce(self):
        """
        Inheritors should *not* redefine this function.
        """
        # We must send the disable announce message *before* we
        # actually disable the object.  That way, the various cleanup
        # tasks can run first and take care of restoring the object to
        # a normal, nondisabled state; and *then* the disable function
        # can properly disable it (for instance, by parenting it to
        # hidden).
        if self.activeState != ESDisabled:
            self.activeState = ESDisabling
            messenger.send(self.uniqueName("disable"))
            self.disable()

    def announceGenerate(self):
        """
        Sends a message to the world after the object has been
        generated and all of its required fields filled in.
        """
        assert(self.notify.debug('announceGenerate(): %s' % (self.doId)))
        if self.activeState != ESGenerated:
            self.activeState = ESGenerated
            messenger.send(self.uniqueName("generate"), [self])

    def disable(self):
        """
        Inheritors should redefine this to take appropriate action on disable
        """
        assert(self.notify.debug('disable(): %s' % (self.doId)))
        if self.activeState != ESDisabled:
            self.activeState = ESDisabled
            self.__callbacks = {}
            if wantOtpServer:
                self.cr.deleteObjectLocation(self.doId, self.__location[0], self.__location[1])
                self.__location = (None, None)
                # TODO: disable my children

    def isDisabled(self):
        """
        Returns true if the object has been disabled and/or deleted,
        or if it is brand new and hasn't yet been generated.
        """
        return (self.activeState < ESGenerating)

    def isGenerated(self):
        """
        Returns true if the object has been fully generated by now,
        and not yet disabled.
        """
        assert self.notify.debugStateCall(self)
        return (self.activeState == ESGenerated)

    def delete(self):
        """
        Inheritors should redefine this to take appropriate action on delete
        """
        assert(self.notify.debug('delete(): %s' % (self.doId)))
        try:
            self.DistributedObject_deleted
        except:
            self.DistributedObject_deleted = 1
            self.cr = None
            self.dclass = None
            
    def generate(self):
        """
        Inheritors should redefine this to take appropriate action on generate
        """
        assert self.notify.debugStateCall(self)
        self.activeState = ESGenerating

    def generateInit(self):
        """
        This method is called when the DistributedObject is first introduced
        to the world... Not when it is pulled from the cache.
        """
        self.activeState = ESGenerating
    
    def getDoId(self):
        """
        Return the distributed object id
        """
        return self.doId
    
    def updateRequiredFields(self, dclass, di):
        dclass.receiveUpdateBroadcastRequired(self, di)
        self.announceGenerate()
    
    def updateAllRequiredFields(self, dclass, di):
        dclass.receiveUpdateAllRequired(self, di)
        self.announceGenerate()

    def updateRequiredOtherFields(self, dclass, di):
        # First, update the required fields
        dclass.receiveUpdateBroadcastRequired(self, di)

        # Announce generate after updating all the required fields,
        # but before we update the non-required fields.
        self.announceGenerate()

        dclass.receiveUpdateOther(self, di)

    def sendUpdate(self, fieldName, args = [], sendToId = None):
        if self.cr:
            self.cr.sendUpdate(self, fieldName, args, sendToId)

    def sendDisableMsg(self):
        self.cr.sendDisableMsg(self.doId)

    def sendDeleteMsg(self):
        self.cr.sendDeleteMsg(self.doId)

    def taskName(self, taskString):
        return (taskString + "-" + str(self.getDoId()))
    
    def uniqueName(self, idString):
        return (idString + "-" + str(self.getDoId()))

    def getCallbackContext(self, callback, extraArgs = []):
        # Some objects implement a back-and-forth handshake operation
        # with the AI via an arbitrary context number.  This method
        # (coupled with doCallbackContext(), below) maps a Python
        # callback onto that context number so that client code may
        # easily call the method and wait for a callback, rather than
        # having to negotiate context numbers.

        # This method generates a new context number and stores the
        # callback so that it may later be called when the response is
        # returned.

        # This is intended to be called within derivations of
        # DistributedObject, not directly by other objects.

        context = self.__nextContext
        self.__callbacks[context] = (callback, extraArgs)
        # We assume the context number is passed as a uint16.
        self.__nextContext = (self.__nextContext + 1) & 0xffff

        return context

    def getCurrentContexts(self):
        # Returns a list of the currently outstanding contexts created
        # by getCallbackContext().
        return self.__callbacks.keys()

    def getCallback(self, context):
        # Returns the callback that was passed in to the previous
        # call to getCallbackContext.
        return self.__callbacks[context][0]

    def getCallbackArgs(self, context):
        # Returns the extraArgs that were passed in to the previous
        # call to getCallbackContext.
        return self.__callbacks[context][1]

    def doCallbackContext(self, context, args):
        # This is called after the AI has responded to the message
        # sent via getCallbackContext(), above.  The context number is
        # looked up in the table and the associated callback is
        # issued.

        # This is intended to be called within derivations of
        # DistributedObject, not directly by other objects.

        tuple = self.__callbacks.get(context)
        if tuple:
            callback, extraArgs = tuple
            completeArgs = args + extraArgs
            if callback != None:
                callback(*completeArgs)
            del self.__callbacks[context]
        else:
            self.notify.warning("Got unexpected context from AI: %s" % (context))
        
    def setBarrierData(self, data):
        # This message is sent by the AI to tell us the barriers and
        # avIds for which the AI is currently waiting.  The client
        # needs to look up its pending context in the table (and
        # ignore the other contexts).  When the client is done
        # handling whatever it should handle in its current state, it
        # should call doneBarrier(), which will send the context
        # number back to the AI.
        dg = PyDatagram(data)
        dgi = PyDatagramIterator(dg)
        while dgi.getRemainingSize() > 0:
            context = dgi.getUint16()
            numToons = dgi.getUint16()
            for i in range(numToons):
                avId = dgi.getUint32()
                if avId == base.localAvatar.doId:
                    # We found localToon's Id; stop here.
                    self.__barrierContext = context
                    assert(self.notify.debug('setBarrierData(%s)' % (context)))
                    return
                
        assert(self.notify.debug('setBarrierData(%s)' % (None)))
        self.__barrierContext = None
        
    def doneBarrier(self):
        # Tells the AI we have finished handling our task.
        assert(self.notify.debug('doneBarrier(%s)' % (self.__barrierContext)))

        # If this is None, it either means we have called
        # doneBarrier() twice, or we have not received a barrier
        # context from the AI.  I think in either case it's ok to
        # silently ignore the error.
        if self.__barrierContext != None:
            self.sendUpdate("setBarrierReady", [self.__barrierContext])
            self.__barrierContext = None
    
    if wantOtpServer:
        def addInterest(self, zoneId, note=""):
            self.cr.addInterest(self.getDoId(), zoneId, note)
        
        def setLocation(self, parentId, zoneId):
            # The store must run first so we know the old location
            self.cr.storeObjectLocation(self.doId, parentId, zoneId)
            self.__location = (parentId, zoneId)

        def getLocation(self):
            return self.__location

    def isLocal(self):
        # This returns true if the distributed object is "local,"
        # which means the client created it instead of the AI, and it
        # gets some other special handling.  Normally, only the local
        # avatar class overrides this to return true.
        return (self.cr.isLocalId(self.doId))

    def updateZone(self, zoneId):
        self.cr.sendUpdateZone(self, zoneId)

