"""Queued message tool bar. Not a public API, but available via MainWindow.appendMessage()
"""

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QEvent, Qt, QTimer
from PyQt5.QtWidgets import (QAbstractButton, QDialogButtonBox, QHBoxLayout,
                             QLabel, QSizePolicy, QToolBar, QWidget)
from PyQt5.QtGui import QColor, QBrush, QPainter, QPixmap

class _QueuedMessage:

    def __init__(self):
        self.milliSeconds = -1
        self.background = QBrush(Qt.NoBrush)
        self.foreground = QBrush(Qt.NoBrush)
        self.slot = None
        self.buttons = {}

class _QueuedMessageWidget(QWidget):

    cleared = pyqtSignal()
    finished = pyqtSignal()
    shown = pyqtSignal()
    closed = pyqtSignal()
    linkActivated = pyqtSignal(str)
    linkHovered = pyqtSignal(str)
    buttonClicked = pyqtSignal(QAbstractButton)

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        self._messages = []
        self._defaultTimeout = 0
        self._defaultPixmap = QPixmap(":/enkiicons/infos.png")
        self._defaultBackground = QBrush(QColor(250, 230, 147))
        self._defaultForeground = QBrush(QColor(0, 0, 0))

        # pixmap
        self.lPixmap = QLabel(self)
        self.lPixmap.setAlignment(Qt.AlignCenter)
        self.lPixmap.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        # message
        self.lMessage = QLabel(self)
        self.lMessage.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
        self.lMessage.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred))
        self.lMessage.setWordWrap(True)
        self.lMessage.setOpenExternalLinks(True)
        self.lMessage.setTextInteractionFlags(Qt.TextBrowserInteraction)

        # button
        self.dbbButtons = QDialogButtonBox(self)

        # if false - buttons don't have neither text nor icons
        self.dbbButtons.setStyleSheet("dialogbuttonbox-buttons-have-icons: true;")

        self.dbbButtons.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum))

        # layout
        self.hbl = QHBoxLayout(self)
        self.hbl.setContentsMargins(0, 0, 0, 0)
        self.hbl.addWidget(self.lPixmap, 0, Qt.AlignCenter)
        self.hbl.addWidget(self.lMessage)
        self.hbl.addWidget(self.dbbButtons, 0, Qt.AlignCenter)

        # connections
        self.lMessage.linkActivated.connect(self.linkActivated)
        self.lMessage.linkHovered.connect(self.linkHovered)
        self.dbbButtons.clicked.connect(self.buttonClicked)

        self._closeTimer = QTimer(self)
        self._closeTimer.setSingleShot(True)
        self._closeTimer.timeout.connect(self.closeMessage)

    def sizeHint(self):
        return QWidget.minimumSizeHint(self)

    def openExternalLinks(self):
        return self.lMessage.openExternalLinks()

    def defaultTimeout(self):
        return self._defaultTimeout

    def defaultPixmap(self):
        return self._defaultPixmap

    def defaultBackground(self):
        return self._defaultBackground

    def defaultForeground(self):
        return self._defaultForeground

    def currentMessageInformations(self):
        return self.currentMessagePixmap(), self.currentMessageBackground(), self.currentMessageForeground()

    def pendingMessageCount(self):
        return len(self._messages)

    def currentMessage(self):
        return self._messages[0]

    def append(self, message, milliSeconds):
        msg = _QueuedMessage()
        msg.message = message
        if milliSeconds == -1:
            msg.milliSeconds = self._defaultTimeout
        else:
            msg.milliSeconds = milliSeconds
        msg.pixmap = self._defaultPixmap
        msg.background = self._defaultBackground
        msg.foreground = self._defaultForeground

        self._messages.append(msg)

        if len(self._messages) == 1:
            self.showMessage()

    def setOpenExternalLinks(self, open):
        self.lMessage.setOpenExternalLinks(open)

    def setDefaultTimeout(self, timeout):
        self._defaultTimeout = timeout

    def setDefaultPixmap(self, pixmap):
        self._defaultPixmap = pixmap

    def setDefaultBackground(self, brush):
        self._defaultBackground = brush

    def setDefaultForeground(self, brush):
        self._defaultForeground = brush

    def remove(self, message):
        raise NotImplemented()  # incorrect port from cpp fresh
        if not self._messages or self._messages.first() == message:
            return

        self._messages.removeOne(message)

    def clear(self):
        self._messages.clear()
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()
        self.cleared.emit()

    def currentMessagePixmap(self):
        msg = self.currentMessage()
        if msg.pixmap.isNull():
            return self._defaultPixmap
        else:
            return msg.pixmap

    def currentMessageBackground(self):
        msg = self.currentMessage()
        if msg.background == QBrush(Qt.NoBrush):
            return self._defaultBackground
        else:
            return msg.background

    def currentMessageForeground(self):
        msg = self.currentMessage()
        if msg.foreground == QBrush(Qt.NoBrush):
            return self._defaultForeground
        else:
            return msg.foreground

    def paintEvent(self, event):
        if self.pendingMessageCount() == 0:
            QWidget.paintEvent(self, event)
            return

        painter = QPainter(self)
        painter.setPen(Qt.NoPen)
        painter.setBrush(self.currentMessageBackground())
        painter.drawRect(self.contentsRect())

    @pyqtSlot(QAbstractButton)
    def buttonClicked(self, button):
        msg = self.currentMessage()
        standardButton = self.dbbButtons.standardButton(button)

        if msg.slot is not None:
            msg.slot(standardButton, msg)

    def showMessage(self):
        # get message
        msg = self.currentMessage()

        # update palette
        pal = self.lMessage.palette()
        pal.setBrush(self.lMessage.foregroundRole(), self.currentMessageForeground())
        self.lMessage.setPalette(pal)

        # format widget
        self.lPixmap.setPixmap(self.currentMessagePixmap())
        self.lMessage.setText(msg.message)
        self.lMessage.setToolTip(msg.message)
        self.lMessage.setWhatsThis(msg.message)

        # set buttons
        if not msg.buttons:
            msg.buttons[QDialogButtonBox.Close] = None

        self.dbbButtons.clear()

        for button in msg.buttons.keys():
            pb = self.dbbButtons.addButton(button)

            if button in msg.buttons:
                pb.setText(msg.buttons[button])

        # auto close if needed
        if msg.milliSeconds == -1:
            timeout = self._defaultTimeout
        else:
            timeout = msg.milliSeconds

        if timeout > 0:
            self._closeTimer.setInterval(timeout)
            self._closeTimer.start()

        # signal.emit
        self.shown.emit()

    @pyqtSlot()
    def closeMessage(self):
        # message.emit
        self.closed.emit()

        # remove remove current message from hash
        self._messages = self._messages[1:]

        # process next if possible, clear gui
        if self._messages:
            self.showMessage()
        else:
            self.clearMessage()

        # finished.emit message if needed
        if not self._messages:
            self.finished.emit()

    def clearMessage(self):
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()

class QueuedMessageToolBar(QToolBar):

    def __init__(self, *args):
        QToolBar.__init__(self, *args)
        self._queuedWidget = _QueuedMessageWidget(self)

        self.setObjectName(self.metaObject().className())
        self.setMovable(False)
        self.setFloatable(False)
        self.setAllowedAreas(Qt.TopToolBarArea)
        self.toggleViewAction().setEnabled(False)
        self.toggleViewAction().setVisible(False)

        self.addWidget(self._queuedWidget)
        self.layout().setContentsMargins(3, 3, 3, 3)

        # connections
        self._queuedWidget.shown.connect(self.messageShown)
        self._queuedWidget.finished.connect(self.messageFinished)

    def queuedMessageWidget(self):
        return self._queuedWidget

    def changeEvent(self, event):
        if event.type() == QEvent.FontChange:
            self._queuedWidget.setFont(self.font())

        QToolBar.changeEvent(self, event)

    def paintEvent(self, event):
        if self._queuedWidget.pendingMessageCount() == 0:
            QToolBar.paintEvent(self, event)
            return

        brush = self._queuedWidget.currentMessageBackground()
        painter = QPainter(self)
        painter.setPen(brush.color().darker(150))
        painter.setBrush(brush)
        painter.drawRect(self.contentsRect().adjusted(0, 0, -1, -1))

    def appendMessage(self, message, milliSeconds=-1):
        return self._queuedWidget.append(message, milliSeconds)

    def removeMessage(self, message):
        self._queuedWidget.remove(message)

    @pyqtSlot()
    def messageShown(self):
        if not self.isVisible():
            self.setVisible(True)

    @pyqtSlot()
    def messageFinished(self):
        if self.isVisible():
            self.setVisible(False)


"""Queued message tool bar. Not a public API, but available via MainWindow.appendMessage()
"""

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QEvent, Qt, QTimer
from PyQt5.QtWidgets import (QAbstractButton, QDialogButtonBox, QHBoxLayout,
                             QLabel, QSizePolicy, QToolBar, QWidget)
from PyQt5.QtGui import QColor, QBrush, QPainter, QPixmap

class _QueuedMessage:

    def __init__(self):
        self.milliSeconds = -1
        self.background = QBrush(Qt.NoBrush)
        self.foreground = QBrush(Qt.NoBrush)
        self.slot = None
        self.buttons = {}

