class IPlanPy(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.CONNECTIONS_FILE = "wii.motes"
        self.card_id = 0
        self.wiimote = None
        self.ir_callback_count = 0
        self.old_x_coord = 0
        self.old_y_coord = 0
        self.all_cards = []
        self.default_delete_card_style = None

        self.my_vector_transform = VectorTransform()
        self.classifier = GestureClassifier()
        self.connections = ConnectionManager()
        self.classifier.register_callback(self.handle_shake_gesture)

        self.init_ui()
        self.load_available_charts()
        self.display_known_wiimotes()

    def display_known_wiimotes(self):
        content = self.get_all_known_connections()
        for address in content:
            self.ui.list_available_wiimotes.addItem(address)
        if len(content) > 0:
            self.ui.list_available_wiimotes.setCurrentRow(0)

    def save_connection_address(self, address):
        content = self.get_all_known_connections()
        if address not in content:
            with open(self.CONNECTIONS_FILE, "a") as f:
                f.write("\n" + address)
                f.close()

    def get_all_known_connections(self):
        with open(self.CONNECTIONS_FILE) as f:
            content = f.readlines()
            f.close()
        return [x.strip() for x in content]

    def init_ui(self):
        self.ui = uic.loadUi("iplanpy.ui", self)
        self.setMouseTracking(True)
        self.ui.fr_save_and_load.setVisible(False)
        self.ui.btn_connect_wiimote.clicked.connect(
            self.toggle_wiimote_connection)
        self.ui.btn_scan_wiimotes.clicked.connect(self.scan_for_wiimotes)
        self.ui.btn_toggle_connection_frame.clicked.connect(
            self.toggle_connection_frame)
        self.ui.btn_toggle_save_and_load_frame.clicked.connect(
            self.toggle_save_and_load_frame)
        self.ui.btn_load_chart.clicked.connect(self.load_chart)
        self.ui.btn_save.clicked.connect(self.on_btn_save_chart)
        self.ui.btn_new_chart.clicked.connect(self.on_btn_new_chart)
        self.default_delete_card_style = self.ui.delete_card.styleSheet()
        self.show()

    def on_btn_new_chart(self, event):
        self.remove_all_cards()
        self.connections.connections.clear()
        self.card_id = 0
        self.update()

    def toggle_connection_frame(self, event):
        value = not self.ui.fr_connection.isVisible()
        self.ui.fr_connection.setVisible(value)
        if value is True:
            self.ui.fr_connection.raise_()
            self.ui.fr_save_and_load.setVisible(not value)
        else:
            self.ui.fr_connection.lower()

    def toggle_save_and_load_frame(self, event):
        value = not self.ui.fr_save_and_load.isVisible()
        self.ui.fr_save_and_load.setVisible(value)
        if value is True:
            self.ui.fr_save_and_load.raise_()
            self.ui.fr_connection.setVisible(not value)
        else:
            self.ui.fr_save_and_load.lower()

    def load_chart(self, event):
        file_name = self.ui.list_chart_selection.currentItem()
        if file_name is not None:
            card_infos, conn_info = self.get_card_info_from_file(
                file_name.text())
            if card_infos is not None:
                try:
                    self.card_id = 0
                    self.remove_all_cards()
                    self.connections.connections.clear()
                    self.update()
                    self.create_card_from_file(card_infos)
                    self.create_conn_from_file(conn_info)
                    self.toggle_save_and_load_frame(None)
                except Exception as e:
                    msg = "Could not create chart from file " + file_name.text(
                    ) + "! Sorry!\nInformation: " + str(e)
                    QMessageBox.critical(self, "Error", msg)

    def create_card_from_file(self, card_infos):
        ids = []
        for info in card_infos:
            info = info.split(";")
            ids.append(int(info[0]))
            card = Card(self, int(info[0]), self.string_to_bool(info[5]))
            card.title_field.setText(info[1])
            card.content_field.setText(ast.literal_eval(info[2]))
            card.move_to(int(info[3]), int(info[4]))
            card.set_background_color(card.available_colors[int(info[6])])
            self.all_cards.append(card)
        self.card_id = max(ids) + 1

    def create_conn_from_file(self, conn_info):
        for info in conn_info:
            info = info.split(";")
            id1 = info[0]
            id2 = info[1]
            card1 = None
            card2 = None
            for card in self.all_cards:
                if str(card.id) == id1:
                    card1 = card
                if str(card.id) == id2:
                    card2 = card
            self.connections.connect((card1, card2))
        self.update()

    def string_to_bool(self, str):
        return str == "True"

    def get_card_info_from_file(self, file_name):
        try:
            conn_found = False
            card_info = []
            conn_info = []
            with open(file_name) as file:
                for line in file:
                    if line == "-\n":
                        conn_found = True
                        continue
                    if conn_found is False:
                        card_info.append(line)
                    else:
                        conn_info.append(line)
                return card_info, conn_info
        except Exception as e:
            QMessageBox.warning(
                self, "Warning",
                "Could not load chart!\nAdditional information:\n" + str(e))
            return None

    def remove_all_cards(self):
        for card in self.all_cards:
            card.delete()
        self.all_cards.clear()

    def on_btn_save_chart(self, event):
        file_name = self.ui.le_save_as.text()
        if file_name is not "":
            if os.path.isfile(file_name + ".chart") is True:
                msg = "Overwrite existing file?"
                res = QMessageBox.question(self, "Warning", msg,
                                           QMessageBox.Yes, QMessageBox.No)
                if res == QMessageBox.Yes:
                    self.save_chart(file_name)
                else:
                    return
            else:
                self.save_chart(file_name)
                QMessageBox.information(self, "Success", "Chart saved!")
        else:
            QMessageBox.warning(self, "Warning", "Choose a name first!")

    def save_chart(self, file_name):
        self.ui.le_save_as.setText("")
        try:
            with open(file_name + ".chart", "w") as new_file:
                self.write_card_data(new_file)
        except Exception as e:
            msg = "Could save file " + file_name + "! Sorry!\nInformation: " + str(
                e)
            QMessageBox.critical(self, "Error", msg)
        self.load_available_charts()

    def write_card_data(self, file):
        for card in self.all_cards:
            cid = card.id
            title = card.title_field.text()
            content = repr(card.content_field.toPlainText())
            x_pos = card.pos().x()
            y_pos = card.pos().y()
            card_type = str(card.has_text_field)
            color = card.color_index
            file.write(
                str(cid) + ";" + title + ";" + content + ";" + str(x_pos) +
                ";" + str(y_pos) + ";" + card_type + ";" + str(color) + ";\n")
        file.write("-\n")
        for conn in self.connections.connections:
            c1, c2 = conn
            file.write(str(c1.id) + ";" + str(c2.id) + ";\n")

    def load_available_charts(self):
        self.ui.list_chart_selection.clear()
        for file in os.listdir(os.getcwd()):
            if file.endswith(".chart"):
                self.ui.list_chart_selection.addItem(file)

    def scan_for_wiimotes(self, event):
        self.ui.btn_scan_wiimotes.setText("Scanning...")
        self.ui.list_available_wiimotes.clear()
        results = wiimote.find()
        for mote in results:
            address, name = mote
            self.ui.list_available_wiimotes.addItem(address)
        if len(results) > 0:
            self.ui.list_available_wiimotes.setCurrentRow(0)
        self.ui.btn_scan_wiimotes.setText("Scan")

    def toggle_wiimote_connection(self):
        if self.wiimote is not None:
            self.disconnect_wiimote()
            return
        self.connect_wiimote()

    def connect_wiimote(self):
        self.ui.btn_connect_wiimote.setText("Connecting...")
        current_item = self.ui.list_available_wiimotes.currentItem()
        if current_item is not None:
            address = current_item.text()
            if address is not "":
                try:
                    self.wiimote = wiimote.connect(address)
                except Exception:
                    QMessageBox.critical(
                        self, "Error", "Could not connect to " + address + "!")
                    self.ui.btn_connect_wiimote.setText("Connect")
                    return

                if self.wiimote is None:
                    self.ui.btn_connect_wiimote.setText("Connect")
                else:
                    self.ui.btn_connect_wiimote.setText("Disconnect")
                    self.ui.lbl_wiimote_address.setText("Connected to " +
                                                        address)
                    self.wiimote.buttons.register_callback(
                        self.on_wiimote_button)
                    self.wiimote.ir.register_callback(self.on_wiimote_ir)
                    self.wiimote.accelerometer.register_callback(
                        self.on_wiimote_accelerometer)
                    self.wiimote.rumble()
                    self.ui.fr_connection.setVisible(False)
                    self.save_connection_address(address)

    def disconnect_wiimote(self):
        self.wiimote.disconnect()
        self.wiimote = None
        self.ui.btn_connect_wiimote.setText("Connect")
        self.ui.lbl_wiimote_address.setText("Not connected")

    def on_wiimote_button(self, event):
        if len(event) is not 0:
            button, is_pressed = event[0]
            if is_pressed:
                card = self.get_card_under_mouse()
                if button == "B":
                    mouse_press_event = QtGui.QMouseEvent(
                        QtCore.QEvent.MouseButtonPress,
                        self.mapFromGlobal(
                            QtCore.QPoint(QtGui.QCursor.pos().x(),
                                          QtGui.QCursor.pos().y())),
                        QtCore.Qt.LeftButton, QtCore.Qt.LeftButton,
                        QtCore.Qt.NoModifier)
                    QtCore.QCoreApplication.postEvent(self, mouse_press_event)
                if button == 'Up' and (card is not None):
                    card.next_color()
                if button == "Down" and (card is not None):
                    card.previous_color()
                if (button == "Left" or button == "Right") and (card
                                                                is not None):
                    card.toggle_type()
                    self.update()
                if button == "Minus":
                    self.connections.remove_last_connection()
                    self.update()
                if button == "Plus":
                    self.connections.restore_connection()
                    self.update()
            else:
                if button == "B":
                    mouse_release_event = QtGui.QMouseEvent(
                        QtCore.QEvent.MouseButtonRelease,
                        self.mapFromGlobal(QtGui.QCursor.pos()),
                        QtCore.Qt.LeftButton, QtCore.Qt.LeftButton,
                        QtCore.Qt.NoModifier)
                    QtCore.QCoreApplication.postEvent(self,
                                                      mouse_release_event)

    def on_wiimote_ir(self, event):
        # Only use every fourth output from ir sensor
        if self.ir_callback_count % 4 == 0:
            if len(event) >= 4:
                x, y = self.my_vector_transform.transform(
                    event,
                    self.size().width(),
                    self.size().height())
                QtGui.QCursor.setPos(self.mapToGlobal(QtCore.QPoint(x, y)))
        self.ir_callback_count = self.ir_callback_count + 1

    def on_wiimote_accelerometer(self, event):
        self.classifier.add_accelerometer_data(event[0], event[1], event[2])

    def keyPressEvent(self, event):
        alt_modifier = (event.modifiers() & QtCore.Qt.AltModifier) != 0
        card = self.get_card_under_mouse()
        if card is not None:
            if alt_modifier and event.key() == QtCore.Qt.Key_Up:
                card.next_color()
            if alt_modifier and event.key() == QtCore.Qt.Key_Down:
                card.previous_color()
            if alt_modifier and (event.key() == QtCore.Qt.Key_Right):
                card.toggle_type()
                self.update()
            if alt_modifier and (event.key() == QtCore.Qt.Key_Left):
                card.toggle_type()
                self.update()

        if event.key() == QtCore.Qt.Key_Control:
            self.connections.remove_last_connection()
            self.update()
        if event.key() == QtCore.Qt.Key_Alt:
            self.connections.restore_connection()
            self.update()

    def mousePressEvent(self, event):
        if self.ui.btn_toggle_connection_frame.underMouse() is True:
            self.toggle_connection_frame(event)
        elif self.ui.btn_toggle_save_and_load_frame.underMouse() is True:
            self.toggle_save_and_load_frame(event)
        elif self.ui.btn_new_chart.underMouse() is True:
            self.on_btn_new_chart(event)

        actual_card = self.get_card_under_mouse()
        if actual_card is not None:
            self.clicked_card_pos = actual_card.pos().x(), actual_card.pos().y(
            )
            self.clicked_card_center = actual_card.center()
        self.__mousePressPos = None
        self.__mouseMovePos = None

        if event.button() == QtCore.Qt.LeftButton:
            for c in self.all_cards:
                c.unfocus()

            self.__mousePressPos = event.globalPos()
            self.__mouseMovePos = event.globalPos()
            if self.ui.lbl_new_card.underMouse():
                self.make_new_card(event)

            card = self.get_card_under_mouse()
            if card is not None:
                card.focus()

    def mouseMoveEvent(self, event):
        if (self.wiimote is not None
                and self.wiimote.buttons["B"]) or (event.buttons()
                                                   & QtCore.Qt.LeftButton):
            focused_card = self.get_focused_card()
            if focused_card is not None:
                self.handle_card_movement(event, focused_card)

        if event.buttons() == QtCore.Qt.LeftButton:
            currPos = self.mapToGlobal(self.pos())
            globalPos = event.globalPos()
            diff = globalPos - self.__mouseMovePos
            newPos = self.mapFromGlobal(currPos + diff)
            self.__mouseMovePos = globalPos

        self.old_y_coord = event.pos().y()
        self.old_x_coord = event.pos().x()

    # Returns the card under the mouse if there is one.
    def get_card_under_mouse(self):
        for c in self.all_cards:
            if c.underMouse() is True:
                return c
        return None

    # Returns the focused card if there is one focused.
    def get_focused_card(self):
        for c in self.all_cards:
            if c.is_focused is True:
                return c
        return None

    # Handles the card movement and collisions with the main window frame.
    def handle_card_movement(self, mouse_event, card):
        new_x = card.pos().x() + mouse_event.pos().x() - self.old_x_coord
        new_y = card.pos().y() + mouse_event.pos().y() - self.old_y_coord
        collides_with_ctrl_frame = card.collides_with(
            self.ui.fr_control_container, new_x, new_y)
        collides_with_main_frame = card.hits_window_frame(self, new_x, new_y)
        if not collides_with_ctrl_frame and not collides_with_main_frame:
            card.move_to(new_x, new_y)
        else:
            QtGui.QCursor.setPos(
                self.mapToGlobal(
                    QtCore.QPoint(self.old_x_coord, self.old_y_coord)))
        self.handle_delete_card_visual(card)
        self.update()

    def handle_delete_card_visual(self, card):
        if self.card_over_delete(card):
            self.ui.delete_card.setStyleSheet(
                "background-color:red; font-size:17px; color:white;")
        else:
            self.ui.delete_card.setStyleSheet(self.default_delete_card_style)

    def mouseReleaseEvent(self, event):
        if self.__mousePressPos is not None:
            moved = event.globalPos() - self.__mousePressPos
            self.check_release_position(event.pos().x(), event.pos().y())
            if moved.manhattanLength() > 3:
                event.ignore()
                return

    # Checks if the release position of the Drag and Drop requires a delete action or new connection.
    def check_release_position(self, posX, posY):
        self.check_for_delete()
        self.check_for_new_connection(posX, posY)

    # Builds a new card of the class Card.
    def make_new_card(self, event):
        card = Card(self, self.card_id)
        new_y = self.ui.fr_control_container.size().height() + 3
        card.move_to(event.pos().x(), new_y)
        self.all_cards.append(card)
        self.card_id = self.card_id + 1

    # Checks if card was released over the delete card button.
    def check_for_delete(self):
        card = self.get_card_under_mouse()
        if card is not None:
            # Cards left half has to be over the delete button in order to delete it.
            if self.card_over_delete(card):
                self.connections.delete_all_card_connections(card, False)
                card.delete()
                self.all_cards.remove(card)
                self.ui.delete_card.setStyleSheet(
                    self.default_delete_card_style)
                self.update()

    # Checks if left half of card is colliding with the delete card
    def card_over_delete(self, card):
        card_x = card.pos().x() + (card.size().width() / 2)
        card_y = card.pos().y()
        card_h = card.size().height()
        card_w = card.size().width() / 2
        del_x = self.ui.delete_card.pos().x()
        del_y = self.ui.delete_card.pos().y()
        del_h = self.ui.delete_card.size().height()
        del_w = self.ui.delete_card.size().width()
        return card_x < del_x + del_w and card_x + card_w > del_x and card_y < del_y + del_h and card_y + card_h > del_y

    # Checks if card was released over another card.
    def check_for_new_connection(self, posX, posY):
        current_card = self.get_card_under_mouse()
        if current_card is not None and len(self.all_cards) > 1:
            for c in self.all_cards:
                # Can collide with itself
                if c is current_card:
                    continue
                # Builds a new connection between the two collided cards.
                if current_card.collides(c):
                    self.connections.connect((current_card, c))
                    x, y = self.clicked_card_pos
                    current_card.move_to(x, y)
                    border = "1px solid black"
                    current_card.set_border(border)
                    c.set_border(border)
                    self.update()

    # Draw connections on every update
    def paintEvent(self, event):
        painter = QtGui.QPainter()
        painter.begin(self)
        pen = QPen()
        pen.setWidth(3)
        pen.setColor(QColor(0, 0, 0))
        painter.setPen(pen)
        for conn in self.connections.connections:
            card1, card2 = conn
            x1, y1 = card1.center()
            x2, y2 = card2.center()
            painter.drawLine(x1, y1, x2, y2)
        painter.end()

    # Callback of gestureclassifier. Gets called when classifier detects "shake" gesture.
    # Deletes all connections from currently focued card.
    def handle_shake_gesture(self):
        for card in self.all_cards:
            if card.is_focused is True:
                self.connections.delete_all_card_connections(card, True)
                self.update()