if True:
    from PyQt5.QtCore import pyqtSlot, QSettings
    from PyQt5.QtWidgets import QApplication, QDialog, QDialogButtonBox, QTableWidgetItem
    from PyQt5.QtXml import QDomDocument
else:
    from PyQt4.QtCore import pyqtSlot, QSettings
    from PyQt4.QtGui import QApplication, QDialog, QDialogButtonBox, QTableWidgetItem
    from PyQt4.QtXml import QDomDocument

# ------------------------------------------------------------------------------------------------------------
# Imports (Custom Stuff)

import ui_catarina
import ui_catarina_addgroup
import ui_catarina_removegroup
import ui_catarina_renamegroup
import ui_catarina_addport
import ui_catarina_removeport
import ui_catarina_renameport
import ui_catarina_connectports
import ui_catarina_disconnectports
from shared_canvasjack import *
from shared_settings import *

# ------------------------------------------------------------------------------------------------------------
# Try Import OpenGL

try:
    from PyQt5.QtOpenGL import QGLWidget
    hasGL = True
except:
    hasGL = False

# ------------------------------------------------------------------------------------------------------------
# Static Variables

iGroupId    = 0
iGroupName  = 1
iGroupSplit = 2
iGroupIcon  = 3

iGroupPosId  = 0
iGroupPosX_o = 1
iGroupPosY_o = 2
iGroupPosX_i = 3
iGroupPosY_i = 4

iPortGroup = 0
iPortId    = 1
iPortName  = 2
iPortMode  = 3
iPortType  = 4

iConnId     = 0
iConnOutput = 1
iConnInput  = 2

# ------------------------------------------------------------------------------------------------------------
# Add Group Dialog

class CatarinaAddGroupW(QDialog, ui_catarina_addgroup.Ui_CatarinaAddGroupW):
    def __init__(self, parent, group_list):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)

        self.m_group_list_names = []

        for group in group_list:
            self.m_group_list_names.append(group[iGroupName])

        self.accepted.connect(self.slot_setReturn)
        self.le_group_name.textChanged.connect(self.slot_checkText)

        self.ret_group_name = ""
        self.ret_group_split = False

    @pyqtSlot(str)
    def slot_checkText(self, text):
        check = bool(text and text not in self.m_group_list_names)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(check)

    @pyqtSlot()
    def slot_setReturn(self):
        self.ret_group_name = self.le_group_name.text()
        self.ret_group_split = self.cb_split.isChecked()

    def done(self, r):
        QDialog.done(self, r)
        self.close()

# ------------------------------------------------------------------------------------------------------------
# Remove Group Dialog

class CatarinaRemoveGroupW(QDialog, ui_catarina_removegroup.Ui_CatarinaRemoveGroupW):
    def __init__(self, parent, group_list):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)

        index = 0
        for group in group_list:
            twi_group_id = QTableWidgetItem(str(group[iGroupId]))
            twi_group_name = QTableWidgetItem(group[iGroupName])
            twi_group_split = QTableWidgetItem("Yes" if (group[iGroupSplit]) else "No")
            self.tw_group_list.insertRow(index)
            self.tw_group_list.setItem(index, 0, twi_group_id)
            self.tw_group_list.setItem(index, 1, twi_group_name)
            self.tw_group_list.setItem(index, 2, twi_group_split)
            index += 1

        self.accepted.connect(self.slot_setReturn)
        self.tw_group_list.cellDoubleClicked.connect(self.accept)
        self.tw_group_list.currentCellChanged.connect(self.slot_checkCell)

        self.ret_group_id = -1

    @pyqtSlot(int)
    def slot_checkCell(self, row):
        check = bool(row >= 0)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(check)

    @pyqtSlot()
    def slot_setReturn(self):
        if self.tw_group_list.rowCount() >= 0:
            self.ret_group_id = int(self.tw_group_list.item(self.tw_group_list.currentRow(), 0).text())

    def done(self, r):
        QDialog.done(self, r)
        self.close()

# ------------------------------------------------------------------------------------------------------------
# Rename Group Dialog

class CatarinaRenameGroupW(QDialog, ui_catarina_renamegroup.Ui_CatarinaRenameGroupW):
    def __init__(self, parent, group_list):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)

        self.m_group_list_names = []

        for group in group_list:
            self.cb_group_to_rename.addItem("%i - %s" % (group[iGroupId], group[iGroupName]))
            self.m_group_list_names.append(group[iGroupName])

        self.accepted.connect(self.slot_setReturn)
        self.cb_group_to_rename.currentIndexChanged[int].connect(self.slot_checkItem)
        self.le_new_group_name.textChanged.connect(self.slot_checkText)

        self.ret_group_id = -1
        self.ret_new_group_name = ""

    @pyqtSlot(int)
    def slot_checkItem(self, ignored):
        self.slot_checkText(self.le_new_group_name.text())

    @pyqtSlot(str)
    def slot_checkText(self, text):
        if self.cb_group_to_rename.count() > 0:
            group_name = self.cb_group_to_rename.currentText().split(" - ", 1)[1]
            check = bool(text and text != group_name and text not in self.m_group_list_names)
            self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(check)

    @pyqtSlot()
    def slot_setReturn(self):
        self.ret_group_id = int(self.cb_group_to_rename.currentText().split(" - ", 1)[0])
        self.ret_new_group_name = self.le_new_group_name.text()

    def done(self, r):
        QDialog.done(self, r)
        self.close()

# ------------------------------------------------------------------------------------------------------------
# Add Port Dialog

class CatarinaAddPortW(QDialog, ui_catarina_addport.Ui_CatarinaAddPortW):
    def __init__(self, parent, group_list, port_id):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.sb_port_id.setValue(port_id)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)

        for group in group_list:
            self.cb_group.addItem("%i - %s" % (group[iGroupId], group[iGroupName]))

        self.accepted.connect(self.slot_setReturn)
        self.le_port_name.textChanged.connect(self.slot_checkText)

        self.ret_group_id  = -1
        self.ret_port_name = ""
        self.ret_port_mode = patchcanvas.PORT_MODE_NULL
        self.ret_port_type = patchcanvas.PORT_TYPE_NULL

    @pyqtSlot(str)
    def slot_checkText(self, text):
        check = bool(text)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(check)

    @pyqtSlot()
    def slot_setReturn(self):
        if self.cb_group.count() > 0:
            self.ret_group_id = int(self.cb_group.currentText().split(" ", 1)[0])
            self.ret_port_name = self.le_port_name.text()
            self.ret_port_mode = patchcanvas.PORT_MODE_INPUT if self.rb_flags_input.isChecked() else patchcanvas.PORT_MODE_OUTPUT
            self.ret_port_type = self.cb_port_type.currentIndex() + 1 # 1, 2, 3 or 4 for patchcanvas types

    def done(self, r):
        QDialog.done(self, r)
        self.close()

# ------------------------------------------------------------------------------------------------------------
# Remove Port Dialog

class CatarinaRemovePortW(QDialog, ui_catarina_removeport.Ui_CatarinaRemovePortW):
    def __init__(self, parent, group_list, port_list):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.tw_port_list.setColumnWidth(0, 25)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)

        self.m_group_list = group_list
        self.m_port_list  = port_list

        self.accepted.connect(self.slot_setReturn)
        self.tw_port_list.cellDoubleClicked.connect(self.accept)
        self.tw_port_list.currentCellChanged.connect(self.slot_checkCell)
        self.rb_input.clicked.connect(self.slot_reAddPorts)
        self.rb_output.clicked.connect(self.slot_reAddPorts)
        self.rb_audio_jack.clicked.connect(self.slot_reAddPorts)
        self.rb_midi_jack.clicked.connect(self.slot_reAddPorts)
        self.rb_midi_a2j.clicked.connect(self.slot_reAddPorts)
        self.rb_midi_alsa.clicked.connect(self.slot_reAddPorts)

        self.ret_port_id = -1
        self.reAddPorts()

    def reAddPorts(self):
        self.tw_port_list.clearContents()
        for x in range(self.tw_port_list.rowCount()):
            self.tw_port_list.removeRow(0)

        port_mode = patchcanvas.PORT_MODE_INPUT if (self.rb_input.isChecked()) else patchcanvas.PORT_MODE_OUTPUT

        if self.rb_audio_jack.isChecked():
            port_type = patchcanvas.PORT_TYPE_AUDIO_JACK
        elif self.rb_midi_jack.isChecked():
            port_type = patchcanvas.PORT_TYPE_MIDI_JACK
        elif self.rb_midi_a2j.isChecked():
            port_type = patchcanvas.PORT_TYPE_MIDI_A2J
        elif self.rb_midi_alsa.isChecked():
            port_type = patchcanvas.PORT_TYPE_MIDI_ALSA
        else:
            print("CatarinaRemovePortW::reAddPorts() - Invalid port type")
            return

        index = 0
        for port in self.m_port_list:
            if port[iPortMode] == port_mode and port[iPortType] == port_type:
                port_name = port[iPortName]
                group_name = self.findPortGroupName(port[iPortGroup])
                tw_port_id = QTableWidgetItem(str(port[iPortId]))
                tw_port_name = QTableWidgetItem("%s:%s" % (group_name, port_name))
                self.tw_port_list.insertRow(index)
                self.tw_port_list.setItem(index, 0, tw_port_id)
                self.tw_port_list.setItem(index, 1, tw_port_name)
                index += 1

    def findPortGroupName(self, group_id):
        for group in self.m_group_list:
            if group[iGroupId] == group_id:
                return group[iGroupName]
        return ""

    @pyqtSlot()
    def slot_reAddPorts(self):
        self.reAddPorts()

    @pyqtSlot(int)
    def slot_checkCell(self, row):
        check = bool(row >= 0)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(check)

    @pyqtSlot()
    def slot_setReturn(self):
        if self.tw_port_list.rowCount() > 0:
            self.ret_port_id = int(self.tw_port_list.item(self.tw_port_list.currentRow(), 0).text())

    def done(self, r):
        QDialog.done(self, r)
        self.close()

# ------------------------------------------------------------------------------------------------------------
# Rename Port Dialog