class _QueuedMessageWidget(QWidget):

    cleared = pyqtSignal()
    finished = pyqtSignal()
    shown = pyqtSignal()
    closed = pyqtSignal()
    linkActivated = pyqtSignal(str)
    linkHovered = pyqtSignal(str)
    buttonClicked = pyqtSignal(QAbstractButton)

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        self._messages = []
        self._defaultTimeout = 0
        self._defaultPixmap = QPixmap(":/enkiicons/infos.png")
        self._defaultBackground = QBrush(QColor(250, 230, 147))
        self._defaultForeground = QBrush(QColor(0, 0, 0))

        # pixmap
        self.lPixmap = QLabel(self)
        self.lPixmap.setAlignment(Qt.AlignCenter)
        self.lPixmap.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        # message
        self.lMessage = QLabel(self)
        self.lMessage.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
        self.lMessage.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred))
        self.lMessage.setWordWrap(True)
        self.lMessage.setOpenExternalLinks(True)
        self.lMessage.setTextInteractionFlags(Qt.TextBrowserInteraction)

        # button
        self.dbbButtons = QDialogButtonBox(self)

        # if false - buttons don't have neither text nor icons
        self.dbbButtons.setStyleSheet("dialogbuttonbox-buttons-have-icons: true;")

        self.dbbButtons.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum))

        # layout
        self.hbl = QHBoxLayout(self)
        self.hbl.setContentsMargins(0, 0, 0, 0)
        self.hbl.addWidget(self.lPixmap, 0, Qt.AlignCenter)
        self.hbl.addWidget(self.lMessage)
        self.hbl.addWidget(self.dbbButtons, 0, Qt.AlignCenter)

        # connections
        self.lMessage.linkActivated.connect(self.linkActivated)
        self.lMessage.linkHovered.connect(self.linkHovered)
        self.dbbButtons.clicked.connect(self.buttonClicked)

        self._closeTimer = QTimer(self)
        self._closeTimer.setSingleShot(True)
        self._closeTimer.timeout.connect(self.closeMessage)

    def sizeHint(self):
        return QWidget.minimumSizeHint(self)

    def openExternalLinks(self):
        return self.lMessage.openExternalLinks()

    def defaultTimeout(self):
        return self._defaultTimeout

    def defaultPixmap(self):
        return self._defaultPixmap

    def defaultBackground(self):
        return self._defaultBackground

    def defaultForeground(self):
        return self._defaultForeground

    def currentMessageInformations(self):
        return self.currentMessagePixmap(), self.currentMessageBackground(), self.currentMessageForeground()

    def pendingMessageCount(self):
        return len(self._messages)

    def currentMessage(self):
        return self._messages[0]

    def append(self, message, milliSeconds):
        msg = _QueuedMessage()
        msg.message = message
        if milliSeconds == -1:
            msg.milliSeconds = self._defaultTimeout
        else:
            msg.milliSeconds = milliSeconds
        msg.pixmap = self._defaultPixmap
        msg.background = self._defaultBackground
        msg.foreground = self._defaultForeground

        self._messages.append(msg)

        if len(self._messages) == 1:
            self.showMessage()

    def setOpenExternalLinks(self, open):
        self.lMessage.setOpenExternalLinks(open)

    def setDefaultTimeout(self, timeout):
        self._defaultTimeout = timeout

    def setDefaultPixmap(self, pixmap):
        self._defaultPixmap = pixmap

    def setDefaultBackground(self, brush):
        self._defaultBackground = brush

    def setDefaultForeground(self, brush):
        self._defaultForeground = brush

    def remove(self, message):
        raise NotImplemented()  # incorrect port from cpp fresh
        if not self._messages or self._messages.first() == message:
            return

        self._messages.removeOne(message)

    def clear(self):
        self._messages.clear()
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()
        self.cleared.emit()

    def currentMessagePixmap(self):
        msg = self.currentMessage()
        if msg.pixmap.isNull():
            return self._defaultPixmap
        else:
            return msg.pixmap

    def currentMessageBackground(self):
        msg = self.currentMessage()
        if msg.background == QBrush(Qt.NoBrush):
            return self._defaultBackground
        else:
            return msg.background

    def currentMessageForeground(self):
        msg = self.currentMessage()
        if msg.foreground == QBrush(Qt.NoBrush):
            return self._defaultForeground
        else:
            return msg.foreground

    def paintEvent(self, event):
        if self.pendingMessageCount() == 0:
            QWidget.paintEvent(self, event)
            return

        painter = QPainter(self)
        painter.setPen(Qt.NoPen)
        painter.setBrush(self.currentMessageBackground())
        painter.drawRect(self.contentsRect())

    @pyqtSlot(QAbstractButton)
    def buttonClicked(self, button):
        msg

        standardButton = self.dbbButtons.standardButton(button)

        if msg.slot is not None:
            msg.slot(standardButton, msg)

        self.closeMessage()

    def showMessage(self):
        # get message
        msg = self.currentMessage()

        # update palette
        pal = self.lMessage.palette()
        pal.setBrush(self.lMessage.foregroundRole(), self.currentMessageForeground())
        self.lMessage.setPalette(pal)

        # format widget
        self.lPixmap.setPixmap(self.currentMessagePixmap())
        self.lMessage.setText(msg.message)
        self.lMessage.setToolTip(msg.message)
        self.lMessage.setWhatsThis(msg.message)

        # set buttons
        if not msg.buttons:
            msg.buttons[QDialogButtonBox.Close] = None

        self.dbbButtons.clear()

        for button in msg.buttons.keys():
            pb = self.dbbButtons.addButton(button)

            if button in msg.buttons:
                pb.setText(msg.buttons[button])

        # auto close if needed
        if msg.milliSeconds == -1:
            timeout = self._defaultTimeout
        else:
            timeout = msg.milliSeconds

        if timeout > 0:
            self._closeTimer.setInterval(timeout)
            self._closeTimer.start()

        # signal.emit
        self.shown.emit()

    @pyqtSlot()
    def closeMessage(self):
        # message.emit
        self.closed.emit()

        # remove remove current message from hash
        self._messages = self._messages[1:]

        # process next if possible, clear gui
        if self._messages:
            self.showMessage()
        else:
            self.clearMessage()

        # finished.emit message if needed
        if not self._messages:
            self.finished.emit()

    def clearMessage(self):
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()

class QueuedMessageToolBar(QToolBar):

    def __init__(self, *args):
        QToolBar.__init__(self, *args)
        self._queuedWidget = _QueuedMessageWidget(self)

        self.setObjectName(self.metaObject().className())
        self.setMovable(False)
        self.setFloatable(False)
        self.setAllowedAreas(Qt.TopToolBarArea)
        self.toggleViewAction().setEnabled(False)
        self.toggleViewAction().setVisible(False)

        self.addWidget(self._queuedWidget)
        self.layout().setContentsMargins(3, 3, 3, 3)

        # connections
        self._queuedWidget.shown.connect(self.messageShown)
        self._queuedWidget.finished.connect(self.messageFinished)

    def queuedMessageWidget(self):
        return self._queuedWidget

    def changeEvent(self, event):
        if event.type() == QEvent.FontChange:
            self._queuedWidget.setFont(self.font())

        QToolBar.changeEvent(self, event)

    def paintEvent(self, event):
        if self._queuedWidget.pendingMessageCount() == 0:
            QToolBar.paintEvent(self, event)
            return

        brush = self._queuedWidget.currentMessageBackground()
        painter = QPainter(self)
        painter.setPen(brush.color().darker(150))
        painter.setBrush(brush)
        painter.drawRect(self.contentsRect().adjusted(0, 0, -1, -1))

    def appendMessage(self, message, milliSeconds=-1):
        return self._queuedWidget.append(message, milliSeconds)

    def removeMessage(self, message):
        self._queuedWidget.remove(message)

    @pyqtSlot()
    def messageShown(self):
        if not self.isVisible():
            self.setVisible(True)

    @pyqtSlot()
    def messageFinished(self):
        if self.isVisible():
            self.setVisible(False)


if not self.isVisible():
            self.setVisible(True)

"""Queued message tool bar. Not a public API, but available via MainWindow.appendMessage()
"""

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QEvent, Qt, QTimer
from PyQt5.QtWidgets import (QAbstractButton, QDialogButtonBox, QHBoxLayout,
                             QLabel, QSizePolicy, QToolBar, QWidget)
from PyQt5.QtGui import QColor, QBrush, QPainter, QPixmap

class _QueuedMessage:

    def __init__(self):
        self.milliSeconds = -1
        self.background = QBrush(Qt.NoBrush)
        self.foreground = QBrush(Qt.NoBrush)
        self.slot = None
        self.buttons = {}

class _QueuedMessageWidget(QWidget):

    cleared = pyqtSignal()
    finished = pyqtSignal()
    shown = pyqtSignal()
    closed = pyqtSignal()
    linkActivated = pyqtSignal(str)
    linkHovered = pyqtSignal(str)
    buttonClicked = pyqtSignal(QAbstractButton)

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        self._messages = []
        self._defaultTimeout = 0
        self._defaultPixmap = QPixmap(":/enkiicons/infos.png")
        self._defaultBackground = QBrush(QColor(250, 230, 147))
        self._defaultForeground = QBrush(QColor(0, 0, 0))

        # pixmap
        self.lPixmap = QLabel(self)
        self.lPixmap.setAlignment(Qt.AlignCenter)
        self.lPixmap.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        # message
        self.lMessage = QLabel(self)
        self.lMessage.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
        self.lMessage.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred))
        self.lMessage.setWordWrap(True)
        self.lMessage.setOpenExternalLinks(True)
        self.lMessage.setTextInteractionFlags(Qt.TextBrowserInteraction)

        # button
        self.dbbButtons = QDialogButtonBox(self)

        # if false - buttons don't have neither text nor icons
        self.dbbButtons.setStyleSheet("dialogbuttonbox-buttons-have-icons: true;")

        self.dbbButtons.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum))

        # layout
        self.hbl = QHBoxLayout(self)
        self.hbl.setContentsMargins(0, 0, 0, 0)
        self.hbl.addWidget(self.lPixmap, 0, Qt.AlignCenter)
        self.hbl.addWidget(self.lMessage)
        self.hbl.addWidget(self.dbbButtons, 0, Qt.AlignCenter)

        # connections
        self.lMessage.linkActivated.connect(self.linkActivated)
        self.lMessage.linkHovered.connect(self.linkHovered)
        self.dbbButtons.clicked.connect(self.buttonClicked)

        self._closeTimer = QTimer(self)
        self._closeTimer.setSingleShot(True)
        self._closeTimer.timeout.connect(self.closeMessage)

    def sizeHint(self):
        return QWidget.minimumSizeHint(self)

    def openExternalLinks(self):
        return self.lMessage.openExternalLinks()

    def defaultTimeout(self):
        return self._defaultTimeout

    def defaultPixmap(self):
        return self._defaultPixmap

    def defaultBackground(self):
        return self._defaultBackground

    def defaultForeground(self):
        return self._defaultForeground

    def currentMessageInformations(self):
        return self.currentMessagePixmap(), self.currentMessageBackground(), self.currentMessageForeground()

    def pendingMessageCount(self):
        return len(self._messages)

    def currentMessage(self):
        return self._messages[0]

    def append(self, message, milliSeconds):
        msg = _QueuedMessage()
        msg.message = message
        if milliSeconds == -1:
            msg.milliSeconds = self._defaultTimeout
        else:
            msg.milliSeconds = milliSeconds
        msg.pixmap = self._defaultPixmap
        msg.background = self._defaultBackground
        msg.foreground = self._defaultForeground

        self._messages.append(msg)

        if len(self._messages) == 1:
            self.showMessage()

    def setOpenExternalLinks(self, open):
        self.lMessage.setOpenExternalLinks(open)

    def setDefaultTimeout(self, timeout):
        self._defaultTimeout = timeout

    def setDefaultPixmap(self, pixmap):
        self._defaultPixmap = pixmap

    def setDefaultBackground(self, brush):
        self._defaultBackground = brush

    def setDefaultForeground(self, brush):
        self._defaultForeground = brush

    def remove(self, message):
        raise NotImplemented()  # incorrect port from cpp fresh
        if not self._messages or self._messages.first() == message:
            return

        self._messages.removeOne(message)

    def clear(self):
        self._messages.clear()
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()
        self.cleared.emit()

    def currentMessagePixmap(self):
        msg = self.currentMessage()
        if msg.pixmap.isNull():
            return self._defaultPixmap
        else:
            return msg.pixmap

    def currentMessageBackground(self):
        msg = self.currentMessage()
        if msg.background == QBrush(Qt.NoBrush):
            return self._defaultBackground
        else:
            return msg.background

    def currentMessageForeground(self):
        msg = self.currentMessage()
        if msg.foreground == QBrush(Qt.NoBrush):
            return self._defaultForeground
        else:
            return msg.foreground

    def paintEvent(self, event):
        if self.pendingMessageCount() == 0:
            QWidget.paintEvent(self, event)
            return

        painter = QPainter(self)
        painter.setPen(Qt.NoPen)
        painter.setBrush(self.currentMessageBackground())
        painter.drawRect(self.contentsRect())

    @pyqtSlot(QAbstractButton)
    def buttonClicked(self, button):
        msg = self.currentMessage()
        standardButton = self.dbbButtons.standardButton(button)

        if msg.slot is not None:
            msg.slot(standardButton, msg)

        self.closeMessage()

    def showMessage(self):
        # get message
        msg = self.currentMessage()

        # update palette
        pal = self.lMessage.palette()
        pal.setBrush(self.lMessage.foregroundRole(), self.currentMessageForeground())
        self.lMessage.setPalette(pal)

        # format widget
        self.lPixmap.setPixmap(self.currentMessagePixmap())
        self.lMessage.setText(msg.message)
        self.lMessage.setToolTip(msg.message)
        self.lMessage.setWhatsThis(msg.message)

        # set buttons
        if not msg.buttons:
            msg.buttons[QDialogButtonBox.Close] = None

        self.dbbButtons.clear()

        for button in msg.buttons.keys():
            pb = self.dbbButtons.addButton(button)

            if button in msg.buttons:
                pb.setText(msg.buttons[button])

        # auto close if needed
        if msg.milliSeconds == -1:
            timeout = self._defaultTimeout
        else:
            timeout = msg.milliSeconds

        if timeout > 0:
            self._closeTimer.setInterval(timeout)
            self._closeTimer.start()

        # signal.emit
        self.shown.emit()

    @pyqtSlot()
    def closeMessage(self):
        # message.emit
        self.closed.emit()

        # remove remove current message from hash
        self._messages = self._messages[1:]

        # process next if possible, clear gui
        if self._messages:
            self.showMessage()
        else:
            self.clearMessage()

        # finished.emit message if needed
        if not self._messages:
            self.finished.emit()

    def clearMessage(self):
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()

