"""
Using ConsoleWidget to interactively inspect exception backtraces


TODO
 - fix uncaught exceptions in threads (python 3.12)
 - allow using qtconsole
 - provide thread info for stacks
 - add thread browser?
 - add object browser?
    - clicking on a stack frame populates list of locals?
 - optional merged exception stacks

"""

import sys
import queue
import functools
import threading
import pyqtgraph as pg
import pyqtgraph.console
from pyqtgraph.Qt import QtWidgets
from pyqtgraph.debug import threadName


def raiseException():
    """Raise an exception
    """
    x = "inside raiseException()"
    raise Exception(f"Raised an exception {x} in {threadName()}")


def raiseNested():
    """Raise an exception while handling another
    """
    x = "inside raiseNested()"
    try:
        raiseException()
    except Exception:
        raise Exception(f"Raised during exception handling {x} in {threadName()}")


def raiseFrom():
    """Raise an exception from another
    """
    x = "inside raiseFrom()"
    try:
        raiseException()
    except Exception as exc:
        raise Exception(f"Raised-from during exception handling {x} in {threadName()}") from exc


def raiseCaughtException():
    """Raise and catch an exception
    """
    x = "inside raiseCaughtException()"
    try:
        raise Exception(f"Raised an exception {x} in {threadName()}")
    except Exception:
        print(f"Raised and caught exception {x} in {threadName()}  trace: {sys._getframe().f_trace}")


def captureStack():
    """Inspect the curent call stack
    """
    x = "inside captureStack()"
    global console
    console.setStack()
    return x


# Background thread for running functions
threadRunQueue = queue.Queue()
def threadRunner():
    global threadRunQueue
    # This is necessary to allow installing trace functions in the thread later on
    sys.settrace(lambda *args: None)
    while True:
        func, args = threadRunQueue.get()
        try:
            print(f"running {func} from thread, trace: {sys._getframe().f_trace}")
            func(*args)
        except Exception:
            sys.excepthook(*sys.exc_info())
thread = threading.Thread(target=threadRunner, name="background_thread", daemon=True)
thread.start()


# functions used to generate a stack a few items deep
def runInStack(func):
    x = "inside runInStack(func)"
    runInStack2(func)
    return x

def runInStack2(func):
    x = "inside runInStack2(func)"
    runInStack3(func)
    return x

def runInStack3(func):
    x = "inside runInStack3(func)"
    runInStack4(func)
    return x

def runInStack4(func):
    x = "inside runInStack4(func)"
    func()
    return x


class SignalEmitter(pg.QtCore.QObject):
    signal = pg.QtCore.Signal(object, object)
    def __init__(self, queued):
        pg.QtCore.QObject.__init__(self)
        if queued:
            self.signal.connect(self.run, pg.QtCore.Qt.ConnectionType.QueuedConnection)
        else:
            self.signal.connect(self.run)
    def run(self, func, args):
        func(*args)
signalEmitter = SignalEmitter(queued=False)
queuedSignalEmitter = SignalEmitter(queued=True)



def runFunc(func):
    if signalCheck.isChecked():
        if queuedSignalCheck.isChecked():
            func = functools.partial(queuedSignalEmitter.signal.emit, runInStack, (func,))
        else:
            func = functools.partial(signalEmitter.signal.emit, runInStack, (func,))
    
    if threadCheck.isChecked():
        threadRunQueue.put((runInStack, (func,)))
    else:
        runInStack(func)



funcs = [
    raiseException,
    raiseNested,
    raiseFrom,
    raiseCaughtException,
    captureStack,
]

app = pg.mkQApp()

win = pg.QtWidgets.QSplitter(pg.QtCore.Qt.Orientation.Horizontal)

ctrl = QtWidgets.QWidget()
ctrlLayout = QtWidgets.QVBoxLayout()
ctrl.setLayout(ctrlLayout)
win.addWidget(ctrl)

btns = []
for func in funcs:
    btn = QtWidgets.QPushButton(func.__doc__)
    btn.clicked.connect(functools.partial(runFunc, func))
    btns.append(btn)
    ctrlLayout.addWidget(btn)

threadCheck = QtWidgets.QCheckBox('Run in thread')
ctrlLayout.addWidget(threadCheck)

signalCheck = QtWidgets.QCheckBox('Run from Qt signal')
ctrlLayout.addWidget(signalCheck)

queuedSignalCheck = QtWidgets.QCheckBox('Use queued Qt signal')
ctrlLayout.addWidget(queuedSignalCheck)

ctrlLayout.addStretch()

console = pyqtgraph.console.ConsoleWidget(text="""
Use ConsoleWidget to interactively inspect exception tracebacks and call stacks!

- Enable "Show next exception" and the next unhandled exception will be displayed below.
- Click any of the buttons to the left to generate an exception.
- When an exception traceback is shown, you can select any of the stack frames and then run commands from that context,
  allowing you to inspect variables along the stack. (hint: most of the functions called by the buttons to the left 
  have a variable named "x" in their local scope)
- Note that this is not like a typical debugger--the program is not paused when an exception is caught; we simply keep
  a reference to the stack frames and continue on.
- By default, we only catch unhandled exceptions. If you need to inspect a handled exception (one that is caught by
  a try:except block), then uncheck the "Only handled exceptions" box. Note, however that this incurs a performance 
  penalty and will interfere with other debuggers.


""")
console.catchNextException()
win.addWidget(console)

win.resize(1400, 800)
win.setSizes([300, 1100])
win.show()

if __name__ == '__main__':
    pg.exec()