class CatarinaRenamePortW(QDialog, ui_catarina_renameport.Ui_CatarinaRenamePortW):
    def __init__(self, parent, group_list, port_list):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.tw_port_list.setColumnWidth(0, 25)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)

        self.m_group_list = group_list
        self.m_port_list  = port_list

        self.accepted.connect(self.slot_setReturn)
        self.tw_port_list.currentCellChanged.connect(self.slot_checkCell)
        self.le_new_name.textChanged.connect(self.slot_checkText)

        self.rb_input.clicked.connect(self.slot_reAddPorts)
        self.rb_output.clicked.connect(self.slot_reAddPorts)
        self.rb_audio_jack.clicked.connect(self.slot_reAddPorts)
        self.rb_midi_jack.clicked.connect(self.slot_reAddPorts)
        self.rb_midi_a2j.clicked.connect(self.slot_reAddPorts)
        self.rb_midi_alsa.clicked.connect(self.slot_reAddPorts)

        self.ret_port_id = -1
        self.ret_new_port_name = ""
        self.reAddPorts()

    def reAddPorts(self):
        self.tw_port_list.clearContents()
        for x in range(self.tw_port_list.rowCount()):
            self.tw_port_list.removeRow(0)

        port_mode = patchcanvas.PORT_MODE_INPUT if (self.rb_input.isChecked()) else patchcanvas.PORT_MODE_OUTPUT

        if self.rb_audio_jack.isChecked():
            port_type = patchcanvas.PORT_TYPE_AUDIO_JACK
        elif self.rb_midi_jack.isChecked():
            port_type = patchcanvas.PORT_TYPE_MIDI_JACK
        elif self.rb_midi_a2j.isChecked():
            port_type = patchcanvas.PORT_TYPE_MIDI_A2J
        elif self.rb_midi_alsa.isChecked():
            port_type = patchcanvas.PORT_TYPE_MIDI_ALSA
        else:
            print("CatarinaRenamePortW::reAddPorts() - Invalid port type")
            return

        index = 0
        for port in self.m_port_list:
            if port[iPortMode] == port_mode and port[iPortType] == port_type:
                port_name = port[iPortName]
                group_name = self.findPortGroupName(port[iPortGroup])
                tw_port_id = QTableWidgetItem(str(port[iPortId]))
                tw_port_name = QTableWidgetItem("%s:%s" % (group_name, port_name))
                self.tw_port_list.insertRow(index)
                self.tw_port_list.setItem(index, 0, tw_port_id)
                self.tw_port_list.setItem(index, 1, tw_port_name)
                index += 1

        self.tw_port_list.setCurrentCell(0, 0)

    def findPortGroupName(self, group_id):
        for group in self.m_group_list:
            if group[iGroupId] == group_id:
                return group[iGroupName]
        return ""

    @pyqtSlot()
    def slot_reAddPorts(self):
        self.reAddPorts()

    @pyqtSlot()
    def slot_checkCell(self):
        self.slot_checkText(self.le_new_name.text())

    @pyqtSlot(str)
    def slot_checkText(self, text):
        check = bool(text and self.tw_port_list.currentRow() >= 0)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(check)

    @pyqtSlot()
    def slot_setReturn(self):
        if self.tw_port_list.rowCount() > 0:
            self.ret_port_id = int(self.tw_port_list.item(self.tw_port_list.currentRow(), 0).text())
            self.ret_new_port_name = self.le_new_name.text()

    def done(self, r):
        QDialog.done(self, r)
        self.close()

# ------------------------------------------------------------------------------------------------------------
# Connect Ports Dialog

class CatarinaConnectPortsW(QDialog, ui_catarina_connectports.Ui_CatarinaConnectPortsW):
    def __init__(self, parent, group_list, port_list):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)

        self.m_group_list = group_list
        self.m_port_list = port_list
        self.m_ports_audio_jack = []
        self.m_ports_midi_jack = []
        self.m_ports_midi_a2j = []
        self.m_ports_midi_alsa = []

        for port in self.m_port_list:
            if port[iPortType] == patchcanvas.PORT_TYPE_AUDIO_JACK:
                self.m_ports_audio_jack.append(port)
            elif port[iPortType] == patchcanvas.PORT_TYPE_MIDI_JACK:
                self.m_ports_midi_jack.append(port)
            elif port[iPortType] == patchcanvas.PORT_TYPE_MIDI_A2J:
                self.m_ports_midi_a2j.append(port)
            elif port[iPortType] == patchcanvas.PORT_TYPE_MIDI_ALSA:
                self.m_ports_midi_alsa.append(port)

        self.accepted.connect(self.slot_setReturn)
        self.rb_audio_jack.clicked.connect(self.slot_portTypeChanged)
        self.rb_midi_jack.clicked.connect(self.slot_portTypeChanged)
        self.rb_midi_a2j.clicked.connect(self.slot_portTypeChanged)
        self.rb_midi_alsa.clicked.connect(self.slot_portTypeChanged)
        self.lw_outputs.currentRowChanged.connect(self.slot_checkOutSelection)
        self.lw_inputs.currentRowChanged.connect(self.slot_checkInSelection)

        self.ret_port_out_id = -1
        self.ret_port_in_id = -1
        self.slot_portTypeChanged()

    def showPorts(self, ports):
        self.lw_outputs.clear()
        self.lw_inputs.clear()

        for port in ports:
            if port[iPortMode] == patchcanvas.PORT_MODE_INPUT:
                self.lw_inputs.addItem("%i - %s:%s" % (port[iPortId], self.findPortGroupName(port[iPortGroup]), port[iPortName]))
            elif port[iPortMode] == patchcanvas.PORT_MODE_OUTPUT:
                self.lw_outputs.addItem("%i - %s:%s" % (port[iPortId], self.findPortGroupName(port[iPortGroup]), port[iPortName]))

    def findPortGroupName(self, group_id):
        for group in self.m_group_list:
            if group[iGroupId] == group_id:
                return group[iGroupName]
        return ""

    def checkSelection(self, out_row, in_row):
        check = bool(out_row >= 0 and in_row >= 0)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(check)

    @pyqtSlot()
    def slot_portTypeChanged(self):
        if self.rb_audio_jack.isChecked():
            ports = self.m_ports_audio_jack
        elif self.rb_midi_jack.isChecked():
            ports = self.m_ports_midi_jack
        elif self.rb_midi_a2j.isChecked():
            ports = self.m_ports_midi_a2j
        elif self.rb_midi_alsa.isChecked():
            ports = self.m_ports_midi_alsa
        else:
            print("CatarinaConnectPortstW::portTypeChanged() - Invalid port type")
            return

        self.showPorts(ports)

    @pyqtSlot(int)
    def slot_checkOutSelection(self, row):
        self.checkSelection(row, self.lw_inputs.currentRow())

    @pyqtSlot(int)
    def slot_checkInSelection(self, row):
        self.checkSelection(self.lw_outputs.currentRow(), row)

    @pyqtSlot()
    def slot_setReturn(self):
        if self.lw_outputs.currentRow() >= 0 and self.lw_inputs.currentRow() >= 0:
            self.ret_port_out_id = int(self.lw_outputs.currentItem().text().split(" - ", 1)[0])
            self.ret_port_in_id = int(self.lw_inputs.currentItem().text().split(" - ", 1)[0])

    def done(self, r):
        QDialog.done(self, r)
        self.close()

# ------------------------------------------------------------------------------------------------------------
# Disconnect Ports Dialog

class CatarinaDisconnectPortsW(QDialog, ui_catarina_disconnectports.Ui_CatarinaDisconnectPortsW):
    def __init__(self, parent, group_list, port_list, connection_list):
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.tw_connections.setColumnWidth(0, 225)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)

        self.m_group_list = group_list
        self.m_port_list  = port_list
        self.m_connection_list = connection_list

        self.accepted.connect(self.slot_setReturn)
        self.tw_connections.cellDoubleClicked.connect(self.accept)
        self.tw_connections.currentCellChanged.connect(self.slot_checkSelection)
        self.rb_audio_jack.clicked.connect(self.slot_portTypeChanged)
        self.rb_midi_jack.clicked.connect(self.slot_portTypeChanged)
        self.rb_midi_a2j.clicked.connect(self.slot_portTypeChanged)
        self.rb_midi_alsa.clicked.connect(self.slot_portTypeChanged)

        self.ret_port_out_id = -1
        self.ret_port_in_id = -1
        self.slot_portTypeChanged()

    def showPorts(self, ptype):
        self.tw_connections.clearContents()
        for x in range(self.tw_connections.rowCount()):
            self.tw_connections.removeRow(0)

        index = 0
        for connection in self.m_connection_list:
            if self.findPortType(connection[iConnOutput]) == ptype:
                port_out_id = connection[iConnOutput]
                port_out_name = self.findPortName(port_out_id)

                port_in_id = connection[iConnInput]
                port_in_name = self.findPortName(port_in_id)

                tw_port_out = QTableWidgetItem("%i - %s" % (port_out_id, port_out_name))
                tw_port_in  = QTableWidgetItem("%i - %s" % (port_in_id, port_in_name))

                self.tw_connections.insertRow(index)
                self.tw_connections.setItem(index, 0, tw_port_out)
                self.tw_connections.setItem(index, 1, tw_port_in)
                index += 1

    def findPortName(self, port_id):
        for port in self.m_port_list:
            if port[iPortId] == port_id:
                return "%s:%s" % (self.findPortGroupName(port[iPortGroup]), port[iPortName])
        return ""

    def findPortType(self, port_id):
        for port in self.m_port_list:
            if port[iPortId] == port_id:
                return port[iPortType]
        return patchcanvas.PORT_TYPE_NULL

    def findPortGroupName(self, group_id):
        for group in self.m_group_list:
            if group[iGroupId] == group_id:
                return group[iGroupName]
        return ""

    @pyqtSlot()
    def slot_portTypeChanged(self):
        if self.rb_audio_jack.isChecked():
            ptype = patchcanvas.PORT_TYPE_AUDIO_JACK
        elif self.rb_midi_jack.isChecked():
            ptype = patchcanvas.PORT_TYPE_MIDI_JACK
        elif self.rb_midi_a2j.isChecked():
            ptype = patchcanvas.PORT_TYPE_MIDI_A2J
        elif self.rb_midi_alsa.isChecked():
            ptype = patchcanvas.PORT_TYPE_MIDI_ALSA
        else:
            print("CatarinaDisconnectPortstW::portTypeChanged() - Invalid port type")
            return

        self.showPorts(ptype)

    @pyqtSlot(int)
    def slot_checkSelection(self, row):
        check = bool(row >= 0)
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(check)

    @pyqtSlot()
    def slot_setReturn(self):
        if self.tw_connections.currentRow() >= 0:
            self.ret_port_out_id = int(self.tw_connections.item(self.tw_connections.currentRow(), 0).text().split(" - ", 1)[0])
            self.ret_port_in_id  = int(self.tw_connections.item(self.tw_connections.currentRow(), 1).text().split(" - ", 1)[0])

    def done(self, r):
        QDialog.done(self, r)
        self.close()

# ------------------------------------------------------------------------------------------------------------
# Main Window

