#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Automated scanning module functionalities for PyMoDAQ

Contains all objects related to the DAQ_Scan module, to do automated scans, saving data...
"""

import sys
from collections import OrderedDict
import numpy as np
import os
import logging

from pyqtgraph.dockarea import Dock
from pyqtgraph.parametertree import Parameter, ParameterTree
import pymodaq.daq_utils.custom_parameter_tree as custom_tree# to be placed after importing Parameter
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import QObject, pyqtSlot, QThread, pyqtSignal, QLocale, QDateTime, QDate, QTime

from pymodaq.daq_utils.daq_utils import getLineInfo
from pymodaq.daq_utils.managers.modules_manager import ModulesManager
from pymodaq.daq_utils.plotting.qled import QLED
from pymodaq.daq_utils import daq_utils as utils
from pymodaq.daq_utils import gui_utils as gutils
from pymodaq.daq_utils.h5modules import H5Saver, H5LogHandler
logger = utils.set_logger(utils.get_module_name(__file__))
try:
    import sqlalchemy
    from pymodaq.daq_utils.db.db_logger.db_logger import DbLoggerGUI, DBLogHandler
    is_sql = True
except Exception as e:
    is_sql = False
    logger.warning(str(e))
    logger.info('To enable logging to database install: sqalchemy and sql_alchemy_utils packages')


class DAQ_Logger(QObject):
    """
    Main class initializing a DAQ_Scan module with its dashboard and scanning control panel
    """
    command_DAQ_signal = pyqtSignal(list)
    status_signal = pyqtSignal(str)

    params = [
        {'title': 'Log Type:', 'name': 'log_type', 'type': 'str', 'value': '', 'readonly': True},
    ]

    def __init__(self, dockarea=None, dashboard=None):
        """

        Parameters
        ----------
        dockarea: (dockarea) instance of the modified pyqtgraph Dockarea (see daq_utils)
        dashboard: (DashBoard) instance of the pymodaq dashboard
        """
        QLocale.setDefault(QLocale(QLocale.English, QLocale.UnitedStates))
        super().__init__()
        self.dockarea = dockarea
        self.dashboard = dashboard
        if dashboard is None:
            raise Exception('No valid dashboard initialized')
        self.mainwindow = self.dockarea.parent()
        self.wait_time = 1000
        
        self.logger_thread = None
        self.detector_modules = self.dashboard.detector_modules
        self.det_modules_log = []
        self.log_types = ['None', 'H5 File']
        if is_sql:
            self.log_types.append('SQL DataBase')

        self.logger = None #should be a reference either to self.h5saver or self.dblogger depending the choice of the user
        self.h5saver = H5Saver(save_type='logger')
        if is_sql:
            self.dblogger = DbLoggerGUI(self.dashboard.preset_file.stem)
        else:
            self.dblogger = None
        self.modules_manager = ModulesManager()

        self.setupUI()
        self.setup_modules(self.dashboard.title)

        self.h5saver.settings_tree.setVisible(False)
        if is_sql:
            self.dblogger.settings_tree.setVisible(False)


    def create_menu(self):
        """
        """
        #%% create Settings menu
        menubar = QtWidgets.QMenuBar()
        menubar.setMaximumHeight(30)
        self.ui.layout.insertWidget(0, menubar)

        self.file_menu = menubar.addMenu('File')


    def quit_fun(self):
        """
            Quit the current instance of DAQ_scan and close on cascade move and detector modules.

            See Also
            --------
            quit_fun
        """
        try:
            self.h5saver.close_file()
        except Exception as e:
            logger.exception(str(e))
        try:
            self.dblogger.close()
        except Exception as e:
            logger.exception(str(e))
        self.dashboard.quit_fun()

    def parameter_tree_changed(self, param, changes):
        """
            Check for changes in the given (parameter,change,information) tuple list.
            In case of value changed, update the DAQscan_settings tree consequently.

            =============== ============================================ ==============================
            **Parameters**    **Type**                                     **Description**
            *param*           instance of pyqtgraph parameter              the parameter to be checked
            *changes*         (parameter,change,information) tuple list    the current changes state
            =============== ============================================ ==============================
        """
        for param, change, data in changes:
            path = self.settings.childPath(param)
            if path is not None:
                childName = '.'.join(path)
            else:
                childName = param.name()
            if change == 'childAdded':pass

            elif change == 'value':
                if param.name() == 'log_type':
                    if param.value() == 'H5 File':
                        self.logger = self.h5saver
                    elif param.value() == 'SQL DataBase':
                        self.logger = self.dblogger
                self.h5saver.settings_tree.setVisible(param.value() == 'H5 File')
                if self.dblogger is not None:
                    self.dblogger.settings_tree.setVisible(param.value() == 'SQL DataBase')
            elif change == 'parent':
                pass

    def setup_modules(self, filename):
        """

        """
        try:
            ######################################################################
            # setting moves and det in tree
            preset_items_det = []
            items_det = [module.title for module in self.detector_modules]
            if items_det != []:
                preset_items_det = items_det

            self.modules_manager.set_detectors(self.detector_modules, preset_items_det)

        except Exception as e:
            logger.exception(str(e))

    def set_continuous_save(self):
        """
            Set a continous save file using the base path located file with
            a header-name containing date as a string.

            See Also
            --------
            daq_utils.set_current_scan_path
        """
        self.do_continuous_save = True
        self.logger.settings.child(('N_saved')).show()
        self.logger.settings.child(('N_saved')).setValue(0)

        settings_str = b'<All_settings>'
        settings_str += custom_tree.parameter_to_xml_string(self.dashboard.settings)
        settings_str += custom_tree.parameter_to_xml_string(self.dashboard.preset_manager.preset_params)
        if self.dashboard.settings.child('loaded_files', 'overshoot_file').value() != '':
            settings_str += custom_tree.parameter_to_xml_string(self.dashboard.overshoot_manager.overshoot_params)
        if self.dashboard.settings.child('loaded_files', 'roi_file').value() != '':
            settings_str += custom_tree.parameter_to_xml_string(self.dashboard.roi_saver.roi_presets)
        settings_str += custom_tree.parameter_to_xml_string(self.settings)
        settings_str += custom_tree.parameter_to_xml_string(self.logger.settings)
        settings_str += b'</All_settings>'

        if self.settings.child(('log_type')).value() == 'H5 File':
            self.logger.settings.child(('base_name')).setValue('DataLogging')
            self.h5saver.init_file(update_h5=True, metadata=dict(settings=settings_str))
            logger.addHandler(H5LogHandler(self.h5saver))
            self.h5saver.h5_file.flush()

        elif self.settings.child(('log_type')).value() == 'SQL DataBase':
            if not self.logger.settings.child('connected_db').value():
                status = self.logger.connect_db()
                if not status:
                    logger.critical('the Database is not and cannot be connnect')
                    self.update_status('the Database is not and cannot be connnect')
                    return False
                    
            self.logger.add_config(settings_str)
            logger.addHandler(DBLogHandler(self.logger))

        return True

    def set_logging(self):
        """

        """
        status = self.set_continuous_save()
        if status:
            det_modules_log = self.modules_manager.detectors
            if det_modules_log != []:
                #check if the modules are initialized
                for module in det_modules_log:
                    if not module.initialized_state:
                        logger.error(f'module {module.title} is not initialized')
                        return False

                #create the detectors in the chosen logger
                for det in det_modules_log:
                    settings_str = b'<All_settings>'
                    settings_str += custom_tree.parameter_to_xml_string(det.settings)
                    for viewer in det.ui.viewers:
                        if hasattr(viewer, 'roi_manager'):
                            settings_str += custom_tree.parameter_to_xml_string(viewer.roi_manager.settings)
                    settings_str += b'</All_settings>'

                    if self.settings.child(('log_type')).value() == 'H5 File':
                        if det.title not in self.h5saver.raw_group.children_name():
                            det_group = self.h5saver.add_det_group(self.h5saver.raw_group, det.title, settings_str)
                            self.h5saver.add_navigation_axis(np.array([0.0, ]),
                                  det_group, 'time_axis', enlargeable=True,
                                  title='Time axis', metadata=dict(label='Time axis', units='timestamp', nav_index=0))

                    elif self.settings.child(('log_type')).value() == 'SQL DataBase':
                        self.logger.add_detectors([dict(name=det.title, xml_settings=settings_str)])

                self.ui.start_button.setEnabled(True)
                self.ui.stop_button.setEnabled(True)
                return True
            else:
                self.update_status('Cannot start logging... No detectors selected')
                self.ui.start_button.setEnabled(False)
                self.ui.stop_button.setEnabled(True)
                return False

        else:
            self.update_status('Cannot start logging... check connections')
            self.ui.start_button.setEnabled(False)
            self.ui.stop_button.setEnabled(True)
            return False

    def show_log(self):
        import webbrowser
        webbrowser.open(logger.handlers[0].baseFilename)

    def setupUI(self):

        self.ui = QObject()

        widget_settings = QtWidgets.QWidget()
        self.ui.layout = QtWidgets.QVBoxLayout()

        widget_settings.setLayout(self.ui.layout)

        widget_buttons = QtWidgets.QWidget()
        layout_buttons = QtWidgets.QHBoxLayout()
        widget_buttons.setLayout(layout_buttons)
        self.ui.layout.addWidget(widget_buttons)
        
        iconquit = QtGui.QIcon()
        iconquit.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/close2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.ui.quit_button = QtWidgets.QPushButton(iconquit, 'Quit')

        iconstart = QtGui.QIcon()
        iconstart.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/run2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.ui.start_button = QtWidgets.QPushButton(iconstart, '')
        self.ui.start_button.setToolTip('Start logging into h5file or database')

        iconstop = QtGui.QIcon()
        iconstop.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/stop.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.ui.stop_button = QtWidgets.QPushButton(iconstop, '')
        self.ui.stop_button.setToolTip('Stop/pause logging')

        log_type_combo = QtWidgets.QComboBox()
        log_type_combo.addItems(self.log_types)
        log_type_combo.currentTextChanged.connect(self.set_log_type)

        iconstartall = QtGui.QIcon()
        iconstartall.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/run_all.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.ui.startall_button = QtWidgets.QPushButton(iconstartall,'')
        self.ui.startall_button.setToolTip('Grab all selected detectors')

        layout_buttons.addWidget(self.ui.quit_button)
        layout_buttons.addStretch()
        layout_buttons.addWidget(self.ui.startall_button)
        layout_buttons.addWidget(log_type_combo)
        layout_buttons.addWidget(self.ui.start_button)
        layout_buttons.addWidget(self.ui.stop_button)


        #%% create logger dock and make it a floating window
        self.ui.logger_dock = Dock("Logger", size=(1, 1), autoOrientation=False)     ## give this dock the minimum possible size
        self.ui.logger_dock.setOrientation('vertical')
        self.ui.logger_dock.addWidget(widget_settings)
        self.dockarea.addDock(self.ui.logger_dock, 'left')
        self.ui.logger_dock.float()

        widget_hor = QtWidgets.QWidget()
        layout_hor = QtWidgets.QHBoxLayout()

        main_sett_widget = QtWidgets.QWidget()
        main_sett_widget.setLayout(QtWidgets.QVBoxLayout())

        widget_hor.setLayout(layout_hor)
        self.ui.layout.addWidget(widget_hor)

        self.settings_tree = ParameterTree()
        self.settings_tree.setMinimumWidth(300)
        layout_hor.addWidget(main_sett_widget)
        main_sett_widget.layout().addWidget(self.settings_tree)
        main_sett_widget.layout().addWidget(self.modules_manager.settings_tree)

        self.modules_manager.settings.child('modules', 'actuators').hide()
        self.modules_manager.settings.child(('data_dimensions')).hide()
        self.modules_manager.settings.child(('actuators_positions')).hide()


        self.h5saver.settings_tree.setMinimumWidth(300)
        layout_hor.addWidget(self.h5saver.settings_tree)

        if is_sql:
            self.dblogger.settings_tree.setMinimumWidth(300)
            layout_hor.addWidget(self.dblogger.settings_tree)

        self.settings = Parameter.create(name='Settings', type='group', children=self.params)
        self.settings_tree.setParameters(self.settings, showTop=False)
        self.settings.sigTreeStateChanged.connect(self.parameter_tree_changed)

        # %% init and set the status bar
        self.ui.statusbar = QtWidgets.QStatusBar(self.dockarea)
        self.ui.statusbar.setMaximumHeight(25)
        self.ui.layout.addWidget(self.ui.statusbar)
        self.ui.log_message = QtWidgets.QLabel('Initializing')
        self.ui.statusbar.addPermanentWidget(self.ui.log_message)

        self.ui.start_log_time = QtWidgets.QDateTimeEdit()
        self.ui.start_log_time.setReadOnly(True)
        self.ui.start_log_time.setToolTip('Logging started at:')

        self.ui.logging_state = QLED()
        self.ui.logging_state.setToolTip('logging status: green (running), red (idle)')
        self.ui.logging_state.clickable = False

        self.ui.statusbar.addPermanentWidget(self.ui.start_log_time)
        self.ui.statusbar.addPermanentWidget(self.ui.logging_state)
        self.ui.layout.addWidget(self.ui.statusbar)


#       connecting
        self.status_signal[str].connect(self.dashboard.add_status)
        self.ui.quit_button.clicked.connect(self.quit_fun)

        self.ui.start_button.clicked.connect(self.start_logging)
        self.ui.stop_button.clicked.connect(self.stop_logging)
        self.ui.startall_button.clicked.connect(self.start_all)

        self.create_menu()


    def start_all(self):
        preset_items_det = self.modules_manager.detectors
        for det in preset_items_det:
            det.ui.grab_pb.click()

    def set_log_type(self, log_type):

        self.settings.child(('log_type')).setValue(log_type)



    def show_file_attributes(self, type_info='dataset'):
        """
            Switch the type_info value.

            In case of :
                * *scan* : Set parameters showing top false
                * *dataset* : Set parameters showing top false
                * *managers* : Set parameters showing top false. Add the save/cancel buttons to the accept/reject dialog (to save managers parameters in a xml file).

            Finally, in case of accepted managers type info, save the managers parameters in a xml file.

            =============== =========== ====================================
            **Parameters**    **Type**    **Description**
            *type_info*       string      The file type information between
                                            * scan
                                            * dataset
                                            * managers
            =============== =========== ====================================

            See Also
            --------
            custom_tree.parameter_to_xml_file, create_menu
        """
        dialog = QtWidgets.QDialog()
        vlayout = QtWidgets.QVBoxLayout()
        tree = ParameterTree()
        tree.setMinimumWidth(400)
        tree.setMinimumHeight(500)
        if type_info == 'scan':
            tree.setParameters(self.scan_attributes, showTop=False)
        elif type_info == 'dataset':
            tree.setParameters(self.dataset_attributes, showTop=False)

        vlayout.addWidget(tree)
        dialog.setLayout(vlayout)
        buttonBox = QtWidgets.QDialogButtonBox(parent=dialog)
        buttonBox.addButton('Cancel', buttonBox.RejectRole)
        buttonBox.addButton('Apply', buttonBox.AcceptRole)
        buttonBox.rejected.connect(dialog.reject)
        buttonBox.accepted.connect(dialog.accept)

        vlayout.addWidget(buttonBox)
        dialog.setWindowTitle('Fill in information about this {}'.format(type_info))
        res = dialog.exec()
        return res

    def show_file_content(self):
        try:
            self.h5saver.init_file(addhoc_file_path=self.h5saver.settings.child(('current_h5_file')).value())
            self.h5saver.show_file_content()
        except Exception as e:
            logger.exception(str(e))

    def start_logging(self):
        """
            Start a logging.
        """
        self.ui.log_message.setText('Starting logging')

        self.overshoot = False
        res = self.set_logging()

        #mandatory to deal with multithreads
        if self.logger_thread is not None:
            self.command_DAQ_signal.disconnect()
            if self.logger_thread.isRunning():
                self.logger_thread.exit()
                while not self.logger_thread.isFinished():
                    QThread.msleep(100)
                self.logger_thread = None

        self.logger_thread = QThread()

        log_acquisition = DAQ_Logging(self.settings, self.logger, self.modules_manager)

        log_acquisition.moveToThread(self.logger_thread)

        self.command_DAQ_signal[list].connect(log_acquisition.queue_command)
        log_acquisition.status_sig[list].connect(self.thread_status)

        self.logger_thread.log_acquisition = log_acquisition
        self.logger_thread.start()

        self.ui.start_button.setEnabled(False)
        QtWidgets.QApplication.processEvents()
        self.ui.logging_state.set_as_false()

        self.command_DAQ_signal.emit(["start_logging"])
        self.ui.log_message.setText('Running acquisition')

    def stop_logging(self):
        """
            Emit the command_DAQ signal "stop_acquisiion".

            See Also
            --------
            set_ini_positions
        """
        preset_items_det = self.modules_manager.detectors
        for det in preset_items_det:
            det.ui.stop_pb.click()
        self.command_DAQ_signal.emit(["stop_acquisition"])

        if not self.dashboard.overshoot:
            status = 'Data Logging has been stopped by user'
        else:
            status = 'Data Logging has been stopped due to overshoot'

        self.update_status(status)
        self.ui.start_button.setEnabled(True)

    @pyqtSlot(list)
    def thread_status(self, status):  # general function to get datas/infos from all threads back to the main
        """
            | General function to get datas/infos from all threads back to the main.
            |

            Switch the status with :
                * *"Update status"* : Update the status bar with the status attribute txt message
                * *"Update_scan_index"* : Set the value of the User Interface - indice_scan_sb attribute.
                * *"Scan_done"* : Save the scan and init the positions
                * *"Timeout"* : Set the "Timeout occured" in the User Interface-log message

            See Also
            --------
            update_status, save_scan, set_ini_positions
        """
        if status[0] == "Update_Status":
            self.update_status(status[1], wait_time=self.wait_time)

        elif status[0] == "Timeout":
            self.ui.log_message.setText('Timeout occurred')

    def update_status(self, txt, wait_time=0):
        """
            Show the txt message in the status bar with a delay of wait_time ms.

            =============== =========== =======================
            **Parameters**    **Type**    **Description**
            *txt*             string      The message to show
            *wait_time*       int         the delay of showing
            *log_type*        string      the type of the log
            =============== =========== =======================
        """
        try:
            self.ui.statusbar.showMessage(txt, wait_time)
            logging.info(txt)
        except Exception as e:
            logger.exception(str(e))


class DAQ_Logging(QObject):
    """
        =========================== ========================================
        **Attributes**               **Type**

        =========================== ========================================

    """
    scan_data_tmp = pyqtSignal(OrderedDict)
    status_sig = pyqtSignal(list)
    def __init__(self, settings=None, logger=None, modules_manager=[]):

        """
            DAQ_Logging deal with the acquisition part of daq_scan.

            See Also
            --------
            custom_tree.parameter_to_xml_string
        """
        QLocale.setDefault(QLocale(QLocale.English, QLocale.UnitedStates))
        super(QObject, self).__init__()

        self.stop_logging_flag = False
        self.settings = settings
        self.ini_time = 0
        self.ind_log = 0
        self.modules_manager = modules_manager

        self.data_logger = logger
        if isinstance(self.data_logger, H5Saver):
            self.logger_type = "h5saver"
        elif isinstance(self.data_logger, DbLoggerGUI):
            self.logger_type = "dblogger"

    @pyqtSlot(list)
    def queue_command(self, command):
        """
            Treat the queue of commands from the current command to act, between :
                * *start_logging*
                * *stop_acquisition*
                * *set_ini_position*
                * *move_stages*

            =============== ============== =========================
            **Parameters**    **Type**      **Description**
            command           string list   the command string list
            =============== ============== =========================

            See Also
            --------
            start_logging, set_ini_positions, move_stages
        """
        if command[0] == "start_logging":
            self.start_logging()

        elif command[0] == "stop_acquisition":
            self.stop_scan_flag = True
            self.stop_logging()

    def do_save_continuous(self, datas):
        """
        method used to perform continuous saving of data, for instance for logging. Will save datas as a function of
        time in a h5 file set when *continuous_saving* parameter as been set.

        Parameters
        ----------
        datas:  list of OrderedDict as exported by detector plugins

        """
        try:
            det_name = datas['name']
            if self.logger_type == 'h5saver':
                det_group = self.data_logger.get_group_by_title(self.data_logger.raw_group, det_name)
                time_array = self.data_logger.get_node(det_group, 'Logger_time_axis')
                time_array.append(np.array([datas['acq_time_s']]))

                data_types = ['data0D', 'data1D']
                if self.data_logger.settings.child(('save_2D')).value():
                    data_types.extend(['data2D', 'dataND'])

                for data_type in data_types:
                    if data_type in datas.keys() and len(datas[data_type]) != 0:
                        if not self.data_logger.is_node_in_group(det_group, data_type):
                            data_group = self.data_logger.add_data_group(det_group, data_type, metadata=dict(type='scan'))
                        else:
                            data_group = self.data_logger.get_node(det_group, utils.capitalize(data_type))
                        for ind_channel, channel in enumerate(datas[data_type]):
                            channel_group = self.data_logger.get_group_by_title(data_group, channel)
                            if channel_group is None:
                                channel_group = self.data_logger.add_CH_group(data_group, title=channel)
                                data_array = self.data_logger.add_data(channel_group, datas[data_type][channel],
                                                                       scan_type='scan1D', enlargeable=True)
                            else:
                                data_array = self.data_logger.get_node(channel_group, 'Data')
                            if data_type == 'data0D':
                                data_array.append(np.array([datas[data_type][channel]['data']]))
                            else:
                                data_array.append(datas[data_type][channel]['data'])
                self.data_logger.h5_file.flush()

            elif self.logger_type == 'dblogger':
                self.data_logger.add_datas(datas)

            self.data_logger.settings.child(('N_saved')).setValue(self.data_logger.settings.child(('N_saved')).value() + 1)

        except Exception as e:
            logger.exception(str(e))

    def stop_logging(self):
        try:
            self.modules_manager.connect_detectors(connect=False, slot=self.do_save_continuous)
        except Exception as e:
            logger.exception(str(e))

        if self.stop_logging_flag:
            status = 'Data Acquisition has been stopped by user'
            self.status_sig.emit(["Update_Status", status])
        if self.logger_type == 'h5saver':
            self.data_logger.flush()


    def start_logging(self):
        try:
            self.modules_manager.connect_detectors(slot=self.do_save_continuous)
            self.stop_logging_flag = False
            self.status_sig.emit(["Update_Status", "Acquisition has started"])


        except Exception as e:
            logger.exception(str(e))


if __name__ == '__main__':
    from pymodaq.dashboard import DashBoard

    app = QtWidgets.QApplication(sys.argv)
    win = QtWidgets.QMainWindow()
    area = gutils.DockArea()
    win.setCentralWidget(area)
    win.resize(1000, 500)
    win.setWindowTitle('PyMoDAQ Dashboard')

    # win.setVisible(False)
    prog = DashBoard(area)
    prog.set_preset_mode('C:\\Users\\weber\\pymodaq_local\\preset_configs\\preset_default.xml')
    # QThread.msleep(4000)

    prog.load_log_module()
    sys.exit(app.exec_())