__all__ = ["install"]

from panda3d.core import *
from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.showbase.PythonUtil import fastRepr
import sys
import traceback

notify = directNotify.newCategory("ExceptionVarDump")

reentry = 0

def _varDump__init__(self, *args, **kArgs):
    global reentry
    if reentry > 0:
        return
    reentry += 1
    # frame zero is this frame
    f = 1
    self._savedExcString = None
    self._savedStackFrames = []
    while True:
        try:
            frame = sys._getframe(f)
        except ValueError as e:
            break
        else:
            f += 1
            self._savedStackFrames.append(frame)
    self._moved__init__(*args, **kArgs)
    reentry -= 1

sReentry = 0

def _varDump__print(exc):
    global sReentry
    global notify
    if sReentry > 0:
        return
    sReentry += 1
    if not exc._savedExcString:
        s = ''
        foundRun = False
        for frame in reversed(exc._savedStackFrames):
            filename = frame.f_code.co_filename
            codename = frame.f_code.co_name
            if not foundRun and codename != 'run':
                # don't print stack frames before run(),
                # they contain builtins and are huge
                continue
            foundRun = True
            s += '\nlocals for %s:%s\n' % (filename, codename)
            locals = frame.f_locals
            for var in locals:
                obj = locals[var]
                rep = fastRepr(obj)
                s += '::%s = %s\n' % (var, rep)
        exc._savedExcString = s
        exc._savedStackFrames = None
    notify.info(exc._savedExcString)
    sReentry -= 1

oldExcepthook = None
# store these values here so that Task.py can always reliably access them
# from its main exception handler
wantStackDumpLog = False
wantStackDumpUpload = False
variableDumpReasons = []
dumpOnExceptionInit = False

class _AttrNotFound:
    pass

def _excepthookDumpVars(eType, eValue, tb):
    origTb = tb
    excStrs = traceback.format_exception(eType, eValue, origTb)
    s = 'printing traceback in case variable repr crashes the process...\n'
    for excStr in excStrs:
        s += excStr
    notify.info(s)
    s = 'DUMPING STACK FRAME VARIABLES'
    #import pdb;pdb.set_trace()
    #foundRun = False
    foundRun = True
    while tb is not None:
        frame = tb.tb_frame
        code = frame.f_code
        # this is a list of every string identifier used in this stack frame's code
        codeNames = set(code.co_names)
        # skip everything before the 'run' method, those frames have lots of
        # not-useful information
        if not foundRun:
            if code.co_name == 'run':
                foundRun = True
            else:
                tb = tb.tb_next
                continue
        s += '\n  File "%s", line %s, in %s' % (
            code.co_filename, frame.f_lineno, code.co_name)
        stateStack = Stack()
        # prime the stack with the variables we should visit from the frame's data structures
        # grab all of the local, builtin and global variables that appear in the code's name list
        name2obj = {}
        for name, obj in frame.f_builtins.items():
            if name in codeNames:
                name2obj[name] = obj
        for name, obj in frame.f_globals.items():
            if name in codeNames:
                name2obj[name] = obj
        for name, obj in frame.f_locals.items():
            if name in codeNames:
                name2obj[name] = obj
        # show them in alphabetical order
        names = list(name2obj.keys())
        names.sort()
        # push them in reverse order so they'll be popped in the correct order
        names.reverse()

        traversedIds = set()

        for name in names:
            stateStack.push([name, name2obj[name], traversedIds])

        while len(stateStack) > 0:
            name, obj, traversedIds = stateStack.pop()
            #notify.info('%s, %s, %s' % (name, fastRepr(obj), traversedIds))
            r = fastRepr(obj, maxLen=10)
            if type(r) is str:
                r = r.replace('\n', '\\n')
            s += '\n    %s = %s' % (name, r)
            # if we've already traversed through this object, don't traverse through it again
            if id(obj) not in traversedIds:
                attrName2obj = {}
                for attrName in codeNames:
                    attr = getattr(obj, attrName, _AttrNotFound)
                    if (attr is not _AttrNotFound):
                        # prevent infinite recursion on method wrappers (__init__.__init__.__init__...)
                        try:
                            className = attr.__class__.__name__
                        except:
                            pass
                        else:
                            if className == 'method-wrapper':
                                continue
                        attrName2obj[attrName] = attr
                if len(attrName2obj):
                    # show them in alphabetical order
                    attrNames = list(attrName2obj.keys())
                    attrNames.sort()
                    # push them in reverse order so they'll be popped in the correct order
                    attrNames.reverse()
                    ids = set(traversedIds)
                    ids.add(id(obj))
                    for attrName in attrNames:
                        obj = attrName2obj[attrName]
                        stateStack.push(['%s.%s' % (name, attrName), obj, ids])

        tb = tb.tb_next

    if foundRun:
        s += '\n'
        if wantStackDumpLog:
            notify.info(s)
        if wantStackDumpUpload:
            excStrs = traceback.format_exception(eType, eValue, origTb)
            for excStr in excStrs:
                s += excStr
            timeMgr = None
            try:
                timeMgr = base.cr.timeManager
            except:
                try:
                    timeMgr = simbase.air.timeManager
                except:
                    pass
            if timeMgr:
                timeMgr.setStackDump(s)

    oldExcepthook(eType, eValue, origTb)

def install(log, upload):
    """Installs the exception hook."""
    global oldExcepthook
    global wantStackDumpLog
    global wantStackDumpUpload
    global dumpOnExceptionInit

    wantStackDumpLog = log
    wantStackDumpUpload = upload

    dumpOnExceptionInit = ConfigVariableBool('variable-dump-on-exception-init', False)
    if dumpOnExceptionInit:
        # this mode doesn't completely work because exception objects
        # thrown by the interpreter don't get created until the
        # stack has been unwound and an except block has been reached
        if not hasattr(Exception, '_moved__init__'):
            Exception._moved__init__ = Exception.__init__
            Exception.__init__ = _varDump__init__
    else:
        if sys.excepthook is not _excepthookDumpVars:
            oldExcepthook = sys.excepthook
            sys.excepthook = _excepthookDumpVars