class QueuedMessageToolBar(QToolBar):

    def __init__(self, *args):
        QToolBar.__init__(self, *args)
        self._queuedWidget = _QueuedMessageWidget(self)

        self.setObjectName(self.metaObject().className())
        self.setMovable(False)
        self.setFloatable(False)
        self.setAllowedAreas(Qt.TopToolBarArea)
        self.toggleViewAction().setEnabled(False)
        self.toggleViewAction().setVisible(False)

        self.addWidget(self._queuedWidget)
        self.layout().setContentsMargins(3, 3, 3, 3)

        # connections
        self._queuedWidget.shown.connect(self.messageShown)
        self._queuedWidget.finished.connect(self.messageFinished)

    def queuedMessageWidget(self):
        return self._queuedWidget

    def changeEvent(self, event):
        if event.type() == QEvent.FontChange:
            self._queuedWidget.setFont(self.font())

        QToolBar.changeEvent(self, event)

    def paintEvent(self, event):
        if self._queuedWidget.pendingMessageCount() == 0:
            QToolBar.paintEvent(self, event)
            return

        brush = self._queuedWidget.currentMessageBackground()
        painter = QPainter(self)
        painter.setPen(brush.color().darker(150))
        painter.setBrush(brush)
        painter.drawRect(self.contentsRect().adjusted(0, 0, -1, -1))

    def appendMessage(self, message, milliSeconds=-1):
        return self._queuedWidget.append(message, milliSeconds)

    def removeMessage(self, message):
        self._queuedWidget.remove(message)

    @pyqtSlot()
    def messageShown(self):

    @pyqtSlot()
    def messageFinished(self):
        if self.isVisible():
            self.setVisible(False)


for button in msg.buttons.keys():
            pb = self.dbbButtons.addButton(button)

            if button in msg.buttons:
                pb.setText(msg.buttons[button])

"""Queued message tool bar. Not a public API, but available via MainWindow.appendMessage()
"""

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QEvent, Qt, QTimer
from PyQt5.QtWidgets import (QAbstractButton, QDialogButtonBox, QHBoxLayout,
                             QLabel, QSizePolicy, QToolBar, QWidget)
from PyQt5.QtGui import QColor, QBrush, QPainter, QPixmap

class _QueuedMessage:

    def __init__(self):
        self.milliSeconds = -1
        self.background = QBrush(Qt.NoBrush)
        self.foreground = QBrush(Qt.NoBrush)
        self.slot = None
        self.buttons = {}

class _QueuedMessageWidget(QWidget):

    cleared = pyqtSignal()
    finished = pyqtSignal()
    shown = pyqtSignal()
    closed = pyqtSignal()
    linkActivated = pyqtSignal(str)
    linkHovered = pyqtSignal(str)
    buttonClicked = pyqtSignal(QAbstractButton)

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        self._messages = []
        self._defaultTimeout = 0
        self._defaultPixmap = QPixmap(":/enkiicons/infos.png")
        self._defaultBackground = QBrush(QColor(250, 230, 147))
        self._defaultForeground = QBrush(QColor(0, 0, 0))

        # pixmap
        self.lPixmap = QLabel(self)
        self.lPixmap.setAlignment(Qt.AlignCenter)
        self.lPixmap.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        # message
        self.lMessage = QLabel(self)
        self.lMessage.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
        self.lMessage.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred))
        self.lMessage.setWordWrap(True)
        self.lMessage.setOpenExternalLinks(True)
        self.lMessage.setTextInteractionFlags(Qt.TextBrowserInteraction)

        # button
        self.dbbButtons = QDialogButtonBox(self)

        # if false - buttons don't have neither text nor icons
        self.dbbButtons.setStyleSheet("dialogbuttonbox-buttons-have-icons: true;")

        self.dbbButtons.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum))

        # layout
        self.hbl = QHBoxLayout(self)
        self.hbl.setContentsMargins(0, 0, 0, 0)
        self.hbl.addWidget(self.lPixmap, 0, Qt.AlignCenter)
        self.hbl.addWidget(self.lMessage)
        self.hbl.addWidget(self.dbbButtons, 0, Qt.AlignCenter)

        # connections
        self.lMessage.linkActivated.connect(self.linkActivated)
        self.lMessage.linkHovered.connect(self.linkHovered)
        self.dbbButtons.clicked.connect(self.buttonClicked)

        self._closeTimer = QTimer(self)
        self._closeTimer.setSingleShot(True)
        self._closeTimer.timeout.connect(self.closeMessage)

    def sizeHint(self):
        return QWidget.minimumSizeHint(self)

    def openExternalLinks(self):
        return self.lMessage.openExternalLinks()

    def defaultTimeout(self):
        return self._defaultTimeout

    def defaultPixmap(self):
        return self._defaultPixmap

    def defaultBackground(self):
        return self._defaultBackground

    def defaultForeground(self):
        return self._defaultForeground

    def currentMessageInformations(self):
        return self.currentMessagePixmap(), self.currentMessageBackground(), self.currentMessageForeground()

    def pendingMessageCount(self):
        return len(self._messages)

    def currentMessage(self):
        return self._messages[0]

    def append(self, message, milliSeconds):
        msg = _QueuedMessage()
        msg.message = message
        if milliSeconds == -1:
            msg.milliSeconds = self._defaultTimeout
        else:
            msg.milliSeconds = milliSeconds
        msg.pixmap = self._defaultPixmap
        msg.background = self._defaultBackground
        msg.foreground = self._defaultForeground

        self._messages.append(msg)

        if len(self._messages) == 1:
            self.showMessage()

    def setOpenExternalLinks(self, open):
        self.lMessage.setOpenExternalLinks(open)

    def setDefaultTimeout(self, timeout):
        self._defaultTimeout = timeout

    def setDefaultPixmap(self, pixmap):
        self._defaultPixmap = pixmap

    def setDefaultBackground(self, brush):
        self._defaultBackground = brush

    def setDefaultForeground(self, brush):
        self._defaultForeground = brush

    def remove(self, message):
        raise NotImplemented()  # incorrect port from cpp fresh
        if not self._messages or self._messages.first() == message:
            return

        self._messages.removeOne(message)

    def clear(self):
        self._messages.clear()
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()
        self.cleared.emit()

    def currentMessagePixmap(self):
        msg = self.currentMessage()
        if msg.pixmap.isNull():
            return self._defaultPixmap
        else:
            return msg.pixmap

    def currentMessageBackground(self):
        msg = self.currentMessage()
        if msg.background == QBrush(Qt.NoBrush):
            return self._defaultBackground
        else:
            return msg.background

    def currentMessageForeground(self):
        msg = self.currentMessage()
        if msg.foreground == QBrush(Qt.NoBrush):
            return self._defaultForeground
        else:
            return msg.foreground

    def paintEvent(self, event):
        if self.pendingMessageCount() == 0:
            QWidget.paintEvent(self, event)
            return

        painter = QPainter(self)
        painter.setPen(Qt.NoPen)
        painter.setBrush(self.currentMessageBackground())
        painter.drawRect(self.contentsRect())

    @pyqtSlot(QAbstractButton)
    def buttonClicked(self, button):
        msg = self.currentMessage()
        standardButton = self.dbbButtons.standardButton(button)

        if msg.slot is not None:
            msg.slot(standardButton, msg)

        self.closeMessage()

    def showMessage(self):
        # get message
        msg = self.currentMessage()

        # update palette
        pal = self.lMessage.palette()
        pal.setBrush(self.lMessage.foregroundRole(), self.currentMessageForeground())
        self.lMessage.setPalette(pal)

        # format widget
        self.lPixmap.setPixmap(self.currentMessagePixmap())
        self.lMessage.setText(msg.message)
        self.lMessage.setToolTip(msg.message)
        self.lMessage.setWhatsThis(msg.message)

        # set buttons
        if not msg.buttons:
            msg.buttons[QDialogButtonBox.Close] = None

        self.dbbButtons.clear()

        # auto close if needed
        if msg.milliSeconds == -1:
            timeout = self._defaultTimeout
        else:
            timeout = msg.milliSeconds

        if timeout > 0:
            self._closeTimer.setInterval(timeout)
            self._closeTimer.start()

        # signal.emit
        self.shown.emit()

    @pyqtSlot()
    def closeMessage(self):
        # message.emit
        self.closed.emit()

        # remove remove current message from hash
        self._messages = self._messages[1:]

        # process next if possible, clear gui
        if self._messages:
            self.showMessage()
        else:
            self.clearMessage()

        # finished.emit message if needed
        if not self._messages:
            self.finished.emit()

    def clearMessage(self):
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()

