# objects that report different types of leaks to the ContainerLeakDetector

from panda3d.core import *
from direct.showbase.DirectObject import DirectObject
from direct.showbase.Job import Job
import gc, sys

if sys.version_info >= (3, 0):
    import builtins
else:
    import __builtin__ as builtins


class LeakDetector:
    def __init__(self):
        # put this object just under __builtins__ where the
        # ContainerLeakDetector will find it quickly
        if not hasattr(builtins, "leakDetectors"):
            builtins.leakDetectors = {}
        self._leakDetectorsKey = self.getLeakDetectorKey()
        if __dev__:
            assert self._leakDetectorsKey not in leakDetectors
        leakDetectors[self._leakDetectorsKey] = self
    def destroy(self):
        del leakDetectors[self._leakDetectorsKey]

    def getLeakDetectorKey(self):
        # this string will be shown to the end user and should ideally contain enough information to
        # point to what is leaking
        return '%s-%s' % (self.__class__.__name__, id(self))

class ObjectTypeLeakDetector(LeakDetector):
    def __init__(self, otld, objType, generation):
        self._otld = otld
        self._objType = objType
        self._generation = generation
        LeakDetector.__init__(self)

    def destroy(self):
        self._otld = None
        LeakDetector.destroy(self)

    def getLeakDetectorKey(self):
        return '%s-%s' % (self._objType, self.__class__.__name__)

    def __len__(self):
        num = self._otld._getNumObjsOfType(self._objType, self._generation)
        self._generation = self._otld._getGeneration()
        return num

class ObjectTypesLeakDetector(LeakDetector):
    # are we accumulating any particular Python object type?
    def __init__(self):
        LeakDetector.__init__(self)
        self._type2ld = {}
        self._type2count = {}
        self._generation = 0
        self._thisLdGen = 0

    def destroy(self):
        for ld in self._type2ld.values():
            ld.destroy()
        LeakDetector.destroy(self)

    def _recalc(self):
        objs = gc.get_objects()
        self._type2count = {}
        for obj in objs:
            objType = safeTypeName(obj)
            if objType not in self._type2ld:
                self._type2ld[objType] = ObjectTypeLeakDetector(self, objType, self._generation)
            self._type2count.setdefault(objType, 0)
            self._type2count[objType] += 1
        self._generation += 1

    def _getGeneration(self):
        return self._generation

    def _getNumObjsOfType(self, objType, otherGen):
        if self._generation == otherGen:
            self._recalc()
        return self._type2count.get(objType, 0)

    def __len__(self):
        if self._generation == self._thisLdGen:
            self._recalc()
        self._thisLdGen = self._generation
        return len(self._type2count)

class GarbageLeakDetector(LeakDetector):
    # are we accumulating Python garbage?
    def __len__(self):
        # do a garbage collection
        oldFlags = gc.get_debug()
        gc.set_debug(0)
        gc.collect()
        numGarbage = len(gc.garbage)
        del gc.garbage[:]
        gc.set_debug(oldFlags)
        return numGarbage

class SceneGraphLeakDetector(LeakDetector):
    # is a scene graph leaking nodes?
    def __init__(self, render):
        LeakDetector.__init__(self)
        self._render = render
        if config.GetBool('leak-scene-graph', 0):
            self._leakTaskName = 'leakNodes-%s' % serialNum()
            self._leakNode()
    def destroy(self):
        if hasattr(self, '_leakTaskName'):
            taskMgr.remove(self._leakTaskName)
        del self._render
        LeakDetector.destroy(self)
    def __len__(self):
        try:
            # this will be available when the build server finishes
            return self._render.countNumDescendants()
        except:
            return self._render.getNumDescendants()
    def __repr__(self):
        return 'SceneGraphLeakDetector(%s)' % self._render
    def _leakNode(self, task=None):
        self._render.attachNewNode('leakNode-%s' % serialNum())
        taskMgr.doMethodLater(10, self._leakNode, self._leakTaskName)