class CatarinaMainW(AbstractCanvasJackClass):
    def __init__(self, parent=None):
        AbstractCanvasJackClass.__init__(self, "Catarina", ui_catarina.Ui_CatarinaMainW, parent)

        self.loadSettings(True)

        # -------------------------------------------------------------
        # Set-up GUI

        setIcons(self, ("canvas",))

        self.ui.act_project_new.setIcon(getIcon("document-new"))
        self.ui.act_project_open.setIcon(getIcon("document-open"))
        self.ui.act_project_save.setIcon(getIcon("document-save"))
        self.ui.act_project_save_as.setIcon(getIcon("document-save-as"))
        self.ui.b_project_new.setIcon(getIcon("document-new"))
        self.ui.b_project_open.setIcon(getIcon("document-open"))
        self.ui.b_project_save.setIcon(getIcon("document-save"))
        self.ui.b_project_save_as.setIcon(getIcon("document-save-as"))

        self.ui.act_patchbay_add_group.setIcon(getIcon("list-add"))
        self.ui.act_patchbay_remove_group.setIcon(getIcon("edit-delete"))
        self.ui.act_patchbay_rename_group.setIcon(getIcon("edit-rename"))
        self.ui.act_patchbay_add_port.setIcon(getIcon("list-add"))
        self.ui.act_patchbay_remove_port.setIcon(getIcon("list-remove"))
        self.ui.act_patchbay_rename_port.setIcon(getIcon("edit-rename"))
        self.ui.act_patchbay_connect_ports.setIcon(getIcon("network-connect"))
        self.ui.act_patchbay_disconnect_ports.setIcon(getIcon("network-disconnect"))
        self.ui.b_group_add.setIcon(getIcon("list-add"))
        self.ui.b_group_remove.setIcon(getIcon("edit-delete"))
        self.ui.b_group_rename.setIcon(getIcon("edit-rename"))
        self.ui.b_port_add.setIcon(getIcon("list-add"))
        self.ui.b_port_remove.setIcon(getIcon("list-remove"))
        self.ui.b_port_rename.setIcon(getIcon("edit-rename"))
        self.ui.b_ports_connect.setIcon(getIcon("network-connect"))
        self.ui.b_ports_disconnect.setIcon(getIcon("network-disconnect"))

        self.scene = patchcanvas.PatchScene(self, self.ui.graphicsView)
        self.ui.graphicsView.setScene(self.scene)
        self.ui.graphicsView.setRenderHint(QPainter.Antialiasing, bool(self.fSavedSettings["Canvas/Antialiasing"] == patchcanvas.ANTIALIASING_FULL))
        self.ui.graphicsView.setRenderHint(QPainter.TextAntialiasing, self.fSavedSettings["Canvas/TextAntialiasing"])
        if self.fSavedSettings["Canvas/UseOpenGL"] and hasGL:
            self.ui.graphicsView.setViewport(QGLWidget(self.ui.graphicsView))
            self.ui.graphicsView.setRenderHint(QPainter.HighQualityAntialiasing, self.fSavedSettings["Canvas/HighQualityAntialiasing"])

        p_options = patchcanvas.options_t()
        p_options.theme_name       = self.fSavedSettings["Canvas/Theme"]
        p_options.auto_hide_groups = self.fSavedSettings["Canvas/AutoHideGroups"]
        p_options.use_bezier_lines = self.fSavedSettings["Canvas/UseBezierLines"]
        p_options.antialiasing     = self.fSavedSettings["Canvas/Antialiasing"]
        p_options.eyecandy         = self.fSavedSettings["Canvas/EyeCandy"]

        p_features = patchcanvas.features_t()
        p_features.group_info   = False
        p_features.group_rename = True
        p_features.port_info    = True
        p_features.port_rename  = True
        p_features.handle_group_pos = True

        patchcanvas.setOptions(p_options)
        patchcanvas.setFeatures(p_features)
        patchcanvas.init("Catarina", self.scene, self.canvasCallback, DEBUG)

        self.ui.act_project_new.triggered.connect(self.slot_projectNew)
        self.ui.act_project_open.triggered.connect(self.slot_projectOpen)
        self.ui.act_project_save.triggered.connect(self.slot_projectSave)
        self.ui.act_project_save_as.triggered.connect(self.slot_projectSaveAs)
        self.ui.b_project_new.clicked.connect(self.slot_projectNew)
        self.ui.b_project_open.clicked.connect(self.slot_projectOpen)
        self.ui.b_project_save.clicked.connect(self.slot_projectSave)
        self.ui.b_project_save_as.clicked.connect(self.slot_projectSaveAs)
        self.ui.act_patchbay_add_group.triggered.connect(self.slot_groupAdd)
        self.ui.act_patchbay_remove_group.triggered.connect(self.slot_groupRemove)
        self.ui.act_patchbay_rename_group.triggered.connect(self.slot_groupRename)
        self.ui.act_patchbay_add_port.triggered.connect(self.slot_portAdd)
        self.ui.act_patchbay_remove_port.triggered.connect(self.slot_portRemove)
        self.ui.act_patchbay_rename_port.triggered.connect(self.slot_portRename)
        self.ui.act_patchbay_connect_ports.triggered.connect(self.slot_connectPorts)
        self.ui.act_patchbay_disconnect_ports.triggered.connect(self.slot_disconnectPorts)
        self.ui.b_group_add.clicked.connect(self.slot_groupAdd)
        self.ui.b_group_remove.clicked.connect(self.slot_groupRemove)
        self.ui.b_group_rename.clicked.connect(self.slot_groupRename)
        self.ui.b_port_add.clicked.connect(self.slot_portAdd)
        self.ui.b_port_remove.clicked.connect(self.slot_portRemove)
        self.ui.b_port_rename.clicked.connect(self.slot_portRename)
        self.ui.b_ports_connect.clicked.connect(self.slot_connectPorts)
        self.ui.b_ports_disconnect.clicked.connect(self.slot_disconnectPorts)

        self.setCanvasConnections()

        self.ui.act_settings_configure.triggered.connect(self.slot_configureCatarina)

        self.ui.act_help_about.triggered.connect(self.slot_aboutCatarina)
        self.ui.act_help_about_qt.triggered.connect(app.aboutQt)

        self.SIGUSR1.connect(self.slot_projectSave)

        # Dummy timer to keep events active
        self.fUpdateTimer = self.startTimer(1000)

        # Start Empty Project
        self.slot_projectNew()

    def canvasCallback(self, action, value1, value2, value_str):
        if action == patchcanvas.ACTION_GROUP_INFO:
            pass

        elif action == patchcanvas.ACTION_GROUP_RENAME:
            group_id = value1
            new_group_name = value_str

            for group in self.m_group_list:
                if group[iGroupName] == new_group_name:
                    QMessageBox.warning(self, self.tr("Warning"), self.tr("There is already a group with this name"))
                    return

            patchcanvas.renameGroup(group_id, new_group_name)

            for group in self.m_group_list:
                if group[iGroupId] == group_id:
                    group[iGroupName] = new_group_name
                    break

        elif action == patchcanvas.ACTION_GROUP_SPLIT:
            group_id = value1
            patchcanvas.splitGroup(group_id)

            for group in self.m_group_list:
                if group[iGroupId] == group_id:
                    group[iGroupSplit] = True
                    break

        elif action == patchcanvas.ACTION_GROUP_JOIN:
            group_id = value1
            patchcanvas.joinGroup(group_id)

            for group in self.m_group_list:
                if group[iGroupId] == group_id:
                    group[iGroupSplit] = False
                    break

        elif action == patchcanvas.ACTION_PORT_INFO:
            port_id = value1

            group_id   = 0
            group_name = ""
            port_name  = ""
            port_mode  = patchcanvas.PORT_MODE_NULL
            port_type  = patchcanvas.PORT_TYPE_NULL

            for port in self.m_port_list:
                if port[iPortId] == port_id:
                    group_id  = port[iPortGroup]
                    port_name = port[iPortName]
                    port_mode = port[iPortMode]
                    port_type = port[iPortType]
                    break

            for group in self.m_group_list:
                if group[iGroupId] == group_id:
                    group_name = group[iGroupName]
                    break

            if port_mode == patchcanvas.PORT_MODE_INPUT:
                mode_text = self.tr("Input")
            elif port_mode == patchcanvas.PORT_MODE_OUTPUT:
                mode_text = self.tr("Output")
            else:
                mode_text = self.tr("Unknown")

            if port_type == patchcanvas.PORT_TYPE_AUDIO_JACK:
                type_text = self.tr("JACK Audio")
            elif port_type == patchcanvas.PORT_TYPE_MIDI_JACK:
                type_text = self.tr("JACK MIDI")
            elif port_type == patchcanvas.PORT_TYPE_MIDI_A2J:
                type_text = self.tr("A2J MIDI")
            elif port_type == patchcanvas.PORT_TYPE_MIDI_ALSA:
                type_text = self.tr("ALSA MIDI")
            else:
                type_text = self.tr("Unknown")

            port_full_name = group_name + ":" + port_name

            info = self.tr(""
                           "<table>"
                           "<tr><td align='right'><b>Group Name:</b></td><td> %1</td></tr>"
                           "<tr><td align='right'><b>Group ID:</b></td><td> %2</td></tr>"
                           "<tr><td align='right'><b>Port Name:</b></td><td> %3</td></tr>"
                           "<tr><td align='right'><b>Port ID:</b></td><td> %4</i></td></tr>"
                           "<tr><td align='right'><b>Full Port Name:</b></td><td> %5</td></tr>"
                           "<tr><td colspan='2'> </td></tr>"
                           "<tr><td align='right'><b>Port Mode:</b></td><td> %6</td></tr>"
                           "<tr><td align='right'><b>Port Type:</b></td><td> %7</td></tr>"
                           "</table>"
            ).arg(group_name).arg(group_id).arg(port_name).arg(port_id).arg(port_full_name).arg(mode_text).arg(type_text)

            QMessageBox.information(self, self.tr("Port Information"), info)

        elif action == patchcanvas.ACTION_PORT_RENAME:
            port_id = value1
            new_port_name = value_str
            patchcanvas.renamePort(port_id, new_port_name)

            for port in self.m_port_list:
                if port[iPortId] == port_id:
                    port[iPortName] = new_port_name
                    break

        elif action == patchcanvas.ACTION_PORTS_CONNECT:
            connection_id = self.m_last_connection_id
            port_out_id   = value1
            port_in_id    = value2
            patchcanvas.connectPorts(connection_id, port_out_id, port_in_id)

            conn_obj = [None, None, None]
            conn_obj[iConnId]     = connection_id
            conn_obj[iConnOutput] = port_out_id
            conn_obj[iConnInput]  = port_in_id

            self.m_connection_list.append(conn_obj)
            self.m_last_connection_id += 1

        elif action == patchcanvas.ACTION_PORTS_DISCONNECT:
            connection_id = value1
            patchcanvas.disconnectPorts(connection_id)

            for connection in self.m_connection_list:
                if connection[iConnId] == connection_id:
                    self.m_connection_list.remove(connection)
                    break

    def initPorts(self):
        for group in self.m_group_list:
            patchcanvas.addGroup(group[iGroupId], group[iGroupName], patchcanvas.SPLIT_YES if (group[iGroupSplit]) else patchcanvas.SPLIT_NO, group[iGroupIcon])

        for group_pos in self.m_group_list_pos:
            patchcanvas.setGroupPosFull(group_pos[iGroupPosId], group_pos[iGroupPosX_o], group_pos[iGroupPosY_o], group_pos[iGroupPosX_i], group_pos[iGroupPosY_i])

        for port in self.m_port_list:
            patchcanvas.addPort(port[iPortGroup], port[iPortId], port[iPortName], port[iPortMode], port[iPortType])

        for connection in self.m_connection_list:
            patchcanvas.connectPorts(connection[iConnId], connection[iConnOutput], connection[iConnInput])

        self.m_group_list_pos = []
        patchcanvas.updateZValues()

    def saveFile(self, path):
        content = ("<?xml version='1.0' encoding='UTF-8'?>\n"
                   "<!DOCTYPE CATARINA>\n"
                   "<CATARINA VERSION='%s'>\n" % VERSION)

        content += " <Groups>\n"
        for i in range(len(self.m_group_list)):
            group = self.m_group_list[i]
            group_id    = group[iGroupId]
            group_name  = group[iGroupName]
            group_split = group[iGroupSplit]
            group_icon  = group[iGroupIcon]
            group_pos_i = patchcanvas.getGroupPos(group_id, patchcanvas.PORT_MODE_INPUT)
            group_pos_o = patchcanvas.getGroupPos(group_id, patchcanvas.PORT_MODE_OUTPUT)
            content    += "  <g%i> <name>%s</name> <data>%i:%i:%i:%f:%f:%f:%f</data> </g%i>\n" % (i, group_name, group_id, group_split, group_icon, group_pos_o.x(), group_pos_o.y(), group_pos_i.x(), group_pos_i.y(), i)
        content += " </Groups>\n"

        content += " <Ports>\n"
        for i in range(len(self.m_port_list)):
            port = self.m_port_list[i]
            content += "  <p%i> <name>%s</name> <data>%i:%i:%i:%i</data> </p%i>\n" % (i, port[iPortName], port[iPortGroup], port[iPortId], port[iPortMode], port[iPortType], i)
        content += " </Ports>\n"

        content += " <Connections>\n"
        for i in range(len(self.m_connection_list)):
            connection = self.m_connection_list[i]
            content += "  <c%i>%i:%i:%i</c%i>\n" % (i, connection[iConnId], connection[iConnOutput], connection[iConnInput], i)
        content += " </Connections>\n"

        content += "</CATARINA>\n"

        try:
            fd = uopen(path, "w")
            fd.write(content)
            fd.close()
        except:
            QMessageBox.critical(self, self.tr("Error"), self.tr("Failed to save file"))

    def loadFile(self, path):
        if not os.path.exists(path):
            QMessageBox.critical(self, self.tr("Error"), self.tr("The file '%s' does not exist" % path))
            self.m_save_path = None
            return

        try:
            fd = uopen(path, "r")
            readState = fd.read()
            fd.close()
        except:
            QMessageBox.critical(self, self.tr("Error"), self.tr("Failed to load file"))
            self.m_save_path = None
            return

        self.m_save_path       = path
        self.m_group_list      = []
        self.m_group_list_pos  = []
        self.m_port_list       = []
        self.m_connection_list = []
        self.m_last_group_id   = 1
        self.m_last_port_id    = 1
        self.m_last_connection_id = 1

        xml = QDomDocument()
        xml.setContent(readState.encode("utf-8"))

        content = xml.documentElement()
        if content.tagName() != "CATARINA":
            QMessageBox.critical(self, self.tr("Error"), self.tr("Not a valid Catarina file"))
            return

        # Get values from XML - the big code
        node = content.firstChild()
        while not node.isNull():
            if node.toElement().tagName() == "Groups":
                group_name = ""
                groups = node.toElement().firstChild()
                while not groups.isNull():
                    group = groups.toElement().firstChild()
                    while not group.isNull():
                        tag = group.toElement().tagName()
                        text = group.toElement().text()
                        if tag == "name":
                            group_name = text
                        elif tag == "data":
                            group_data = text.split(":")
                            if len(group_data) == 7 and group_data[0].isdigit() and group_data[1].isdigit() and group_data[2].isdigit() and isNumber(group_data[3]) and isNumber(group_data[4]) and isNumber(group_data[5]) and isNumber(group_data[6]):
                                group_obj = [None, None, None, None]
                                group_obj[iGroupId]    = int(group_data[0])
                                group_obj[iGroupName]  = group_name
                                group_obj[iGroupSplit] = int(group_data[1])
                                group_obj[iGroupIcon]  = int(group_data[2])

                                group_pos_obj = [None, None, None, None, None]
                                group_pos_obj[iGroupPosId]  = int(group_data[0])
                                group_pos_obj[iGroupPosX_o] = float(group_data[3])
                                group_pos_obj[iGroupPosY_o] = float(group_data[4])
                                group_pos_obj[iGroupPosX_i] = float(group_data[5])
                                group_pos_obj[iGroupPosY_i] = float(group_data[6])

                                self.m_group_list.append(group_obj)
                                self.m_group_list_pos.append(group_pos_obj)

                                group_id = group_obj[iGroupId]
                                if group_id > self.m_last_group_id:
                                    self.m_last_group_id = group_id + 1
                        group = group.nextSibling()
                    groups = groups.nextSibling()

            elif node.toElement().tagName() == "Ports":
                port_name = ""
                ports = node.toElement().firstChild()
                while not ports.isNull():
                    port = ports.toElement().firstChild()
                    while not port.isNull():
                        tag = port.toElement().tagName()
                        text = port.toElement().text()
                        if tag == "name":
                            port_name = text
                        elif tag == "data":
                            port_data = text.split(":")
                            if len(port_data) == 4 and port_data[0].isdigit() and port_data[1].isdigit() and port_data[2].isdigit() and port_data[3].isdigit():
                                new_port = [None, None, None, None, None]
                                new_port[iPortGroup] = int(port_data[0])
                                new_port[iPortId]    = int(port_data[1])
                                new_port[iPortName]  = port_name
                                new_port[iPortMode]  = int(port_data[2])
                                new_port[iPortType]  = int(port_data[3])
                                self.m_port_list.append(new_port)

                                if new_port[iPortId] > self.m_last_port_id:
                                    self.m_last_port_id = new_port[iPortId] + 1

                        port = port.nextSibling()
                    ports = ports.nextSibling()

            elif node.toElement().tagName() == "Connections":
                conns = node.toElement().firstChild()
                while not conns.isNull():
                    conn_data = conns.toElement().text().split(":")
                    if len(conn_data) == 3 and conn_data[0].isdigit() and conn_data[1].isdigit() and conn_data[2].isdigit():
                        conn_obj = [None, None, None]
                        conn_obj[iConnId]     = int(conn_data[0])
                        conn_obj[iConnOutput] = int(conn_data[1])
                        conn_obj[iConnInput]  = int(conn_data[2])

                        connection_id = conn_obj[iConnId]
                        self.m_connection_list.append(conn_obj)

                        if connection_id >= self.m_last_connection_id:
                            self.m_last_connection_id = connection_id + 1

                    conns = conns.nextSibling()
            node = node.nextSibling()

        self.m_last_group_id += 1
        self.m_last_port_id  += 1
        self.m_last_connection_id += 1

        patchcanvas.clear()
        self.initPorts()

        self.scene.zoom_fit()
        self.scene.zoom_reset()

    @pyqtSlot()
    def slot_projectNew(self):
        self.m_group_list      = []
        self.m_group_list_pos  = []
        self.m_port_list       = []
        self.m_connection_list = []
        self.m_last_group_id = 1
        self.m_last_port_id  = 1
        self.m_last_connection_id = 1
        self.m_save_path = None
        patchcanvas.clear()

    @pyqtSlot()
    def slot_projectOpen(self):
        path, _ = QFileDialog.getOpenFileName(self, self.tr("Load State"), filter=self.tr("Catarina XML Document (*.xml)"))
        if path:
            self.loadFile(path)

    @pyqtSlot()
    def slot_projectSave(self):
        if self.m_save_path:
            self.saveFile(self.m_save_path)
        else:
            self.slot_projectSaveAs()

    @pyqtSlot()
    def slot_projectSaveAs(self):
        path, _ = QFileDialog.getSaveFileName(self, self.tr("Save State"), filter=self.tr("Catarina XML Document (*.xml)"))
        if path:
            self.m_save_path = path
            self.saveFile(path)

    @pyqtSlot()
    def slot_groupAdd(self):
        dialog = CatarinaAddGroupW(self, self.m_group_list)
        if dialog.exec_():
            group_id     = self.m_last_group_id
            group_name   = dialog.ret_group_name
            group_split  = dialog.ret_group_split
            group_splitR = patchcanvas.SPLIT_YES if group_split else patchcanvas.SPLIT_NO
            group_icon   = patchcanvas.ICON_HARDWARE if group_split else patchcanvas.ICON_APPLICATION
            patchcanvas.addGroup(group_id, group_name, group_splitR, group_icon)

            group_obj = [None, None, None, None]
            group_obj[iGroupId]    = group_id
            group_obj[iGroupName]  = group_name
            group_obj[iGroupSplit] = group_split
            group_obj[iGroupIcon]  = group_icon

            self.m_group_list.append(group_obj)
            self.m_last_group_id += 1

    @pyqtSlot()
    def slot_groupRemove(self):
        if len(self.m_group_list) > 0:
            dialog = CatarinaRemoveGroupW(self, self.m_group_list)
            if dialog.exec_():
                group_id = dialog.ret_group_id

                # Remove port connections first
                for port in self.m_port_list:
                    if port[iPortGroup] == group_id:
                        port_id = port[iPortId]

                        h = 0
                        for i in range(len(self.m_connection_list)):
                            connection = self.m_connection_list[i-h]
                            if connection[iConnOutput] == port_id or connection[iConnInput] == port_id:
                                patchcanvas.disconnectPorts(connection[iConnId])
                                self.m_connection_list.pop(i-h)
                                h += 1

                # Remove ports
                h = 0
                for i in range(len(self.m_port_list)):
                    port = self.m_port_list[i-h]
                    if port[iPortGroup] == group_id:
                        port_id = port[iPortId]

                        patchcanvas.removePort(port[iPortId])
                        self.m_port_list.pop(i-h)
                        h += 1

                # Now remove group
                patchcanvas.removeGroup(group_id)

                for group in self.m_group_list:
                    if group[iGroupId] == group_id:
                        self.m_group_list.remove(group)
                        break

        else:
            QMessageBox.warning(self, self.tr("Warning"), self.tr("Please add a Group first!"))

    @pyqtSlot()
    def slot_groupRename(self):
        if len(self.m_group_list) > 0:
            dialog = CatarinaRenameGroupW(self, self.m_group_list)
            if dialog.exec_():
                group_id = dialog.ret_group_id
                new_group_name = dialog.ret_new_group_name
                patchcanvas.renameGroup(group_id, new_group_name)

                for group in self.m_group_list:
                    if group[iGroupId] == group_id:
                        group[iGroupName] = new_group_name
                        break

        else:
            QMessageBox.warning(self, self.tr("Warning"), self.tr("Please add a Group first!"))

    @pyqtSlot()
    def slot_portAdd(self):
        if len(self.m_group_list) > 0:
            dialog = CatarinaAddPortW(self, self.m_group_list, self.m_last_port_id)
            if dialog.exec_():
                group_id  = dialog.ret_group_id
                port_name = dialog.ret_port_name
                port_mode = dialog.ret_port_mode
                port_type = dialog.ret_port_type
                patchcanvas.addPort(group_id, self.m_last_port_id, port_name, port_mode, port_type)

                new_port = [None, None, None, None, None]
                new_port[iPortGroup] = group_id
                new_port[iPortId]    = self.m_last_port_id
                new_port[iPortName]  = port_name
                new_port[iPortMode]  = port_mode
                new_port[iPortType]  = port_type

                self.m_port_list.append(new_port)
                self.m_last_port_id += 1

        else:
            QMessageBox.warning(self, self.tr("Warning"), self.tr("Please add a Group first!"))

    @pyqtSlot()
    def slot_portRemove(self):
        if len(self.m_port_list) > 0:
            dialog = CatarinaRemovePortW(self, self.m_group_list, self.m_port_list)
            if dialog.exec_():
                port_id = dialog.ret_port_id

                h = 0
                for i in range(len(self.m_connection_list)):
                    connection = self.m_connection_list[i-h]
                    if connection[iConnOutput] == port_id or connection[iConnInput] == port_id:
                        patchcanvas.disconnectPorts(connection[iConnId])
                        self.m_connection_list.pop(i-h)
                        h += 1

                patchcanvas.removePort(port_id)

                for port in self.m_port_list:
                    if port[iPortId] == port_id:
                        self.m_port_list.remove(port)
                        break

        else:
            QMessageBox.warning(self, self.tr("Warning"), self.tr("Please add a Port first!"))

    @pyqtSlot()
    def slot_portRename(self):
        if len(self.m_port_list) > 0:
            dialog = CatarinaRenamePortW(self, self.m_group_list, self.m_port_list)
            if dialog.exec_():
                port_id = dialog.ret_port_id
                new_port_name = dialog.ret_new_port_name
                patchcanvas.renamePort(port_id, new_port_name)

                for port in self.m_port_list:
                    if port[iPortId] == port_id:
                        port[iPortName] = new_port_name
                        break

        else:
            QMessageBox.warning(self, self.tr("Warning"), self.tr("Please add a Port first!"))

    @pyqtSlot()
    def slot_connectPorts(self):
        if len(self.m_port_list) > 0:
            dialog = CatarinaConnectPortsW(self, self.m_group_list, self.m_port_list)
            if dialog.exec_():
                connection_id = self.m_last_connection_id
                port_out_id   = dialog.ret_port_out_id
                port_in_id    = dialog.ret_port_in_id

                for connection in self.m_connection_list:
                    if connection[iConnOutput] == port_out_id and connection[iConnInput] == port_in_id:
                        QMessageBox.warning(self, self.tr("Warning"), self.tr("Ports already connected!"))
                        return

                patchcanvas.connectPorts(connection_id, port_out_id, port_in_id)

                conn_obj = [None, None, None]
                conn_obj[iConnId]     = connection_id
                conn_obj[iConnOutput] = port_out_id
                conn_obj[iConnInput]  = port_in_id

                self.m_connection_list.append(conn_obj)
                self.m_last_connection_id += 1

        else:
            QMessageBox.warning(self, self.tr("Warning"), self.tr("Please add some Ports first!"))

    @pyqtSlot()
    def slot_disconnectPorts(self):
        if len(self.m_connection_list) > 0:
            dialog = CatarinaDisconnectPortsW(self, self.m_group_list, self.m_port_list, self.m_connection_list)
            if dialog.exec_():
                connection_id = -1
                port_out_id = dialog.ret_port_out_id
                port_in_id  = dialog.ret_port_in_id

                for connection in self.m_connection_list:
                    if connection[iConnOutput] == port_out_id and connection[iConnInput] == port_in_id:
                        connection_id = connection[iConnId]
                        self.m_connection_list.remove(connection)
                        break

                patchcanvas.disconnectPorts(connection_id)

        else:
            QMessageBox.warning(self, self.tr("Warning"), self.tr("Please make some Connections first!"))

    @pyqtSlot()
    def slot_configureCatarina(self):
        dialog = SettingsW(self, "catarina", hasGL)
        if dialog.exec_():
            self.loadSettings(False)
            patchcanvas.clear()

            p_options = patchcanvas.options_t()
            p_options.theme_name       = self.fSavedSettings["Canvas/Theme"]
            p_options.auto_hide_groups = self.fSavedSettings["Canvas/AutoHideGroups"]
            p_options.use_bezier_lines = self.fSavedSettings["Canvas/UseBezierLines"]
            p_options.antialiasing     = self.fSavedSettings["Canvas/Antialiasing"]
            p_options.eyecandy         = self.fSavedSettings["Canvas/EyeCandy"]

            patchcanvas.setOptions(p_options)
            patchcanvas.init("Catarina", self.scene, self.canvasCallback, DEBUG)

            self.initPorts()

    @pyqtSlot()
    def slot_aboutCatarina(self):
        QMessageBox.about(self, self.tr("About Catarina"), self.tr("<h3>Catarina</h3>"
                                                                   "<br>Version %s"
                                                                   "<br>Catarina is a testing ground for the 'PatchCanvas' module.<br>"
                                                                   "<br>Copyright (C) 2010-2022 falkTX") % VERSION)

    def saveSettings(self):
        settings = QSettings()

        settings.setValue("Geometry", self.saveGeometry())
        settings.setValue("ShowToolbar", self.ui.frame_toolbar.isVisible())

    def loadSettings(self, geometry):
        settings = QSettings()

        if geometry:
            self.restoreGeometry(settings.value("Geometry", b""))

            showToolbar = settings.value("ShowToolbar", True, type=bool)
            self.ui.act_settings_show_toolbar.setChecked(showToolbar)
            self.ui.frame_toolbar.setVisible(showToolbar)

        self.fSavedSettings = {
            "Canvas/Theme": settings.value("Canvas/Theme", patchcanvas.getDefaultThemeName(), type=str),
            "Canvas/AutoHideGroups": settings.value("Canvas/AutoHideGroups", False, type=bool),
            "Canvas/UseBezierLines": settings.value("Canvas/UseBezierLines", True, type=bool),
            "Canvas/EyeCandy": settings.value("Canvas/EyeCandy", patchcanvas.EYECANDY_SMALL, type=int),
            "Canvas/UseOpenGL": settings.value("Canvas/UseOpenGL", False, type=bool),
            "Canvas/Antialiasing": settings.value("Canvas/Antialiasing", patchcanvas.ANTIALIASING_SMALL, type=int),
            "Canvas/TextAntialiasing": settings.value("Canvas/TextAntialiasing", True, type=bool),
            "Canvas/HighQualityAntialiasing": settings.value("Canvas/HighQualityAntialiasing", False, type=bool)
        }

    def timerEvent(self, event):
        if event.timerId() == self.fUpdateTimer:
            self.update()
        QMainWindow.timerEvent(self, event)

    def closeEvent(self, event):
        self.saveSettings()
        patchcanvas.clear()
        QMainWindow.closeEvent(self, event)

