from direct.distributed import DoHierarchy
import re

#hack:
BAD_DO_ID = BAD_ZONE_ID = 0 # 0xFFFFFFFF
BAD_CHANNEL_ID = 0 # 0xFFFFFFFFFFFFFFFF


class DoCollectionManager:
    def __init__(self):
        # Dict of {DistributedObject ids: DistributedObjects}
        self.doId2do = {}
        # (parentId, zoneId) to dict of doId->DistributedObjectAI
        ## self.zoneId2doIds={}
        if self.hasOwnerView():
            # Dict of {DistributedObject ids: DistributedObjects}
            # for 'owner' views of objects
            self.doId2ownerView = {}
        # Dict of {
        #   parent DistributedObject id:
        #     { zoneIds: [child DistributedObject ids] }}
        self._doHierarchy = DoHierarchy.DoHierarchy()

    def getDo(self, doId):
        return self.doId2do.get(doId)

    def getGameDoId(self):
        return self.GameGlobalsId

    def callbackWithDo(self, doId, callback):
        do = self.doId2do.get(doId)
        if do is not None:
            callback(do)
        else:
            relatedObjectMgr(doId, allCallback=callback)

    def getOwnerView(self, doId):
        assert self.hasOwnerView()
        return self.doId2ownerView.get(doId)

    def callbackWithOwnerView(self, doId, callback):
        assert self.hasOwnerView()
        do = self.doId2ownerView.get(doId)
        if do is not None:
            callback(do)
        else:
            pass #relatedObjectMgr(doId, allCallback=callback)

    def getDoTable(self, ownerView):
        if ownerView:
            assert self.hasOwnerView()
            return self.doId2ownerView
        else:
            return self.doId2do

    def doFind(self, str):
        """
        Returns list of distributed objects with matching str in value.
        """
        for value in self.doId2do.values():
            if repr(value).find(str) >= 0:
                return value

    def doFindAll(self, str):
        """
        Returns list of distributed objects with matching str in value.
        """
        matches = []
        for value in self.doId2do.values():
            if repr(value).find(str) >= 0:
                matches.append(value)
        return matches

    def doFindAllMatching(self, str):
        """
        Returns list of distributed objects with matching str in value.
        """
        matches = []
        for value in self.doId2do.values():
            if re.search(str,repr(value)):
                matches.append(value)
        return matches

    def doFindAllOfType(self, query):
        """
        Useful method for searching through the Distributed Object collection
        for objects of a particular type
        """
        matches = []
        for value in self.doId2do.values():
            if query in str(value.__class__):
                matches.append(value)
        return matches, len(matches)

    def doFindAllInstances(self, cls):
        matches = []
        for value in self.doId2do.values():
            if isinstance(value, cls):
                matches.append(value)
        return matches

    def _getDistanceFromLA(self, do):
        if hasattr(do, 'getPos'):
            return do.getPos(localAvatar).length()
        return None

    def _compareDistance(self, do1, do2):
        dist1 = self._getDistanceFromLA(do1)
        dist2 = self._getDistanceFromLA(do2)
        if dist1 is None and dist2 is None:
            return 0
        if dist1 is None:
            return 1
        if dist2 is None:
            return -1
        if (dist1 < dist2):
            return -1
        return 1

    def dosByDistance(self):
        objs = list(self.doId2do.values())
        objs.sort(cmp=self._compareDistance)
        return objs

    def doByDistance(self):
        objs = self.dosByDistance()
        for obj in objs:
            print('%s\t%s\t%s' % (obj.doId, self._getDistanceFromLA(obj),
                                  obj.dclass.getName()))

    if __debug__:
        def printObjects(self):
            format="%10s %10s %10s %30s %20s"
            title=format%("parentId", "zoneId", "doId", "dclass", "name")
            print(title)
            print('-'*len(title))
            for distObj in self.doId2do.values():
                print(format%(
                    distObj.__dict__.get("parentId"),
                    distObj.__dict__.get("zoneId"),
                    distObj.__dict__.get("doId"),
                    distObj.dclass.getName(),
                    distObj.__dict__.get("name")))

    def _printObjects(self, table):
        class2count = {}
        for obj in self.getDoTable(ownerView=False).values():
            className = obj.__class__.__name__
            class2count.setdefault(className, 0)
            class2count[className] += 1
        count2classes = invertDictLossless(class2count)
        counts = list(count2classes.keys())
        counts.sort()
        counts.reverse()
        for count in counts:
            count2classes[count].sort()
            for name in count2classes[count]:
                print('%s %s' % (count, name))
        print('')

    def _returnObjects(self, table):
        class2count = {}
        stringToReturn = ''
        for obj in self.getDoTable(ownerView=False).values():
            className = obj.__class__.__name__
            class2count.setdefault(className, 0)
            class2count[className] += 1
        count2classes = invertDictLossless(class2count)
        counts = list(count2classes.keys())
        counts.sort()
        counts.reverse()
        for count in counts:
            count2classes[count].sort()
            for name in count2classes[count]:
                # print '%s %s' % (count, name)
                stringToReturn = '%s%s %s\n' % (stringToReturn, count, name)
        # print ''
        return stringToReturn

    def webPrintObjectCount(self):
        strToReturn = '==== OBJECT COUNT ====\n'
        if self.hasOwnerView():
            strToReturn = '%s == doId2do\n' % (strToReturn)
        strToReturn = '%s%s' % (strToReturn, self._returnObjects(self.getDoTable(ownerView=False)))
        if self.hasOwnerView():
            strToReturn = '%s\n== doId2ownerView\n' % (strToReturn)
            strToReturn = '%s%s' % (strToReturn, self._returnObjects(self.getDoTable(ownerView=False)))
        return strToReturn

    def printObjectCount(self):
        # print object counts by distributed object type
        print('==== OBJECT COUNT ====')
        if self.hasOwnerView():
            print('== doId2do')
        self._printObjects(self.getDoTable(ownerView=False))
        if self.hasOwnerView():
            print('== doId2ownerView')
            self._printObjects(self.getDoTable(ownerView=True))

    def getDoList(self, parentId, zoneId=None, classType=None):
        """
        Args:
            parentId: any distributed object id.
            zoneId: a uint32, defaults to None (all zones).  Try zone 2 if
                you're not sure which zone to use (0 is a bad/null zone and
                1 has had reserved use in the past as a no messages zone, while
                2 has traditionally been a global, uber, misc stuff zone).
            dclassType: a distributed class type filter, defaults to None
                (no filter).

        If dclassName is None then all objects in the zone are returned;
        otherwise the list is filtered to only include objects of that type.
        """
        return [self.doId2do.get(i)
            for i in self.getDoIdList(parentId, zoneId, classType)]

    def getDoIdList(self, parentId, zoneId=None, classType=None):
        return self._doHierarchy.getDoIds(self.getDo,
                                          parentId, zoneId, classType)

    def hasOwnerViewDoId(self, doId):
        assert self.hasOwnerView()
        return doId in self.doId2ownerView

    def getOwnerViewDoList(self, classType):
        assert self.hasOwnerView()
        l = []
        for obj in self.doId2ownerView.values():
            if isinstance(obj, classType):
                l.append(obj)
        return l

    def getOwnerViewDoIdList(self, classType):
        assert self.hasOwnerView()
        l = []
        for doId, obj in self.doId2ownerView.items():
            if isinstance(obj, classType):
                l.append(doId)
        return l

    def countObjects(self, classType):
        """
        Counts the number of objects of the given type in the
        repository (for testing purposes)
        """
        count = 0
        for dobj in self.doId2do.values():
            if isinstance(dobj, classType):
                count += 1
        return count


    def getAllOfType(self, type):
        # Returns a list of all DistributedObjects in the repository
        # of a particular type.
        result = []
        for obj in self.doId2do.values():
            if isinstance(obj, type):
                result.append(obj)
        return result

    def findAnyOfType(self, type):
        # Searches the repository for any object of the given type.
        for obj in self.doId2do.values():
            if isinstance(obj, type):
                return obj
        return None

    #----------------------------------

    def deleteDistributedObjects(self):
        # Get rid of all the distributed objects
        for doId, do in list(self.doId2do.items()):
            self.deleteDistObject(do)

        # Get rid of everything that manages distributed objects
        self.deleteObjects()

        # the zoneId2doIds table should be empty now
        if not self._doHierarchy.isEmpty():
            self.notify.warning(
                '_doHierarchy table not empty: %s' % self._doHierarchy)
            self._doHierarchy.clear()

    def handleObjectLocation(self, di):
        # CLIENT_OBJECT_LOCATION
        doId = di.getUint32()
        parentId = di.getUint32()
        zoneId = di.getUint32()
        obj = self.doId2do.get(doId)
        if obj is not None:
            self.notify.debug(
                "handleObjectLocation: doId: %s parentId: %s zoneId: %s"%
                (doId, parentId, zoneId))
            # Let the object finish the job
            # calls storeObjectLocation()
            obj.setLocation(parentId, zoneId)
        else:
            self.notify.warning(
                "handleObjectLocation: Asked to update non-existent obj: %s" % (doId))

    def handleSetLocation(self, di):
        # This was initially added because creating a distributed quest
        # object would cause a message like this to be generated.
        assert self.notify.debugStateCall(self)
        parentId = di.getUint32()
        zoneId = di.getUint32()
        distObj = self.doId2do.get(self.getMsgChannel())
        if distObj is not None:
            distObj.setLocation(parentId, zoneId)
        else:
            self.notify.warning('handleSetLocation: object %s not present' % self.getMsgChannel())

    def storeObjectLocation(self, object, parentId, zoneId):
        oldParentId = object.parentId
        oldZoneId = object.zoneId
        if (oldParentId != parentId):
            # notify any existing parent that we're moving away
            oldParentObj = self.doId2do.get(oldParentId)
            if oldParentObj is not None:
                oldParentObj.handleChildLeave(object, oldZoneId)
            self.deleteObjectLocation(object, oldParentId, oldZoneId)

        elif (oldZoneId != zoneId):
            # Remove old location
            oldParentObj = self.doId2do.get(oldParentId)
            if oldParentObj is not None:
                oldParentObj.handleChildLeaveZone(object, oldZoneId)
            self.deleteObjectLocation(object, oldParentId, oldZoneId)
        else:
            # object is already at that parent and zone
            return

        if ((parentId is None) or (zoneId is None) or
            (parentId == zoneId == 0)):
            # Do not store null values
            object.parentId = None
            object.zoneId = None
        else:
            # Add to new location
            self._doHierarchy.storeObjectLocation(object, parentId, zoneId)
            # this check doesn't work because of global UD objects;
            # should they have a location?
            #assert len(self._doHierarchy) == len(self.doId2do)

            # Set the new parent and zone on the object
            object.parentId = parentId
            object.zoneId = zoneId


        if oldParentId != parentId:
            # Give the parent a chance to run code when a new child
            # sets location to it. For example, the parent may want to
            # scene graph reparent the child to some subnode it owns.
            parentObj = self.doId2do.get(parentId)
            if parentObj is not None:
                parentObj.handleChildArrive(object, zoneId)
            elif parentId not in (None, 0, self.getGameDoId()):
                self.notify.warning('storeObjectLocation(%s): parent %s not present' %
                                    (object.doId, parentId))
        elif oldZoneId != zoneId:
            parentObj = self.doId2do.get(parentId)
            if parentObj is not None:
                parentObj.handleChildArriveZone(object, zoneId)
            elif parentId not in (None, 0, self.getGameDoId()):
                self.notify.warning('storeObjectLocation(%s): parent %s not present' %
                                    (object.doId, parentId))

    def deleteObjectLocation(self, object, parentId, zoneId):
        # Do not worry about null values
        if ((parentId is None) or (zoneId is None) or
            (parentId == zoneId == 0)):
            return

        self._doHierarchy.deleteObjectLocation(object, parentId, zoneId)

    def addDOToTables(self, do, location=None, ownerView=False):
        assert self.notify.debugStateCall(self)
        #assert not hasattr(do, "isQueryAllResponse") or not do.isQueryAllResponse
        if not ownerView:
            if location is None:
                location = (do.parentId, do.zoneId)

        doTable = self.getDoTable(ownerView)

        # make sure the object is not already present
        if do.doId in doTable:
            if ownerView:
                tableName = 'doId2ownerView'
            else:
                tableName = 'doId2do'
            self.notify.error('doId %s already in %s [%s stomping %s]' % (
                do.doId, tableName, do.__class__.__name__,
                doTable[do.doId].__class__.__name__))

        doTable[do.doId]=do

        if not ownerView:
            if self.isValidLocationTuple(location):
                self.storeObjectLocation(do, location[0], location[1])
                ##assert do.doId not in self.zoneId2doIds.get(location, {})
                ##self.zoneId2doIds.setdefault(location, {})
                ##self.zoneId2doIds[location][do.doId]=do

    def isValidLocationTuple(self, location):
        return (location is not None
            and location != (0xffffffff, 0xffffffff)
            and location != (0, 0))

    if __debug__:
        def isInDoTables(self, doId):
            assert self.notify.debugStateCall(self)
            return doId in self.doId2do

    def removeDOFromTables(self, do):
        assert self.notify.debugStateCall(self)
        #assert not hasattr(do, "isQueryAllResponse") or not do.isQueryAllResponse
        #assert do.doId in self.doId2do
        location = do.getLocation()
        if location:
           oldParentId, oldZoneId = location
           oldParentObj = self.doId2do.get(oldParentId)
           if oldParentObj:
               oldParentObj.handleChildLeave(do, oldZoneId)
        self.deleteObjectLocation(do, do.parentId, do.zoneId)
        ## location = do.getLocation()
        ## if location is not None:
        ##     if location not in self.zoneId2doIds:
        ##         self.notify.warning(
        ##             'dobj %s (%s) has invalid location: %s' %
        ##             (do, do.doId, location))
        ##     else:
        ##         assert do.doId in self.zoneId2doIds[location]
        ##         del self.zoneId2doIds[location][do.doId]
        ##         if len(self.zoneId2doIds[location]) == 0:
        ##             del self.zoneId2doIds[location]
        if do.doId in self.doId2do:
            del self.doId2do[do.doId]

    ## def changeDOZoneInTables(self, do, newParentId, newZoneId, oldParentId, oldZoneId):
    ##     if 1:
    ##         self.storeObjectLocation(do.doId, newParentId, newZoneId)
    ##     else:
    ##         #assert not hasattr(do, "isQueryAllResponse") or not do.isQueryAllResponse
    ##         oldLocation = (oldParentId, oldZoneId)
    ##         newLocation = (newParentId, newZoneId)
    ##         # HACK: DistributedGuildMemberUD starts in -1, -1, which isnt ever put in the
    ##         # zoneId2doIds table
    ##         if self.isValidLocationTuple(oldLocation):
    ##             assert self.notify.debugStateCall(self)
    ##             assert oldLocation in self.zoneId2doIds
    ##             assert do.doId in self.zoneId2doIds[oldLocation]
    ##             assert do.doId not in self.zoneId2doIds.get(newLocation, {})
    ##             # remove from old zone
    ##             del(self.zoneId2doIds[oldLocation][do.doId])
    ##             if len(self.zoneId2doIds[oldLocation]) == 0:
    ##                 del self.zoneId2doIds[oldLocation]
    ##         if self.isValidLocationTuple(newLocation):
    ##             # add to new zone
    ##             self.zoneId2doIds.setdefault(newLocation, {})
    ##             self.zoneId2doIds[newLocation][do.doId]=do

    def getObjectsInZone(self, parentId, zoneId):
        """
        returns dict of doId:distObj for a zone.
        returned dict is safely mutable.
        """
        assert self.notify.debugStateCall(self)
        doDict = {}
        for doId in self.getDoIdList(parentId, zoneId):
            doDict[doId] = self.getDo(doId)
        return doDict

    def getObjectsOfClassInZone(self, parentId, zoneId, objClass):
        """
        returns dict of doId:object for a zone, containing all objects
        that inherit from 'class'. returned dict is safely mutable.
        """
        assert self.notify.debugStateCall(self)
        doDict = {}
        for doId in self.getDoIdList(parentId, zoneId, objClass):
            doDict[doId] = self.getDo(doId)
        return doDict
