# -*- coding: utf-8 -*-

##
# @file   meapp.py
# @brief  Measuring app
#
# @copyright MC
#
# @addtogroup gui
# @{


import datetime
import sys
import os
import PySide


from meapp_form import *
from comm_commands import *
from comm_interface import *
from comm_structures import *
from comm_packing import *
from gui_util import *
import constants
import time
import sched, time
from threading import Timer

ORGANIZATION_NAME = 'MC'

PROGRAM_VERSION = '0.0.2'
PROGRAM_NAME = 'Encoder measuring Tool %s' % PROGRAM_VERSION
QSETTINGS_FILE = 'meapp_settings.ini'

FILTER_YAML = 'YAML definition file (*.yaml)'
FILTER_SAVE = 'Log data file (*.xml *.csv)'
FILTER_OPEN = 'Log data file (*.xml)'

LOG_FILENAME_PATTERN = '{folder}\LOG_{date}_{source}.csv'
LOG_FILENAME_DATEFORMAT = '%y%m%d_%H%M%S'


class ControlMainWindow(QtGui.QMainWindow):
    logText = QtCore.Signal(str, str)
    selectedParam = "ALL"
    selectedParamDone = 0

    # GUI update signals (to make sure all gui elements are accessed only from gui thread)
    doUpdateEncoderInGui = QtCore.Signal(object)
    doUpdateCountersInGui = QtCore.Signal(object)
    doUpdateRMPInGui = QtCore.Signal(object)
    doUpdateLogInGui = QtCore.Signal(object)
    # GUI database update signal

    def __init__(self, parent=None):
        super(ControlMainWindow, self).__init__(parent)
        
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.setWindowTitle(PROGRAM_NAME)


        # command interface
        self.ci = None

        # connect gui signals
        self.connectGuiSignals()

        # setup logging
        self.redirectStdout()
        self.setupLogging()

        # prepare GUI before loading settings
        self.addToolbarControls()
        self.registerSignals()

        # get the settings

        self.readSettings()

        # sequence

        self.sequence = sched.scheduler(time.time, time.sleep)
        self.events = []
        self.timers = []

    #region Service Functions

    def closeEvent(self, event):
        '''
        This method is called when the close event is raised.
        :param event:
        :return:
        '''

        self.closePort()
        self.writeSettings()
        QtGui.QMainWindow.closeEvent(self, event)


    def writeSettings(self):
        '''
        This method writes current settings into the settings file so the app will open with last used settings.
        :return:
        '''
        self.log.debug('Writing settings file')

        try:
            settings = QtCore.QSettings(QSETTINGS_FILE, QtCore.QSettings.IniFormat)
            settings.setValue('mainWindowGeometry', self.saveGeometry())
            settings.setValue('mainWindowState', self.saveState())
            settings.setValue('comPort', self.ui.comPort.text())
            settings.setValue('comBaud', self.ui.comBaud.text())
            #settings.setValue('refreshFreq', str(self.ui.sbAutoUpdateFreq.value()))
            settings.sync()
        except Exception, ex:
            self.log.warning('Could not write the INI file: %s' % ex, exc_info=True)


    def readSettings(self):
        '''
        Reads the settings file and puts values into the textboxes
        :return:
        '''
        self.log.debug('Reading settings file')
        settings = QtCore.QSettings(QSETTINGS_FILE, QtCore.QSettings.IniFormat)

        self.restoreGeometry(settings.value('mainWindowGeometry', QtCore.QByteArray()))
        self.restoreState(settings.value('mainWindowState', QtCore.QByteArray()))
        self.ui.comPort.setText(settings.value('comPort', 'COM1'))
        self.ui.comBaud.setText(settings.value('comBaud', '57600'))
        self.ui.sbAutoUpdateFreq.setValue(int(settings.value('refreshFreq', 10)))


    def redirectStdout(self):
        self.logText.connect(self.appendLog)
        sys.stdout = WriteStream(self.logText, 'white', sys.stdout)
        sys.stderr = WriteStream(self.logText, 'red', sys.stderr)


    def appendLog(self, text, color):
        text = text.replace('\t', '    ')
        text = text.replace(' ', '&nbsp;')
        text = text.replace('\n', '<br>')
        out = '<span style="color: %s">%s</style>' % (color, text)

        self.ui.textLogBox.moveCursor(QtGui.QTextCursor.End)
        self.ui.textLogBox.textCursor().insertHtml(out)
        self.ui.textLogBox.moveCursor(QtGui.QTextCursor.End)


    def setupLogging(self):
        rootlog = logging.getLogger()
        rootlog.setLevel(logging.DEBUG)
        self.log = logging.getLogger(self.__class__.__name__)

        # create a colored logBox handler
        strm = WriteStream(self.logText, 'white', None)
        ch = LogBoxStreamHandler(strm)
        ch.setLevel(logging.INFO)
        ch.setFormatter(NoExceptionFormatter(constants.LOG_FORMAT_CONSOLE,
                                             constants.LOG_DATEFMT_CONSOLE))
        NoExceptionFormatter.active = True
        self.log.addHandler(ch)

        # create a global file handler
        try:
            DATE_STR = datetime.date.today().isoformat()
            TIME_STR = datetime.datetime.now().time().strftime('%H-%M-%S')
            LOG_FOLDER = os.path.join('_log', '%s' % (DATE_STR))

            # set up the directory
            if not os.path.lexists(LOG_FOLDER):
                os.makedirs(LOG_FOLDER)
            if not os.path.isdir(LOG_FOLDER):
                raise Exception('Could not write to log directory %s. Aborting.' % LOG_FOLDER)

            fh = logging.FileHandler(os.path.join(LOG_FOLDER, '%s_%s.log' % (DATE_STR, TIME_STR)))
            fh.setLevel(logging.DEBUG)
            fh.setFormatter(logging.Formatter(constants.LOG_FORMAT_FILE, constants.LOG_DATEFMT_FILE))
            rootlog.addHandler(fh)
        except Exception, ex:
            print str(ex)


    def addToolbarControls(self):
        # add COM prompt
        self.ui.comPort = QtGui.QLineEdit(self)
        self.ui.comPort.setObjectName('comPort')
        self.ui.comPort.setText('COM1')
        self.ui.comPort.setMaximumWidth(50)
        self.ui.toolBar.addWidget(self.ui.comPort)

        # add baudrate input
        self.ui.comBaud = QtGui.QLineEdit(self)
        self.ui.comBaud.setObjectName('comBaud')
        self.ui.comBaud.setText('115200')
        self.ui.comBaud.setMaximumWidth(50)
        self.ui.toolBar.addWidget(self.ui.comBaud)

        # add Connect button
        self.ui.actionConnect = QtGui.QAction(self)
        self.ui.actionConnect.setObjectName('actionConnect')
        self.ui.actionConnect.setText('Connect')
        self.ui.actionConnect.setIcon(self.style().standardIcon(QtGui.QStyle.SP_ComputerIcon))
        self.ui.toolBar.addAction(self.ui.actionConnect)

        # add Disconnect button
        self.ui.actionDisconnect = QtGui.QAction(self)
        self.ui.actionDisconnect.setObjectName('actionDisconnect')
        self.ui.actionDisconnect.setText('Disconnect')
        self.ui.actionDisconnect.setEnabled(False)
        self.ui.actionDisconnect.setIcon(self.style().standardIcon(QtGui.QStyle.SP_BrowserStop))
        self.ui.toolBar.addAction(self.ui.actionDisconnect)

        # finally force reasonable size to all buttons
        ws = self.ui.toolBar.findChildren(QtGui.QWidget)
        for w in ws:
            if not isinstance(w, QtGui.QMenu):
                w.setMinimumHeight(25)
                w.setMaximumHeight(25)


    def connectGuiSignals(self):
        # motor callbacks
        self.doUpdateEncoderInGui.connect(self._updateEncoderInGui)
        # self.doUpdateLogInGui.connect(self._updateLogInGui)

    def registerSignals(self):
        # actions
        self.ui.actionConnect.triggered.connect(self.openPort)
        self.ui.actionDisconnect.triggered.connect(self.closePort)
        self.ui.sbAutoUpdateFreq.valueChanged.connect(self.doAutoUpdateFreqChange)

        # general
        self.ui.chbCounterUpdate.clicked.connect(self.doEncoderUpdate)
        
        # motor
        self.ui.btnSetMotorRPM.clicked.connect(self.doSetRPM)
        self.ui.btnStopMotor.clicked.connect(self.doMotorStop)
        self.ui.btnMotorStartSequence.clicked.connect(self.doMotorStartSequence)
        self.ui.btnMotorStopSequence.clicked.connect(self.doMotorStopSequence)

        # log
        self.ui.btnStartLog.clicked.connect(self.doStartLog)
        self.ui.btnSegment.clicked.connect(self.doNewSegment)
        self.ui.btnStopLog.clicked.connect(self.doStopLog)
        self.ui.btnResetLog.clicked.connect(self.doResetLog)

        # self.ui.chbLogUpdate.clicked.connect(self.doLogUpdate)

    def openPort(self):
        try:
            if self.ci is None:
                self.ci = CommInterface()
            self.ci.connect(port=self.ui.comPort.text(), baud=int(self.ui.comBaud.text()))
            self.ci.set_refresh_freq(self.ui.sbAutoUpdateFreq.value())
            self.ui.comPort.setEnabled(False)
            self.ui.actionConnect.setEnabled(False)
            self.ui.actionDisconnect.setEnabled(True)
            self.doSystemReadInfo()
        except Exception, ex:
            self.log.warning('Not connected: %s' % ex, exc_info=True)

            self.ui.txConnected.setText("Not connected")
            self.ui.txConnected.setStyleSheet("background-color: red;color: yellow; ")
        self.version_mismatch = False


    def closePort(self):
        self.ui.txConnected.setText("Disconnected")
        self.ui.txConnected.setStyleSheet("background-color: white;")
        try:
            if self.ci is not None:
                self.ci.disconnect()
        except Exception, ex:
            self.log.debug('Disconnecting hickup: %s' % ex, exc_info=True)

        self.ui.comPort.setEnabled(True)
        self.ui.actionConnect.setEnabled(True)
        self.ui.actionDisconnect.setEnabled(False)


    #region SystemUpdate
    def doSystemReadInfo(self):
        '''
        Reads the system info
        :return: None
        '''
        if self.ci is None:
            return

        try:
            self.log.info('Reading system info')
            self.log.info(self.ci.command(Commands.COMMON_SYSTEM, callback=self.reSystemReadInfo))
        except Exception, ex:
            self.log.warning('failed: %s' % ex, exc_info=True)


    def reSystemReadInfo(self, response):
        data = SystemInfo()
        data.decode(response.data)
        self.ui.txSysFWVersion.setText("{}.{}.{}".format(data.fw_rev_major, data.fw_rev_minor, data.fw_rev_patch))
        self.ui.txConnected.setText("Connected")
        self.ui.txConnected.setStyleSheet("background-color: rgb(0, 255, 0);")

    # endregion
    #region AutoUpdate

    def doAutoUpdateFreqChange(self):
        '''
        Changes the auto update frequency due to the value in the spin box.
        :return: None
        '''
        if self.ci is None:
            return

        self.ci.set_refresh_freq(self.ui.sbAutoUpdateFreq.value())

    # endregion

    # region Status
    def doEncoderUpdate(self):
        '''
        Starts or stops periodic update of the mowing motor status
        :return:
        '''
        if self.ci is None:
            return

        try:
            if self.ui.chbCounterUpdate.isChecked():
                self.log.info('Update Encoder data started')
                self.log.info(self.ci.command_periodic(Commands.ENCODER_STATUS, callback=self.reEncoderUpdate))
            else:
                self.log.info('Update Encoder data stopped')
                self.log.info(
                    self.ci.command_periodic_detach(Commands.ENCODER_STATUS, callback=self.reEncoderUpdate))
        except Exception, ex:
            self.log.warning('Encoder Update change failed: %s' % ex, exc_info=True)


    def reEncoderUpdate(self, response):
        '''
        Emits signal in order to do all GUI related stuff in GUI thread
        '''
        self.doUpdateEncoderInGui.emit(response)

    def _updateEncoderInGui(self, response):
        data = CounterStatus()
        data.decode(response.data)
        self.ui.txHallCounter.setText("{0:.0f}".format(data.hall_count))
        self.ui.txRevCounter.setText("{0:.0f}".format(data.revolutions))
        self.ui.txMotorFreq.setText("{0:.0f}".format(data.rpm))
        # TODO other data
        #self.ui.txMMCurrReal.setText("{0:.6f}".format(data.current))
        #self.ui.txMMCurrTarget.setText("{0:6f}".format(data.current_target))
        #self.ui.txMMCurrTarget.setText("{0:.1f}".format(data.current_target))
        #self.ui.txMMDutyCycle.setText("{0:.1f}".format(data.duty_cycle))
        #self.ui.txMMCurrRealAVG.setText("{0:6f}".format(self.filter_current_mm))

    # endregion

    # region Motor
    def doMotorStartSequence(self):
        repetitions = int(self.ui.sbSequenceRepeats.text())
        steps = int(self.ui.sbSequenceSteps.text())
        duration = int(self.ui.sbSequenceStepDuration.text())
        maxRPM = int(self.ui.sbSequenceMaxRPM.text())
        rpmStep = round(maxRPM / (steps + 1))
        rep = range(0,repetitions,1)
        rep = [x*(2*steps+3) for x in rep]

        for offset in rep:
            up = range(0,(steps+2),1)
            upInd = range(0,(steps + 2), 1)
            down = range((steps+2),(2*steps+3),1)
            downInd = range(0, (steps + 1), 1)
            upStart = [x*duration+duration*offset for x in up]
            downStart = [x*duration +duration*offset for x in down]
            upRPM = [x * rpmStep for x in up]
            downRPM = [x*(-1)+2+2*steps for x in down]
            downRPM = [x * rpmStep for x in downRPM]

            for x in upInd:
                Timer(upStart[x], self.doSetRPMAutomated, args =[upRPM[x]]).start()
                #self.sequence.enter(upStart[x], 1, self.doSetRPMAutomated, ([upRPM[x]]))
                self.log.info('Added event: in %d seconds rpm = %d .', upStart[x],upRPM[x])

            for x in downInd:
                Timer(downStart[x], self.doSetRPMAutomated, args=[downRPM[x]]).start()
                #self.sequence.enter(downStart[x], 1, self.doSetRPMAutomated, ([downRPM[x]]))
                self.log.info('Added event: in %d seconds rpm = %d .', downStart[x], downRPM[x])

    def doMotorStopSequence(self):
        # empty scheduler
        #for x in self.events:
            #self.sequence.cancel(x)

            #self.log.info('Cancelling events.')
        for thing in threading.enumerate():
            if isinstance(thing, threading._Timer):
                thing.cancel()
        self.doMotorStop()
        self.log.info('Sequence stopped')
        pass

    def doSetRPMAutomated(self,reqRPM):
        '''
        Sets RPM
        :return: None
        '''
        self.log.info('Setting rpm: %d .', reqRPM)

        if self.ci is None:
            return

        try:
            if self.ui.chbMotorEnable.isChecked():
                self.log.info('Setting target RPM')
                #input1 = self.ui.sbSetRPM.text()
                input2 = int(reqRPM)
                #input3 = int(input2*STEPS_PER_REVOLUTION)
                data = pack_sint16(input2)
                self.log.info(self.ci.command(Commands.MOTOR_SET_RPM, data, callback=self.reSetRPM))
            else:
                self.log.info('Target RPM not set, enable motor first')
        except Exception, ex:
            self.log.warning('failed: %s' % ex, exc_info=True)
        self.ui.sbSetRPM.setStyleSheet("background-color: red;")

    def doSetRPM(self):
        '''
        Sets RPM
        :return: None
        '''
        if self.ci is None:
            return

        try:
            if self.ui.chbMotorEnable.isChecked():
                self.log.info('Setting target RPM')
                input1 = self.ui.sbSetRPM.text()
                input2 = int(input1)
                #input3 = int(input2*STEPS_PER_REVOLUTION)
                data = pack_sint16(input2)
                self.log.info(self.ci.command(Commands.MOTOR_SET_RPM, data, callback=self.reSetRPM))
            else:
                self.log.info('Target RPM not set, enable motor first')
        except Exception, ex:
            self.log.warning('failed: %s' % ex, exc_info=True)
        self.ui.sbSetRPM.setStyleSheet("background-color: red;")

    def reSetRPM(self, response):
        self.ui.sbSetRPM.setStyleSheet("background-color: green;")

    def doMotorStop(self):
        if self.ci is None:
            return

        try:
            self.log.info('Stopping Motor')
            self.log.info(self.ci.command(Commands.MOTOR_STOP))
            self.ui.sbSetRPM.setStyleSheet("background-color: orange;")
        except Exception, ex:
            self.log.warning('failed: %s' % ex, exc_info=True)

    # endregion

    # region Log

    def doStartLog(self):
        if self.ci is None:
            return
        # TODO get com, run putty on that com
        try:
            self.log.info('Starting Log')
            self.log.info(self.ci.command(Commands.LOG_START))
        except Exception, ex:
            self.log.warning('failed: %s' % ex, exc_info=True)

    def doNewSegment(self):
        if self.ci is None:
            return
        # TODO display segment?
        try:
            self.log.info('New Segment started')
            self.log.info(self.ci.command(Commands.LOG_SEGMENT))
        except Exception, ex:
            self.log.warning('failed: %s' % ex, exc_info=True)

    def doStopLog(self):
        if self.ci is None:
            return
        # TODO kill putty?
        try:
            self.log.info('Logging stopped')
            self.log.info(self.ci.command(Commands.LOG_STOP))
        except Exception, ex:
            self.log.warning('failed: %s' % ex, exc_info=True)

    def doResetLog(self):
        if self.ci is None:
            return

        try:
            self.log.info('Resetting log counter')
            self.log.info(self.ci.command(Commands.LOG_RESET))
        except Exception, ex:
            self.log.warning('failed: %s' % ex, exc_info=True)

    # endregion

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)

    QtCore.QCoreApplication.setOrganizationName(ORGANIZATION_NAME)
    QtCore.QCoreApplication.setApplicationName(PROGRAM_NAME)
    QtCore.QCoreApplication.setApplicationVersion(PROGRAM_VERSION)

    mySW = ControlMainWindow()
    mySW.show()

    sys.exit(app.exec_())

# @}