#--------------- main ------------------
if __name__ == '__main__':
    # App initialization
    app = QApplication(sys.argv)
    app.setApplicationName("Catarina")
    app.setApplicationVersion(VERSION)
    app.setOrganizationName("Cadence")
    app.setWindowIcon(QIcon(":/scalable/catarina.svg"))

    # Show GUI
    gui = CatarinaMainW()

    # Set-up custom signal handling
    setUpSignals(gui)

    gui.show()

    if len(app.arguments()) > 1:
        if not app.arguments()[0].endswith("Python.exe"):
            gui.loadFile(app.arguments()[1])
        elif len(app.arguments()) > 2:
            gui.loadFile(app.arguments()[2])

    # App-Loop
    sys.exit(app.exec_())


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

# OpenPGPpy OpenPGPcard : OpenPGP smartcard communication library for Python
# Copyright (C) 2020-2022  BitLogiK
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>

from logging import getLogger
import time
from .der_coding import encode_der, decode_do

try:
    from smartcard.System import readers
    from smartcard.util import toBytes, toHexString
    from smartcard.Exceptions import CardConnectionException
except ModuleNotFoundError as exc:
    raise ModuleNotFoundError("pyscard not installed ?") from exc

logger = getLogger(__name__)

# Exception classes for OpenPGPcard

