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

from panda3d.core import *
from panda3d.direct import *
from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.distributed.DistributedObjectBase import DistributedObjectBase
#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

# update this table if the values above change
ESNum2Str = {
    ESNew: 'ESNew',
    ESDeleted: 'ESDeleted',
    ESDisabling: 'ESDisabling',
    ESDisabled: 'ESDisabled',
    ESGenerating: 'ESGenerating',
    ESGenerated: 'ESGenerated',
    }


class DistributedObject(DistributedObjectBase):
    """
    The Distributed Object class is the base class for all network based
    (i.e. distributed) objects.  These will usually (always?) have a
    dclass entry in a \\*.dc file.
    """
    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
            DistributedObjectBase.__init__(self, cr)

            # 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 is for Toontown only, see toontown.distributed.DelayDeletable
            self._token2delayDeleteName = {}
            self._delayDeleteForceAllow = False
            self._delayDeleted = 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

    if __debug__:
        def status(self, indent=0):
            """
            print out "doId(parentId, zoneId) className
                and conditionally show generated, disabled, neverDisable,
                or cachable"
            """
            spaces = ' ' * (indent + 2)
            try:
                print("%s%s:" % (' ' * indent, self.__class__.__name__))

                flags = []
                if self.activeState == ESGenerated:
                    flags.append("generated")
                if self.activeState < ESGenerating:
                    flags.append("disabled")
                if self.neverDisable:
                    flags.append("neverDisable")
                if self.cacheable:
                    flags.append("cacheable")

                flagStr = ""
                if len(flags):
                    flagStr = " (%s)" % (" ".join(flags))

                print("%sfrom DistributedObject doId:%s, parent:%s, zone:%s%s" % (
                    spaces, self.doId, self.parentId, self.zoneId, flagStr))
            except Exception as e:
                print("%serror printing status %s" % (spaces, e))

    def getAutoInterests(self):
        # returns the sub-zones under this object that are automatically
        # opened for us by the server.
        # have we already cached it?
        def _getAutoInterests(cls):
            # returns set of auto-interests for this class and all derived
            # have we already computed this class's autoInterests?
            if 'autoInterests' in cls.__dict__:
                autoInterests = cls.autoInterests
            else:
                autoInterests = set()
                # grab autoInterests from base classes
                for base in cls.__bases__:
                    autoInterests.update(_getAutoInterests(base))
                # grab autoInterests from this class
                if cls.__name__ in self.cr.dclassesByName:
                    dclass = self.cr.dclassesByName[cls.__name__]
                    field = dclass.getFieldByName('AutoInterest')
                    if field is not None:
                        p = DCPacker()
                        p.setUnpackData(field.getDefaultValue())
                        len = p.rawUnpackUint16() // 4
                        for i in range(len):
                            zone = int(p.rawUnpackUint32())
                            autoInterests.add(zone)
                    autoInterests.update(autoInterests)
                    cls.autoInterests = autoInterests
            return set(autoInterests)
        autoInterests = _getAutoInterests(self.__class__)
        # if the server starts supporting multiple auto-interest per class, this check
        # should be removed
        if len(autoInterests) > 1:
            self.notify.error(
                'only one auto-interest allowed per DC class, %s has %s autoInterests (%s)' %
                (self.dclass.getName(), len(autoInterests), list(autoInterests)))
        _getAutoInterests = None
        return list(autoInterests)

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

    def getNeverDisable(self):
        return self.neverDisable

    def _retrieveCachedData(self):
        # once we know our doId, grab any data that might be stored in the data cache
        # from the last time we were on the client
        if self.cr.doDataCache.hasCachedData(self.doId):
            self._cachedData = self.cr.doDataCache.popCachedData(self.doId)

    def setCachedData(self, name, data):
        assert type(name) == type('')
        # ownership of the data passes to the repository data cache
        self.cr.doDataCache.setCachedData(self.doId, name, data)

    def hasCachedData(self, name):
        assert type(name) == type('')
        if not hasattr(self, '_cachedData'):
            return False
        return name in self._cachedData

    def getCachedData(self, name):
        assert type(name) == type('')
        # ownership of the data passes to the caller of this method
        data = self._cachedData[name]
        del self._cachedData[name]
        return data

    def flushCachedData(self, name):
        assert type(name) == type('')
        # call this to throw out cached data from a previous instantiation
        self._cachedData[name].flush()

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

    def getCacheable(self):
        return self.cacheable

    def deleteOrDelay(self):
        if len(self._token2delayDeleteName) > 0:
            if not self._delayDeleted:
                self._delayDeleted = 1
                # Object is delayDeleted. Clean up DistributedObject state,
                # remove from repository tables, so that we won't crash if
                # another instance of the same object gets generated while
                # this instance is still delayDeleted.
                messenger.send(self.getDelayDeleteEvent())
                if len(self._token2delayDeleteName) > 0:
                    self.delayDelete()
                    if len(self._token2delayDeleteName) > 0:
                        self._deactivateDO()
        else:
            self.disableAnnounceAndDelete()

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

    def getDelayDeleteCount(self):
        return len(self._token2delayDeleteName)

    def getDelayDeleteEvent(self):
        return self.uniqueName("delayDelete")

    def getDisableEvent(self):
        return self.uniqueName("disable")

    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.getDisableEvent())
            self.disable()
            self.activeState = ESDisabled
            if not self._delayDeleted:
                # if the object is DelayDeleted, _deactivateDO has
                # already been called
                self._deactivateDO()

    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))


    def _deactivateDO(self):
        # after this is called, the object is no longer an active DistributedObject
        # and it may be placed in the cache
        if not self.cr:
            # we are going to crash, output the destroyDo stacktrace
            self.notify.warning('self.cr is none in _deactivateDO %d' % self.doId)
            if hasattr(self, 'destroyDoStackTrace'):
                print(self.destroyDoStackTrace)
        self.__callbacks = {}
        self.cr.closeAutoInterests(self)
        self.setLocation(0,0)
        self.cr.deleteObjectLocation(self, self.parentId, self.zoneId)

    def _destroyDO(self):
        # after this is called, the object is no longer a DistributedObject
        # but may still be used as a DelayDeleted object
        if __debug__:
            # StackTrace is omitted in packed versions
            from direct.showbase.PythonUtil import StackTrace
            self.destroyDoStackTrace = StackTrace()
        # check for leftover cached data that was not retrieved or flushed by this object
        # this will catch typos in the data name in calls to get/setCachedData
        if hasattr(self, '_cachedData'):
            for name, cachedData in self._cachedData.items():
                self.notify.warning('flushing unretrieved cached data: %s' % name)
                cachedData.flush()
            del self._cachedData
        self.cr = None
        self.dclass = None

    def disable(self):
        """
        Inheritors should redefine this to take appropriate action on disable
        """
        assert self.notify.debug('disable(): %s' % (self.doId))
        pass

    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

    def generate(self):
        """
        Inheritors should redefine this to take appropriate action on generate
        """
        assert self.notify.debugStateCall(self)
        self.activeState = ESGenerating
        # this has already been set at this point
        #self.cr.storeObjectLocation(self, self.parentId, self.zoneId)
        # semi-hack: we seem to be calling generate() more than once for objects that multiply-inherit
        if not hasattr(self, '_autoInterestHandle'):
            self.cr.openAutoInterests(self)

    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


    #This message was moved out of announce generate
    #to avoid ordering issues.
    def postGenerateMessage(self):
        if self.activeState != ESGenerated:
            self.activeState = ESGenerated
            messenger.send(self.uniqueName("generate"), [self])

    def updateRequiredFields(self, dclass, di):
        dclass.receiveUpdateBroadcastRequired(self, di)
        self.announceGenerate()
        self.postGenerateMessage()

    def updateAllRequiredFields(self, dclass, di):
        dclass.receiveUpdateAllRequired(self, di)
        self.announceGenerate()
        self.postGenerateMessage()

    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()
        self.postGenerateMessage()

        dclass.receiveUpdateOther(self, di)

    def sendUpdate(self, fieldName, args = [], sendToId = None):
        if self.cr:
            dg = self.dclass.clientFormatUpdate(
                fieldName, sendToId or self.doId, args)
            self.cr.send(dg)
        else:
            assert self.notify.error("sendUpdate failed, because self.cr is not set")

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

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

    def taskName(self, taskString):
        return ("%s-%s" % (taskString, self.doId))

    def uniqueName(self, idString):
        return ("%s-%s" % (idString, self.doId))

    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 list(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.
        for context, name, avIds in data:
            for avId in avIds:
                if self.cr.isLocalId(avId):
                    # We found the local avatar's id; stop here.
                    self.__barrierContext = (context, name)
                    assert self.notify.debug('setBarrierData(%s, %s)' % (context, name))
                    return

        # This barrier didn't involve this client; ignore it.
        assert self.notify.debug('setBarrierData(%s)' % (None))
        self.__barrierContext = None

    def getBarrierData(self):
        # Return a trivially-empty (context, name, avIds) value.
        return ((0, '', []),)

    def doneBarrier(self, name = None):
        # Tells the AI we have finished handling our task.  If the
        # optional name parameter is specified, it must match the
        # barrier name specified on the AI, or the barrier is ignored.
        # This is used to ensure we are not clearing the wrong
        # barrier.

        # 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:
            context, aiName = self.__barrierContext
            if name == None or name == aiName:
                assert self.notify.debug('doneBarrier(%s, %s)' % (context, aiName))
                self.sendUpdate("setBarrierReady", [context])
                self.__barrierContext = None
            else:
                assert self.notify.debug('doneBarrier(%s) ignored; current barrier is %s' % (name, aiName))
        else:
            assert self.notify.debug('doneBarrier(%s) ignored; no active barrier.' % (name))

    def addInterest(self, zoneId, note="", event=None):
        return self.cr.addInterest(self.getDoId(), zoneId, note, event)

    def removeInterest(self, handle, event=None):
        return self.cr.removeInterest(handle, event)

    def b_setLocation(self, parentId, zoneId):
        self.d_setLocation(parentId, zoneId)
        self.setLocation(parentId, zoneId)

    def d_setLocation(self, parentId, zoneId):
        self.cr.sendSetLocation(self.doId, parentId, zoneId)

    def setLocation(self, parentId, zoneId):
        self.cr.storeObjectLocation(self, parentId, zoneId)

    def getLocation(self):
        try:
            if self.parentId == 0 and self.zoneId == 0:
                return None
            # This is a -1 stuffed into a uint32
            if self.parentId == 0xffffffff and self.zoneId == 0xffffffff:
                return None
            return (self.parentId, self.zoneId)
        except AttributeError:
            return None

    def getParentObj(self):
        if self.parentId is None:
            return None
        return self.cr.doId2do.get(self.parentId)

    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 and self.cr.isLocalId(self.doId)

    def isGridParent(self):
        # If this distributed object is a DistributedGrid return 1.  0 by default
        return 0

    def execCommand(self, string, mwMgrId, avId, zoneId):
        pass
