"""
Contains code for viewing Parameters in a separate gui
"""
import time
import threading
import logging
import numpy as np
import sys

import multiprocessing as mp

from qtpy import QtWidgets
from qtpy.QtCore import Signal, Slot
import pyqtgraph
from qtt import pgeometry
from functools import partial

# %%


class QCodesTimer(threading.Thread):

    def __init__(self, callback_function, dt=2, **kwargs):
        """ Simple timer to perform periodic execution of function """
        super().__init__(**kwargs)
        self.callback_function = callback_function
        self.dt = dt

    def run(self):
        self._run = True
        while self._run:
            logging.debug('QCodesTimer: start sleep')
            time.sleep(self.dt)
            logging.debug('QCodesTimer: execute callback function')
            self.callback_function()

    def stop(self):
        self._run = False


class ParameterViewer(QtWidgets.QTreeWidget):
    """ Simple class to show qcodes parameters """

    def __init__(self, instruments, instrumentnames=None,
                 name='QuTech Parameter Viewer', start_timer=False, **kwargs):
        """ Simple class to show qcodes parameters

        Args:
            instruments (list): list of Qcodes Instruments to show
            instrumentnames (None or list): optional list of names to show
            name (str, optional): string used in the window title
            start_timer (bool): If True, then start the updating
        """
        super().__init__(**kwargs)
        self.name = name
        w = self
        w.setGeometry(1700, 50, 300, 600)
        w.setColumnCount(3)
        self.verbose = 1
        header = QtWidgets.QTreeWidgetItem(["Parameter", "Value"])
        w.setHeaderItem(header)
        w.setWindowTitle(name)

        # check to prevent nasty error (e.g. random values on a sample)
        for i in instruments:
            if getattr(i, 'lock', 'nolock') is None:
                raise Exception(
                    'do not use multi-threading with an Instrument that is not thread safe')

        if instrumentnames is None:
            instrumentnames = [i.name for i in instruments]
        self._instruments = instruments
        self._instrumentnames = instrumentnames
        self._itemsdict = dict()
        for i in instrumentnames:
            self._itemsdict[i] = dict()
        self._timer = None
        self.init()
        self.show()

        self.callbacklist = []

        self.update_field.connect(self._set_field)

        self.updatecallback()

    def close(self):
        self.stop()
        super(ParameterViewer, self).close()

    def init(self):
        """ Initialize parameter viewer

        This function created all the GUI elements.
        """
        for ii, iname in enumerate(self._instrumentnames):
            instr = self._instruments[ii]
            pp = instr.parameters
            ppnames = sorted(instr.parameters.keys())

            ppnames = [p for p in ppnames if hasattr(
                instr.parameters[p], 'get')]
            gatesroot = QtWidgets.QTreeWidgetItem(self, [iname])
            for parameter_name in ppnames:
                # hack to make this semi thread-safe
                si = min(sys.getswitchinterval(), 0.1)
                # hack to make this semi thread-safe
                sys.setswitchinterval(100)
                sys.setswitchinterval(si)  # hack to make this semi thread-safe
                box = QtWidgets.QDoubleSpinBox()
                # do not emit signals when still editing
                box.setKeyboardTracking(False)
                box.setMinimum(-10000)
                box.setMaximum(10000)
                box.setSingleStep(5)

                v = ''
                A = QtWidgets.QTreeWidgetItem(gatesroot, [parameter_name, v])
                self._itemsdict[iname][parameter_name] = A

                if hasattr(pp[parameter_name], 'set'):
                    qq = A
                    self.setItemWidget(qq, 1, box)
                    self._itemsdict[iname][parameter_name] = box

                box.valueChanged.connect(partial(self._valueChanged, iname, parameter_name))

        self.setSortingEnabled(True)
        self.expandAll()

    def is_running(self):
        if self._timer is None:
            return False
        if self._timer.isAlive():
            return True
        else:
            return False

    def setParamSingleStep(self, instr, param, value):
        """ Set the default step size for a parameter in the viewer

        Args:
            instr (str): instrument
            param (str): parameter of the instrument
            value (float): step size
        """
        try:
            box = self._itemsdict[instr][param]
            box.setSingleStep(value)
        except Exception as ex:
            print(ex)

    def setSingleStep(self, value, instrument_name=None):
        """ Set the default step size for all parameters in the viewer

        Args:
            value (float): step size
        """
        if instrument_name is None:
            names = self._instrumentnames
        else:
            names = [instrument_name]
        for iname in names:
            lst = self._itemsdict[iname]
            for p in lst:
                box = lst[p]
                try:
                    box.setSingleStep(value)
                except:
                    pass

    def _valueChanged(self, iname, param, value, *args, **kwargs):
        """ Callback used to update values in an instrument """
        instr = self._instruments[self._instrumentnames.index(iname)]
        logging.info('set %s.%s to %s' % (iname, param, value))
        instr.set(param, value)

    def updatecallback(self, start=True, dt=3):
        """ Update the data and start the restarts timer """
        if self._timer is not None:
            del self._timer

        self.updatedata()

        if start:
            self._timer = QCodesTimer(callback_function=self.updatedata, dt=dt)
            self._timer.start()
        else:
            self._timer = None

    update_field = Signal(str, str, object, bool)

    def stop(self):
        """ Stop readout of the parameters in the widget """
        self.setWindowTitle(self.name + ': stopped')
        self._timer.stop()

    @Slot(str, str, object, bool)
    def _set_field(self, iname, parameter_name, value, force_update):
        """ Helper function

        Update field of parameter viewer with a string value
        """
        if self.verbose >= 2:
            print('_set_field: %s %s: %s' % (iname, parameter_name, str(value)))
        sb = self._itemsdict[iname][parameter_name]

        if isinstance(sb, QtWidgets.QTreeWidgetItem):
            sb.setText(1, str(value))
        else:
            # update a float value

            try:
                update_value = np.abs(sb.value() - value) > 1e-9
            except:
                update_value = True
            if update_value or force_update:
                if not sb.hasFocus():  # do not update when editing
                    logging.debug('update %s to %s' % (parameter_name, value))
                    try:
                        oldstate = sb.blockSignals(True)
                        sb.setValue(value)
                        sb.blockSignals(oldstate)
                    except Exception as e:
                        pass

    def updatedata(self, force_update=False):
        """ Update data in viewer using station.snapshow """
        logging.debug('ParameterViewer: update values')
        for iname in self._instrumentnames:
            instr = self._instruments[self._instrumentnames.index(iname)]

            try:
                pp = instr.parameters
            except AttributeError as ex:
                # instrument was removed
                print('instrument was removed, stopping ParameterViewer')
                self._timer.stop()

            ppnames = sorted(instr.parameters.keys())

            si = sys.getswitchinterval()

            for parameter_name in ppnames:
                # hack to make this semi thread-safe
                sys.setswitchinterval(100)
                value = pp[parameter_name].get_latest()
                sys.setswitchinterval(si)

                self.update_field.emit(iname, parameter_name, value, force_update)

        for f in self.callbacklist:
            try:
                f()
            except Exception as e:
                logging.debug('update function failed')
                logging.debug(str(e))