class PGPBaseException(Exception):
    pass

class PGPCardException(PGPBaseException):
    def __init__(self, sw_byte1, sw_byte2):
        self.sw_byte1 = sw_byte1
        self.sw_byte2 = sw_byte2
        self.sw_code = (sw_byte1 << 8) | sw_byte2
        self.message = "Error status : 0x%02X%02X" % (sw_byte1, sw_byte2)
        super().__init__(self.message)

class ConnectionException(PGPBaseException):
    pass

class BadInputException(PGPBaseException):
    pass

class DataException(PGPBaseException):
    pass

class PinException(PGPBaseException):
    def __init__(self, num_retries):
        self.retries_left = num_retries
        if num_retries >= 2:
            self.message = f"Wrong PIN. {num_retries} tries left"
        else:
            self.message = f"Wrong PIN. {num_retries} try left"
        super().__init__(self.message)

HEX_SYMBOLS = "0123456789abcdefABCDEF"
APDU_SHORT = 256
APDU_LONG = 65536

# Utils helpers

def ishex(istring):
    return all(c in HEX_SYMBOLS for c in istring)

def check_hex(func):
    """Decorator to check the first method argument
      is 2/4 string hex (a DO short address)
    Expands the hex string from 2 to 4 hex chars (adds leading 0)
    """

    def func_wrapper(*args):
        if len(args) < 2:
            BadInputException(
                "First argument must be filehex : 1 or 2 bytes hex string"
            )
        if not isinstance(args[1], str):
            BadInputException("filehex provided must be a string")
        args_list = [*args]
        if len(args_list[1]) == 2:
            # A single byte address : param_1=0
            args_list[1] = "00" + args_list[1]
        if len(args_list[1]) != 4 or not ishex(args_list[1]):
            raise BadInputException("filehex provided must be 2 or 4 hex chars")
        return func(*args_list)

    return func_wrapper

def to_list(binstr):
    return toBytes(binstr.hex())

def print_list(liststr):
    """Output a list pretty in the debug logger."""
    for item in liststr:
        logger.debug(" - %s", item)

# Core class OpenPGPcard