class CppMemoryUsage(LeakDetector):
    def __len__(self):
        haveMemoryUsage = True
        try:
            MemoryUsage
        except:
            haveMemoryUsage = False
        if haveMemoryUsage:
            return int(MemoryUsage.getCurrentCppSize())
        else:
            return 0

class TaskLeakDetectorBase:
    def _getTaskNamePattern(self, taskName):
        # get a generic string pattern from a task name by removing numeric characters
        for i in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9):
            taskName = taskName.replace('%s' % i, '')
        return taskName

class _TaskNamePatternLeakDetector(LeakDetector, TaskLeakDetectorBase):
    # tracks the number of each individual task type
    # e.g. are we leaking 'examine-<doId>' tasks
    def __init__(self, taskNamePattern):
        self._taskNamePattern = taskNamePattern
        LeakDetector.__init__(self)

    def __len__(self):
        # count the number of tasks that match our task name pattern
        numTasks = 0
        for task in taskMgr.getTasks():
            if self._getTaskNamePattern(task.name) == self._taskNamePattern:
                numTasks += 1
        for task in taskMgr.getDoLaters():
            if self._getTaskNamePattern(task.name) == self._taskNamePattern:
                numTasks += 1
        return numTasks

    def getLeakDetectorKey(self):
        return '%s-%s' % (self._taskNamePattern, self.__class__.__name__)

class TaskLeakDetector(LeakDetector, TaskLeakDetectorBase):
    # tracks the number task 'types' and creates leak detectors for each task type
    def __init__(self):
        LeakDetector.__init__(self)
        self._taskName2collector = {}

    def destroy(self):
        for taskName, collector in self._taskName2collector.items():
            collector.destroy()
        del self._taskName2collector
        LeakDetector.destroy(self)

    def _processTaskName(self, taskName):
        # if this is a new task name pattern, create a leak detector for that pattern
        namePattern = self._getTaskNamePattern(taskName)
        if namePattern not in self._taskName2collector:
            self._taskName2collector[namePattern] = _TaskNamePatternLeakDetector(namePattern)

    def __len__(self):
        self._taskName2collector = {}
        # update our table of task leak detectors
        for task in taskMgr.getTasks():
            self._processTaskName(task.name)
        for task in taskMgr.getDoLaters():
            self._processTaskName(task.name)
        # are we leaking task types?
        return len(self._taskName2collector)

class MessageLeakDetectorBase:
    def _getMessageNamePattern(self, msgName):
        # get a generic string pattern from a message name by removing numeric characters
        for i in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9):
            msgName = msgName.replace('%s' % i, '')
        return msgName

class _MessageTypeLeakDetector(LeakDetector, MessageLeakDetectorBase):
    # tracks the number of objects that are listening to each message
    def __init__(self, msgNamePattern):
        self._msgNamePattern = msgNamePattern
        self._msgNames = set()
        LeakDetector.__init__(self)

    def addMsgName(self, msgName):
        # for efficiency, we keep the actual message names around
        # for queries on the messenger
        self._msgNames.add(msgName)

    def __len__(self):
        toRemove = set()
        num = 0
        for msgName in self._msgNames:
            n = messenger._getNumListeners(msgName)
            if n == 0:
                toRemove.add(msgName)
            else:
                num += n
        # remove message names that are no longer in the messenger
        self._msgNames.difference_update(toRemove)
        return num

    def getLeakDetectorKey(self):
        return '%s-%s' % (self._msgNamePattern, self.__class__.__name__)

class _MessageTypeLeakDetectorCreator(Job):
    def __init__(self, creator):
        Job.__init__(self, uniqueName(typeName(self)))
        self._creator = creator

    def destroy(self):
        self._creator = None
        Job.destroy(self)

    def finished(self):
        Job.finished(self)

    def run(self):
        for msgName in messenger._getEvents():
            yield None
            namePattern = self._creator._getMessageNamePattern(msgName)
            if namePattern not in self._creator._msgName2detector:
                self._creator._msgName2detector[namePattern] = _MessageTypeLeakDetector(namePattern)
            self._creator._msgName2detector[namePattern].addMsgName(msgName)
        yield Job.Done

