#!/usr/bin/env python
#
#                               Copyright 2003
#                                     by
#                        The Board of Trustees of the
#                     Leland Stanford Junior University.
#                            All rights reserved.
#

__facility__ = "Online"
__abstract__ = "Classes for launching Qt objects from non-GUI threads"
__author__   = "S. Tuvi <stuvi@SLAC.Stanford.edu> SLAC - GLAST LAT I&T/Online"
__date__     = "12/19/2003"
__version__  = "$Revision: 2.8 $"
__credits__  = "SLAC"


import logging       as     log
from   qt            import *
from   Queue         import Queue
import threading
import sys


class GUIbridge(object):
  """\brief Class for providing a bridge between the GUI thread and other threads.

  This bridge is needed whenever a non-GUI thread needs to interact with the
  GUI.  The GUI is handled by the 'GUI thread'.  A 'GUI thread' is defined to
  be the one in which the Qt QApplication class's exec_loop() method is
  executed.  There can be only one GUI thread in a program.

  The solution to the problem is handled by sending a message (an 'event', in
  Qt terms) from the non-GUI thread to the GUI thread.  The message contains
  information on how it is to be handled.  In some cases, this handling produces
  a result that must be sent back to the initiating non-GUI thread.  The
  requests and responses are passed around via queues.  Separate queues are
  maintained for the different non-GUI threads so that responses don't end up
  handed off to the wrong thread.
  """
  GUI_CMD_INST    = QEvent.Type(QEvent.User + 1)
  GUI_CMD_FUNC    = QEvent.Type(QEvent.User + 2)
  GUI_CMD_FUNC_NR = QEvent.Type(QEvent.User + 3)

  def __init__(self, guiThread):
    """\brief This is the constructor of the GUIbridge class.

    It must be called from the GUI thread.
    """
    self.__guiThread  = guiThread
    self.__cmdQueue   = Queue()
    self.__respQueues = {}
    self.__qtBridge   = QtThreadBridge("QtBridge", self.__cmdQueue)
    self.__qtBridge.start()
    self.__pyBridge   = PyThreadBridge("PyBridge", self.__cmdQueue)

  def shutdown(self):
    """\brief This method is used to shut the GUI bridge down.
    """
    self.__qtBridge.quit()

  def isGUIthread(self):
    """\brief Returns True if this method is being called from the GUI thread.
    """
    return threading.currentThread() == self.__guiThread

  def createGUI(self, qObject, obj, *args, **kwargs):
    """\brief Method used for instantiating a GUI from a non-GUI thread.

    \param qObject - The Qt object that will receive the customEvent callback
    \param obj     - The GUI object class type to create
    \param args    - Arguments for the object class constructor
    \param kwargs  - Keyword arguments for the object class constructor
    \return An instance of the requested GUI object class
    """
    # Don't use the GUI bridge if we're being called from the GUI thread
    if self.isGUIthread():  return apply(obj, args, kwargs)

    currentThread = threading.currentThread()
    if currentThread not in self.__respQueues:
      self.__respQueues[currentThread] = Queue()

    respQ = self.__respQueues[currentThread]
    self.__pyBridge.pushEvent((GUIbridge.GUI_CMD_INST, qObject,
                               (obj,) + args + (kwargs, respQ)))
    return self.__pyBridge.pullResponse(respQ)

  def execGUImethodNR(self, qObject, func, *args, **kwargs):
    """\brief Method used for executing a GUI function from a non-GUI thread.

    Use this method when no (useful) response is expected or
    when waiting for one could cause a deadlock.  Any response from the
    function is lost.
    \param qObject - The Qt object that will receive the customEvent callback
    \param func    - The GUI function or method to call
    \param args    - Arguments for the function or method
    \param kwargs  - Keyword arguments for the function or method
    """
    if not self.isGUIthread():
      respQ = None                      # No response expected or required
      self.__pyBridge.pushEvent((GUIbridge.GUI_CMD_FUNC_NR, qObject,
                                 (func,) + args + (kwargs, respQ)))
    else:
      apply(func, args, kwargs)

  def execGUImethod(self, qObject, func, *args, **kwargs):
    """\brief Method used for executing a GUI function from a non-GUI thread.

    Use this method when a response is expected from the function and
    when it is appropriate to wait for it (no deadlock arises)
    \param qObject - The Qt object that will receive the customEvent callback
    \param func    - The GUI function or method to call
    \param args    - Arguments for the function or method
    \param kwargs  - Keyword arguments for the function or method
    \return The called GUI function's return value
    """
    # Don't use the GUI bridge if we're being called from the GUI thread
    if self.isGUIthread():  return apply(func, args, kwargs)

    currentThread = threading.currentThread()
    if currentThread not in self.__respQueues:
      self.__respQueues[currentThread] = Queue()

    respQ = self.__respQueues[currentThread]
    self.__pyBridge.pushEvent((GUIbridge.GUI_CMD_FUNC, qObject,
                               (func,) + args + (kwargs, respQ)))
    return self.__pyBridge.pullResponse(respQ)

  if __name__ == "__main__":
    def execGUImethod2(self, qObject, func, *args, **kwargs):
      # Don't use the GUI bridge if we're being called from the GUI thread
      if self.isGUIthread():  return apply(func, args, kwargs)

      currentThread = threading.currentThread()
      if currentThread not in self.__respQueues:
        if '_GUIbridge__respQueue' not in self.__dict__:
          self.__respQueue = Queue()
        self.__respQueues[currentThread] = self.__respQueue #Queue()

      respQ = self.__respQueues[currentThread]
      self.__pyBridge.pushEvent((GUIbridge.GUI_CMD_FUNC, qObject,
                                 (func,) + args + (kwargs, respQ)))
      #time.sleep(0.01)                    # Delay to allow a task switch
      return self.__pyBridge.pullResponse(respQ)

  def handleCustomEvent(self, e):
    try:
      # This method overrides the QObject base class's
      data = e.data()
      func     = data[0]
      args     = data[1:-3]
      kwargs   = data[-3]
      respQ    = data[-2]
      qtBridge = data[-1]
      if e.type() == GUIbridge.GUI_CMD_INST:
        qtBridge.pushResponse(respQ, func(*args, **kwargs))
      elif e.type() == GUIbridge.GUI_CMD_FUNC:
        qtBridge.pushResponse(respQ, func(*args, **kwargs))
      elif e.type() == GUIbridge.GUI_CMD_FUNC_NR:
        func(*args, **kwargs)
    except Exception, exc:
      log.exception("GUIbridge.handleCustomEvent(): Caught exception")
      if e.type() == GUIbridge.GUI_CMD_INST or \
         e.type() == GUIbridge.GUI_CMD_FUNC:
        qtBridge.pushResponse(respQ, None, exc)