class OpenPGPcard:

    AppID = toBytes("D27600012401")
    default_manufacturer_name = "- unknown -"
    manufacturer_list = {
        0x0001: "PPC Card Systems",
        0x0002: "Prism",
        0x0003: "OpenFortress",
        0x0004: "Wewid",
        0x0005: "ZeitControl",
        0x0006: "Yubico",
        0x0007: "OpenKMS",
        0x0008: "LogoEmail",
        0x0009: "Fidesmo",
        0x000A: "Dangerous Things",
        0x000B: "Feitian Technologies",
        0x002A: "Magrathea",
        0x0042: "GnuPG",
        0x1337: "Warsaw Hackerspace",
        0x2342: "Warpzone",
        0x2C97: "Ledger",
        0x4354: "Confidential Technologies",
        0x5443: "TIF-IT",
        0x63AF: "Trustica",
        0xAFAF: "ANSSI",
        0xBA53: "c-base",
        0xBD0E: "Paranoidlabs",
        0xF517: "FSIJ",
        0xF5EC: "F-Secure",
    }

    def __init__(self, reader_index=None):
        applet_detected = False
        readers_list = readers()
        if len(readers_list) > 0:
            if reader_index is None:
                logger.debug("Trying to reach the OpenPGP app in all readers")
            logger.debug("Available readers :")
            print_list(readers_list)
            if reader_index is not None:
                if not isinstance(reader_index, int):
                    raise ValueError("reader_index must be int.")
                if reader_index < 0:
                    raise ValueError("reader_index is a positive index, starts at 0.")
                if len(readers_list) > reader_index:
                    readers_list = readers_list[reader_index : reader_index + 1]
                else:
                    raise ConnectionException("Reader index out of readers detected")
                logger.debug("Using reader index #%i", reader_index)
            for reader in readers_list:
                applet_detected = False
                try:
                    logger.debug("Trying with reader : %s", reader)
                    self.connection = reader.createConnection()
                    self.connection.connect()
                    apdu_select = [0x00, 0xA4, 0x04, 0x00]
                    self.send_apdu(apdu_select, OpenPGPcard.AppID)
                    applet_detected = hasattr(self, "connection")
                except Exception:
                    logger.debug("Fail with this reader")
                    if reader_index is not None:
                        raise ConnectionException("No OpenPGP applet on this reader.")
                    continue
                if applet_detected:
                    logger.debug("An OpenPGP applet detected, using %s", reader.name)
                    self.name = reader.name
                    break
        if applet_detected:
            self.longer = 0
            # Read device info
            self.get_identifier()
            self.get_application_data()
            self.get_length()
            self.get_features()

        else:
            raise ConnectionException("Can't find any OpenPGP device connected.")
        # The object has the following attributes :
        #  self.name = str, name of the device (or the card reader used)
        #  self.pgpvermaj = int, OpenPGP application major version (3)
        #  self.pgpvermin = int, OpenPGP application minor version
        #  self.pgpverstr = string, OpenPGP application "maj.min"
        #  self.manufacturer_id = string, hex string of the manufacturer ID "0xXXXX"
        #  self.manufacturer = string, name of the manufacturer (or "- unknown -")
        #  self.serial = int, serial number
        #  self.max_cmd : int, maximum command length
        #  self.max_rsp : int, maximum response length
        #  self.display : bool, has a display ?
        #  self.bio : bool, has a biometric sensor ?
        #  self.button : bool, has a button ?
        #  self.keypad : bool, has a keypad ?
        #  self.led : bool, has a LED ?
        #  self.speaker : bool, has a speaker ?
        #  self.mic : bool, has a microphone ?
        #  self.touchscreen : bool, has a touchescreen ?

    def __del__(self):
        """Disconnect device."""
        if hasattr(self, "connection"):
            del self.connection

    def send_apdu(self, apdu_header, cmd_data, exp_resp_len=0):
        """send APDU 7816-4 with extended length
        apdu_header    : [ INS, CLA, P1, P2 ] ISO7816 APDU header,
                         without length info (Lc nor Le)
        cmd_data field : bytes list of the command data
        exp_resp_len   : Expected response length, must be set to 65536
                         when expecting a long answser (with a short command)
        """
        len_data = len(cmd_data)
        # Lc is 1 or 3 bytes
        if len_data < APDU_SHORT and exp_resp_len <= APDU_SHORT:
            # Standard APDU : Lc 1 byte : short command and short response
            apdu = apdu_header + [len_data] + cmd_data
        elif len_data < APDU_LONG:
            # Extended APDU : Lc 3 bytes : extended command and extended response
            apdu = apdu_header + [0, len_data >> 8, len_data & 255] + cmd_data
        else:
            raise DataException("Command data too large")
        if exp_resp_len > 0:
            # Le present
            if exp_resp_len < APDU_SHORT:
                # Le fixed and short
                apdu += [exp_resp_len]
            elif exp_resp_len == APDU_SHORT:
                # Le short : max response len 255 bytes
                apdu += [0]
            elif exp_resp_len < APDU_LONG:
                # Le fixed and long
                apdu += [exp_resp_len >> 8, exp_resp_len & 255]
            elif exp_resp_len == APDU_LONG:
                # Le long : max response len 65535 bytes
                apdu += [0, 0]
            else:
                raise DataException("Expected data response too large")
        logger.debug(
            f" Sending 0x{apdu_header[1]:X} command with {len_data} bytes data"
        )
        if exp_resp_len > 0:
            logger.debug(f"  with Le={exp_resp_len}")
        logger.debug(f"-> {toHexString(apdu)}")
        t_env = time.time()
        try:
            data, sw_byte1, sw_byte2 = self.connection.transmit(apdu)
        except CardConnectionException:
            raise ConnectionException(
                "Error when communicating with the OpenGPG device."
            )
        t_ans = (time.time() - t_env) * 1000
        logger.debug(
            " Received %i bytes data : SW 0x%02X%02X - duration: %.1f ms"
            % (len(data), sw_byte1, sw_byte2, t_ans)
        )
        if len(data) > 0:
            logger.debug(f"<- {toHexString(data)}")
        while sw_byte1 == 0x61:
            t_env = time.time()
            datacompl, sw_byte1, sw_byte2 = self.connection.transmit(
                [0x00, 0xC0, 0, 0, 0]
            )
            t_ans = (time.time() - t_env) * 1000
            logger.debug(
                " Received remaining %i bytes : 0x%02X%02X - duration: %.1f ms"
                % (len(datacompl), sw_byte1, sw_byte2, t_ans)
            )
            logger.debug(f"<- {toHexString(datacompl)}")
            data += datacompl
        if sw_byte1 == 0x63 and sw_byte2 & 0xF0 == 0xC0:
            raise PinException(sw_byte2 - 0xC0)
        if sw_byte1 != 0x90 or sw_byte2 != 0x00:
            raise PGPCardException(sw_byte1, sw_byte2)
        return data

    @check_hex
    def select_data(self, filehex, param_1=0, param_2=4):
        """Select a data object : filehex is 2 bytes (4 string hex)."""
        apdu_command = [
            0x00,
            0xA5,
            param_1,
            param_2,
        ]
        data = toBytes("60 04 5C 02" + filehex)
        self.send_apdu(apdu_command, data)

    @check_hex
    def get_data(self, filehex, data_hex=""):
        """Binary read / ISO read the object"""
        logger.debug(f"Read Data {data_hex} in 0x{filehex}")
        param_1 = int(filehex[0:2], 16)
        param_2 = int(filehex[2:4], 16)
        apdu_command = [0x00, 0xCA, param_1, param_2]
        if len(data_hex) == 2:
            data_hex = "00" + data_hex
        dataresp = self.send_apdu(apdu_command, toBytes(data_hex), self.longer)
        return dataresp

    def get_next_data(self, param_1=0, param_2=0, data_hex=""):
        """Continue read."""
        logger.debug("Read next data %s", data_hex)
        apdu_command = [0x00, 0xCC, param_1, param_2]
        blkdata = self.send_apdu(apdu_command, toBytes(data_hex))
        return blkdata

    @check_hex
    def put_data(self, filehex, data_hex=""):
        logger.debug(f"Put data {data_hex} in 0x{filehex}")
        param_1 = int(filehex[0:2], 16)
        param_2 = int(filehex[2:4], 16)
        apdu_command = [0x00, 0xDA, param_1, param_2]  # or 0xDB command
        blkdata = self.send_apdu(apdu_command, toBytes(data_hex))
        return blkdata

    def get_identifier(self):
        """Full application identifier"""
        resp = self.get_data("4F")
        if len(resp) != 16:
            raise DataException("Application identifier data shall be 16 bytes long.")
        if resp[:6] != OpenPGPcard.AppID:
            raise DataException(
                "Start of application identifier data shall be the OpenGPG AID."
            )
        self.pgpvermaj = resp[6]
        self.pgpvermin = resp[7]
        self.pgpverstr = f"{resp[6]}.{resp[7]}"
        self.manufacturer_id = f"0x{resp[8]:02X}{resp[9]:02X}"
        manufacturer_id_int = int(self.manufacturer_id, 16)
        if manufacturer_id_int in OpenPGPcard.manufacturer_list:
            self.manufacturer = OpenPGPcard.manufacturer_list[manufacturer_id_int]
        else:
            self.manufacturer = OpenPGPcard.default_manufacturer_name
        self.serial = int.from_bytes(resp[10:14], "big")
        if self.pgpvermaj >= 3:
            self.longer = APDU_LONG
        logger.debug(f"PGP version : {self.pgpverstr}")
        logger.debug(f"Manufacturer : {self.manufacturer} ({self.manufacturer_id})")
        logger.debug(f"Serial : {self.serial}")

    def get_length(self):
        """Extended length info DO 7F66 : 0202 xxxx 0202 xxxx
        Also bit 7 in Application Data "0x73"
        """
        self.max_cmd = 256
        self.max_rsp = 256
        if self.pgpvermaj >= 3:
            resp = self.get_data("7F66")
            if len(resp) == 8:  # Simple DO
                self.max_cmd = int.from_bytes(resp[2:4], "big")
                self.max_rsp = int.from_bytes(resp[6:8], "big")
            elif len(resp) == 11 and resp[:3] == [0x7F, 0x66, 8]:  # Constructed DO
                self.max_cmd = int.from_bytes(resp[5:7], "big")
                self.max_rsp = int.from_bytes(resp[9:11], "big")
            else:
                raise DataException("Extended length info incorrect format.")

    def get_pwstatus(self):
        return self.get_data("C4")

    def get_features(self):
        """Features optional DO 7F74"""
        self.display = False
        self.bio = False
        self.button = False
        self.keypad = False
        self.led = False
        self.speaker = False
        self.mic = False
        self.touchscreen = False
        try:
            resp = self.get_data("7F74")
        except PGPCardException as exc:
            if exc.sw_code == 0x6B00 or exc.sw_code == 0x6A83 or exc.sw_code == 0x6A88:
                self.display_features()
                return
            raise
        if resp[:3] == [0x7F, 0x74, 3]:  # Turn constructed DO to simple DO
            resp = resp[3:]
        if resp[:2] != [0x81, 1]:
            raise DataException("Features data shall start with 0x81 0x01.")
        if len(resp) != 3:
            raise DataException("Features data shall be 3 bytes long.")
        feature_int = resp[2]

        def check_bit(integ, bit_pos):
            # Check bit 8..1
            powertwo = 1 << (bit_pos - 1)
            return (integ & powertwo) == powertwo

        self.display = check_bit(feature_int, 8)
        self.bio = check_bit(feature_int, 7)
        self.button = check_bit(feature_int, 6)
        self.keypad = check_bit(feature_int, 5)
        self.led = check_bit(feature_int, 4)
        self.speaker = check_bit(feature_int, 3)
        self.mic = check_bit(feature_int, 2)
        self.touchscreen = check_bit(feature_int, 1)
        self.display_features()

    def display_features(self):
        """Print features for debug"""

        def capability_message(capability):
            return "Yes" if capability else "No"

        # logger.debug("Display ? %s", capability_message(self.display))
        # logger.debug("Biometric sensor ? %s", capability_message(self.bio))
        logger.debug("Button ? %s", capability_message(self.button))
        # logger.debug("Keypad ? %s", capability_message(self.keypad))
        # logger.debug("LED ? %s", capability_message(self.led))
        # logger.debug("Speaker ? %s", capability_message(self.speaker))
        # logger.debug("Microphone ? %s", capability_message(self.mic))
        # logger.debug("TouchScreen ? %s", capability_message(self.touchscreen))

    def get_historical_bytes(self):
        """Historical bytes DO 5F52"""
        return self.get_data("5F52")

    def get_application_data(self):
        """Application Related Data DO 6E"""
        try:
            resp = self.get_data("6E")
        except PGPCardException as exc:
            if exc.sw_code == 0x6D00:
                # Retry after 3 seconds
                time.sleep(3)
                # Select again the applet
                self.send_apdu([0x00, 0xA4, 0x04, 0x00], OpenPGPcard.AppID)
                time.sleep(1)
                return self.get_application_data()
        app_rel_data = decode_do(resp)
        if resp[0] == 0x6E:
            app_rel_data = app_rel_data["6E"]
        # Set the attribute about max PW length
        pwstatus_data = bytes.fromhex(app_rel_data["73"]["C4"])
        if pwstatus_data[1] < 128:
            self.pw1_maxlen = pwstatus_data[1]
        if pwstatus_data[1] < 128:
            self.pw3_maxlen = pwstatus_data[3]
        return app_rel_data

    def terminate_df(self):
        self.send_apdu([0, 0xE6, 0, 0], [])

    def activate_file(self):
        self.send_apdu([0, 0x44, 0, 0], [])

    def reset(self, pin3):
        self.verify_pin(3, pin3)
        self.terminate_df()
        self.activate_file()

    def get_random(self, len_data):
        """Get challenge INS=0x84
        Return len bytes of random (not integer)
        ToDo : make it as optional, 6D00 error?
        """
        return bytes(self.send_apdu([0, 0x84, 0, 0], [], len_data))

    def get_pin_status(self, pin_bank):
        """Return remaining tries left for the given PIN bank address (1, 2 or 3)
        If return 0 : PIN is blocked, if 9000 : PIN has been verified
        """
        if self.pgpvermaj * 10000 + self.pgpvermin >= 30001:  # >= v 3.1
            try:
                self.verify_pin(pin_bank, "")
                return 9000
            except PinException as exc:
                return exc.retries_left
            except PGPCardException as exc:
                if exc.sw_code == 0x6983:
                    return 0
                if exc.sw_code != 0x6A80:
                    raise exc
        # Fallback to PW status "C4"
        resp = self.get_pwstatus()
        if len(resp) != 7:
            raise PGPCardException("Bad PW status status data")
        if pin_bank == 1:
            return resp[4]
        elif pin_bank == 3:
            return resp[6]
        raise PGPCardException("Only PW1 and PW3 are available for status")

    def change_pin(self, old_pin, new_pin, pin_index):
        """Change PIN index number (index : 1 or 3)."""
        if pin_index not in (3, 1):
            raise DataException("Bad PIN index, must be 1 or 3.")
        old_pin_bin = old_pin.encode("utf8")
        new_pin_bin = new_pin.encode("utf8")
        pin_min_len = 6
        if pin_index == 3:
            pin_min_len = 8
        if len(old_pin_bin) < pin_min_len or len(new_pin_bin) < pin_min_len:
            raise BadInputException(
                f"Bad PIN #{pin_index} length, must be {pin_min_len} bytes."
            )
        data = old_pin_bin + new_pin_bin
        self.send_apdu([0, 0x24, 0, 0x80 + pin_index], to_list(data))

    def verify_pin(self, pin_bank, pin_string):
        """Verify PIN code : pin_bank is 1, 2 or 3 for SW1, SW2 or SW3
        Call CHANGE REFERENCE DATA card command
        """
        if pin_bank not in (1, 2, 3):
            raise DataException("Bad PIN index, must be 1, 2 or 3.")
        if pin_string:
            self.send_apdu(
                [0, 0x20, 0, 0x80 + pin_bank], to_list(pin_string.encode("utf8"))
            )
        else:
            self.send_apdu([0, 0x20, 0, 0x80 + pin_bank], [])

    @check_hex
    def gen_key(self, keypos_hex):
        """Generate an asymmetric key pair in keypos slot address
        Digital signature : 0xB600 : gen key according to algorithm data in C1
        Confidentiality :   0xB800 : gen key according to algorithm data in C2
        Authentication  :   0xA400 : gen key according to algorithm data in C3
        """
        return bytes(
            self.send_apdu([0, 0x47, 0x80, 0], toBytes(keypos_hex), self.longer)
        )

    @check_hex
    def get_public_key(self, keypos_hex):
        """Get the public part of the key pair in keypos slot address"""
        return bytes(
            self.send_apdu([0, 0x47, 0x81, 0], toBytes(keypos_hex), self.longer)
        )

    def sign(self, data):
        """Sign data, with Compute Digital Signature command"""
        return bytes(self.send_apdu([0, 0x2A, 0x9E, 0x9A], to_list(data)))

    def sign_ec_der(self, hashdata):
        """Sign with ECDSA hash data and output signature as ASN1 DER encoded
        hashdata is the same size in bits of the EC key
        """
        return encode_der(self.sign(hashdata))

    def encipher(self):
        """Call ENC command
        ToDo
        """
        raise NotImplementedError()

    def decipher(self, data):
        return bytes(self.send_apdu([0, 0x2A, 0x80, 0x86], to_list(data)))

    def decipher_25519(self, ext_pubkey):
        """For ECDH with Curve25519
        ext_pubkey is a 32 bytes "x" public key
        """
        data_field = b"\xA6\x12\x7F\x49\x22\x86\x20" + ext_pubkey
        return self.decipher(data_field)