class MessageTypesLeakDetector(LeakDetector, MessageLeakDetectorBase):
    def __init__(self):
        LeakDetector.__init__(self)
        self._msgName2detector = {}
        self._createJob = None
        if config.GetBool('leak-message-types', 0):
            self._leakers = []
            self._leakTaskName = uniqueName('leak-message-types')
            taskMgr.add(self._leak, self._leakTaskName)

    def _leak(self, task):
        self._leakers.append(DirectObject())
        self._leakers[-1].accept('leak-msg', self._leak)
        return task.cont

    def destroy(self):
        if hasattr(self, '_leakTaskName'):
            taskMgr.remove(self._leakTaskName)
            for leaker in self._leakers:
                leaker.ignoreAll()
            self._leakers = None
        if self._createJob:
            self._createJob.destroy()
        self._createJob = None
        for msgName, detector in self._msgName2detector.items():
            detector.destroy()
        del self._msgName2detector
        LeakDetector.destroy(self)

    def __len__(self):
        if self._createJob:
            if self._createJob.isFinished():
                self._createJob.destroy()
                self._createJob = None
        self._createJob = _MessageTypeLeakDetectorCreator(self)
        jobMgr.add(self._createJob)
        # are we leaking message types?
        return len(self._msgName2detector)

class _MessageListenerTypeLeakDetector(LeakDetector):
    # tracks the number of each object type that is listening for events
    def __init__(self, typeName):
        self._typeName = typeName
        LeakDetector.__init__(self)

    def __len__(self):
        numObjs = 0
        for obj in messenger._getObjects():
            if typeName(obj) == self._typeName:
                numObjs += 1
        return numObjs

    def getLeakDetectorKey(self):
        return '%s-%s' % (self._typeName, self.__class__.__name__)

class _MessageListenerTypeLeakDetectorCreator(Job):
    def __init__(self, creator):
        Job.__init__(self, uniqueName(typeName(self)))
        self._creator = creator

    def destroy(self):
        self._creator = None
        Job.destroy(self)

    def finished(self):
        Job.finished(self)

    def run(self):
        for obj in messenger._getObjects():
            yield None
            tName = typeName(obj)
            if tName not in self._creator._typeName2detector:
                self._creator._typeName2detector[tName] = (
                    _MessageListenerTypeLeakDetector(tName))
        yield Job.Done

class MessageListenerTypesLeakDetector(LeakDetector):
    def __init__(self):
        LeakDetector.__init__(self)
        self._typeName2detector = {}
        self._createJob = None
        if config.GetBool('leak-message-listeners', 0):
            self._leakers = []
            self._leakTaskName = uniqueName('leak-message-listeners')
            taskMgr.add(self._leak, self._leakTaskName)

    def _leak(self, task):
        self._leakers.append(DirectObject())
        self._leakers[-1].accept(uniqueName('leak-msg-listeners'), self._leak)
        return task.cont

    def destroy(self):
        if hasattr(self, '_leakTaskName'):
            taskMgr.remove(self._leakTaskName)
            for leaker in self._leakers:
                leaker.ignoreAll()
            self._leakers = None
        if self._createJob:
            self._createJob.destroy()
        self._createJob = None
        for typeName, detector in self._typeName2detector.items():
            detector.destroy()
        del self._typeName2detector
        LeakDetector.destroy(self)

    def __len__(self):
        if self._createJob:
            if self._createJob.isFinished():
                self._createJob.destroy()
                self._createJob = None
        self._createJob = _MessageListenerTypeLeakDetectorCreator(self)
        jobMgr.add(self._createJob)
        # are we leaking listener types?
        return len(self._typeName2detector)