class QtThreadBridge(QThread):
  def __init__(self, name, cmdQueue):
    QThread.__init__(self)
    self._cmdQueue = cmdQueue
    self.__quit = False

  def quit(self):
    self.__quit = True
    self._cmdQueue.put( (None) )

  def pushResponse(self, respQueue, event, exception=None):
    respQueue.put((event, exception))

  def run(self):
    while not self.__quit:
      try:
        tplEvent = self._cmdQueue.get(True)
        if tplEvent is None:  break     # Quit signal is None
        tplEventType = tplEvent[0]
        tplEventDest = tplEvent[1]
        tplEventData = tplEvent[2]
        evt = QCustomEvent(tplEventType)
        evt.setData(tplEventData + (self,))
        QApplication.postEvent(tplEventDest, evt)
        self.msleep(1)                  # RiC: Why's this here?
      except Exception, e:
        log.exception(e)

      # Drop references to this object to allow it to be garbage collected
      tplEvent     = None
      tplEventType = None
      tplEventDest = None
      tplEventData = None

    # logging already shut down?
    #log.info(self.__class__.__name__ + ": terminating")


class PyThreadBridge(object):
  def __init__(self,name, cmdQueue):
    self._cmdQueue = cmdQueue

  def pushEvent(self, event):
    try:
      self._cmdQueue.put(event)
    except Exception, e:
      log.exception(e)

  def pullResponse(self, respQueue, blnBlock = 1):
    try:
      response, exception = respQueue.get(blnBlock)
      if exception is None:  return response
      raise exception
    except Exception, e:
      log.exception(e)
    #  raise e