from celery import shared_task
from time import sleep
from .models import Movie

@shared_task
def increase_ranking():
    # increase_ranking: This task increases the ranking of upcoming movies by 10 every 5 minutes. It gets a list of
    # all upcoming movies from the database, iterates over them, and adds 10 to each movie's ranking field. The
    # changes are saved back to the database.

    upcoming_movies = Movie.objects.filter(status='upcoming')
    for movie in upcoming_movies:
        movie.ranking += 10
        movie.save()

@shared_task
def add(x, y):
    # add: This task simulates a long-running task by sleeping for 5 seconds, and then returns the sum of two numbers
    # x and y. This task is used as an example to demonstrate how to define a simple task with Celery.

    sleep(5)  # Simulate a long-running task
    return x + y


from bs4 import BeautifulSoup
import spacy
import os
#nlp = spacy.load("nl_core_news_lg")
nlp = spacy.load("en_core_web_lg")
import regex as re
from nltk import ngrams
import pickle
import json
from augment.replace import BertSampler
from sacremoses import MosesDetokenizer
md = MosesDetokenizer(lang='en')

def position_of_ngram(words, hyp):
    length = len(words)
    for i, sublist in enumerate((hyp[i:i + length] for i in range(len(hyp)))):
        if words == sublist:
            return i, i+length
    return None, None

def wordtokenizer(text):
    nlptext = nlp(text)
    #Tokenize the text using SpaCy
    tokenlist = [token.text for token in nlptext if token.text != ' ']
    for idx, token in enumerate(tokenlist):
        #SpaCy struggles with the E2E templates (e.g., __NAME__ ). As it tends to make all underscores separate tokens. Let's fix that.
        #First, we find the start of the template
        try:
            if (tokenlist[idx] == '_') and (tokenlist[idx+1] == '_'):
                wordgroup = tokenlist[idx]
                dellist = []
                nextidx = idx
                #Go to the next words after the start of the template until you reach the end (market by two underscores and a non-underscore word).
                #Then we will group all the separate tokens into the one template token, and use the collected index information to delete the part-of-template tokens.
                while True:
                    nextidx += 1
                    try:
                        if (nextidx+2 == len(tokenlist)) or ((tokenlist[nextidx] == '_') and (tokenlist[nextidx+1] == '_') and (tokenlist[nextidx+2] != '_')):
                            dellist = dellist + [nextidx, nextidx+1]
                            wordgroup += tokenlist[nextidx] + tokenlist[nextidx+1]
                            break
                        else:
                            dellist.append(nextidx)
                            wordgroup += tokenlist[nextidx]
                    except IndexError:
                        return ['ERROR ERROR']

                #We reverse the indexlist to make sure the deletion doesn't affect the next index
                tokenlist[idx] = wordgroup
                for delnum in dellist:
                    tokenlist[delnum] = ''
        except IndexError:
            return ['ERROR ERROR']

    tokenlist = [x for x in tokenlist if x != '']

    return tokenlist