def createParameterWidgetRemote(instruments, doexec=True):
    """ Create a parameter widget in a remote process.

    Note: this can only be used if all the Instruments are remote instruments.
    """
    p = mp.Process(target=createParameterWidget, args=(instruments,))
    p.start()
    return p


def createParameterWidget(instruments, doexec=False, remote=False):
    """ Create a parameter widget

    Args:
        instruments (list)
        doexec (bool): if True execute as a standalone Qt app
        remote (bool): if True, then start in a remote process.
                       Note: this can only be used if all the Instruments are remote instruments.
    """
    if remote:
        p = mp.Process(target=createParameterWidget,
                       args=(instruments, doexec))
        p.start()
        return p

    instrumentnames = [i.name for i in instruments]
    app = pyqtgraph.mkQApp()

    ms = pgeometry.monitorSizes()[-1]
    p = ParameterViewer(instruments=instruments,
                        instrumentnames=instrumentnames)
    p.setGeometry(ms[0] + ms[2] - 320, 30, 300, 600)
    p.show()
    p.updatecallback()

    logging.info('created update widget...')

    if doexec:
        app.exec()
    return p

# %%

def test_parameterviewer():
    import pyqtgraph
    qtapp = pyqtgraph.mkQApp()
    import qtt.measurements.scans
    from qtt.instrument_drivers.virtual_instruments import VirtualIVVI

    ivvi = VirtualIVVI(name=qtt.measurements.scans.instrumentName('dummyivvi'), model=None)
    p = ParameterViewer(instruments=[ivvi], instrumentnames=['ivvi'])
    p.show()
    p.updatecallback()
    assert(p.is_running())
    p.setGeometry(10, 10, 360, 600)

    p.stop()
    p.close()
    qtapp.processEvents()
    ivvi.close()

# %% Debugging code


if __name__ == '__main__':
    test_parameterviewer()
