from matplotlib import animation
import pygame
import wx
import logging
from wx.lib.agw import floatspin
from wx.richtext import RichTextCtrl
from Scope import trend_random, trend_get_HR, trend_get_max, trend_get_lastPeakValue, trend_get_avgPeakValue, \
    trend_get_avg
from Scope import ScopePanel, TrendScope
from monitor.BaseClasses import Experiment, Drug
from monitor.ComediObjects import ComediStreamer
# from monitor.sampling import PhysioStreamer
# noinspection PyUnresolvedReferences
import datetime
import os
import time
from os import path
import wx.lib.buttons

MAXINT = 0x7fffffff
VALID_TREND_FUNCTIONS = {"random", "get_HR", "get_max", "get_lastPeakValue", "get_avgPeakValue", "get_avg"}


# noinspection PyMethodOverriding
class textValidator(wx.PyValidator):
    """ This validator is used to ensure that the user has entered something
         into the text object editor dialog's text field.
     """

    def __init__(self, showDlg=True):
        """ Standard constructor.
         """
        wx.PyValidator.__init__(self)
        self.showDlg = showDlg

    def Clone(self):
        # Standard cloner.
        #     Note that every validator must implement the Clone() method.
        return textValidator(self.showDlg)

    def Validate(self, win):
        # Validate the contents of the given text control.
        textCtrl = self.GetWindow()
        text = textCtrl.GetValue()

        if len(text) == 0:
            if self.showDlg:
                wx.MessageBox("A text object must contain some text!", "Error")
            textCtrl.SetBackgroundColour("pink")
            textCtrl.SetFocus()
            textCtrl.Refresh()
            return False
        else:
            textCtrl.SetBackgroundColour(
                wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
            textCtrl.Refresh()
            return True

    def TransferToWindow(self):
        """ Transfer data from validator to window.

             The default implementation returns False, indicating that an error
             occurred.  We simply return True, as we don't do any data transfer.
         """
        return True  # Prevent wxDialog from complaining.

    def TransferFromWindow(self):
        """ Transfer data from window to validator.

             The default implementation returns False, indicating that an error
             occurred.  We simply return True, as we don't do any data transfer.
         """
        return True  # Prevent wxDialog from complaining.


# noinspection PyMethodOverriding
class drugListValidator(wx.PyValidator):
    def __init__(self, obj, attr, required=True):
        super(drugListValidator, self).__init__()
        self.obj = obj
        self.attr = attr
        self.required = required

    def Clone(self):
        return self.__class__(self.obj, self.attr, self.required)

    def Validate(self, parent):
        wgt = self.GetWindow()
        retVal = True
        if self.required and wgt.GetCount() == 0:
            retVal = False
            wx.MessageBox("You must provide at least one drug!", "Error", style=wx.ICON_ERROR)
        return retVal

    def TransferToWindow(self):
        wgt = self.GetWindow()
        wgt.Clear()
        drugList = getattr(self.obj, self.attr)
        for drug in drugList:
            wgt.Append(drug.printForList())
        return True

    def TransferFromWindow(self):
        wgt = self.GetWindow()
        drugList = []
        for item in wgt.GetItems():
            drugTuple = item.partition("|")
            if drugTuple[1] == "":
                # the string did not contain |
                return False
            else:
                drugName = drugTuple[0].strip()
                drugDose = int(drugTuple[2].replace(" uL", "").strip(), 10)
                drugList.append(Drug(name=drugName, dose=drugDose))
        setattr(self.obj, self.attr, drugList)


# noinspection PyMethodOverriding
class mouseDoBValidator(wx.PyValidator):
    def __init__(self, required=True):
        wx.PyValidator.__init__(self)
        self.required = required

    def Clone(self):
        return self.__class__(self.required)

    def Validate(self, win):
        dateCtlr = self.GetWindow()
        dateString = dateCtlr.GetValue()
        expDateCtlr = win.FindWindowByName('expDateBox')
        date = wx.DateTime()
        date.ParseDate(dateString)
        expDate = expDateCtlr.GetValue()
        if len(dateString) == 0:
            return not self.required  # return False if the info is required
        else:
            if date.GetYear() < 2000:
                wx.MessageBox("The date of birth of the mouse should be AFTER the year 2000", "Error")
                dateCtlr.SetBackgroundColour("pink")
                dateCtlr.SetFocus()
                dateCtlr.Refresh()
                return False
            if date > expDate:
                wx.MessageBox("The date of birth of the mouse cannot be later than %s" % expDate.FormatDate(),
                              "Error")
                dateCtlr.SetBackgroundColour("pink")
                dateCtlr.SetFocus()
                dateCtlr.Refresh()
                return False
            # all other cases
            dateCtlr.SetBackgroundColour(
                wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
            dateCtlr.Refresh()
            return True

    def TransferToWindow(self):
        """ Transfer data from validator to window.

             The default implementation returns False, indicating that an error
             occurred.  We simply return True, as we don't do any data transfer.
         """
        return True  # Prevent wxDialog from complaining.

    def TransferFromWindow(self):
        """ Transfer data from window to validator.

             The default implementation returns False, indicating that an error
             occurred.  We simply return True, as we don't do any data transfer.
         """
        return True  # Prevent wxDialog from complaining.


# noinspection PyMethodOverriding
class mouseListsValidator(wx.PyValidator):
    def __init__(self, obj, attr, propName, required=True):
        super(mouseListsValidator, self).__init__()
        self.obj = obj
        self.attr = attr
        self.propName = propName
        self.required = required

    def Clone(self):
        return self.__class__(self.obj, self.attr, self.propName, self.required)

    def Validate(self, parent):
        wgt = self.GetWindow()
        myVal = wgt.GetValue()
        retVal = True
        myList = getattr(self.obj, self.attr)
        if len(myVal) == 0:
            if self.required:
                wx.MessageBox("You must provide the %s of the mouse" % self.propName,
                              "Error",
                              style=wx.ICON_ERROR | wx.CENTER)
                retVal = False
        elif myVal not in myList:
            a = wx.MessageBox("You are creating a new %s name, is that what you meant to do?" % self.propName,
                              "Query", style=wx.ICON_QUESTION | wx.YES_NO)
            if a == wx.YES:
                retVal = True
                wgt.SetBackgroundColour(
                    wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
            else:
                retVal = False
                wgt.SetBackgroundColour("pink")
                wgt.SetFocus()
                wgt.Refresh()
        return retVal

    def TransferToWindow(self):
        return True

    def TransferFromWindow(self):
        wgt = self.GetWindow()
        myVal = wgt.GetValue()
        myList = getattr(self.obj, self.attr)
        myList.add(myVal)
        setattr(self.obj, self.attr, myList)


# noinspection PyMethodOverriding
class drugDoseValidator(wx.PyValidator):
    def __init__(self):
        super(drugDoseValidator, self).__init__()

    def Clone(self):
        return self.__class__()

    def Validate(self, parent):
        wgt = self.GetWindow()
        dose = wgt.GetValue()
        if dose <= 0:
            wgt.SetBackgroundColour("pink")
            wgt.SetFocus()
            wgt.Refresh()
            return False
        else:
            wgt.SetBackgroundColour(
                wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
            return True

    def TransferToWindow(self):
        return True

    def TransferFromWindow(self):
        return True


class clockPanel(wx.Panel):
    def __init__(self, parent, id=wx.ID_ANY, timeFormat="%H:%M", dateFormat="%b %d, %Y", refreshPeriod=1000,
                 minSize=(200, 100)):
        super(clockPanel, self).__init__(parent, id)
        self.SetMinSize(minSize)
        # self.SetBackgroundColour((0, 125, 125))
        self._timeFormat = timeFormat
        self._dateFormat = dateFormat
        self._refreshPeriod = refreshPeriod
        self.__clockTimer = wx.Timer(self, id=wx.ID_ANY)
        self.__clockTimer.Start(self._refreshPeriod)
        self.Bind(wx.EVT_TIMER, self.onTimer)

        self._timeLabel = wx.StaticText(self, id=wx.ID_ANY, label=timeFormat, style=wx.ALIGN_CENTER)
        font = self._timeLabel.GetFont()
        font.SetPointSize(30)
        font.SetWeight(wx.FONTWEIGHT_BOLD)
        self._timeLabel.SetFont(font)

        self._dateLabel = wx.StaticText(self, id=wx.ID_ANY, label=dateFormat, style=wx.ALIGN_CENTER)
        font = self._dateLabel.GetFont()
        font.SetPointSize(20)
        self._dateLabel.SetFont(font)

        self.onTimer(None)  # update the content of the labels

        # sizer = wx.GridSizer(2, 1)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self._timeLabel, 0,
                  wx.CENTRE | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_BOTTOM | wx.TOP,
                  border=10)
        sizer.Add(self._dateLabel, 1,
                  wx.CENTRE | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_TOP
                  )
        self.SetSizer(sizer)

    # noinspection PyUnusedLocal
    def onTimer(self, event):
        # logging.debug("in clockPanelClass.onTimer(). Event: %s" % event)
        curTime = datetime.datetime.now().strftime(self._timeFormat)
        curDate = datetime.datetime.now().strftime(self._dateFormat)
        if self._timeLabel.GetLabelText() != curTime:
            self._timeLabel.SetLabel(curTime)
            self.Layout()
        if self._dateLabel.GetLabelText() != curDate:
            self._dateLabel.SetLabel(curDate)
            self.Layout()

    @property
    def timeFormat(self):
        return self._timeFormat

    @timeFormat.setter
    def timeFormat(self, value):
        self._timeFormat = value
        self.onTimer(None)

    @property
    def dateFormat(self):
        return self._dateFormat

    @dateFormat.setter
    def dateFormat(self, value):
        self._dateFormat = value
        self.onTimer(None)

    @property
    def refreshPeriod(self):
        return self._refreshPeriod

    @refreshPeriod.setter
    def refreshPeriod(self, value):
        self._refreshPeriod = value
        self.__clockTimer.Stop()
        self.__clockTimer.Start(self._refreshPeriod)


class customValueDialog(wx.Dialog):
    def __init__(self, parent, pos, value, id=wx.ID_ANY, title=''):
        # call the parent constructor
        super(customValueDialog, self).__init__(parent, id=id, title=title, pos=pos, size=wx.DefaultSize,
                                                style=wx.CAPTION, name=wx.DialogNameStr)

        box = wx.BoxSizer(wx.HORIZONTAL)
        outer = wx.BoxSizer(wx.HORIZONTAL)

        # spin box
        self.drugDoseBox = wx.SpinCtrl(self, id=wx.ID_ANY, size=wx.Size(125, -1))
        self.drugDoseBox.SetRange(0, MAXINT)
        self.drugDoseBox.SetValue(value)
        self.drugDoseBox.SetSelection(0, MAXINT)

        self.okButton = wx.Button(self, id=wx.ID_OK, style=wx.BU_EXACTFIT)

        box.Add(self.drugDoseBox, 1)
        box.Add(self.okButton, 0)
        outer.Add(box, 1, wx.ALL, border=5)
        self.SetSizer(outer)

        self.Fit()

        # bind keyboard listener
        # there was some problems capturing the key presses. I bound the listener
        # to all the widgets in the dialog to make sure I get them all.
        self.Bind(wx.EVT_KEY_UP, self.onChar)
        for child in self.GetChildren():
            child.Bind(wx.EVT_KEY_UP, self.onChar)
            for grandchild in child.GetChildren():
                grandchild.Bind(wx.EVT_KEY_UP, self.onChar)
        self.Bind(wx.EVT_ACTIVATE, self.onActivate)  # will be called on window creation to set focus

    def onChar(self, event):
        logging.debug("in customValueDlgClass.onChar(). Event: %s" % event)
        key = event.GetKeyCode()
        logging.debug("got key: %s" % key)

        if key == wx.WXK_ESCAPE:
            logging.debug("got ESCAPE, closing modal with return code %d" % wx.ID_CANCEL)
            self.EndModal(wx.ID_CANCEL)
            return

        if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
            logging.debug("got RETURN, closing modal with return code %d" % wx.ID_OK)
            self.EndModal(wx.ID_OK)
            return

        logging.debug("got uninteresting key, skipping")
        event.Skip()

    # noinspection PyUnusedLocal
    def onActivate(self, event):
        self.drugDoseBox.SetFocus()


class customDrugDoseDialog(customValueDialog):
    def __init__(self, parent, pos, drugDose, drugName, id=wx.ID_ANY, title='Inject custom drug',
                 label='Inject custom drug'):
        # call the parent constructor
        super(customDrugDoseDialog, self).__init__(parent, pos, drugDose, id=id, title=title)
        self.dlgLabel = wx.StaticText(self, id=wx.ID_ANY, label=label)
        self.drugNameBox = wx.TextCtrl(self, id=wx.ID_ANY, size=(200, -1),
                                       validator=textValidator(showDlg=False))
        self.drugNameBox.SetValue(drugName)
        # the parent class

        sizer = wx.GridBagSizer(vgap=5, hgap=5)
        sizer.Add(self.dlgLabel, pos=(0, 0), span=wx.GBSpan(1, 2))
        sizer.Add(wx.StaticText(self, id=wx.ID_ANY, label="Drug Name"), pos=(1, 0))
        sizer.Add(self.drugNameBox, pos=(1, 1))
        sizer.Add(wx.StaticText(self, id=wx.ID_ANY, label="Drug Dose (uL)"), pos=(2, 0))
        sizer.Add(self.drugDoseBox, pos=(2, 1))
        self.okButton.Destroy()
        buttonSizer = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL)
        sizer.Add(buttonSizer, pos=(3, 0), span=(1, 2), flag=wx.ALIGN_RIGHT)
        # sizer.Add(self.okButton, (2, 1))
        # sizer.Add(self.cancelButton, (2, 2))

        outSizer = self.GetSizer()
        outSizer.Clear()
        outSizer.Add(sizer, 1, wx.ALL, border=5)
        self.Fit()


class drugPanel(wx.Panel):
    def __init__(self, parent, drugName, drugDose, alarmSoundFile=None, logBox=None,
                 printFormat='{time:s}\t{drug:s}\t\n',
                 id=wx.ID_ANY):
        super(drugPanel, self).__init__(parent, id)
        self._printFormat = printFormat
        BORDERSIZE = 5

        sizer = wx.GridBagSizer(hgap=BORDERSIZE, vgap=BORDERSIZE)

        # drug info
        self._drugName = drugName
        self._drugDose = drugDose
        self._customDose = 0.0
        self.__ALARM_DURATION = 10  # in sec
        self.__ALARM_TIMER_FLASH_PERIOD = 500  # in ms
        self._alarmSound = None
        if alarmSoundFile is not None:
            # initialize the mixer if it hasn't already
            if pygame.mixer.get_init() is None:
                pygame.mixer.pre_init(44100, -16, 2, 2048)
                pygame.mixer.init()
            self._alarmSound = pygame.mixer.Sound(alarmSoundFile)
            # logging.debug('loaded sound %s [%s] - length: %f' %
            #               (self._alarmSound, alarmSoundFile, self._alarmSound.get_length()))

        # GUI windows
        self._logBox = logBox
        self._label = wx.StaticText(self, label='', style=wx.ALIGN_LEFT)
        font = self._label.GetFont()
        font.SetWeight(wx.FONTWEIGHT_BOLD)
        self._label.SetFont(font)

        self._drugTimerLabel = wx.StaticText(self, label='00:00',
                                             style=wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL,
                                             size=(100, -1))
        font = self._drugTimerLabel.GetFont()
        font.SetPointSize(30)
        font.SetWeight(wx.FONTWEIGHT_BOLD)
        self._drugTimerLabel.SetFont(font)

        self._fullDoseButton = wx.Button(self, label='Full dose', id=wx.ID_ANY)
        self._halfDoseButton = wx.Button(self, label='1/2 Dose', id=wx.ID_ANY)
        self._quartDoseButton = wx.Button(self, label='1/4 Dose', id=wx.ID_ANY)
        self._customDoseButton = wx.Button(self, label='Custom', id=wx.ID_ANY)
        self._alarmsRadioBox = wx.RadioBox(self, id=wx.ID_ANY, label="Alarms (min)", pos=wx.DefaultPosition,
                                           size=wx.DefaultSize, majorDimension=4, style=wx.RA_SPECIFY_COLS,
                                           choices=['None', '30', '10', '...'])

        sizer.Add(self._label, pos=(0, 0), span=(1, 4))
        sizer.Add(self._fullDoseButton, pos=(1, 0), flag=wx.EXPAND)
        sizer.Add(self._halfDoseButton, pos=(1, 1), flag=wx.EXPAND)
        sizer.Add(self._quartDoseButton, pos=(1, 2), flag=wx.EXPAND)
        sizer.Add(self._customDoseButton, pos=(1, 3), flag=wx.EXPAND)
        sizer.Add(self._drugTimerLabel, pos=(2, 0),
                  flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)
        sizer.Add(self._alarmsRadioBox, pos=(2, 1), span=(1, 3),
                  flag=wx.CENTRE | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)

        outerSizer = wx.BoxSizer(wx.HORIZONTAL)
        outerSizer.Add(sizer, 1, wx.EXPAND | wx.ALL, border=BORDERSIZE)
        self.SetSizer(outerSizer)
        self._updateLabel()

        # timers
        self.__drugTimer = wx.Timer(self, wx.ID_ANY)
        self.__alarmTimer = wx.Timer(self, wx.ID_ANY)
        self.__alarmSoundTimer = wx.Timer(self, wx.ID_ANY)
        # event times
        self.__drugInjectionTime = None
        self.__alarmTime = None

        # other variables
        self.__LABEL_FG_NORMAL = self._drugTimerLabel.GetForegroundColour()
        self.__LABEL_FG_ALARM = "#FF0000"

        self.Bind(wx.EVT_BUTTON, self._onFullDoseButton, id=self._fullDoseButton.GetId())
        self.Bind(wx.EVT_BUTTON, self._onHalfDoseButton, id=self._halfDoseButton.GetId())
        self.Bind(wx.EVT_BUTTON, self._onQuartDoseButton, id=self._quartDoseButton.GetId())
        self.Bind(wx.EVT_BUTTON, self._onCustomDoseButton, id=self._customDoseButton.GetId())
        self.Bind(wx.EVT_TIMER, self._onDrugTimer, id=self.__drugTimer.GetId())
        self.Bind(wx.EVT_TIMER, self._onAlarmTimer, id=self.__alarmTimer.GetId())
        self._drugTimerLabel.Bind(wx.EVT_LEFT_DCLICK, self._onDrugTimerLabelClick)
        self._label.Bind(wx.EVT_LEFT_DCLICK, self._onLabelClick)
        self._alarmsRadioBox.Bind(wx.EVT_RADIOBOX, self._onAlarmChoose, id=self._alarmsRadioBox.GetId())
        # self.SetBackgroundColour((0, 200, 200))  # for debug

    # noinspection PyMethodOverriding
    def Enable(self, enable=True):
        for child in self.GetChildren():
            child.Enable(enable)

    @property
    def drugName(self):
        return self._drugName

    @drugName.setter
    def drugName(self, value):
        self._drugName = value
        self._updateLabel()

    @property
    def drugDose(self):
        return self._drugDose

    @drugDose.setter
    def drugDose(self, value):
        self._drugDose = value
        self._updateLabel()

    def _updateLabel(self):
        label = "%s (%d uL)" % (self._drugName, self._drugDose)
        self._label.SetLabel(label)
        self.Layout()

    def _onLabelClick(self, event):
        logging.debug("in drugPanel.onLabelClick(). event: %s" % event)
        # create a dialog to modify the name and dosage of the drug
        editDrugDlg = wx.Dialog(self)
        drugNameLabel = wx.StaticText(editDrugDlg, label='Drug Name:')
        drugNameBox = wx.TextCtrl(editDrugDlg, size=(100, -1))
        drugDoseLabel = wx.StaticText(editDrugDlg, label='Drug Dose (uL):')
        drugDoseBox = wx.SpinCtrl(editDrugDlg, size=(100, -1))
        okButton = wx.Button(editDrugDlg, id=wx.ID_OK)
        cancelButton = wx.Button(editDrugDlg, id=wx.ID_CANCEL)
        drugNameBox.SetValue(self._drugName)
        drugDoseBox.SetValue(self._drugDose)
        sizer = wx.GridBagSizer(hgap=5, vgap=5)
        sizer.Add(drugNameLabel, pos=(0, 0), flag=wx.ALIGN_RIGHT)
        sizer.Add(drugNameBox, pos=(0, 1), flag=wx.ALIGN_LEFT)
        sizer.Add(drugDoseLabel, pos=(1, 0), flag=wx.ALIGN_RIGHT)
        sizer.Add(drugDoseBox, pos=(1, 1), flag=wx.ALIGN_LEFT)
        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
        sizer2.Add(okButton)
        sizer2.Add(cancelButton)
        sizer.Add(sizer2, pos=(2, 0), span=(1, 2), flag=wx.ALIGN_RIGHT)
        sizer3 = wx.BoxSizer(wx.HORIZONTAL)
        sizer3.Add(sizer, 1, wx.ALL, border=10)
        editDrugDlg.SetSizerAndFit(sizer3)
        if editDrugDlg.ShowModal() == wx.ID_OK:
            self._drugName = drugNameBox.GetValue()
            self._drugDose = drugDoseBox.GetValue()
            self._updateLabel()
        editDrugDlg.Destroy()

    def _onFullDoseButton(self, event):
        logging.debug("in drugPanelClass.onFullDoseButton(). event: %s" % event)
        self.printDose(self._drugDose)
        self.startDrugTimer()

    def _onHalfDoseButton(self, event):
        logging.debug("in drugPanelClass.onHalfDoseButton(). event: %s" % event)
        self.printDose(self._drugDose / 2)
        self.startDrugTimer()

    def _onQuartDoseButton(self, event):
        logging.debug("in drugPanelClass.onQuartDoseButton(). event: %s" % event)
        self.printDose(self._drugDose / 4)
        self.startDrugTimer()

    def _onCustomDoseButton(self, event):
        buttonRect = event.GetEventObject().GetScreenRect()
        newPos = wx.Point()
        newPos.x = buttonRect.GetLeft() + buttonRect.GetWidth() / 2
        newPos.y = buttonRect.GetTop() + buttonRect.GetHeight() / 2
        customValueDlg = customValueDialog(None, newPos, self._customDose)
        if customValueDlg.ShowModal() == wx.ID_OK:
            self._customDose = customValueDlg.drugDoseBox.GetValue()
            self.printDose(self._customDose)
            self.startDrugTimer()
        customValueDlg.Destroy()

    def startDrugTimer(self):
        logging.debug("drugPanelClass.startDrugTimer()")
        self.resetAlarm()
        self.__drugInjectionTime = datetime.datetime.now()
        self.__drugTimer.Start(250)  # clock event every 250 ms

    def printDose(self, dose):
        message = self._printFormat.format(time=datetime.datetime.now().strftime("%H:%M"),
                                           drug=('%4.0f uL %s' % (dose, self._drugName)))

        if self._logBox is not None:
            if (not self._logBox.IsEmpty()) and self._logBox.GetValue()[-1] != '\n':
                self._logBox.AppendText('\n')
            self._logBox.AppendText(message)

            # AppendText doesn't seem to send a wx.EVT_TEXT to logBox
            # generate one
            event = wx.PyEvent()
            event.SetEventType(wx.wxEVT_COMMAND_TEXT_UPDATED)
            self._logBox.GetEventHandler().ProcessEvent(event)
        else:
            logging.info(message)

    # noinspection PyUnusedLocal
    def _onDrugTimer(self, event):
        # logging.debug('in drugPanel._onDrugTimer():')
        if self.__drugInjectionTime is not None:
            timeDiff = datetime.datetime.now() - self.__drugInjectionTime
            m, s = divmod(timeDiff.seconds, 60)
            m %= 60
            self._drugTimerLabel.SetLabel("%02d:%02d" % (m, s))
            self.Layout()
            if self._alarmsRadioBox.GetSelection() > 0:
                try:
                    alarmTimeInMin = int(self._alarmsRadioBox.GetStringSelection(), 10)
                except ValueError:
                    alarmTimeInMin = MAXINT
                if timeDiff.seconds / 60 >= alarmTimeInMin and self.__alarmTime is None:
                    # alerts the user that the timer has reached the time set by their alarm
                    logging.debug("The Alarm time (%d) has been reached (%d)!" %
                                  (alarmTimeInMin, timeDiff.seconds / 60))
                    self.startAlarm()
                    # maybe do something here
                    self.alarmAction()

    def startAlarm(self):
        logging.debug('in drugPanel.startAlarm(). alarmTime is %s, starting timer with period %d ms',
                      datetime.datetime.now(), self.__ALARM_TIMER_FLASH_PERIOD)
        self.__alarmTime = datetime.datetime.now()
        self.__alarmTimer.Start(self.__ALARM_TIMER_FLASH_PERIOD)
        if self._alarmSound is not None:
            logging.debug('  starting play loop')
            self._alarmSound.pause()  # stops previous sound, just in case
            self._alarmSound.play(loops=-1)

    def resetAlarm(self):
        logging.debug('in drugPanel.resetAlarm()')
        self.__alarmTimer.Stop()
        self.__alarmTime = None
        self._drugTimerLabel.SetForegroundColour(self.__LABEL_FG_NORMAL)
        if self._alarmSound is not None:
            self._alarmSound.pause()

    # noinspection PyUnusedLocal
    def _onAlarmTimer(self, event):
        logging.debug('in drugPanel._onAlarmTimer()')
        if self.__alarmTime is None:  # alarm has been reset while the timer was running
            self.resetAlarm()
        elif (self.__alarmTimer.IsRunning()) and \
                ((datetime.datetime.now() - self.__alarmTime).seconds < self.__ALARM_DURATION):
            # logging.debug('  time is (%d / %d) -> flashing label',
            # (datetime.datetime.now() - self.__alarmTime).seconds,
            # self.__ALARM_DURATION)
            if self._drugTimerLabel.GetForegroundColour() == self.__LABEL_FG_NORMAL:
                self._drugTimerLabel.SetForegroundColour(self.__LABEL_FG_ALARM)
            else:
                self._drugTimerLabel.SetForegroundColour(self.__LABEL_FG_NORMAL)
        else:
            # logging.debug('  time is (%d / %d) -> stopping alarm',
            # (datetime.datetime.now() - self.__alarmTime).seconds,
            # self.__ALARM_DURATION)
            alarmTime = self.__alarmTime
            self.resetAlarm()  # stops the flashing and the beeping
            # but keep the foreground color
            self._drugTimerLabel.SetForegroundColour(self.__LABEL_FG_ALARM)
            self.__alarmTime = alarmTime  # this ensures that the alarm will not be re-triggered

    def alarmAction(self):
        pass

    # noinspection PyUnusedLocal
    def _onDrugTimerLabelClick(self, event):
        self.resetAlarm()
        self.__drugTimer.Stop()
        self._drugTimerLabel.SetLabel("00:00")
        self.Layout()

    def _onAlarmChoose(self, event):
        # reset the alarm timer
        self.resetAlarm()
        if self._alarmsRadioBox.GetSelection() == 3:  # custom button
            buttonRect = event.GetEventObject().GetScreenRect()
            newPos = wx.Point()
            newPos.x = buttonRect.GetLeft() + buttonRect.GetWidth() / 2
            newPos.y = buttonRect.GetTop() + buttonRect.GetHeight() / 2
            try:
                customValue = int(self._alarmsRadioBox.GetItemLabel(3), 10)
            except ValueError:
                customValue = 0
            customValueDlg = customValueDialog(None, newPos, customValue)
            if customValueDlg.ShowModal() == wx.ID_OK:
                self._alarmsRadioBox.SetItemLabel(3, str(customValueDlg.drugDoseBox.GetValue()))
            customValueDlg.Destroy()


class drugPumpPanel(drugPanel):
    def __init__(self, parent, drugName, drugDose, alarmSoundFile=None, logBox=None, pump=None,
                 printFormat='{time:s}\t{drug:s}\t\n',
                 id=wx.ID_ANY):
        super(drugPumpPanel, self).__init__(parent, drugName, drugDose, alarmSoundFile=alarmSoundFile, logBox=logBox,
                                            id=id, printFormat=printFormat)

        self._pump = pump
        self.__INJECTION_RATE = 0.25
        self.__INJECTION_UNITS = 1

        self.__currDirection = None
        self.__currRate = None
        self.__currUnits = None

        self._startPerfButton = wx.ToggleButton(self, id=wx.ID_ANY, label='Start Perf')
        self._perfRateSpinCtrl = floatspin.FloatSpin(self, id=wx.ID_ANY,
                                                     value=0.0, min_val=None, max_val=None, increment=0.1, digits=2,
                                                     agwStyle=floatspin.FS_RIGHT)
        self._perfUnitChoice = wx.Choice(self, id=wx.ID_ANY, size=(100, -1))
        self._autoInjectCheckBox = wx.CheckBox(self, id=wx.ID_ANY, label='Auto-inject')

        outSizer = self.GetSizer()
        sizer = wx.GridBagSizer(hgap=5, vgap=5)
        sizer.Add(self._label, pos=(0, 0), span=(1, 4), flag=wx.EXPAND)
        sizer.Add(self._startPerfButton, pos=(1, 0), flag=wx.EXPAND)
        sizer.Add(self._perfRateSpinCtrl, pos=(1, 1), flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)
        sizer.Add(self._perfUnitChoice, pos=(1, 2), flag=wx.EXPAND)
        sizer.Add(self._fullDoseButton, pos=(2, 0), flag=wx.EXPAND)
        sizer.Add(self._halfDoseButton, pos=(2, 1), flag=wx.EXPAND)
        sizer.Add(self._quartDoseButton, pos=(2, 2), flag=wx.EXPAND)
        sizer.Add(self._customDoseButton, pos=(2, 3), flag=wx.EXPAND)
        sizer.Add(self._drugTimerLabel, pos=(3, 0), flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)
        sizer.Add(self._alarmsRadioBox, pos=(3, 1), span=(1, 3),
                  flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)
        sizer.Add(self._autoInjectCheckBox, pos=(4, 0), span=(1, 4), flag=wx.EXPAND)
        sizer.Layout()
        outSizer.Clear()
        outSizer.Add(sizer, 1, wx.EXPAND | wx.ALL, border=5)
        self.SetSizer(outSizer)

        if self._pump is not None:
            self._perfUnitChoice.SetItems(self._pump.getPossibleUnits())

        # event binding
        self.Bind(wx.EVT_TOGGLEBUTTON, self.onStartPerfButton, id=self._startPerfButton.GetId())
        self.Bind(wx.EVT_SPINCTRL, self.onPerfRateChange, id=self._perfRateSpinCtrl.GetId())
        self.Bind(wx.EVT_CHOICE, self.onPerfUnitChange, id=self._perfUnitChoice.GetId())

        self._updateGUI()
        # self.SetBackgroundColour((0, 125, 125))  # for debug

    def _updateGUI(self):
        if self._pump is not None:
            # fill in properties based on the state of the pump
            self.__currDirection = 1
            self._pump.setDirection(self.__currDirection)
            self.__currRate = self._pump.getRate()
            self.__currUnits = self._pump.getUnits()

            # update GUI elements in consequence
            if not self._pump.isRunning():
                self._startPerfButton.SetValue(False)
                self._perfRateSpinCtrl.Enable(True)
                self._perfUnitChoice.Enable(True)
            else:
                self._startPerfButton.SetValue(True)
                self._perfRateSpinCtrl.Enable(False)
                self._perfUnitChoice.Enable(False)

            self._perfRateSpinCtrl.SetValue(self.__currRate)
            self._perfUnitChoice.SetSelection(self.__currUnits)
        else:
            self.Enable(False)

    # noinspection PyUnusedLocal
    def onStartPerfButton(self, event):
        if self._startPerfButton.GetValue():  # Value is TRUE when pressed
            rate = float(self._perfRateSpinCtrl.GetValue())
            unitsIndex = self._perfUnitChoice.GetSelection()
            if self._pump is not None:
                try:
                    self._pump.setRate(rate, unitsIndex)
                    self._pump.clearTargetVolume()
                    self.__currRate = self._pump.getRate()
                    self.__currUnits = self._pump.getUnits()
                    self._pump.start()

                    timeStr = time.strftime("%H:%M\t", time.localtime())
                    # noinspection PyProtectedMember
                    message = self._printFormat.format(time=timeStr,
                                                       drug="Start perf %s [%.3f %s]" %
                                                            (self.drugName, self.__currRate,
                                                             self._pump._units[self.__currUnits])
                                                       )
                    self._logBox.AppendText(message)

                except Exception as e:
                    logging.error('Exception caught in drugPumpPanel.onStartPerfButton(): %s', e)
                    wx.Bell()

                    self._perfRateSpinCtrl.SetFocus()
                    logging.debug("setting RED color in onStartPerfButton()")  # FIXME-this does not work
                    self._perfRateSpinCtrl.SetBackgroundColour((255, 0, 0))
                    # self.perfRateSpinCtrl.SetForegroundColour(wx.RED)

            else:  # there is no pump
                # write in the log
                timeStr = time.strftime("%H:%M\t", time.localtime())
                # noinspection PyProtectedMember
                message = self._printFormat.format(time=timeStr,
                                                   drug="Start perf %s [%.3f %s]" %
                                                        (self.drugName, rate, '')  # units are blank in this case
                                                   )
                self._logBox.AppendText(message)

            # disable the rate and unit controls
            self._perfRateSpinCtrl.Enable(False)
            self._perfUnitChoice.Enable(False)
        else:  # unclick button
            if self._pump is not None:
                if self._pump.isRunning():
                    self._pump.pause()

            # reactivate rate and unit buttons
            self._perfRateSpinCtrl.Enable(True)
            self._perfUnitChoice.Enable(True)

            # write in the log
            timeStr = time.strftime("%H:%M\t", time.localtime())
            # noinspection PyProtectedMember
            message = self._printFormat.format(time=timeStr,
                                               drug="Stop perf %s" % self.drugName)
            self._logBox.AppendText(message)

        # finally, update GUI
        self._updateGUI()

    def onPerfRateChange(self, event):
        pass

    def onPerfUnitChange(self, event):
        pass

    def alarmAction(self):
        logging.debug("in drugPumpPanel.alarmAction()")
        super(drugPumpPanel, self).alarmAction()
        # if auto-inject check box is checked
        if self._autoInjectCheckBox.GetValue():
            logging.debug("  injecting full dose")
            time.sleep(1)
            self.onFullDoseButton(None)

    def onFullDoseButton(self, event):
        super(drugPumpPanel, self)._onFullDoseButton(event)
        self.injectVolume(self.drugDose)

    def onHalfDoseButton(self, event):
        super(drugPumpPanel, self)._onHalfDoseButton(event)
        self.injectVolume(self.drugDose / 2)

    def onQuartDoseButton(self, event):
        super(drugPumpPanel, self)._onQuartDoseButton(event)
        self.injectVolume(self.drugDose / 4)

    def onCustomDoseButton(self, event):
        super(drugPumpPanel, self)._onCustomDoseButton(event)
        self.injectVolume(self._customDose)

    def injectVolume(self, injVolume):
        logging.debug("in drugPumpPanel.injectVolume()")
        if self._pump is not None:
            logging.debug("  direction is currently %d" % self.__currDirection)
            oldDirection = self._pump.getDirection()
            if self._pump.isRunning():
                self._pump.pause()

            self._pump.setRate(self.__INJECTION_RATE, self.__INJECTION_UNITS)
            tempVolume = injVolume / 1000.0
            self._pump.setTargetVolume(tempVolume)
            self._pump.start()

            # wait for the injection to finish
            while self._pump.isRunning():
                logging.debug("  injection still going, waiting...")
                time.sleep(0.25)

            self._pump.clearTargetVolume()
            self._pump.setRate(self.__currRate, self.__currUnits)

            if oldDirection > 0:
                logging.debug("  direction before injection was %d, so restarting the pump" % oldDirection)
                self._pump.start()
            else:
                if self._pump.isRunning():
                    self._pump.pause()

            self._updateGUI()


class otherDrugPanel(drugPanel):
    # this class is used when injecting a customizable amount of an other drug
    # than those defined in drugList
    def __init__(self, parent, alarmSoundFile=None, logBox=None,
                 printFormat='{time:s}\t{drug:s}\t\n',
                 id=wx.ID_ANY):
        # call parent constructor
        super(otherDrugPanel, self).__init__(parent, drugName="", drugDose=0.0, alarmSoundFile=alarmSoundFile,
                                             logBox=logBox, printFormat=printFormat, id=id)

        # remove unused dose buttons
        self._halfDoseButton.Destroy()
        self._quartDoseButton.Destroy()
        self._customDoseButton.Destroy()
        self._label.SetLabel("Other drug...")
        self._fullDoseButton.SetLabel("Inject bolus")

        # self.SetBackgroundColour((0, 225, 225))  # for debug

    def _onFullDoseButton(self, event):
        buttonRect = event.GetEventObject().GetScreenRect()
        newPos = wx.Point()
        newPos.x = buttonRect.GetLeft() + buttonRect.GetWidth() / 2
        newPos.y = buttonRect.GetTop() + buttonRect.GetHeight() / 2
        customValueDlg = customDrugDoseDialog(None, newPos, self.drugDose, self.drugName)
        if customValueDlg.ShowModal() == wx.ID_OK:
            self.drugDose = customValueDlg.drugDoseBox.GetValue()
            self.drugName = customValueDlg.drugNameBox.GetValue()
            self.printDose(self.drugDose)
            self.startDrugTimer()
        customValueDlg.Destroy()

    def _onLabelClick(self, event):
        # event when dbl click on the label
        # in this case, do nothing
        pass


class newExpDialog(wx.Dialog):
    def __init__(self, parent, exp=Experiment()):
        super(newExpDialog, self).__init__(parent, id=wx.ID_ANY, title="New Experiment")

        self.exp = exp

        outSizer = wx.BoxSizer(wx.HORIZONTAL)
        bagSizer = wx.GridBagSizer(hgap=5, vgap=5)
        buttonSizer = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL)
        drugSizer = wx.BoxSizer(wx.VERTICAL)
        drugButtonSizer = wx.BoxSizer(wx.HORIZONTAL)

        self._dateLabel = wx.StaticText(self, label="Exp Date:")
        self._mouseStrainLabel = wx.StaticText(self, label="Mouse Strain:")
        self._mouseGenotypeLabel = wx.StaticText(self, label="Genotype:")
        self._mouseSexLabel = wx.StaticText(self, label="Sex:")
        self._mouseDoBLabel = wx.StaticText(self, label="Date of Birth:")
        self._mouseWeigthLabel = wx.StaticText(self, label="Weight (g):")
        self._commentsLabel = wx.StaticText(self, label="Comments:")
        self._drugLabel = wx.StaticText(self, label="Drugs && doses:")

        self._expDateBox = wx.DatePickerCtrl(self, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY,
                                             dt=wx.DateTime.Now(),
                                             name='expDateBox')

        self._mouseStrainBox = wx.ComboBox(self,
                                           validator=mouseListsValidator(
                                               self.exp, "prevStrainList", "Strain", required=True),
                                           choices=list(self.exp.prevStrainList),
                                           # style=wx.CB_SORT,
                                           #  FIXME: wxCB_SORT not currently supported by wxOSX/Cocoa
                                           value='')
        self._mouseStrainBox.SetSelection(wx.NOT_FOUND)  # start with empty selection

        self._mouseGenotypeBox = wx.ComboBox(self,
                                             validator=mouseListsValidator(
                                                 self.exp, "prevGenotypeList", "Genotype", required=True),
                                             choices=list(self.exp.prevGenotypeList),
                                             # style=wx.CB_SORT,
                                             #  FIXME: wxCB_SORT not currently supported by wxOSX/Cocoa
                                             value='')
        self._mouseGenotypeBox.SetSelection(wx.NOT_FOUND)  # start with empty selection

        self._mouseSexBox = wx.RadioBox(self, choices=['male', 'female'], style=wx.RA_SPECIFY_COLS)

        # self._mouseDoBBox = wx.DatePickerCtrl(self, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY,
        #                                       validator=mouseDoBValidator(required=False),
        #                                       name='mouseDoBBox')
        # FIXME: it would be better to have a DatePickerCtrl, but it would need to allow empty
        # content for the cases when the DoB of the mouse is unknown
        self._mouseDoBBox = wx.TextCtrl(self, size=(-1, -1), validator=mouseDoBValidator(required=False))

        self._mouseWeigthBox = wx.SpinCtrl(self)

        self._commentsBox = \
            wx.TextCtrl(self,
                        style=wx.TE_MULTILINE | wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_LINEWRAP,
                        size=(100, 100))

        self._drugBox = wx.ListBox(self, validator=drugListValidator(self.exp, "prevDrugList", required=True))
        self._addDrugButton = wx.Button(self, label="+", style=wx.BU_EXACTFIT)
        self._remDrugButton = wx.Button(self, label="-", style=wx.BU_EXACTFIT)
        self._editDrugButton = wx.Button(self, label="Edit...", style=wx.BU_EXACTFIT)
        font = self._drugBox.GetFont()
        font.SetFaceName('Courier New')
        self._drugBox.SetFont(font)

        bagSizer.Add(self._dateLabel, flag=wx.ALIGN_RIGHT, pos=(0, 0))
        bagSizer.Add(self._expDateBox, flag=wx.EXPAND | wx.ALIGN_LEFT, pos=(0, 1))
        bagSizer.Add(self._mouseStrainLabel, flag=wx.ALIGN_RIGHT, pos=(1, 0))
        bagSizer.Add(self._mouseStrainBox, flag=wx.EXPAND | wx.ALIGN_LEFT, pos=(1, 1))
        bagSizer.Add(self._mouseGenotypeLabel, flag=wx.ALIGN_RIGHT, pos=(2, 0))
        bagSizer.Add(self._mouseGenotypeBox, flag=wx.EXPAND | wx.ALIGN_LEFT, pos=(2, 1))
        bagSizer.Add(self._mouseSexLabel, flag=wx.ALIGN_RIGHT, pos=(3, 0))
        bagSizer.Add(self._mouseSexBox, flag=wx.EXPAND | wx.ALIGN_LEFT, pos=(3, 1))
        bagSizer.Add(self._mouseDoBLabel, flag=wx.ALIGN_RIGHT, pos=(4, 0))
        bagSizer.Add(self._mouseDoBBox, flag=wx.EXPAND | wx.ALIGN_LEFT, pos=(4, 1))
        bagSizer.Add(self._mouseWeigthLabel, flag=wx.ALIGN_RIGHT, pos=(5, 0))
        bagSizer.Add(self._mouseWeigthBox, flag=wx.EXPAND | wx.ALIGN_LEFT, pos=(5, 1))
        bagSizer.Add(self._commentsLabel, flag=wx.ALIGN_RIGHT, pos=(7, 0))
        bagSizer.Add(self._commentsBox, flag=wx.EXPAND | wx.ALIGN_LEFT, pos=(7, 1), span=(1, 2))

        drugSizer.Add(self._drugLabel, 0, flag=wx.EXPAND)
        drugSizer.Add(self._drugBox, 1, flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=10)
        drugButtonSizer.Add(self._addDrugButton, 0, flag=wx.EXPAND)
        drugButtonSizer.Add(self._remDrugButton, 0, flag=wx.EXPAND)
        drugButtonSizer.Add(self._editDrugButton, 0, flag=wx.EXPAND)
        drugSizer.Add(drugButtonSizer, 0, flag=wx.EXPAND)
        bagSizer.Add(drugSizer, flag=wx.EXPAND, pos=(0, 2), span=(7, 1))
        bagSizer.Add(buttonSizer, flag=wx.EXPAND, pos=(8, 0), span=(1, 3))
        outSizer.Add(bagSizer, 0, flag=wx.ALL, border=10)
        self.SetSizerAndFit(outSizer)

        # Bind buttons
        self.Bind(wx.EVT_BUTTON, self.onAddDrug, id=self._addDrugButton.GetId())
        self.Bind(wx.EVT_BUTTON, self.onDelDrug, id=self._remDrugButton.GetId())
        self.Bind(wx.EVT_BUTTON, self.onEditDrug, id=self._editDrugButton.GetId())
        self._drugBox.Bind(wx.EVT_LEFT_DCLICK, self.onEditDrug)

    # noinspection PyUnusedLocal
    def onAddDrug(self, event):
        # logging.debug('in nexExpDialog.onAddDrug(). Event: %s' % event)
        editDrugDlg = customDrugDoseDialog(self, pos=wx.DefaultPosition, drugDose=0., drugName="")
        editDrugDlg.SetTitle("Add a new drug")
        editDrugDlg.dlgLabel.SetLabel("Add a new drug")
        if editDrugDlg.ShowModal() == wx.ID_OK:
            drug = Drug(editDrugDlg.drugNameBox.GetValue(), editDrugDlg.drugDoseBox.GetValue())
            self.exp.prevDrugList.append(drug)
            self._drugBox.Append(drug.printForList())
        editDrugDlg.Destroy()

    # noinspection PyUnusedLocal
    def onDelDrug(self, event):
        # logging.debug('in nexExpDialog.onDelDrug(). Event: %s' % event)
        self._drugBox.Delete(self._drugBox.GetSelection())

    # noinspection PyUnusedLocal
    def onEditDrug(self, event):
        # logging.debug('in nexExpDialog.onEditDrug(). Event: %s' % event)
        selected = self._drugBox.GetSelection()
        if selected > wx.NOT_FOUND:
            drug = self.exp.prevDrugList[selected]
            pos = self.exp.prevDrugList.index(drug)

            editDrugDlg = customDrugDoseDialog(self, pos=wx.DefaultPosition, drugDose=0., drugName="")
            editDrugDlg.SetTitle("Edit drug name")
            editDrugDlg.dlgLabel.SetLabel("Edit drug name")
            editDrugDlg.drugNameBox.SetValue(drug.name)
            editDrugDlg.drugDoseBox.SetValue(drug.dose)
            if editDrugDlg.ShowModal() == wx.ID_OK:
                newDrug = Drug(editDrugDlg.drugNameBox.GetValue(), editDrugDlg.drugDoseBox.GetValue())
                self.exp.prevDrugList.insert(pos, newDrug)
                self.exp.prevDrugList.remove(drug)
                self._drugBox.Delete(selected)
                self._drugBox.Insert(newDrug.printForList(), selected)
                self._drugBox.SetSelection(selected)
            editDrugDlg.Destroy()

    @property
    def mouseGenotype(self):
        return self._mouseGenotypeBox.GetValue()

    @property
    def mouseStrain(self):
        return self._mouseStrainBox.GetValue()

    @property
    def mouseDoB(self):
        dateStr = self._mouseDoBBox.GetValue()
        date = wx.DateTime()
        if len(dateStr) > 0:
            date.ParseDate(dateStr)
            return date
        else:
            return None

    @property
    def mouseSex(self):
        return self._mouseSexBox.GetItemLabel(self._mouseSexBox.GetSelection())

    @property
    def mouseWeight(self):
        return self._mouseWeigthBox.GetValue()

    @property
    def comments(self):
        return self._commentsBox.GetValue()


class mainFrame(wx.Frame):
    def __init__(self, exp=Experiment(), info=wx.AboutDialogInfo(), pump=None, config=None):
        wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title="PhysioMonitor", size=(800, 600),
                          style=wx.DEFAULT_FRAME_STYLE)
        self.exp = exp
        self.appInfo = info
        self.config = config
        self.pump = pump

        self.diagTimer = wx.Timer(self, id=wx.ID_ANY)
        self.Bind(wx.EVT_TIMER, self.onDiagTimer, id=self.diagTimer.GetId())

        self.leftPanel = wx.Panel(self, id=wx.ID_ANY)
        self.nb = wx.Notebook(self, id=wx.ID_ANY)
        self.logBox = RichTextCtrl(self.nb, id=wx.ID_ANY,
                                   style=wx.TE_DONTWRAP | wx.TE_MULTILINE | wx.HSCROLL | wx.TE_RICH | wx.TE_RICH2)
        self.logBox.SetFont(wx.Font(12, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL,
                                    wx.FONTWEIGHT_NORMAL,
                                    faceName="Courier"))

        clock = clockPanel(self.leftPanel)
        self.newExpButton = wx.Button(self.leftPanel, id=wx.ID_ANY, label="Start new experiment")

        self.hbox = wx.BoxSizer(wx.HORIZONTAL)  # sizer for left and right part
        self.vbox = wx.BoxSizer(wx.VERTICAL)  # stack for clock and new exp button
        self.vbox2 = wx.BoxSizer(wx.VERTICAL)  # stack for drugPanels

        self.previousComment = ''
        self.startLogPos = 0

        # create matplotlib panel/figure to put all the plots in
        self.scopePanel = ScopePanel(self.nb, id=wx.ID_ANY)

        # CREATING PLOTS
        logging.debug("Creating plots")
        scopeList = []
        for chan in config['comedi']['channels']:
            if chan['trend-function'] is not None and chan['trend-function'] not in VALID_TREND_FUNCTIONS:
                raise ValueError("incorrect trend function for channel: %s (must be one of: %s)" % (
                    chan['trend-function'], VALID_TREND_FUNCTIONS))
            else:
                if chan['trend-function'] == 'random':
                    func = trend_random
                elif chan['trend-function'] == 'get_HR':
                    func = trend_get_HR
                elif chan['trend-function'] == 'get_max':
                    func = trend_get_max
                elif chan['trend-function'] == 'get_lastPeakValue':
                    func = trend_get_lastPeakValue
                elif chan['trend-function'] == 'get_avgPeakValue':
                    func = trend_get_avgPeakValue
                elif chan['trend-function'] == 'get_avg':
                    func = trend_get_avg
                else:
                    func = None
            scope = TrendScope(self.scopePanel.figure, windowSize=chan['window-size'],
                               dt=1. / config['comedi']['sample-rate'], remanence=chan['remanence'],
                               linecolor=chan['line-color'], linewidth=chan['line-width'], scaling=chan['scale'],
                               offset=chan['offset'], title=chan['label'], units=chan['units'],
                               triggerMode=chan['trigger-mode'], autoDefineThreshold=chan['auto-define-threshold'],
                               thresholdWindow=chan['threshold-window'],
                               relThresholdLevel=chan['relative-threshold-level'],
                               thresholdLevel=chan['threshold-level'], autoscale=chan['autoscale'], ymin=chan['ymin'],
                               ymax=chan['ymax'], trendMin=chan['trend-ymin'], trendMax=chan['trend-ymax'],
                               trendWidth=chan['trend-width'], trendPeriod=chan['trend-period'], trendFunction=func,
                               trendFuncArgs=chan['trend-function-args'], trendFormat=chan['trend-format'],
                               trendUnits=chan['trend-units'],
                               alarmEnabled=chan['alarm-enabled'], alarmLow=chan['alarm-low'],
                               alarmHigh=chan['alarm-high'], alarmSoundFile=chan['alarm-sound-file'])
            scopeList.append(scope)
        self.scopePanel.addScopes(scopeList)

        self.startButton = wx.lib.buttons.GenBitmapToggleButton(
            self, id=wx.ID_ANY, size=(45, 45),
            bitmap=wx.Bitmap('media/play-icon.png', type=wx.BITMAP_TYPE_PNG))
        self.startButton.SetBitmapSelected(wx.Bitmap('media/stop-icon.png', type=wx.BITMAP_TYPE_PNG))
        self.startButton.Enable(False)

        # Menu Bar
        mainMenuBar = wx.MenuBar()
        fileMenu = wx.Menu()
        editMenu = wx.Menu()
        fileMenu.Append(id=wx.ID_SAVEAS)
        fileMenu.Append(id=wx.ID_ABOUT)
        fileMenu.AppendSeparator()
        fileMenu.Append(id=wx.ID_EXIT)
        mainMenuBar.Append(fileMenu, "&File")
        mainMenuBar.Append(editMenu, "&Edit")
        self.SetMenuBar(mainMenuBar)

        self.Bind(wx.EVT_MENU, self.onQuitMenu, id=wx.ID_EXIT)
        self.Bind(wx.EVT_MENU, self.onAboutMenu, id=wx.ID_ABOUT)
        self.Bind(wx.EVT_MENU, self.onSaveAsMenu, id=wx.ID_SAVEAS)
        self.Bind(wx.EVT_CLOSE, self.onClose)
        self.Bind(wx.EVT_BUTTON, self.onNewExpButton, None, self.newExpButton.GetId())
        self.Bind(wx.EVT_BUTTON, self.onStartButton, None, id=self.startButton.GetId())
        self.logBox.Bind(wx.EVT_TEXT, self.onUpdateLog)
        # Bind the update canvas event to some function:
        # self.Bind(EVT_UPDATE_CANVAS, self.onUpdateCanvas)

        # self.streamer = PhysioStreamer(nChan=len(self.scopePanel.scopeList),
        #                                sampleFreq=self.config['comedi']['sample-rate'],
        #                                filename='media/surgery.txt', pause=True)
        self.streamer = ComediStreamer(self.config, start=False)
        self.animation = animation.FuncAnimation(self.scopePanel.figure,
                                                 self.scopePanel.append,
                                                 self.streamer.iterator,
                                                 init_func=self.scopePanel.reset,
                                                 interval=100, blit=False)

        self.vbox.Add(clock, 0, wx.EXPAND)
        self.vbox.Add(self.newExpButton, 0, flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=10)

        self.createLeftPanel(enabled=False)

        self.vbox.Add(self.vbox2)
        self.leftPanel.SetSizer(self.vbox)

        self.hbox.Add(self.leftPanel, 0)
        self.hbox.Add(self.nb, 1, wx.EXPAND)
        self.nb.AddPage(self.scopePanel, text='oscilloscopes', select=True)

        self.SetSizer(self.hbox)
        self.Centre()

    # noinspection PyUnusedLocal
    def onStartButton(self, event):
        logging.debug('in mainFrame.onStartButton()')
        logging.debug('   start button state is: %s', self.startButton.GetValue())
        if self.startButton.GetValue():
            self.scopePanel.reset()
            self.streamer.start()
            if self.config['print-trend-data']:
                self.diagTimer.Start(self.config['print-trend-data-period'] * 1000, wx.TIMER_CONTINUOUS)
        else:
            if self.streamer is not None:
                self.streamer.stop()
            for scope in self.scopePanel.scopeList:
                scope.resetAlarm()
            if self.diagTimer.IsRunning():
                self.diagTimer.Stop()

    def createLeftPanel(self, enabled):
        printFormat = "{time:s}\t" + ("\t" * len(self.scopePanel.scopeList)) + "{drug:s}\t\n"
        if len(self.exp.prevDrugList) > 0:
            pumpDrug = self.exp.prevDrugList[0]
        else:
            pumpDrug = Drug("dummy", 0)
        thisDrugPumpPanel = drugPumpPanel(self.leftPanel,
                                          pumpDrug.name, pumpDrug.dose,
                                          alarmSoundFile=self.config['timer-sound-file'],
                                          logBox=self.logBox,
                                          pump=self.pump,
                                          printFormat=printFormat)
        thisDrugPumpPanel.Enable(enabled)
        self.vbox2.Add(thisDrugPumpPanel, 0, flag=wx.BOTTOM, border=10)

        for drug in self.exp.prevDrugList[1:]:
            thisDrugPanel = drugPanel(self.leftPanel,
                                      drug.name, drug.dose,
                                      alarmSoundFile=self.config['timer-sound-file'],
                                      logBox=self.logBox,
                                      printFormat=printFormat)
            thisDrugPanel.Enable(enabled)
            self.vbox2.Add(thisDrugPanel, 0, flag=wx.BOTTOM, border=10)

        thisOtherDrugPanel = otherDrugPanel(self.leftPanel, alarmSoundFile=None, logBox=self.logBox,
                                            printFormat=printFormat)
        thisOtherDrugPanel.Enable(enabled)
        self.vbox2.Add(thisOtherDrugPanel, 0, flag=wx.BOTTOM, border=10)

        addCommentButton = wx.Button(self.leftPanel, wx.ID_ANY, "Add comment...")
        addCommentButton.Enable(enabled)
        self.vbox2.Add(addCommentButton, 0, flag=wx.EXPAND)

        self.Bind(wx.EVT_BUTTON, self.onCommentButton, id=addCommentButton.GetId())

    # noinspection PyUnusedLocal
    def onClose(self, event):
        self.Destroy()

    # noinspection PyUnusedLocal
    def onQuitMenu(self, event):
        self.Close()

    def onSaveAsMenu(self, event):
        pass

    # noinspection PyUnusedLocal
    def onCommentButton(self, event):
        tempComment = wx.GetTextFromUser("Enter a comment:", "Enter a comment", self.previousComment)
        if len(tempComment) > 0:
            self.previousComment = tempComment
            timeStr = time.strftime("%H:%M", time.localtime())
            message = "{time:s}\t" + ("\t" * len(self.scopePanel.scopeList)) + "\t{comment:s}\n"

            self.logBox.AppendText(message.format(time=timeStr, comment=self.previousComment))
            # AppendText doesn't seem to send a wx.EVT_TEXT to logBox
            # generate one
            event = wx.PyEvent()
            event.SetEventType(wx.wxEVT_COMMAND_TEXT_UPDATED)
            self.logBox.GetEventHandler().ProcessEvent(event)

    # noinspection PyMethodMayBeStatic
    def onDiagTimer(self, event):
        logging.debug("in mainFrame.onDiagTimer() - event %s" % event)
        message = time.strftime("%H:%M\t", time.localtime())
        for scope in self.scopePanel.scopeList:
            message += (scope.trendOutputFormat + '\t') % scope.latestTrendData
        message += '\t\t\n'
        self.logBox.AppendText(message)
        # AppendText doesn't seem to send a wx.EVT_TEXT to logBox
        # generate one
        event = wx.PyEvent()
        event.SetEventType(wx.wxEVT_COMMAND_TEXT_UPDATED)
        self.logBox.GetEventHandler().ProcessEvent(event)

    # noinspection PyUnusedLocal
    def onAboutMenu(self, event):
        wx.AboutBox(self.appInfo)

    # noinspection PyUnusedLocal
    def onNewExpButton(self, event):
        myNewExpDlg = newExpDialog(self, exp=self.exp)
        if myNewExpDlg.ShowModal() == wx.ID_OK:
            self.exp.mouse.dob = myNewExpDlg.mouseDoB
            self.exp.mouse.sex = myNewExpDlg.mouseSex
            self.exp.mouse.weight = myNewExpDlg.mouseWeight
            self.exp.mouse.genotype = myNewExpDlg.mouseGenotype
            self.exp.mouse.strain = myNewExpDlg.mouseStrain
            self.exp.comments = myNewExpDlg.comments

            self.vbox2.Clear(True)
            self.createLeftPanel(True)

            self.leftPanel.SetSizerAndFit(self.vbox)
            self.leftPanel.Layout()
            self.Refresh()

            # store the mouse genotype/strain in the exp record to be saved at the end of the experiment
            self.exp.prevStrainList.add(myNewExpDlg.mouseStrain)
            self.exp.prevGenotypeList.add(myNewExpDlg.mouseGenotype)

            # create record directory if needed
            if self.config['create-record-folder']:
                folder = path.join(self.config['record-root'],
                                   self.exp.date.Format(self.config['record-folder-name-format']))
                if not path.exists(folder):
                    os.makedirs(folder)

            # check if logFile already exists, and load content
            fileName = path.join(self.config['record-root'],
                                 self.exp.date.Format(self.config['record-folder-name-format']),
                                 self.config['log-filename'])
            if path.exists(fileName):
                self.logBox.LoadFile(fileName)
                self.logBox.AppendText('\n')
                self.logBox.AppendText('\n')
                self.logBox.AppendText('#' * 80)
                self.logBox.AppendText('\n')
                self.logBox.AppendText('\n')

            # write in text box
            if (not self.logBox.IsEmpty()) and (self.logBox.GetValue()[-1] != '\n'):
                self.logBox.AppendText('\n')
            self.logBox.AppendText(self.exp.writeHeader())
            self.logBox.AppendText("\n")
            self.logBox.AppendText("\n")
            # write column headers
            header = '     \t'  # space for the time displayed at each line
            for scope in self.scopePanel.scopeList:
                # noinspection PyProtectedMember
                header += '%s (%s)\t' % (scope._scopeTitle, scope.trendUnits)
            header += 'drug injections\tcomments\n'
            self.logBox.AppendText(header)
            self.onUpdateLog(wx.PyEvent())

            # show the log in the notebook
            self.nb.AddPage(self.logBox, text="Log file", select=False)

            self.startLogPos = self.logBox.GetInsertionPoint()
            self.startButton.Enable(True)

        myNewExpDlg.Destroy()

    def onUpdateLog(self, event):
        # print "onUpdateLog"
        if self.config['autosave-log']:
            fileName = path.join(self.config['record-root'],
                                 self.exp.date.Format(self.config['record-folder-name-format']),
                                 self.config['log-filename'])
            if not self.logBox.SaveFile(fileName):
                errorDlg = wx.MessageDialog(None, "Error writing file '%s' to disk" % fileName,
                                            style=wx.OK | wx.ICON_ERROR)
                errorDlg.ShowModal()
                errorDlg.Destroy()
        event.Skip()