class QueuedMessageToolBar(QToolBar):

    def __init__(self, *args):
        QToolBar.__init__(self, *args)
        self._queuedWidget = _QueuedMessageWidget(self)

        self.setObjectName(self.metaObject().className())
        self.setMovable(False)
        self.setFloatable(False)
        self.setAllowedAreas(Qt.TopToolBarArea)
        self.toggleViewAction().setEnabled(False)
        self.toggleViewAction().setVisible(False)

        self.addWidget(self._queuedWidget)
        self.layout().setContentsMargins(3, 3, 3, 3)

        # connections
        self._queuedWidget.shown.connect(self.messageShown)
        self._queuedWidget.finished.connect(self.messageFinished)

    def queuedMessageWidget(self):
        return self._queuedWidget

    def changeEvent(self, event):
        if event.type() == QEvent.FontChange:
            self._queuedWidget.setFont(self.font())

        QToolBar.changeEvent(self, event)

    def paintEvent(self, event):
        if self._queuedWidget.pendingMessageCount() == 0:
            QToolBar.paintEvent(self, event)
            return

        brush = self._queuedWidget.currentMessageBackground()
        painter = QPainter(self)
        painter.setPen(brush.color().darker(150))
        painter.setBrush(brush)
        painter.drawRect(self.contentsRect().adjusted(0, 0, -1, -1))

    def appendMessage(self, message, milliSeconds=-1):
        return self._queuedWidget.append(message, milliSeconds)

    def removeMessage(self, message):
        self._queuedWidget.remove(message)

    @pyqtSlot()
    def messageShown(self):
        if not self.isVisible():
            self.setVisible(True)

    @pyqtSlot()
    def messageFinished(self):
        if self.isVisible():
            self.setVisible(False)


"""Queued message tool bar. Not a public API, but available via MainWindow.appendMessage()
"""

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QEvent, Qt, QTimer
from PyQt5.QtWidgets import (QAbstractButton, QDialogButtonBox, QHBoxLayout,
                             QLabel, QSizePolicy, QToolBar, QWidget)
from PyQt5.QtGui import QColor, QBrush, QPainter, QPixmap

class _QueuedMessage:

    def __init__(self):
        self.milliSeconds = -1
        self.background = QBrush(Qt.NoBrush)
        self.foreground = QBrush(Qt.NoBrush)
        self.slot = None
        self.buttons = {}

class _QueuedMessageWidget(QWidget):

    cleared = pyqtSignal()
    finished = pyqtSignal()
    shown = pyqtSignal()
    closed = pyqtSignal()
    linkActivated = pyqtSignal(str)
    linkHovered = pyqtSignal(str)
    buttonClicked = pyqtSignal(QAbstractButton)

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        self._messages = []
        self._defaultTimeout = 0
        self._defaultPixmap = QPixmap(":/enkiicons/infos.png")
        self._defaultBackground = QBrush(QColor(250, 230, 147))
        self._defaultForeground = QBrush(QColor(0, 0, 0))

        # pixmap
        self.lPixmap = QLabel(self)
        self.lPixmap.setAlignment(Qt.AlignCenter)
        self.lPixmap.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        # message
        self.lMessage = QLabel(self)
        self.lMessage.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
        self.lMessage.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred))
        self.lMessage.setWordWrap(True)
        self.lMessage.setOpenExternalLinks(True)
        self.lMessage.setTextInteractionFlags(Qt.TextBrowserInteraction)

        # button
        self.dbbButtons = QDialogButtonBox(self)

        # if false - buttons don't have neither text nor icons
        self.dbbButtons.setStyleSheet("dialogbuttonbox-buttons-have-icons: true;")

        self.dbbButtons.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))

        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum))

        # layout
        self.hbl = QHBoxLayout(self)
        self.hbl.setContentsMargins(0, 0, 0, 0)
        self.hbl.addWidget(self.lPixmap, 0, Qt.AlignCenter)
        self.hbl.addWidget(self.lMessage)
        self.hbl.addWidget(self.dbbButtons, 0, Qt.AlignCenter)

        # connections
        self.lMessage.linkActivated.connect(self.linkActivated)
        self.lMessage.linkHovered.connect(self.linkHovered)
        self.dbbButtons.clicked.connect(self.buttonClicked)

        self._closeTimer = QTimer(self)
        self._closeTimer.setSingleShot(True)
        self._closeTimer.timeout.connect(self.closeMessage)

    def sizeHint(self):
        return QWidget.minimumSizeHint(self)

    def openExternalLinks(self):
        return self.lMessage.openExternalLinks()

    def defaultTimeout(self):
        return self._defaultTimeout

    def defaultPixmap(self):
        return self._defaultPixmap

    def defaultBackground(self):
        return self._defaultBackground

    def defaultForeground(self):
        return self._defaultForeground

    def currentMessageInformations(self):
        return self.currentMessagePixmap(), self.currentMessageBackground(), self.currentMessageForeground()

    def pendingMessageCount(self):
        return len(self._messages)

    def currentMessage(self):
        return self._messages[0]

    def append(self, message, milliSeconds):
        msg = _QueuedMessage()
        msg.message = message
        if milliSeconds == -1:
            msg.milliSeconds = self._defaultTimeout
        else:
            msg.milliSeconds = milliSeconds
        msg.pixmap = self._defaultPixmap
        msg.background = self._defaultBackground
        msg.foreground = self._defaultForeground

        self._messages.append(msg)

        if len(self._messages) == 1:
            self.showMessage()

    def setOpenExternalLinks(self, open):
        self.lMessage.setOpenExternalLinks(open)

    def setDefaultTimeout(self, timeout):
        self._defaultTimeout = timeout

    def setDefaultPixmap(self, pixmap):
        self._defaultPixmap = pixmap

    def setDefaultBackground(

self, brush):
        self._defaultBackground = brush

    def setDefaultForeground(self, brush):
        self._defaultForeground = brush

    def remove(self, message):
        raise NotImplemented()  # incorrect port from cpp fresh
        if not self._messages or self._messages.first() == message:
            return

        self._messages.removeOne(message)

    def clear(self):
        self._messages.clear()
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()
        self.cleared.emit()

    def currentMessagePixmap(self):
        msg = self.currentMessage()
        if msg.pixmap.isNull():
            return self._defaultPixmap
        else:
            return msg.pixmap

    def currentMessageBackground(self):
        msg = self.currentMessage()
        if msg.background == QBrush(Qt.NoBrush):
            return self._defaultBackground
        else:
            return msg.background

    def currentMessageForeground(self):
        msg = self.currentMessage()
        if msg.foreground == QBrush(Qt.NoBrush):
            return self._defaultForeground
        else:
            return msg.foreground

    def paintEvent(self, event):
        if self.pendingMessageCount() == 0:
            QWidget.paintEvent(self, event)
            return

        painter = QPainter(self)
        painter.setPen(Qt.NoPen)
        painter.setBrush(self.currentMessageBackground())
        painter.drawRect(self.contentsRect())

    @pyqtSlot(QAbstractButton)
    def buttonClicked(self, button):
        msg = self.currentMessage()
        standardButton = self.dbbButtons.standardButton(button)

        if msg.slot is not None:
            msg.slot(standardButton, msg)

        self.closeMessage()

    def showMessage(self):
        # get message
        msg = self.currentMessage()

        # update palette
        pal = self.lMessage.palette()
        pal.setBrush(self.lMessage.foregroundRole(), self.currentMessageForeground())
        self.lMessage.setPalette(pal)

        # format widget
        self.lPixmap.setPixmap(self.currentMessagePixmap())
        self.lMessage.setText(msg.message)
        self.lMessage.setToolTip(msg.message)
        self.lMessage.setWhatsThis(msg.message)

        # set buttons
        if not msg.buttons:
            msg.buttons[QDialogButtonBox.Close] = None

        self.dbbButtons.clear()

        for button in msg.buttons.keys():
            pb = self.dbbButtons.addButton(button)

            if button in msg.buttons:
                pb.setText(msg.buttons[button])

        # auto close if needed
        if msg.milliSeconds == -1:
            timeout = self._defaultTimeout
        else:
            timeout = msg.milliSeconds

        if timeout > 0:
            self._closeTimer.setInterval(timeout)
            self._closeTimer.start()

        # signal.emit
        self.shown.emit()

    @pyqtSlot()
    def closeMessage(self):
        # message.emit
        self.closed.emit()

        # remove remove current message from hash
        self._messages = self._messages[1:]

        # process next if possible, clear gui
        if self._messages:
            self.showMessage()
        else:
            self.clearMessage()

        # finished.emit message if needed
        if not self._messages:
            self.finished.emit()

    def clearMessage(self):
        self.lPixmap.clear()
        self.lMessage.clear()
        self.dbbButtons.clear()

class QueuedMessageToolBar(QToolBar):

    def __init__(self, *args):
        QToolBar.__init__(self, *args)
        self._queuedWidget = _QueuedMessageWidget(self)

        self.setObjectName(self.metaObject().className())
        self.setMovable(False)
        self.setFloatable(False)
        self.setAllowedAreas(Qt.TopToolBarArea)
        self.toggleViewAction().setEnabled(False)
        self.toggleViewAction().setVisible(False)

        self.addWidget(self._queuedWidget)
        self.layout().setContentsMargins(3, 3, 3, 3)

        # connections
        self._queuedWidget.shown.connect(self.messageShown)
        self._queuedWidget.finished.connect(self.messageFinished)

    def queuedMessageWidget(self):
        return self._queuedWidget

    def changeEvent(self, event):
        if event.type() == QEvent.FontChange:
            self._queuedWidget.setFont(self.font())

        QToolBar.changeEvent(self, event)

    def paintEvent(self, event):
        if self._queuedWidget.pendingMessageCount() == 0:
            QToolBar.paintEvent(self, event)
            return

        brush = self._queuedWidget.currentMessageBackground()
        painter = QPainter(self)
        painter.setPen(brush.color().darker(150))
        painter.setBrush(brush)
        painter.drawRect(self.contentsRect().adjusted(0, 0, -1, -1))

    def appendMessage(self, message, milliSeconds=-1):
        return self._queuedWidget.append(message, milliSeconds)

    def removeMessage(self, message):
        self._queuedWidget.remove(message)

    @pyqtSlot()
    def messageShown(self):
        if not self.isVisible():
            self.setVisible(True)

    @pyqtSlot()
    def messageFinished(self):
        if self.isVisible():
            self.setVisible(False)