def main():
    #Gather all E2E files
    filelist = []

    for path, subdirs, files in os.walk('C:/Users/cvdrl/Desktop/EnrichedE2E-main'):
        for name in files:
            if name.endswith('.xml'):
                filelist.append(os.path.join(path, name))

    allentrytemplateinfo = {}

    currentpath = os.getcwd()

    for e2efile in filelist:
        #Open the file, gather all entries, and all lexicalizations for that entry, then also find the template and text for that lexicalization
        with open(e2efile, 'rb') as f:
            soup = BeautifulSoup(f, 'lxml')
        entrylist = soup.find('entries').find_all('entry')
        fileentrytemplateinfo = []
        for entry in entrylist:
            targetlist = entry.find_all('target')
            entrytemplatelist = []
            for target in targetlist:
                targettext = target.find('text').text
                targettemplate = target.find('template').text
                #Tokenize the targettext and template the same way as will be done in the Data_Augmentation file
                tokentargettext = wordtokenizer(targettext)
                targettext = ' '.join(tokentargettext)
                tokentargettemplate = wordtokenizer(targettemplate)
                if (tokentargettemplate == ['ERROR ERROR']) or (tokentargettext == ['ERROR ERROR']):
                    continue

                targettemplatedict = {'eid': entry['eid'], 'lid': target['lid'], 'info': []}

                templateissue = 'n'

                #Iterate over the target text until the word index overlaps with a template indicator in the template text
                for wordidx, word in enumerate(tokentargettext):
                    try:
                        if re.search(r'(__[A-Z]+_?[A-Z]+?__)', tokentargettemplate[wordidx]):
                            templatedict = {'tag': re.search(r'(__[A-Z]+_?[A-Z]+?__)', tokentargettemplate[wordidx]).group(1), 'wordmatches': [tokentargettext[wordidx]], 'indices': [wordidx], 'text': targettext, 'template': targettemplate, 'text_tokenized': tokentargettext, 'template_tokenized': tokentargettemplate}
                            nextlist = tokentargettext[wordidx+1:].copy()

                            for nextwordidx, nextword in enumerate(nextlist):
                                #If there is no next word in the template text anymore, add all remaining words to the dict.
                                if wordidx + 1 >= len(tokentargettemplate):
                                    templatedict['wordmatches'].append(nextword)
                                    templatedict['indices'].append(wordidx+1 + nextwordidx)
                                #Else stop if the next template word is found.
                                elif nextword == tokentargettemplate[wordidx+1]:
                                    break
                                else:
                                    templatedict['wordmatches'].append(nextword)
                                    templatedict['indices'].append(wordidx+1 + nextwordidx)

                            targettemplatedict['info'].append(templatedict)

                            matchindices = templatedict['indices'].copy()
                            if len(matchindices) > 1:
                                matchindices = matchindices[1:]
                                for matchidx in matchindices:
                                    tokentargettemplate.insert(matchidx, '_FILLER_')
                    except IndexError:
                        #print(tokentargettemplate)
                        #print(tokentargettext)
                        #print(targettext)
                        #print(e2efile)
                        #exit(2)
                        templateissue = 'y'

                if templateissue == 'y':
                    continue

                #ADD INFORMATION IF THE TEXT OVERLAPS WITH THE DATA AND WHERE IT OVERLAPS, SO THAT WE CAN CHANGE THIS WITH THE DATA AUGMENTATION
                data_inputlist = entry.find('source').find_all('input')
                for data_input in data_inputlist:
                    #TRY TO FIND N-GRAM MATCHES FOR MAX, THEN FOR MAX-1, MAX-2, etc.
                    #Iterate over the template info we collected
                    for idx, template_input in enumerate(targettemplatedict['info']):
                        #If the template_tag matches the data tag, let's see if there's overlapping text
                        if template_input['tag'] == data_input['tag']:
                            targettemplatedict['info'][idx].update({'data': {'attribute': data_input['attribute'], 'tag': data_input['tag'], 'value': data_input['value']}})
                            lexlist = template_input['indices'].copy()
                            ngramrange = list(range(len(lexlist), 0, -1))
                            ngramfound = 'n'
                            for ngramlen in ngramrange:
                                if ngramfound == 'n':
                                    lexngramspositions = list(ngrams(lexlist, ngramlen))
                                    lexngramspositions = [list(x) for x in lexngramspositions]
                                    for lexngram in lexngramspositions:
                                        wordmatchstart, wordmatchend = position_of_ngram(lexngram, lexlist)
                                        wordmatchinput = template_input['wordmatches'][wordmatchstart:wordmatchend]
                                        tokeninput = wordtokenizer(data_input['value'])
                                        startposition, endposition = position_of_ngram(wordmatchinput, tokeninput)
                                        if startposition != None:
                                            ngramfound = 'y'
                                            targettemplatedict['info'][idx].update({'overlap': lexngram})
                                            break
                                if ngramfound == 'y':
                                    break

                #print(targettemplatedict)
                entrytemplatelist.append(targettemplatedict)
            fileentrytemplateinfo.append(entrytemplatelist)
        allentrytemplateinfo.update({e2efile: fileentrytemplateinfo})

    with open(currentpath + '/Data/AllEntryTemplateInfo.json', 'w') as outfile:
        json.dump(allentrytemplateinfo, outfile, indent=4, separators=(',', ': '))

def convert_data(candidate, targettemplatedict, idxlist):
    tokenizedcandidate = wordtokenizer(candidate)
    l = [1, 2, 3]
    datadict = {}
    for output_element in targettemplatedict['info']:
        replaceindices = []
        for idx in idxlist:
            if ('overlap' in output_element) and (idx in output_element['overlap']):
                replaceindices.append([output_element['overlap'].index(idx), idx])
        try:
            datavalue = output_element['data']['value']
        except KeyError:
            print('ERROR ERROR', flush=True)
            return 'ERROR ERROR'
        datavaluelist = wordtokenizer(datavalue)
        for replaceidx in replaceindices:
            datavaluelist[replaceidx[0]] = tokenizedcandidate[replaceidx[1]]

        datavaluestring = md.detokenize(datavaluelist)
        datadict.update({output_element['data']['attribute']: datavaluestring})

    datalist = []

    for entry in datadict:
        datalist.append(entry.upper() + '(' + entry + '="' + datadict[entry] + '")')

    datastring = ' '.join(datalist)

    return datastring

def data_augmentation(allentrytemplateinfo):
    candidates125list = []
    candidates250list = []
    candidates500list = []
    candidates1000list = []

    rep = BertSampler(sim_threshold=0.001)
    currentpath = os.getcwd()

    if os.path.isfile(currentpath + '/DonePickle.pkl'):
        previousdonelist = []
        with open(currentpath + '/DonePickle.pkl', 'rb') as fr:
            try:
                while True:
                    previousdonelist.append(pickle.load(fr))
            except EOFError:
                pass
        startsearch = 'y'
    else:
        startsearch = 'n'

    for entrytemplateidx, entrytemplate in enumerate(allentrytemplateinfo):
        for targettemplatedictidx, targettemplatedict in enumerate(entrytemplate):
            if startsearch == 'y':
                entryfound = 'n'
                for prevdone in previousdonelist:
                    if (entrytemplateidx == prevdone['entrytemplateidx']) and (targettemplatedictidx == prevdone['targettemplatedictidx']):
                        entryfound = 'y'
                        break
                if entryfound == 'y':
                    continue
                else:
                    startsearch = 'n'

            try:
                doc = nlp(targettemplatedict['info'][0]['text'])
            except IndexError:
                continue
            idxlist = []
            for idx, token in enumerate(doc):
                #print(token.text, token.lemma_, token.pos_, token.tag_, token.dep_, token.shape_, token.is_alpha, token.is_stop)
                if (token.tag_.startswith('NN')) or (token.pos_ == 'ADJ') or (token.pos_ == 'ADV') or (token.pos_ == 'NUM'): #or (token.pos_ == 'VERB'):
                    idxlist.append(idx)

            #candidateslist = [o for o in rep(targettemplatedict['info'][0]['text'], idxlist, 20, dropout=0.2)]

            candidateslist = [o for o in rep(targettemplatedict['info'][0]['text'], idxlist, 20, dropout=0.2)]
            print(candidateslist, flush=True)

            with open(currentpath + '/DonePickle.pkl', 'ab') as f:
                pickle.dump({'entrytemplateidx': entrytemplateidx, 'targettemplatedictidx': targettemplatedictidx, 'candidateslist': candidateslist, 'targettemplatedict': targettemplatedict, 'idxlist': idxlist}, f)

            for candidateidx, candidate in enumerate(candidateslist):
                candidatedatastring = convert_data(candidate, targettemplatedict, idxlist)

                if candidatedatastring == 'ERROR ERROR':
                    break

                elif candidateidx < 1:
                    candidates125list.append([candidate, candidatedatastring])
                    candidates250list.append([candidate, candidatedatastring])
                    candidates500list.append([candidate, candidatedatastring])
                    candidates1000list.append([candidate, candidatedatastring])
                elif candidateidx < 2:
                    candidates250list.append([candidate, candidatedatastring])
                    candidates500list.append([candidate, candidatedatastring])
                    candidates1000list.append([candidate, candidatedatastring])
                elif candidateidx < 5:
                    candidates500list.append([candidate, candidatedatastring])
                    candidates1000list.append([candidate, candidatedatastring])
                elif candidateidx < 10:
                    candidates1000list.append([candidate, candidatedatastring])
                else:
                    break

    rep = None  # NOTE: clear out GPU memory

    candidatesdict = {'125': candidates125list, '250': candidates250list, '500': candidates500list, '1000': candidates1000list}

    for candlist in candidatesdict:
        candidatestrg = [x[0] for x in candidatesdict[candlist]]
        candidatessrc = [x[1] for x in candidatesdict[candlist]]

        alltrgstring = '\n'.join(candidatestrg)
        allsrcstring = '\n'.join(candidatessrc)

        with open(currentpath + '/Predictions/Extended' + candlist + '_trg.txt', 'wb') as f:
            f.write(bytes(alltrgstring, 'UTF-8'))

        with open(currentpath + '/Predictions/Extended' + candlist + '_src.txt', 'wb') as f:
            f.write(bytes(allsrcstring, 'UTF-8'))

def collect_dict():
    currentpath = os.getcwd()

    with open(currentpath + '/Data/AllEntryTemplateInfo.json', 'r') as infile:
        allentrytemplateinfo = json.load(infile)

    fulltrain = []

    for e2efile in allentrytemplateinfo:
        if '\\train\\' in e2efile:
            fulltrain += allentrytemplateinfo[e2efile]

    data_augmentation(fulltrain)

#allentrytemplateinfo = main()
collect_dict()


# SPDX-License-Identifier: GPL-2.0-only

import threading
from pprint import pprint

import pytest
from flask import url_for

import libeagle
from tests.simulator import eagle200sim

import re

@pytest.fixture(scope="session", autouse=True)
def app():
    app = eagle200sim.create_app()
    return app

@pytest.mark.usefixtures("live_server")
class TestLiveServer:
    def test_eagle200(self):

        url = url_for("process_request", _external=True)
        port = int(re.search(":([0-9]+)/", url)[1])

        conn = libeagle.Connection("localhost", "0077dd", "6e61a3a94882eef9", port=port, debug=True)

        devices = conn.device_list()
        pprint(devices)

        details = conn.device_details(devices[0]["HardwareAddress"])
        pprint(details)

        query = conn.device_query(
            devices[0]["HardwareAddress"],
            details[0]["Name"],
            details[0]["Variables"][0],
        )
        pprint(query)

        assert (
            query[0]["Variables"]["zigbee:InstantaneousDemand"] == "21.499 kW"
        )

        query = conn.device_query(devices[0]["HardwareAddress"])
        pprint(query)

        assert (
            query[0]["Variables"]["zigbee:Message"] == "Hello, World!"
        )


import os

import pandas as pd
from darts import TimeSeries
from darts.models import LightGBMModel
from enfobench import AuthorInfo, ModelInfo, ForecasterType
from enfobench.evaluation.server import server_factory
from enfobench.evaluation.utils import periods_in_duration

class DartsLightGBMModel:
    def __init__(self, seasonality: str):
        self.seasonality = seasonality.upper()

    def info(self) -> ModelInfo:
        return ModelInfo(
            name=f"Darts.LightGBM.Direct.{self.seasonality}",
            authors=[
                AuthorInfo(name="Mohamad Khalil", email="coo17619@newcastle.ac.uk")
            ],
            type=ForecasterType.point,
            params={
                "seasonality": self.seasonality,
            },
        )

    def forecast(
        self,
        horizon: int,
        history: pd.DataFrame,
        past_covariates: pd.DataFrame | None = None,
        future_covariates: pd.DataFrame | None = None,
        **kwargs,
    ) -> pd.DataFrame:
        # Fill missing values
        history = history.fillna(history.y.mean())

        # Create model
        periods = periods_in_duration(history.index, duration=self.seasonality)
        model = LightGBMModel(
            lags=list(range(-periods, 0)),
            output_chunk_length=horizon,
            multi_models=False,
        )

        # Fit model
        series = TimeSeries.from_dataframe(history, value_cols=["y"])
        model.fit(series)

        # Make forecast
        pred = model.predict(horizon)

        # Postprocess forecast
        forecast = (
            pred.pd_dataframe().rename(columns={"y": "yhat"}).fillna(history.y.mean())
        )

        return forecast

# Load parameters
seasonality = os.getenv("ENFOBENCH_MODEL_SEASONALITY")

# Instantiate your model
model = DartsLightGBMModel(seasonality=seasonality)

# Create a forecast server by passing in your model
app = server_factory(model)