if __name__ == "__main__":
  import time

  class GUIbridgeTest(threading.Thread):
    def __init__(self, qObject, guiBridge, id, label):
      threading.Thread.__init__(self)

      self.__qObject   = qObject
      self.__guiBridge = guiBridge
      self.__id        = id
      self.__label     = label
      self.__quit      = False

    def shutdown(self):
      self.__quit  = True

    def run(self):
      qo = self.__qObject
      gb = self.__guiBridge
      tf = self.__label
      id = self.__id
      count = 0
      start = time.time()
      while not self.__quit:
        string = "Thread %d: %12d" % (id, count)

        # Write value
        gb.execGUImethodNR(qo, tf.setText, string)

        # Old method:
        #v = gb.execGUImethod2(qo, tf.text)
        # New method:
        v = gb.execGUImethod(qo, tf.text)

        # Bypass GUI thread:
        #v = tf.text("Thread %d: %d" % (id, count)) # This doesn't reliably work

        # Compare result
        if str(v) != string:  print "Thread %d: '%s' != '%s'" % (id, string, v)

        #time.sleep(.001)
        if id == 1:
          count += 1
          time.sleep(0.123) # To get some beating betwee the two tasks
        else:        count -= 1
      end  = time.time()
      if id == 2:  count = -count
      rate = count / (end - start)
      print "Exiting thread %d, %d calls = %.1f setText()s per second" % \
            (self.__id, count, rate)


  class TestWindow(QMainWindow):
    def __init__(self, parent = None, name = None, fl = 0):
      QMainWindow.__init__(self, parent, name, fl)

      self.setName("TestWindow")

      self.mainWidget = QWidget(self) # dummy widget to containt the layout mgr
      self.setCentralWidget(self.mainWidget)
      layout = QVBoxLayout(self.centralWidget(), 11, 6, "Test_Window_Layout")

      self.__label1 = QLabel("Thread 1:", self.mainWidget)
      self.__label2 = QLabel("Thread 2:", self.mainWidget)
      self.__quit   = QPushButton("Quit", self.mainWidget, "quit")
      self.connect(self.__quit, SIGNAL("clicked()"), self.quit)

      layout.addWidget(self.__label1)
      layout.addWidget(self.__label2)
      layout.addWidget(self.__quit)

      self.__guiBridge = GUIbridge(threading.currentThread())

      self.__gbt1 = GUIbridgeTest(self, self.__guiBridge, 1, self.__label1)
      self.__gbt2 = GUIbridgeTest(self, self.__guiBridge, 2, self.__label2)

      self.__gbt1.start()
      self.__gbt2.start()

    def customEvent(self, e):
      self.__guiBridge.handleCustomEvent(e)

    def quit(self):
      self.__gbt1.shutdown()
      self.__gbt2.shutdown()
      time.sleep(0.1) # Seems to be required to yield and allow things to exit
      self.__guiBridge.shutdown()
      self.close()

  def startTest(args):
    app = QApplication(args)
    app.connect(app, SIGNAL("lastWindowClosed()"), app, SLOT("quit()"))

    win = TestWindow()
    app.setMainWidget(win)
    win.show()

    app.exec_loop()


  # Start the test
  startTest(sys.argv)