if slot != 0 and slot % 1000 == 0:
            sa_RL.save('./save/sa_ddqn', slot)

from spectrum_allocation_power_control.topology import SingleCell
from spectrum_allocation_power_control.ddqn_keras import DDQNAgent

if __name__ == '__main__':
    slot_num = 2000  # 循环次数
    radius = 500  # m
    cue_num = 20
    d2d_num = 40
    rb_num = 20
    up_or_down_link = 'up'
    d_tx2rx = 10  # m
    power_level_num = 10

    '''
    int(action / power_level_num) = rb_id
    action % power_level_num = power_level
    action = 0 : 选择 0 RB 0 power level
    action = 1 : 选择 0 RB 1 power level
    '''

    single_cell = SingleCell(radius, cue_num, d2d_num, rb_num, up_or_down_link, d_tx2rx, power_level_num)
    single_cell.initial()

    sa_RL = DDQNAgent(4 * rb_num + 3, rb_num)

    for slot in range(slot_num):
        print("********************循环次数: ", slot, " ********************")
        single_cell.sa_train_work(slot, sa_RL)
        single_cell.update()


for slot in range(slot_num):
        print("********************循环次数: ", slot, " ********************")
        single_cell.sa_train_work(slot, sa_RL)
        single_cell.update()
        if slot != 0 and slot % 1000 == 0:
            sa_RL.save('./save/sa_ddqn', slot)

from spectrum_allocation_power_control.topology import SingleCell
from spectrum_allocation_power_control.ddqn_keras import DDQNAgent

if __name__ == '__main__':
    slot_num = 2000  # 循环次数
    radius = 500  # m
    cue_num = 20
    d2d_num = 40
    rb_num = 20
    up_or_down_link = 'up'
    d_tx2rx = 10  # m
    power_level_num = 10

    '''
    int(action / power_level_num) = rb_id
    action % power_level_num = power_level
    action = 0 : 选择 0 RB 0 power level
    action = 1 : 选择 0 RB 1 power level
    '''

    single_cell = SingleCell(radius, cue_num, d2d_num, rb_num, up_or_down_link, d_tx2rx, power_level_num)
    single_cell.initial()

    sa_RL = DDQNAgent(4 * rb_num + 3, rb_num)


from spectrum_allocation_power_control.topology import SingleCell
from spectrum_allocation_power_control.ddqn_keras import DDQNAgent

