#!/usr/bin/env python3.8
# coding: latin-1
# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2022
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
cytoflowgui.run
---------------

The entry-point for the GUI -- sets up and starts the remote process, configures
logging, loads the Envisage plugins, and starts the GUI loop.
"""

try:
    import faulthandler       # @UnresolvedImport
    faulthandler.enable()     # @UndefinedVariable
except:
    # if there's no console, this fails
    pass

import sys, multiprocessing, logging, traceback, threading, argparse

logger = logging.getLogger(__name__)

def log_notification_handler(_, trait_name, old, new):
    """
    Exception handler for traits notifications
    """
    
    (exc_type, exc_value, tb) = sys.exc_info()
    logger.debug('Exception occurred in traits notification '
                  'handler for object: %s, trait: %s, old value: %s, '
                  'new value: %s.\n%s\n' % ( object, trait_name, old, new,
                  ''.join( traceback.format_exception(exc_type, exc_value, tb) ) ) )

    err_string = traceback.format_exception_only(exc_type, exc_value)[0]
    err_loc = traceback.format_tb(tb)[-1]
    err_ctx = threading.current_thread().name
    
    logging.error("Error: {0}\nLocation: {1}Thread: {2}" \
                  .format(err_string, err_loc, err_ctx) )
    
def log_excepthook(typ, val, tb):
    """Exception handler for global exceptions"""
    tb_str = "".join(traceback.format_tb(tb))
    logger.debug("Global exception: {0}\n{1}: {2}".format(tb_str, typ, val))
    
    tb_str = traceback.format_tb(tb)[-1]
    logging.error("Error: {0}: {1}\nLocation: {2}Thread: Main"
                  .format(typ, val, tb_str))
                         
def run_gui():
    """Run the GUI!"""
    
    try:
        # if we're running as a one-click from a MacOS app,
        # we need to reset the working directory
        import os; os.chdir(sys._MEIPASS)  # @UndefinedVariable
    except:
        # if we're not running as a one-click, fail gracefully
        pass
    
    # shut up the warning about Open GL (see below)
    from pyface.qt import QtGui, QtCore
    QtGui.QApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)  
    
    # use high resolution pixmaps
    QtGui.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)

    # this is ridiculous, but here's the situation.  Qt5 now uses Chromium
    # as their web renderer.  Chromium needs OpenGL.  if you don't
    # initialize OpenGL here, things crash on some platforms.
    
    # so now i guess we depend on opengl too. 
    
    try:
        from OpenGL import GL  # @UnresolvedImport @UnusedImport
    except ImportError:
        logger.info("Patching util.find_library for MacOS")
        from ctypes import util
        orig_find_library = util.find_library

        def new_find_library(name):
            res = orig_find_library(name)
            if res: return res
            return '/System/Library/Frameworks/' + name + '.framework/' + name
        util.find_library = new_find_library
        from OpenGL import GL
        util.find_library = orig_find_library

    # need to import these before a QCoreApplication is instantiated.  and that seems
    # to happen in .... 'import cytoflow' ??
    import pyface.qt.QtWebKit  # @UnusedImport  

    # take care of the 3 places in the cytoflow module that
    # need different behavior in a GUI
    import cytoflow
    cytoflow.RUNNING_IN_GUI = True
            
    # check that we're using the right Qt API
    from pyface.qt import qt_api

    cmd_line = " ".join(sys.argv)
    
    if qt_api == "pyside":
        print("Cytoflow uses PyQT; but it is trying to use PySide instead.")
        print(" - Make sure PyQT is installed.")
        print(" - If both are installed, and you don't need both, uninstall PySide.")
        print(" - If you must have both installed, select PyQT by setting the")
        print("   environment variable QT_API to \"pyqt5\"")
        print("   * eg, on Linux, type on the command line:")
        print("     QT_API=\"pyqt5\" " + cmd_line)
        print("   * on Windows, try: ")
        print("     setx QT_API \"pyqt5\"")

        sys.exit(1)
    
    # parse args
    parser = argparse.ArgumentParser(description = 'Cytoflow GUI')
    parser.add_argument("--debug", action = 'store_true')
    parser.add_argument("filename", nargs='?', default = "")
    
    args = parser.parse_args()
    
    # start the remote process

    remote_process, remote_workflow_connection, remote_canvas_connection, queue_listener = start_remote_process()
    
    # getting real tired of the matplotlib deprecation warnings
    import warnings
    warnings.filterwarnings('ignore', '.*is deprecated and replaced with.*')
    
    # if we're frozen, add _MEIPASS to the pyface search path for icons etc
    if getattr(sys, 'frozen', False):
        from pyface.resource_manager import resource_manager
        resource_manager.extra_paths.append(sys._MEIPASS)    # @UndefinedVariable
        
    # these three lines stop pkg_resources from trying to load resources
    # from the __main__ module, which is frozen (and thus not loadable.)
    from pyface.image_resource import ImageResource
    icon = ImageResource('icon')
    icon.search_path = []
    
    # monkey patch the resource manager to use SVGs for icons
    import pyface.resource.resource_manager
    pyface.resource.resource_manager.ResourceManager.IMAGE_EXTENSIONS.append('.svg')
    
    # monkey patch checklist editor to stop lowercasing
    import traitsui.qt4.check_list_editor  # @UnusedImport
    traitsui.qt4.check_list_editor.capitalize = lambda s: s
    
    # define and install a message handler for Qt errors
    from traits.api import push_exception_handler
                             
    def QtMsgHandler(msg_type, msg_context, msg_string):
        # Convert Qt msg type to logging level
        log_level = [logging.DEBUG,
                     logging.WARN,
                     logging.ERROR,
                     logging.FATAL] [ int(msg_type) ]
        logging.log(log_level, 'Qt message: ' + msg_string)
        
    from pyface.qt.QtCore import qInstallMessageHandler  # @UnresolvedImport
    qInstallMessageHandler(QtMsgHandler)
    
    # install a global (gui) error handler for traits notifications
    push_exception_handler(handler = log_notification_handler,
                           reraise_exceptions = False, 
                           main = True)
    
    sys.excepthook = log_excepthook
    
    # Import, then load, the envisage plugins
    
    from envisage.core_plugin import CorePlugin
    from envisage.ui.tasks.tasks_plugin import TasksPlugin
    
    from cytoflowgui.flow_task import FlowTaskPlugin
    from cytoflowgui.export_task import ExportFigurePlugin
    from cytoflowgui.cytoflow_application import CytoflowApplication

    from cytoflowgui.op_plugins import (OpPluginManager,
                                        ImportPlugin, 
                                        ThresholdPlugin,
                                        RangePlugin, 
                                        QuadPlugin,
                                        Range2DPlugin, 
                                        PolygonPlugin, 
                                        BinningPlugin,
                                        GaussianMixture1DPlugin, 
                                        GaussianMixture2DPlugin,
                                        DensityGatePlugin, 
                                        BleedthroughLinearPlugin,
                                        BeadCalibrationPlugin, 
                                        AutofluorescencePlugin,
                                        ColorTranslationPlugin, 
                                        TasbePlugin, 
                                        ChannelStatisticPlugin,
                                        TransformStatisticPlugin,
                                        RatioPlugin,
                                        FlowPeaksPlugin,
                                        KMeansPlugin,
                                        PCAPlugin)
    
    from cytoflowgui.view_plugins import (ViewPluginManager,
                                          HistogramPlugin, 
                                          Histogram2DPlugin, 
                                          ScatterplotPlugin,
                                          BarChartPlugin,
                                          Stats1DPlugin, 
                                          Kde1DPlugin, 
                                          Kde2DPlugin,
                                          ViolinPlotPlugin,
                                          TablePlugin, 
                                          Stats2DPlugin, 
                                          DensityPlugin,
                                          ParallelCoordinatesPlugin,
                                          RadvizPlugin,
                                          ExportFCSPlugin)

    plugins = [CorePlugin(), TasksPlugin(), FlowTaskPlugin(), ExportFigurePlugin()]

    # ordered as we want them to show up in the toolbar    
    view_plugins = [ViewPluginManager(),
                    HistogramPlugin(),
                    ScatterplotPlugin(),
                    Histogram2DPlugin(),
                    DensityPlugin(),
                    Kde1DPlugin(),
                    Kde2DPlugin(),
                    RadvizPlugin(),
                    ParallelCoordinatesPlugin(),
                    ViolinPlotPlugin(),
                    BarChartPlugin(),
                    Stats1DPlugin(),
                    Stats2DPlugin(),
                    TablePlugin(),
                    ExportFCSPlugin()]
    
    plugins.extend(view_plugins)
    
    op_plugins = [OpPluginManager(),
                  ImportPlugin(),
                  ThresholdPlugin(),
                  RangePlugin(),
                  QuadPlugin(),
                  Range2DPlugin(),
                  PolygonPlugin(),
                  ChannelStatisticPlugin(),
                  TransformStatisticPlugin(),
                  RatioPlugin(),
                  BinningPlugin(),
                  GaussianMixture1DPlugin(),
                  GaussianMixture2DPlugin(),
                  DensityGatePlugin(),
                  KMeansPlugin(),
                  FlowPeaksPlugin(),
                  PCAPlugin(),
                  AutofluorescencePlugin(),
                  BleedthroughLinearPlugin(),
                  BeadCalibrationPlugin(),
                  ColorTranslationPlugin(),
                  TasbePlugin()]

    plugins.extend(op_plugins)
    
    # start the app

    app = CytoflowApplication(id = 'edu.mit.synbio.cytoflow',
                              plugins = plugins,
                              icon = icon,
                              remote_process = remote_process,
                              remote_workflow_connection = remote_workflow_connection,
                              remote_canvas_connection = remote_canvas_connection,
                              filename = args.filename,
                              debug = args.debug)

    QtGui.QApplication.instance().setStyle(QtGui.QStyleFactory.create('Fusion'))

    app.run()

    remote_process.join()
    queue_listener.stop()
    logging.shutdown()
    

        
def monitor_remote_process(proc):
    """The main method for the (local) thread that monitors the remote process"""
    proc.join()
    if proc.exitcode:
        logging.error("Remote process exited with {}".format(proc.exitcode))
    

def start_remote_process():
    """
    Start the remote process.  Creates pipes and synchronization primitives, 
    sets up logging, and starts the remote process and the monitoring thread.
    """

    # communications channels
    
    parent_workflow_conn, child_workflow_conn = multiprocessing.Pipe()  
    parent_mpl_conn, child_matplotlib_conn = multiprocessing.Pipe()
    running_event = multiprocessing.Event()

    # logging
    from logging.handlers import QueueListener
    from cytoflowgui.utility import CallbackHandler
    log_q = multiprocessing.Queue()
    def handle(record):
        logger = logging.getLogger(record.name)
        if logger.isEnabledFor(record.levelno):
            logger.handle(record)
            
    handler = CallbackHandler(handle)
    queue_listener = QueueListener(log_q, handler)
    queue_listener.start()

    remote_process = multiprocessing.Process(target = remote_main,
                                             name = "remote process",
                                             args = [parent_workflow_conn,
                                                     parent_mpl_conn,
                                                     log_q,
                                                     running_event])
    
    remote_process.daemon = True
    remote_process.start() 
    running_event.wait()
    
    remote_process_thread = threading.Thread(target = monitor_remote_process,
                                             name = "monitor remote process",
                                             args = [remote_process])
    remote_process_thread.daemon = True
    remote_process_thread.start()
    
    return (remote_process, child_workflow_conn, child_matplotlib_conn, queue_listener)
 

def remote_main(parent_workflow_conn, parent_mpl_conn, log_q, running_event):
    """
    The main method for the remote process. Configures logging, sets up
    the matplotlib backend, and instantiates the `RemoteWorkflow`.
    """
    
    # this should only ever be main method after a spawn() call 
    # (not fork). So we should have a fresh logger to set up.
        
    # messages that end up at the root logger to go log_q
    from logging.handlers import QueueHandler
    h = QueueHandler(log_q) 
    logging.getLogger().addHandler(h)
    
    # make sure the root logger has a level of DEBUG -- we'll sort out what
    # to show or not on the local logger
    logging.getLogger().setLevel(logging.DEBUG)
    
    # We want matplotlib to use our backend .... in both the GUI and the
    # remote process.  Must be called BEFORE cytoflow is imported
    
    import matplotlib
    matplotlib.use('module://cytoflowgui.matplotlib_backend_remote')

    # install a global (gui) error handler for traits notifications
        
    from traits.api import push_exception_handler        
    push_exception_handler(handler = log_notification_handler,
                           reraise_exceptions = False,
                           main = True)
    
    sys.excepthook = log_excepthook
    
    # take care of the 3 places in the cytoflow module that
    # need different behavior in a GUI
    import cytoflow
    cytoflow.RUNNING_IN_GUI = True
    
    running_event.set()
    
    from cytoflowgui.workflow import RemoteWorkflow
    RemoteWorkflow().run(parent_workflow_conn, parent_mpl_conn)

if __name__ == '__main__':
    multiprocessing.freeze_support()
    multiprocessing.set_start_method('spawn')
    run_gui()