if __name__ == '__main__':
    slot_num = 2000  # 循环次数
    radius = 500  # m
    cue_num = 20
    d2d_num = 40
    rb_num = 20
    up_or_down_link = 'up'
    d_tx2rx = 10  # m
    power_level_num = 10

    '''
    int(action / power_level_num) = rb_id
    action % power_level_num = power_level
    action = 0 : 选择 0 RB 0 power level
    action = 1 : 选择 0 RB 1 power level
    '''

    single_cell = SingleCell(radius, cue_num, d2d_num, rb_num, up_or_down_link, d_tx2rx, power_level_num)
    single_cell.initial()

    sa_RL = DDQNAgent(4 * rb_num + 3, rb_num)

    for slot in range(slot_num):
        print("********************循环次数: ", slot, "

 ********************")
        single_cell.sa_train_work(slot, sa_RL)
        single_cell.update()
        if slot != 0 and slot % 1000 == 0:
            sa_RL.save('./save/sa_ddqn', slot)

    sa_RL.save('./save/sa_ddqn', slot_num)


"""
A point (x,y), on the cartesian plane, makes an angle theta with the positive direction of the x-axis. Theta varies in
the interval [0 ,2PI) radians, i.e, greater than or equal to zero; but less than 2*PI radians.

For example, the polar angle of the point (1,2) as marked in this plane below, is (approximately) 63.4 degrees (multiply
by PI/180 to convert to radians)

Ref http://eldar.mathstat.uoguelph.ca/dashlock/Outreach/Articles/images/PRfig1.jpg

The Task

Given a list of points in the 2D plane, sort them in ascending order of their polar angle. In case multiple points share
exactly the same polar angle, the one with lesser distance from the origin (0,0) should occur earlier in the sorted list.

Input Format
The first line contains an integer N.
This is followed by N lines containing pairs of space separated integers, x and y which represent the coordinates of the
points in the cartesian plane.
"""
import math

__author__ = 'Danyang'

class Solution(object):
    def solve(self, cipher):
        """
        main solution function
        :param cipher: the cipher
        """
        cipher.sort(cmp=self.cmp)

    def cmp_polar(self, a, b):
        """
        cross product, but what if \pi?
        error, can only sort by clockwise
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        cross_product = x1 * y2 - x2 * y1
        if cross_product > 0:
            return -1
        elif cross_product < 0:
            return 1
        else:
            if x1 * x1 >= 0 and y1 * y1 >= 0:
                return x1 * x1 + y1 * y1 - x2 * x2 - y2 * y2
            else:
                if y1 > 0:
                    return -1
                if y2 > 0:
                    return 1
                if y1 == 0 and x1 > 0:
                    return -1
                else:
                    return 1

"""
        polar coordinate
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        r1 = x1 * x1 + y1 * y1
        r2 = x2 * x2 + y2 * y2
        phi1 = math.atan2(y1, x1)
        phi2 = math.atan2(y2, x2)
        if phi1 < 0:
            phi1 += math.pi * 2
        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

if __name__ == "__main__":
    import sys

    f = open("1.in", "r")
    # f = sys.stdin
    N = int(f.readline().strip())

    cipher = []
    for t in xrange(N):
        # construct cipher
        cipher.append(map(int, f.readline().strip().split(' ')))
        # solve
    Solution().solve(cipher)
    for point in cipher:
        print "%d %d" % (point[0], point[1])


"""
        polar coordinate
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        r1 = x1 * x1 + y1 * y1
        r2 = x2 * x2 + y2 * y2
        phi1 = math.atan2(y1, x1)
        phi2 = math.atan2(y2, x2)
        if phi1 < 0:
            phi1 += math.pi * 2
        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

"""
A point (x,y), on the cartesian plane, makes an angle theta with the positive direction of the x-axis. Theta varies in
the interval [0 ,2PI) radians, i.e, greater than or equal to zero; but less than 2*PI radians.

For example, the polar angle of the point (1,2) as marked in this plane below, is (approximately) 63.4 degrees (multiply
by PI/180 to convert to radians)

Ref http://eldar.mathstat.uoguelph.ca/dashlock/Outreach/Articles/images/PRfig1.jpg

The Task

Given a list of points in the 2D plane, sort them in ascending order of their polar angle. In case multiple points share
exactly the same polar angle, the one with lesser distance from the origin (0,0) should occur earlier in the sorted list.

Input Format
The first line contains an integer N.
This is followed by N lines containing pairs of space separated integers, x and y which represent the coordinates of the
points in the cartesian plane.
"""
import math

__author__ = 'Danyang'

class Solution(object):
    def solve(self, cipher):
        """
        main solution function
        :param cipher: the cipher
        """
        cipher.sort(cmp=self.cmp)

    def cmp_polar(self, a, b):
        """
        cross product, but what if \pi?
        error, can only sort by clockwise
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        cross_product = x1 * y2 - x2 * y1
        if cross_product > 0:
            return -1
        elif cross_product < 0:
            return 1
        else:
            if x1 * x1 >= 0 and y1 * y1 >= 0:
                return x1 * x1 + y1 * y1 - x2 * x2 - y2 * y2
            else:
                if y1 > 0:
                    return -1
                if y2 > 0:
                    return 1
                if y1 == 0 and x1 > 0:
                    return -1
                else:
                    return 1

    def cmp(self, a, b):

if __name__ == "__main__":
    import sys

    f = open("1.in", "r")
    # f = sys.stdin
    N = int(f.readline().strip())

    cipher = []
    for t in xrange(N):
        # construct cipher
        cipher.append(map(int, f.readline().strip().split(' ')))
        # solve
    Solution().solve(cipher)
    for point in cipher:
        print "%d %d" % (point[0], point[1])


"""
        polar coordinate
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]

        y2 = b[1]
        r1 = x1 * x1 + y1 * y1
        r2 = x2 * x2 + y2 * y2
        phi1 = math.atan2(y1, x1)
        phi2 = math.atan2(y2, x2)
        if phi1 < 0:
            phi1 += math.pi * 2
        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

if __name__ == "__main__":
    import sys

    f = open("1.in", "r")
    # f = sys.stdin
    N = int(f.readline().strip())

    cipher = []
    for t in xrange(N):
        # construct cipher
        cipher.append(map(int, f.readline().strip().split(' ')))
        # solve
    Solution().solve(cipher)
    for point in cipher:
        print "%d %d" % (point[0], point[1])


"""
A point (x,y), on the cartesian plane, makes an angle theta with the positive direction of the x-axis. Theta varies in
the interval [0 ,2PI) radians, i.e, greater than or equal to zero; but less than 2*PI radians.

For example, the polar angle of the point (1,2) as marked in this plane below, is (approximately) 63.4 degrees (multiply
by PI/180 to convert to radians)

Ref http://eldar.mathstat.uoguelph.ca/dashlock/Outreach/Articles/images/PRfig1.jpg

The Task

Given a list of points in the 2D plane, sort them in ascending order of their polar angle. In case multiple points share
exactly the same polar angle, the one with lesser distance from the origin (0,0) should occur earlier in the sorted list.

Input Format
The first line contains an integer N.
This is followed by N lines containing pairs of space separated integers, x and y which represent the coordinates of the
points in the cartesian plane.
"""
import math

__author__ = 'Danyang'

class Solution(object):
    def solve(self, cipher):
        """
        main solution function
        :param cipher: the cipher
        """
        cipher.sort(cmp=self.cmp)

    def cmp_polar(self, a, b):
        """
        cross product, but what if \pi?
        error, can only sort by clockwise
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        cross_product = x1 * y2 - x2 * y1
        if cross_product > 0:
            return -1
        elif cross_product < 0:
            return 1
        else:
            if x1 * x1 >= 0 and y1 * y1 >= 0:
                return x1 * x1 + y1 * y1 - x2 * x2 - y2 * y2
            else:
                if y1 > 0:
                    return -1
                if y2 > 0:
                    return 1
                if y1 == 0 and x1 > 0:
                    return -1
                else:
                    return 1

    def cmp(self, a, b):
        """
        polar coordinate
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]

        phi2 = math.atan2(y2, x2)
        if phi1 < 0:
            phi1 += math.pi * 2
        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

if __name__ == "__main__":
    import sys

    f = open("1.in", "r")
    # f = sys.stdin
    N = int(f.readline().strip())

    cipher = []
    for t in xrange(N):
        # construct cipher
        cipher.append(map(int, f.readline().strip().split(' ')))
        # solve
    Solution().solve(cipher)
    for point in cipher:
        print "%d %d" % (point[0], point[1])


phi2 = math.atan2(y2, x2)
        if phi1 < 0:
            phi1 += math.pi * 2
        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

"""
A point (x,y), on the cartesian plane, makes an angle theta with the positive direction of the x-axis. Theta varies in
the interval [0 ,2PI) radians, i.e, greater than or equal to zero; but less than 2*PI radians.

For example, the polar angle of the point (1,2) as marked in this plane below, is (approximately) 63.4 degrees (multiply
by PI/180 to convert to radians)

Ref http://eldar.mathstat.uoguelph.ca/dashlock/Outreach/Articles/images/PRfig1.jpg

The Task

Given a list of points in the 2D plane, sort them in ascending order of their polar angle. In case multiple points share
exactly the same polar angle, the one with lesser distance from the origin (0,0) should occur earlier in the sorted list.

Input Format
The first line contains an integer N.
This is followed by N lines containing pairs of space separated integers, x and y which represent the coordinates of the
points in the cartesian plane.
"""
import math

__author__ = 'Danyang'

class Solution(object):
    def solve(self, cipher):
        """
        main solution function
        :param cipher: the cipher
        """
        cipher.sort(cmp=self.cmp)

    def cmp_polar(self, a, b):
        """
        cross product, but what if \pi?
        error, can only sort by clockwise
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        cross_product = x1 * y2 - x2 * y1
        if cross_product > 0:
            return -1
        elif cross_product < 0:
            return 1
        else:
            if x1 * x1 >= 0 and y1 * y1 >= 0:
                return x1 * x1 + y1 * y1 - x2 * x2 - y2 * y2
            else:
                if y1 > 0:
                    return -1
                if y2 > 0:
                    return 1
                if y1 == 0 and x1 > 0:
                    return -1
                else:
                    return 1

    def cmp(self, a, b):
        """
        polar coordinate
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        r1 = x1 * x1 + y1 * y1
        r2 = x2 * x2 + y2 * y2
        phi1 = math.atan2(y1, x1)


"""
A point (x,y), on the cartesian plane, makes an angle theta with the positive direction of the x-axis. Theta varies in
the interval [0 ,2PI) radians, i.e, greater than or equal to zero; but less than 2*PI radians.

For example, the polar angle of the point (1,2) as marked in this plane below, is (approximately) 63.4 degrees (multiply
by PI/180 to convert to radians)

Ref http://eldar.mathstat.uoguelph.ca/dashlock/Outreach/Articles/images/PRfig1.jpg

The Task

Given a list of points in the 2D plane, sort them in ascending order of their polar angle. In case multiple points share
exactly the same polar angle, the one with lesser distance from the origin (0,0) should occur earlier in the sorted list.

Input Format
The first line contains an integer N.
This is followed by N lines containing pairs of space separated integers, x and y which represent the coordinates of the
points in the cartesian plane.
"""
import math

__author__ = 'Danyang'

class Solution(object):
    def solve(self, cipher):
        """
        main solution function
        :param cipher: the cipher
        """
        cipher.sort(cmp=self.cmp)

    def cmp_polar(self, a, b):
        """
        cross product, but what if \pi?
        error, can only sort by clockwise
        """
        x1 = a[0]
        y1 = a[1]
        x

        y2 = b[1]
        cross_product = x1 * y2 - x2 * y1
        if cross_product > 0:
            return -1
        elif cross_product < 0:
            return 1
        else:
            if x1 * x1 >= 0 and y1 * y1 >= 0:
                return x1 * x1 + y1 * y1 - x2 * x2 - y2 * y2
            else:
                if y1 > 0:
                    return -1
                if y2 > 0:
                    return 1
                if y1 == 0 and x1 > 0:
                    return -1
                else:
                    return 1

    def cmp(self, a, b):
        """
        polar coordinate
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        r1 = x1 * x1 + y1 * y1
        r2 = x2 * x2 + y2 * y2
        phi1 = math.atan2(y1, x1)
        phi2 = math.atan2(y2, x2)
        if phi1 < 0:
            phi1 += math.pi * 2
        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

if __name__ == "__main__":
    import sys

    f = open("1.in", "r")
    # f = sys.stdin
    N = int(f.readline().strip())

    cipher = []
    for t in xrange(N):
        # construct cipher
        cipher.append(map(int, f.readline().strip().split(' ')))
        # solve
    Solution().solve(cipher)
    for point in cipher:
        print "%d %d" % (point[0], point[1])


        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        r1 = x1 * x1 + y1 * y1
        r2 = x2 * x2 + y2 * y2
        phi1 = math.atan2(y1, x1)
        phi2 = math.atan2(y2, x2)
        if phi1 < 0:
            phi1 += math.pi * 2
        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

"""
A point (x,y), on the cartesian plane, makes an angle theta with the positive direction of the x-axis. Theta varies in
the interval [0 ,2PI) radians, i.e, greater than or equal to zero; but less than 2*PI radians.

For example, the polar angle of the point (1,2) as marked in this plane below, is (approximately) 63.4 degrees (multiply
by PI/180 to convert to radians)

Ref http://eldar.mathstat.uoguelph.ca/dashlock/Outreach/Articles/images/PRfig1.jpg

The Task

Given a list of points in the 2D plane, sort them in ascending order of their polar angle. In case multiple points share
exactly the same polar angle, the one with lesser distance from the origin (0,0) should occur earlier in the sorted list.

Input Format
The first line contains an integer N.
This is followed by N lines containing pairs of space separated integers, x and y which represent the coordinates of the
points in the cartesian plane.
"""
import math

__author__ = 'Danyang'

class Solution(object):
    def solve(self, cipher):
        """
        main solution function
        :param cipher: the cipher
        """
        cipher.sort(cmp=self.cmp)

    def cmp_polar(self, a, b):
        """
        cross product, but what if \pi?
        error, can only sort by clockwise
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        cross_product = x1 * y2 - x2 * y1
        if cross_product > 0:
            return -1
        elif cross_product < 0:
            return 1
        else:
            if x1 * x1 >= 0 and y1 * y1 >= 0:
                return x1 * x1 + y1 * y1 - x2 * x2 - y2 * y2
            else:
                if y1 > 0:
                    return -1
                if y2 > 0:
                    return 1
                if y1 == 0 and x1 > 0:
                    return -1
                else:
                    return 1

    def cmp(self, a, b):
        """
        polar coordinate
        """


if phi1 < 0:
            phi1 += math.pi * 2

"""
A point (x,y), on the cartesian plane, makes an angle theta with the positive direction of the x-axis. Theta varies in
the interval [0 ,2PI) radians, i.e, greater than or equal to zero; but less than 2*PI radians.

For example, the polar angle of the point (1,2) as marked in this plane below, is (approximately) 63.4 degrees (multiply
by PI/180 to convert to radians)

Ref http://eldar.mathstat.uoguelph.ca/dashlock/Outreach/Articles/images/PRfig1.jpg

The Task

Given a list of points in the 2D plane, sort them in ascending order of their polar angle. In case multiple points share
exactly the same polar angle, the one with lesser distance from the origin (0,0) should occur earlier in the sorted list.

Input Format
The first line contains an integer N.
This is followed by N lines containing pairs of space separated integers, x and y which represent the coordinates of the
points in the cartesian plane.
"""
import math

__author__ = 'Danyang'

class Solution(object):
    def solve(self, cipher):
        """
        main solution function
        :param cipher: the cipher
        """
        cipher.sort(cmp=self.cmp)

    def cmp_polar(self, a, b):
        """
        cross product, but what if \pi?
        error, can only sort by clockwise
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        cross_product = x1 * y2 - x2 * y1
        if cross_product > 0:
            return -1
        elif cross_product < 0:
            return 1
        else:
            if x1 * x1 >= 0 and y1 * y1 >= 0:
                return x1 * x1 + y1 * y1 - x2 * x2 - y2 * y2
            else:
                if y1 > 0:
                    return -1
                if y2 > 0:
                    return 1
                if y1 == 0 and x1 > 0:
                    return -1
                else:
                    return 1

    def cmp(self, a, b):
        """
        polar coordinate
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        r1 = x1 * x1 + y1 * y1
        r2 = x2 * x2 + y2 * y2
        phi1 = math.atan2(y1, x1)
        phi2 = math.atan2(y2, x2)

        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

if __name__ == "__main__":
    import sys

    f = open("1.in", "r")
    # f = sys.stdin
    N = int(f.readline().strip())

    cipher = []
    for t in xrange(N):
        # construct cipher
        cipher.append(map(int, f.readline().strip().split(' ')))
        # solve
    Solution().solve(cipher)
    for point in cipher:
        print "%d %d" % (point[0], point[1])


for point in cipher:
        print "%d %d" % (point[0], point[1])

"""
A point (x,y), on the cartesian plane, makes an angle theta with the positive direction of the x-axis. Theta varies in
the interval [0 ,2PI) radians, i.e, greater than or equal to zero; but less than 2*PI radians.

For example, the polar angle of the point (1,2) as marked in this plane below, is (approximately) 63.4 degrees (multiply
by PI/180 to convert to radians)

Ref http://eldar.mathstat.uoguelph.ca/dashlock/Outreach/Articles/images/PRfig1.jpg

The Task

Given a list of points in the 2D plane, sort them in ascending order of their polar angle. In case multiple points share
exactly the same polar angle, the one with lesser distance from the origin (0,0) should occur earlier in the sorted list.

Input Format
The first line contains an integer N.
This is followed by N lines containing pairs of space separated integers, x and y which represent the coordinates of the
points in the cartesian plane.
"""
import math

__author__ = 'Danyang'

class Solution(object):
    def solve(self, cipher):
        """
        main solution function
        :param cipher: the cipher
        """
        cipher.sort(cmp=self.cmp)

    def cmp_polar(self, a, b):
        """
        cross product, but what if \pi?
        error, can only sort by clockwise
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        cross_product = x1 * y2 - x2 * y1
        if cross_product > 0:
            return -1
        elif cross_product < 0:
            return 1
        else:
            if x1 * x1 >= 0 and y1 * y1 >= 0:
                return x1 * x1 + y1 * y1 - x2 * x2 - y2 * y2
            else:
                if y1 > 0:
                    return -1
                if y2 > 0:
                    return 1
                if y1 == 0 and x1 > 0:
                    return -1
                else:
                    return 1

    def cmp(self, a, b):
        """
        polar coordinate
        """
        x1 = a[0]
        y1 = a[1]
        x2 = b[0]
        y2 = b[1]
        r1 = x1 * x1 + y1 * y1
        r2 = x2 * x2 + y2 * y2
        phi1 = math.atan2(y1, x1)
        phi2 = math.atan2(y2, x2)
        if phi1 < 0:
            phi1 += math.pi * 2
        if phi2 < 0:
            phi2 += math.pi * 2
        if phi1 < phi2:
            return -1
        elif phi1 > phi2:
            return 1
        else:
            return r1 - r2

if __name__ == "__main__":
    import sys

    f = open("1.in", "r")
    # f = sys.stdin
    N = int(f.readline().strip())

    cipher = []
    for t in xrange(N):
        # construct cipher
        cipher.append(map(int, f.readline().strip().split(' ')))
        # solve
    Solution().solve(cipher)


# Part of the PsychoPy library

#    This code was mostly written by Jon Peirce.
#    CRS Ltd provided support as needed.

# Acknowledgements
#    This code was written by Jon Peirce

from __future__ import absolute_import, division, print_function

from builtins import bytes
from builtins import range
from builtins import object
import sys
try:
    import serial
except Exception:
    serial = False
import numpy

__docformat__ = "restructuredtext en"

# try to use psychopy logging but revert to system logging
try:
    from psychopy import logging  # from 1.73 onwards
except ImportError:
    import logging  # use the standard python logging
eol = "\n\r"  # unusual for a serial port?!

class ColorCAL(object):
    """A class to handle the CRS Ltd ColorCAL device
    """

    # PsychoPy uses these two variables for matching classes to photometers
    longName = "CRS ColorCAL"
    driverFor = ["colorcal"]

    def __init__(self, port=None, maxAttempts=2):
        """Open serial port connection with Colorcal II device

        :Usage:

            cc = ColorCAL(port, maxAttempts)

       If no port is provided then the following defaults will be tried:
           - /dev/cu.usbmodem0001 (OSX)
           - /dev/ttyACM0
           - COM3 (windows)

        """
        super(ColorCAL, self).__init__()

        if not serial:
            raise ImportError('The module serial is needed to connect to '
                              'photometers. On most systems this can be '
                              'installed with\n\t easy_install pyserial')

        # try to deduce port
        if port is None:
            if sys.platform == 'darwin':
                port = '/dev/cu.usbmodem0001'
            elif sys.platform.startswith('linux'):
                port = '/dev/ttyACM0'
            elif sys.platform.startswith('win'):
                port = 3
        if type(port) in (int, float):
            # add one so that port 1=COM1
            self.portNumber = port
            self.portString = 'COM%i' % self.portNumber
        else:
            self.portString = port
            self.portNumber = None

        self.isOpen = 0
        self.lastLum = None
        self.lastCmd = ''
        self.type = 'ColorCAL'
        self.com = False
        self.OK = True  # until we fail
        self.maxAttempts = maxAttempts
        self._zeroCalibrated = False

        # try to open the port
        try:
            self.com = serial.Serial(self.portString)
        except Exception:
            msg = ("Couldn't connect to port %s. Is it being used by "
                   "another program?")
            self._error(msg % self.portString)

        # setup the params for serial port
        if self.OK:
            self.com.close()  # not sure why this helps but on win32 it does!!
            try:
                self.com.setBaudrate(115200)  # actually, any baudrate is fine?
            except:
                self.com.baudrate = 115200  # setBaudrate() remov pyserial v3.0
            try:
                if not self.com.isOpen():
                    self.com.open()
            except Exception:
                msg = "Opened serial port %s, but couldn't connect to ColorCAL"
                self._error(msg % self.portString)
            else:
                self.isOpen = 1

        # check that we can communicate with it
        self.ok, self.serialNum, self.firm, self.firmBuild = self.getInfo()
        self.calibMatrix = self.getCalibMatrix()

    def sendMessage(self, message, timeout=0.1):
        """Send a command to the photometer and wait an alloted
        timeout for a response.
        """
        # flush the read buffer first
        # read as many chars as are in the buffer
        prevOut = self.com.read(self.com.inWaiting()).decode('utf-8')
        if len(prevOut) and prevOut not in ('>' + eol, eol):
            # do not use log messages here
            print('Resp found to prev cmd (%s):%s' % (self.lastCmd, prevOut))
        self.lastCmd = message

        if type(message) is not bytes:
            message = message.encode('utf-8')
        if not (message.endswith(b'\n') or message.endswith(b'\n\r')):
            message += b"\n"  # append a newline if necess
        # send the message
        self.com.write(message)
        self.com.flush()
        self.com.timeout = timeout
        logging.debug('Sent command:%s' % (message[:-1]))  # send complete msg

        # get output lines using self.readline, not self.com.readline
        # colorcal signals the end of a message by giving a command prompt
        lines = []
        thisLine = ''
        nEmpty = 0
        while (thisLine != '>') and (nEmpty <= self.maxAttempts):
            # self.com.readline can't handle custom eol
            thisLine = self.readline(eol=eol).decode('utf-8')
            if thisLine in (eol, '>', ''):  # lines we don't care about
                nEmpty += 1
                continue
            else:
                # line without any eol chars
                lines.append(thisLine.strip(eol))
                nEmpty = 0

        # got all lines and reached '>'
        if len(lines) == 1:
            return lines[0]  # return the string
        else:
            return lines  # a list of lines

    def measure(self):
        """Conduct a measurement and return the X,Y,Z values

        Usage::

            ok, X, Y, Z = colorCal.measure()

        Where:
            ok is True/False
            X, Y, Z are the CIE coordinates (Y is luminance in cd/m**2)

        Following a call to measure, the values ColorCAL.lastLum will also be
        populated with, for compatibility with other devices used by PsychoPy
        (notably the PR650/PR655)

        """
        # use a long timeout for measurement:
        val = self.sendMessage('MES', timeout=5)
        valstrip = val.strip('\n\r>')
        vals = valstrip.split(',')
        ok = (vals[0] == 'OK00')
        # transform raw x,y,z by calibration matrix
        xyzRaw = numpy.array([vals[1].strip(), vals[2].strip(), vals[3].strip()], dtype=float)
        X, Y, Z = numpy.dot(self.calibMatrix, xyzRaw)
        self.ok, self.lastLum = ok, Y
        return ok, X, Y, Z

    def getLum(self):
        """Conducts a measurement and returns the measured luminance

        .. note::
            The luminance is always also stored as .lastLum
        """
        self.measure()
        return self.lastLum

    def getInfo(self):
        """Queries the device for information

        usage::
            (ok, serialNumber,
                firmwareVersion, firmwareBuild) = colorCal.getInfo()

        `ok` will be True/False
        Other values will be a string or None.

        """
        val = self.sendMessage(b'IDR')
        valstrip = val.strip('\n\r>')
        val = valstrip.split(',')
        ok = (val[0] == 'OK00')
        if ok:
            firmware = val[2]
            serialNum = val[4]
            firmBuild = val[-1]
        else:
            firmware = 0
            serialNum = 0
            firmBuild = 0
        return ok, serialNum, firmware, firmBuild

    def getNeedsCalibrateZero(self):
        """Check whether the device needs a dark calibration

        In initial versions of CRS ColorCAL mkII the device stored
        its zero calibration in volatile memory and needed to be
        calibrated in darkness each time you connected it to the USB

        This function will check whether your device requires that (based
        on firmware build number and whether you've already done it
        since python connected to the device).

        :returns: True or False
        """

        if self.firmBuild < '877' and not self._zeroCalibrated:
            return True
        else:
            return False

    def calibrateZero(self):
        """Perform a calibration to zero light.

        For early versions of the ColorCAL this had to be called after
        connecting to the device. For later versions the dark calibration
        was performed at the factory and stored in non-volatile memory.

        You can check if you need to run a calibration with::

            ColorCAL.getNeedsCalibrateZero()
        """
        val = self.sendMessage(b"UZC", timeout=1.0)
        val = val.strip('\n\r>')
        if val == 'OK00':
            pass
        elif val == 'ER11':
            logging.error(
                "Could not calibrate ColorCAL2. Is it properly covered?")
            return False
        else:  # unlikely
            logging.warning(
                "Received surprising result from ColorCAL2: %s)" % repr(val))
            return False
        # then take a measurement to see if we are close to zero lum (ie is it
        self.ok, x, y, z = self.measure()
        if y > 3:
            logging.error('There seems to be some light getting to the '
                          'detector. It should be well-covered for zero '
                          'calibration')
            return False
        self._zeroCalibrated = True
        self.calibMatrix = self.getCalibMatrix()
        return True

    def getCalibMatrix(self):
        """Get the calibration matrix from the device, needed for transforming
        measurements into real-world values.

        This is normally retrieved during __init__ and stored as
        `ColorCal.calibMatrix` so most users don't need to call this function.
        """
        matrix = numpy.zeros((3, 3), dtype=float)
        # alternatively use 'r99' which gets all rows at once, but then more
        for rowN in range(3):
            rowName = 'r0%i' % (rowN + 1)
            val = self.sendMessage(rowName.encode('ascii'), timeout=1.0)
            valstrip = val.strip('\n\r>')
            vals = valstrip.split(',')  # convert to list of values
            if vals[0] == 'OK00' and len(vals) > 1:
                # convert to numpy array
                rawVals = numpy.array(vals[1:], dtype=int)
                floats = _minolta2float(rawVals)
                matrix[rowN, :] = floats
            else:
                msg = 'ColorCAL got this from command %s: %s'
                print(msg % (rowName, repr(val)))
        return matrix

    def _error(self, msg):
        self.OK = False
        logging.error(msg)

    def readline(self, size=None, eol='\n\r'):
        """This should be used in place of the standard serial.Serial.readline()
        because that doesn't allow us to set the eol character
        """
        # The code here is adapted from
        leneol = len(eol)
        line = bytearray()
        while True:
            # NB timeout is applied here, so to each char read
            c = self.com.read(1)
            if c:
                line += c
                if line[-leneol:] == eol:
                    break
                if size is not None and len(line) >= size:
                    break
            else:
                break
        return bytes(line).strip()

"""Takes a number, or numeric array (any shape) and returns the appropriate
    float.

    minolta stores;
        +ve values as val * 10000
        -ve values as -val * 10000 + 50000

    >>> _minolta2Float(50347)  # NB returns a single float
    -0.034700000000000002
    >>> _minolta2Float(10630)
    1.0630999999999999
    >>> _minolta2Float([10635, 50631])  # NB returns a numpy array
    array([ 1.0635, -0.0631])

    """
    # convert  to array if needed
    arr = numpy.asarray(inVal)
    # handle single vals
    if arr.shape == ():
        if inVal < 50000:
            return inVal/10000.0
        else:
            return (-inVal + 50000.0)/10000.0
    # handle arrays
    negs = (arr > 50000)  # find negative values
    out = arr/10000.0  # these are the positive values
    out[negs] = (-arr[negs] + 50000.0)/10000.0
    return out


"""Conducts a measurement and returns the measured luminance

        .. note::
            The luminance is always also stored as .lastLum
        """
        self.measure()
        return self.lastLum

# Part of the PsychoPy library

#    This code was mostly written by Jon Peirce.
#    CRS Ltd provided support as needed.

# Acknowledgements
#    This code was written by Jon Peirce

from __future__ import absolute_import, division, print_function

from builtins import bytes
from builtins import range
from builtins import object
import sys
try:
    import serial
except Exception:
    serial = False
import numpy

__docformat__ = "restructuredtext en"

# try to use psychopy logging but revert to system logging
try:
    from psychopy import logging  # from 1.73 onwards
except ImportError:
    import logging  # use the standard python logging
eol = "\n\r"  # unusual for a serial port?!

class ColorCAL(object):
    """A class to handle the CRS Ltd ColorCAL device
    """

    # PsychoPy uses these two variables for matching classes to photometers
    longName = "CRS ColorCAL"
    driverFor = ["colorcal"]

    def __init__(self, port=None, maxAttempts=2):
        """Open serial port connection with Colorcal II device

        :Usage:

            cc = ColorCAL(port, maxAttempts)

       If no port is provided then the following defaults will be tried:
           - /dev/cu.usbmodem0001 (OSX)
           - /dev/ttyACM0
           - COM3 (windows)

        """
        super(ColorCAL, self).__init__()

        if not serial:
            raise ImportError('The module serial is needed to connect to '
                              'photometers. On most systems this can be '
                              'installed with\n\t easy_install pyserial')

        # try to deduce port
        if port is None:
            if sys.platform == 'darwin':
                port = '/dev/cu.usbmodem0001'
            elif sys.platform.startswith('linux'):
                port = '/dev/ttyACM0'
            elif sys.platform.startswith('win'):
                port = 3
        if type(port) in (int, float):
            # add one so that port 1=COM1
            self.portNumber = port
            self.portString = 'COM%i' % self.portNumber
        else:
            self.portString = port
            self.portNumber = None

        self.isOpen = 0
        self.lastLum = None
        self.lastCmd = ''
        self.type = 'ColorCAL'
        self.com = False
        self.OK = True  # until we fail
        self.maxAttempts = maxAttempts
        self._zeroCalibrated = False

        # try to open the port
        try:
            self.com = serial.Serial(self.portString)
        except Exception:
            msg = ("Couldn't connect to port %s. Is it being used by "
                   "another program?")
            self._error(msg % self.portString)

        # setup the params for serial port
        if self.OK:
            self.com.close()  # not sure why this helps but on win32 it does!!
            try:
                self.com.setBaudrate(115200)  # actually, any baudrate is fine?
            except:
                self.com.baudrate = 115200  # setBaudrate() remov pyserial v3.0
            try:
                if not self.com.isOpen():
                    self.com.open()
            except Exception:
                msg = "Opened serial port %s, but couldn't connect to ColorCAL"
                self._error(msg % self.portString)
            else:
                self.isOpen = 1

        # check that we can communicate with it
        self.ok, self.serialNum, self.firm, self.firmBuild = self.getInfo()
        self.calibMatrix = self.getCalibMatrix()

    def sendMessage(self, message, timeout=0.1):
        """Send a command to the photometer and wait an alloted
        timeout for a response.
        """
        # flush the read buffer first
        # read as many chars as are in the buffer
        prevOut = self.com.read(self.com.inWaiting()).decode('utf-8')
        if len(prevOut) and prevOut not in ('>' + eol, eol):
            # do not use log messages here
            print('Resp found to prev cmd (%s):%s' % (self.lastCmd, prevOut))
        self.lastCmd = message

        if type(message) is not bytes:
            message = message.encode('utf-8')
        if not (message.endswith(b'\n') or message.endswith(b'\n\r')):
            message += b"\n"  # append a newline if necess
        # send the message
        self.com.write(message)
        self.com.flush()
        self.com.timeout = timeout
        logging.debug('Sent command:%s' % (message[:-1]))  # send complete msg

        # get output lines using self.readline, not self.com.readline
        # colorcal signals the end of a message by giving a command prompt
        lines = []
        thisLine = ''
        nEmpty = 0
        while (thisLine != '>') and (nEmpty <= self.maxAttempts):
            # self.com.readline can't handle custom eol
            thisLine = self.readline(eol=eol).decode('utf-8')
            if thisLine in (eol, '>', ''):  # lines we don't care about
                nEmpty += 1
                continue
            else:
                # line without any eol chars
                lines.append(thisLine.strip(eol))
                nEmpty = 0

        # got all lines and reached '>'
        if len(lines) == 1:
            return lines[0]  # return the string
        else:
            return lines  # a list of lines

    def measure(self):
        """Conduct a measurement and return the X,Y,Z values

        Usage::

            ok, X, Y, Z = colorCal.measure()

        Where:
            ok is True/False
            X, Y, Z are the CIE coordinates (Y is luminance in cd/m**2)

        Following a call to measure, the values ColorCAL.lastLum will also be
        populated with, for compatibility with other devices used by PsychoPy
        (notably the PR650/PR655)

        """
        # use a long timeout for measurement:
        val = self.sendMessage('MES', timeout=5)
        valstrip = val.strip('\n\r>')
        vals = valstrip.split(',')
        ok = (vals[0] == 'OK00')
        # transform raw x,y,z by calibration matrix
        xyzRaw = numpy.array([vals[1].strip(), vals[2].strip(), vals[3].strip()], dtype=float)
        X, Y, Z = numpy.dot(self.calibMatrix, xyzRaw)
        self.ok, self.lastLum = ok, Y
        return ok, X, Y, Z

    def getLum(self):

    def getInfo(self):
        """Queries the device for information

        usage::
            (ok, serialNumber,
                firmwareVersion, firmwareBuild) = colorCal.getInfo()

        `ok` will be True/False
        Other values will be a string or None.

        """
        val = self.sendMessage(b'IDR')
        valstrip = val.strip('\n\r>')
        val = valstrip.split(',')
        ok = (val[0] == 'OK00')
        if ok:
            firmware = val[2]
            serialNum = val[4]
            firmBuild = val[-1]
        else:
            firmware = 0
            serialNum = 0
            firmBuild = 0
        return ok, serialNum, firmware, firmBuild

    def getNeedsCalibrateZero(self):
        """Check whether the device needs a dark calibration

        In initial versions of CRS ColorCAL mkII the device stored
        its zero calibration in volatile memory and needed to be
        calibrated in darkness each time you connected it to the USB

        This function will check whether your device requires that (based
        on firmware build number and whether you've already done it
        since python connected to the device).

        :returns: True or False
        """

        if self.firmBuild < '877' and not self._zeroCalibrated:
            return True
        else:
            return False

    def calibrateZero(self):
        """Perform a calibration to zero light.

        For early versions of the ColorCAL this had to be called after
        connecting to the device. For later versions the dark calibration
        was performed at the factory and stored in non-volatile memory.

        You can check if you need to run a calibration with::

            ColorCAL.getNeedsCalibrateZero()
        """
        val = self.sendMessage(b"UZC", timeout=1.0)
        val = val.strip('\n\r>')
        if val == 'OK00':
            pass
        elif val == 'ER11':
            logging.error(
                "Could not calibrate ColorCAL2. Is it properly covered?")
            return False
        else:  # unlikely
            logging.warning(
                "Received surprising result from ColorCAL2: %s)" % repr(val))
            return False
        # then take a measurement to see if we are close to zero lum (ie is it
        self.ok, x, y, z = self.measure()
        if y > 3:
            logging.error('There seems to be some light getting to the '
                          'detector. It should be well-covered for zero '
                          'calibration')
            return False
        self._zeroCalibrated = True
        self.calibMatrix = self.getCalibMatrix()
        return True

    def getCalibMatrix(self):
        """Get the calibration matrix from the device, needed for transforming
        measurements into real-world values.

        This is normally retrieved during __init__ and stored as
        `ColorCal.calibMatrix` so most users don't need to call this function.
        """
        matrix = numpy.zeros((3, 3), dtype=float)
        # alternatively use 'r99' which gets all rows at once, but then more
        for rowN in range(3):
            rowName = 'r0%i' % (rowN + 1)
            val = self.sendMessage(rowName.encode('ascii'), timeout=1.0)
            valstrip = val.strip('\n\r>')
            vals = valstrip.split(',')  # convert to list of values
            if vals[0] == 'OK00' and len(vals) > 1:
                # convert to numpy array
                rawVals = numpy.array(vals[1:], dtype=int)
                floats = _minolta2float(rawVals)
                matrix[rowN, :] = floats
            else:
                msg = 'ColorCAL got this from command %s: %s'
                print(msg % (rowName, repr(val)))
        return matrix

    def _error(self, msg):
        self.OK = False
        logging.error(msg)

    def readline(self, size=None, eol='\n\r'):
        """This should be used in place of the standard serial.Serial.readline()
        because that doesn't allow us to set the eol character
        """
        # The code here is adapted from
        leneol = len(eol)
        line = bytearray()
        while True:
            # NB timeout is applied here, so to each char read
            c = self.com.read(1)
            if c:
                line += c
                if line[-leneol:] == eol:
                    break
                if size is not None and len(line) >= size:
                    break
            else:
                break
        return bytes(line).strip()

def _minolta2float(inVal):
    """Takes a number, or numeric array (any shape) and returns the appropriate
    float.

    minolta stores;
        +ve values as val * 10000
        -ve values as -val * 10000 + 50000

    >>> _minolta2Float(50347)  # NB returns a single float
    -0.034700000000000002
    >>> _minolta2Float(10630)
    1.0630999999999999
    >>> _minolta2Float([10635, 50631])  # NB returns a numpy array
    array([ 1.0635, -0.0631])

    """
    # convert  to array if needed
    arr = numpy.asarray(inVal)
    # handle single vals
    if arr.shape == ():
        if inVal < 50000:
            return inVal/10000.0
        else:
            return (-inVal + 50000.0)/10000.0
    # handle arrays
    negs = (arr > 50000)  # find negative values
    out = arr/10000.0  # these are the positive values
    out[negs] = (-arr[negs] + 50000.0)/10000.0
    return out