import os
from dataclasses import dataclass
from typing import Optional

from PyQt5.QtWidgets import QGraphicsScene, QGraphicsTextItem, QWizardPage, QGraphicsObject, QGraphicsView, QMessageBox, \
    QGraphicsLineItem
from PyQt5.QtGui import QBrush, QPen, QPainter, QColor, QFont
from PyQt5.QtCore import Qt, pyqtSignal, QRectF, QSizeF, QTimer

from qgis.PyQt import uic
from qgis.utils import canUninstallPlugin
from .name_mapper_controller import NameMapperController
from ....i18n import tr

FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), '..', 'name_mapper.ui'))


# ---------------------------------------------------------------------------
# Layout constants
# ---------------------------------------------------------------------------
MARGIN_LEFT = 30                      # left margin of canvas
MARGIN_RIGHT = 0                      # right margin of canvas
MARGIN_DELETE_LEFT = 10               # delete buttons margin of left side
GAP_TEXT_POINT = 10                   # Space between right border and the text and it's point.
GAP_COLUMNS = 120                     # Minimum distance between the left and right point columns.
GAP_POINT_TEXT_RIGHT = 20             # Space between right button and it's text.
GAP_DELETE_BUTTON = 8                 # Space between right border of the right text and it's delete button
HEADER_HEIGHT = 30                    # Reserved space for the header before START_HEIGHT.
CANVAS_MARGIN = 10                    # Extra margin around the bounding rect of the scene.
START_HEIGHT = 20 + HEADER_HEIGHT     # Initial height of the first element
HEIGHT_STEP = 40                      # Vertical separation between elements
HEIGHT_TEXT_OFFSET = -2               # Fine-tuning of text/point alignment
POINT_RADIUS = 15                     # Radius of the connection point.



# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
@dataclass
class RowItem:
    """Data class with the object necessary for a row"""
    text: QGraphicsTextItem
    point: "ConnectionPoint"
    delete_button: Optional["DeleteButton"]
    y: float


# ---------------------------------------------------------------------------
# Graphics items
# ---------------------------------------------------------------------------

class HeaderTextItem(QGraphicsTextItem):
    """
        Custom item for the header of the QGraphicsView
    """
    def __init__(self, text: str):
        """Constructor. Sets the default color, the font as bold."""
        super().__init__(text)
        font = QFont()
        font.setBold(True)
        self.setFont(font)
        self.setDefaultTextColor(QColor(60, 60, 60))


class SingleLineTextItem(QGraphicsTextItem):
    """
        Custom text that only uses one line no matters text length.
    """
    def __init__(self, text: str = ""):
        """
        Constructor. Sets the default color, the font as bold.
        """
        super().__init__(text)
        self.setTextWidth(-1)
        self.document().setPageSize(QSizeF(-1, -1))

    def keyPressEvent(self, event):
        if event.key() in (Qt.Key_Return, Qt.Key_Enter):
            event.accept()
            return
        super().keyPressEvent(event)


class AddButton(QGraphicsObject):
    """
        Custom class to paint a button as a green add button.
        :ivar clicked: signal launched when the button is clicked.
        :vartype clicked: pyqtSignal
        :ivar hover: indicates wheter the button is being hovered by the mouse or not.
        :vartype hover: bool
    """

    clicked = pyqtSignal(object)

    def __init__(self):
        """Consturctor"""
        super().__init__()
        self.hover = False
        self.setAcceptHoverEvents(True)
        self.setAcceptedMouseButtons(Qt.LeftButton)

    def boundingRect(self) -> QRectF:
        """Bounding rect of the button"""
        return QRectF(0, 0, 18, 18)

    def paint(self, painter: QPainter, option, widget):
        """
        Creates the + graphic.
        """
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setBrush(QBrush(QColor(34, 139, 34) if self.hover else QColor(60, 179, 60)))
        painter.setPen(Qt.NoPen)
        painter.drawRoundedRect(0, 0, 18, 18, 4, 4)
        painter.setPen(QPen(Qt.white, 2))
        painter.drawLine(9, 4, 9, 14)
        painter.drawLine(4, 9, 14, 9)

    def mousePressEvent(self, event):
        """When the button is clicked, launch the clicked event."""
        self.clicked.emit(self)

    def hoverEnterEvent(self, event):
        """
            On hover enter event.
        """
        self.hover = True
        self.update()

    def hoverLeaveEvent(self, event):
        """
            On hover leave event.
        """
        self.hover = False
        self.update()


class DeleteButton(QGraphicsObject):

    """
        Custom class to paint a button as red trash can.
        :ivar clicked: signal launched when the button is clicked.
        :vartype clicked: pyqtSignal
        :ivar hover: indicates whether the button is being hovered by the mouse or not.
        :vartype hover: bool
        :ivar my_text: text of the same row that accompanies the delete button.
        :vartype my_text: QGraphicsTextItem
        :ivar my_point: point of the same row that accompanies the delete button.
        :vartype my_point: ConnectionPoint
    """

    clicked = pyqtSignal(object)

    def __init__(self, text: QGraphicsTextItem, point: "ConnectionPoint"):
        """
            Constructor.
        """
        super().__init__()
        self.hover = False
        self.setAcceptHoverEvents(True)
        self.my_text = text
        self.my_point = point
        self.setFlag(QGraphicsObject.ItemIsSelectable, False)
        self.setCursor(Qt.PointingHandCursor)
        self.setFlag(QGraphicsObject.ItemIsMovable, False)
        self.setAcceptedMouseButtons(Qt.LeftButton)
        self.setEnabled(True)
        self.setActive(True)

    def boundingRect(self) -> QRectF:
        """
        Bounding rect of the button
        """
        return QRectF(0, 0, 18, 18)

    def paint(self, painter: QPainter, option, widget):
        """
            Creates the trash can graphic.
        """
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setBrush(QBrush(Qt.darkRed if self.hover else Qt.red))
        painter.setPen(Qt.NoPen)
        painter.drawRoundedRect(3, 7, 12, 10, 2, 2)  # body
        painter.drawRoundedRect(2, 4, 14, 3, 1, 1)   # cover
        painter.drawRoundedRect(6, 2, 6, 3, 1, 1)    # handle
        painter.setPen(QPen(Qt.white, 1))
        painter.drawLine(7, 9, 7, 15)
        painter.drawLine(9, 9, 9, 15)
        painter.drawLine(11, 9, 11, 15)

    def mousePressEvent(self, event):
        """
            On mouse press event.
        """
        self.clicked.emit(self)

    def hoverEnterEvent(self, event):
        """
            On hover enter event.
        """
        self.hover = True
        self.update()

    def hoverLeaveEvent(self, event):
        """
            On hover leave event.
        """
        self.hover = False
        self.update()


class ConnectionPoint(QGraphicsObject):

    """
        Custom class to paint a point for the connections.
        :ivar clicked: signal launched when the button is clicked.
        :vartype clicked: pyqtSignal
        :ivar brush: the column to which the point belongs
        :vartype brush: QBrush
        :ivar pen: Radius of the point.
        :vartype pen: QPen
    """

    clicked = pyqtSignal(object)

    def __init__(self, column: int, x: float, y: float, radius: int = POINT_RADIUS):
        """
            Constructor.
            :param column: the column to which the point belongs.
            :param x: x coordinate of the point.
            :param y: y coordinate of the point.
            :param radius: Radius of the point.
        """
        super().__init__()
        self.column = column
        self.radius = radius
        self.setPos(x, y)
        self.brush = QBrush(Qt.gray)
        self.pen = QPen(Qt.black, 2)
        self.setFlag(QGraphicsObject.ItemIsSelectable, False)
        self.setCursor(Qt.PointingHandCursor)

    def boundingRect(self) -> QRectF:
        """
            Bounding rect of the connections.
        """
        return QRectF(0, 0, self.radius, self.radius)

    def paint(self, painter: QPainter, option, widget):
        """
        Creates the graphic of the point
        """
        painter.setBrush(self.brush)
        painter.setPen(self.pen)
        painter.drawEllipse(0, 0, self.radius, self.radius)

    def mousePressEvent(self, event):
        """
        On mouse press event.
        """
        self.clicked.emit(self)

    def highlight(self, enabled: bool):
        """
        Highlight the selected point.
        """
        self.brush = QBrush(Qt.green if enabled else Qt.gray)
        self.update()


# ---------------------------------------------------------------------------
# Wizard page
# ---------------------------------------------------------------------------
class NameMapperUI(QWizardPage, FORM_CLASS):

    """
        Main controller of the name_mapper.ui QWizardPage.
        :ivar scene: Scene of the QGraphicView.
        :vartype scene: QGraphicsScene
        :ivar selected_point: current selected point if there is one.
        :vartype selected_point: ConnectionPoint
        :ivar left_items: List of items that form the left side of the graphic (every row of delete_button, text and connection point).
        :vartype left_items: list
        :ivar right_items: List of items that form the right side of the graphic (every row of delete_button, text and connection point).
        :vartype right_items: list
        :ivar lines: list of tuples with right button, line and left button.
        :vartype lines: list
        :ivar left_add_button: instance of the left add button.
        :vartype left_add_button: AddButton
        :ivar right_add_button: instance of the right add button.
        :vartype right_add_button: AddButton
        :ivar separators: list of separators of items.
        :vartype separators: list
        :ivar left_header: text for the header of the left-hand column.
        :vartype left_header: HeaderTextItem
        :ivar right_header: text for the header of the right-hand column.
        :vartype right_header: HeaderTextItem
        :ivar progress: instance of the progress bar widget.
        :vartype progress: QProgressBar
        :ivar controller: instance of the controller of this page.
        :vartype controller: NameMapperController
    """

    def __init__(self):
        """
        Constructor.
        """
        super().__init__()
        self.setupUi(self)
        self.resize(382, 282)
        self.name_mapper_view.setInteractive(True)

        self.scene = QGraphicsScene()
        self.name_mapper_view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
        self.name_mapper_view.setScene(self.scene)
        self.name_mapper_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.name_mapper_view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.name_mapper_view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)

        self.selected_point: Optional[ConnectionPoint] = None
        self.left_items: list[RowItem] = []
        self.right_items: list[RowItem] = []
        self.lines: list[tuple] = []
        self.left_add_button: Optional[AddButton] = None
        self.right_add_button: Optional[AddButton] = None
        self.separators: list = []
        self.left_header: Optional[HeaderTextItem] = None
        self.right_header: Optional[HeaderTextItem] = None
        self._create_headers()

        self.progress = None
        self.controller = None


    # ------------------------------------------------------------------
    # Initialization
    # ------------------------------------------------------------------

    def initializePage(self):
        """
            Initializes the progress bar and the controller. Initializes the scene with the initial data.
        """
        if not self.controller:
            self.controller = NameMapperController(self.wizard())
            self.controller.request_validation.connect(self.on_request_validation)
            self.controller.show_progress_bar.connect(self.on_show_progress_bar)
        if not self.progress:
            self.progress = self.wizard().create_progress_bar()
            self.layout().addWidget(self.progress)

        pc_files, device_files, connections = self.controller.initialize_data()

        self.load_data(pc_files, device_files, connections)


    def on_show_progress_bar(self, show: bool):
        """
            Shows or hides the progress bar.
            :param show: True if the progress bar should be shown, False otherwise.
        """
        if self.progress:
            if show:
                self.progress.show()
            else:
                self.progress.hide()

    def _create_row_items(self, name_list: list, column: int) -> list[RowItem]:
        """
            Creates the list of items for one of the columns.
            :param name_list: list of names and deletable bools.
            :param column: the column to which the list of items belongs.
            :return: list of row items for a column.
        """
        items = []
        for i, (name, can_delete) in enumerate(name_list):
            y = START_HEIGHT + i * HEIGHT_STEP
            text = SingleLineTextItem(name)
            self.scene.addItem(text)
            text.document().contentsChanged.connect(self.update_layout)

            point = ConnectionPoint(column, 0, 0)
            point.clicked.connect(self.point_clicked)
            self.scene.addItem(point)

            del_button = None
            if can_delete:
                text.setTextInteractionFlags(Qt.TextEditorInteraction)
                del_button = DeleteButton(text, point)
                del_button.clicked.connect(self._delete_row)
                self.scene.addItem(del_button)

            items.append(RowItem(text=text, point=point, delete_button=del_button, y=y))

        return items

    def _add_row(self, items: list, column: int):
        """
            Adds a row to the list of RowItem of a column.
            :param items: list of row RowItem of the column.
            :param column: the column
        """
        y = START_HEIGHT + len(items) * HEIGHT_STEP
        text = SingleLineTextItem(tr("new_element.sqlite"))
        text.setTextInteractionFlags(Qt.TextEditorInteraction)
        self.scene.addItem(text)
        text.document().contentsChanged.connect(self.update_layout)

        point = ConnectionPoint(column, 0, 0)
        point.clicked.connect(self.point_clicked)
        self.scene.addItem(point)

        del_button = DeleteButton(text, point)
        del_button.clicked.connect(self._delete_row)
        self.scene.addItem(del_button)

        items.append(RowItem(text=text, point=point, delete_button=del_button, y=y))
        self.update_layout()

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------
    def _get_max_text_width(self, items: list) -> float:
        """
            Gets the maximum width of the greater length text of a list.
            :param items: list of RowItem.
            :return: maximum width of the greater length text of a list.
        """
        widths = []
        for item in items:
            doc = item.text.document()
            doc.setTextWidth(-1)
            widths.append(doc.idealWidth())
        return max(widths, default=0)

    # ------------------------------------------------------------------
    # Dinamic layout
    # ------------------------------------------------------------------
    def update_layout(self):
        """
            Updates the layout of the wizard every time there is a change in it.
            Make the represented objects in the QGraphicView to behave responsibly.

        """
        # Ocultar separadores para que no inflen itemsBoundingRect
        self.scene.setSceneRect(QRectF())

        for sep in self.separators:
            sep.setVisible(False)

        w = self.name_mapper_view.viewport().width()

        # --- Lado izquierdo ---
        if self.left_items:
            max_left_w = self._get_max_text_width(self.left_items)
            left_point_x = MARGIN_LEFT + max_left_w + GAP_TEXT_POINT
            left_add_x = MARGIN_LEFT + max_left_w / 2 - 9
        else:
            left_point_x = MARGIN_LEFT + GAP_TEXT_POINT
            left_add_x = MARGIN_LEFT

        for item in self.left_items:
            item.text.setPos(MARGIN_LEFT, item.y + HEIGHT_TEXT_OFFSET)
            item.point.setPos(left_point_x, item.y)
            if item.delete_button:
                item.delete_button.setPos(MARGIN_DELETE_LEFT, item.y)

        if self.left_add_button:
            self.left_add_button.setPos(left_add_x, START_HEIGHT + len(self.left_items) * HEIGHT_STEP)
        if self.left_header:
            self.left_header.setPos(MARGIN_LEFT, 5)

        # --- Lado derecho: anclado a la derecha, con distancia mínima desde izquierda ---
        if self.right_items:
            max_right_w = self._get_max_text_width(self.right_items)
        else:
            max_right_w = 0

        has_right_delete = any(item.delete_button for item in self.right_items)
        delete_btn_w = 18
        right_block_w = (
            POINT_RADIUS
            + GAP_POINT_TEXT_RIGHT
            + max_right_w
            + (GAP_DELETE_BUTTON + delete_btn_w if has_right_delete else 0)
        )

        # Anclar a la derecha pero respetar GAP_COLUMNS mínimo desde el punto izquierdo
        right_point_x = max(
            w - MARGIN_RIGHT - right_block_w,
            left_point_x + GAP_COLUMNS
        )
        right_text_x = right_point_x + GAP_POINT_TEXT_RIGHT
        right_delete_x = right_text_x + max_right_w + GAP_DELETE_BUTTON

        # Extremo derecho real del contenido, calculado desde posiciones conocidas
        # sin depender del sceneRect ni del viewport
        if has_right_delete:
            right_content_end = right_delete_x + delete_btn_w
        else:
            right_content_end = right_text_x + max_right_w
        # Si el bloque está anclado a la derecha del viewport, llegar hasta ese borde
        right_edge_in_scene = self.name_mapper_view.mapToScene(
            self.name_mapper_view.viewport().width(), 0
        ).x()
        right_content_end = max(right_content_end, right_edge_in_scene)

        right_add_x = right_text_x + max_right_w / 2 - 9

        for item in self.right_items:
            item.point.setPos(right_point_x, item.y)
            item.text.setPos(right_text_x, item.y + HEIGHT_TEXT_OFFSET)
            if item.delete_button:
                item.delete_button.setPos(right_delete_x, item.y)

        if self.right_add_button:
            self.right_add_button.setPos(right_add_x, START_HEIGHT + len(self.right_items) * HEIGHT_STEP)

        if self.right_header:
            # Centrar sobre el bloque de texto derecho
            header_w = self.right_header.boundingRect().width()
            self.right_header.setPos(right_text_x + max_right_w / 2 - header_w / 2, 5)

        # --- Líneas de conexión ---
        for left_pt, line, right_pt in self.lines:
            self._redraw_line(line, left_pt, right_pt)

        # --- SceneRect con origen fijo en 0 ---
        rect = self.scene.itemsBoundingRect()
        rect.adjust(-CANVAS_MARGIN, -CANVAS_MARGIN, CANVAS_MARGIN, CANVAS_MARGIN)
        rect.setLeft(0)
        self.scene.setSceneRect(rect)

        self._update_separators(left_point_x, right_point_x, right_content_end)

    def _fit_wizard_to_content(self):
        """
            Adjust the size of the wizard to the content of the scene.
        """
        if not self.wizard():
            return
        rect = self.scene.itemsBoundingRect()
        # Difference between the size of the wizard and the viewport
        extra_w = self.wizard().width() - self.name_mapper_view.viewport().width()
        extra_h = self.wizard().height() - self.name_mapper_view.viewport().height()
        desired_w = int(rect.width()) + extra_w + CANVAS_MARGIN * 2
        desired_h = int(rect.height()) + extra_h + CANVAS_MARGIN * 2
        max_w = 1200
        max_h = 800
        self.wizard().resize(
            min(desired_w, max_w),
            min(desired_h, max_h)
        )

    def _update_separators(self, left_point_x: float, right_point_x: float, right_content_end: float):
        """
            Update the separator between RowItem in the scene.
            :param left_point_x: size between left border of the QGraphicView and the start of the ConnectionPoint of the
            longest text in the left.
            :param left_point_x: size between right border of the QGraphicView and the start of the ConnectionPoint of the
            longest text in the right.
            :param right_content_end: width size where the right border of the QGraphicView ends.
        """
        for sep in self.separators:
            self.scene.removeItem(sep)
        self.separators.clear()

        pen = QPen(QColor(180, 180, 180), 1)
        # Las líneas izquierdas van desde x=0 hasta el borde derecho del punto izquierdo
        left_line_end = left_point_x + POINT_RADIUS
        # Las líneas derechas van desde el borde izquierdo del punto derecho hasta el borde del contenido
        right_line_start = right_point_x
        right_line_end = right_content_end

        # Al inicio de _update_separators, tras crear el pen gris:
        header_pen = QPen(QColor(100, 100, 100), 1)
        header_y = START_HEIGHT - HEIGHT_STEP / 2  # línea entre cabecera y primer elemento
        self.separators.append(self.scene.addLine(0, header_y, left_line_end, header_y, header_pen))
        self.separators.append(self.scene.addLine(right_line_start, header_y, right_content_end, header_y, header_pen))

        def add_separator(y: float, draw_left: bool, draw_right: bool):
            if draw_left:
                self.separators.append(self.scene.addLine(0, y, left_line_end, y, pen))
            if draw_right:
                self.separators.append(self.scene.addLine(right_line_start, y, right_line_end, y, pen))

        def separators_for_side(items: list, draw_left: bool, draw_right: bool):
            if not items:
                return
            half = POINT_RADIUS / 2  # centro visual del punto para centrar los separadores
            for i in range(1, len(items)):
                y = (items[i - 1].y + items[i].y) / 2 + half
                add_separator(y, draw_left, draw_right)
            add_separator(items[-1].y + half + HEIGHT_STEP / 2, draw_left, draw_right)

        separators_for_side(self.left_items, draw_left=True, draw_right=False)
        separators_for_side(self.right_items, draw_left=False, draw_right=True)

    def _create_headers(self):
        """
            Initialize the headers of the columns.
        """
        self.left_header = HeaderTextItem(tr("PC files"))
        self.scene.addItem(self.left_header)
        self.right_header = HeaderTextItem(tr("Device files"))
        self.scene.addItem(self.right_header)

    # ------------------------------------------------------------------
    # Borrado de filas
    # ------------------------------------------------------------------
    def _delete_row(self, button: DeleteButton):
        """
            Delete the selected RowItem, reordenate the rest and reassign the connections if there was any to this RowItem.
            It won't delete the item if this will mean that the number of permanent RowItems in the right will be less than the number
            of RowItems in the left.
            :param button: Delete button pressed.
        """
        item, items, is_left = self._find_row_by_button(button)
        if item is None:
            return
        can_continue = True
        if is_left:
            permanent_right_items = 0
            for o_item in self.right_items:
                if not o_item.delete_button:
                    permanent_right_items += 1
            if permanent_right_items < len(self.left_items):
                can_continue = True
            else:
                can_continue = False
                QMessageBox.warning(self, tr("Can't delete item."), tr("All the permanent device files must have a connection. If you want to delete this pc entry, create a new entry"
                                          "and assign the connection to it before trying to delete it."), QMessageBox.Ok)
                self.wizard().logger.info("Can't delete " + item.text.toPlainText() + " because there are not enough connections to fill"
                                          " the right column permanent items.")

        if can_continue:
            line_entry = self._find_line_entry(item.point)
            if line_entry is not None:
                idx, left_pt, line, right_pt = line_entry
                deleted_pt = item.point
                surviving_pt = right_pt if deleted_pt == left_pt else left_pt
                surviving_is_left = surviving_pt.column == 2

                candidate = self._find_free_point(item, items)
                if candidate is not None:
                    new_left = candidate if not surviving_is_left else surviving_pt
                    new_right = surviving_pt if not surviving_is_left else candidate
                    self.lines[idx] = (new_left, line, new_right)
                    self._redraw_line(line, new_left, new_right)
                else:
                    self.scene.removeItem(line)
                    self.lines.pop(idx)

            self.scene.removeItem(item.text)
            self.scene.removeItem(item.point)
            if item.delete_button:
                self.scene.removeItem(item.delete_button)

            items.remove(item)

            for i, row in enumerate(items):
                row.y = START_HEIGHT + i * HEIGHT_STEP

            self.update_layout()

    def _find_row_by_button(self, button: DeleteButton):
        """
            Find the RowItem by the delete button.
            :param button: Delete button.
            :return: a tuple with the RowItem found, the list from where it was found, and a boolean indicating if the row was found on the left.
        """
        for item in self.left_items:
            if item.delete_button == button:
                return item, self.left_items, True
        for item in self.right_items:
            if item.delete_button == button:
                return item, self.right_items, False
        return None, None, None

    def _find_line_entry(self, point: ConnectionPoint):
        """
            Finds a line from the lines list by the ConnectionPoint.
            :param point: Connection point.
            :return: Tuple with the index, left point, line and right point if the ConnectionPoint was found. None otherwise.
        """
        for i, (lp, line, rp) in enumerate(self.lines):
            if lp == point or rp == point:
                return i, lp, line, rp
        return None

    def _find_free_point(self, deleted_item: RowItem, items: list):
        """
            Finds a free point without connection from the index of a deleted RowItem. First it search from the
            deleted RowItem index to the last RowItem, and then from the deleted RowItem index to the first RowItem.
            :param deleted_item: Deleted RowItem.
            :param items: List of RowItems to look.
            :return: The first free ConnectionPoint found, None if there is no free point.
        """
        linked_points = {lp for lp, _, _ in self.lines} | {rp for _, _, rp in self.lines}
        idx = items.index(deleted_item)
        for row in items[idx + 1:]:
            if row.point not in linked_points:
                return row.point
        for row in reversed(items[:idx]):
            if row.point not in linked_points:
                return row.point
        return None

    def _redraw_line(self, line:QGraphicsLineItem, left_pt: ConnectionPoint, right_pt: ConnectionPoint):
        """
            Redraws a line between two ConnectionPoints.
            :param line: Line to redraw.
            :param left_pt: Connection point of the left column.
            :param right_pt: Connection point of the right column.
        """
        r = left_pt.radius / 2
        lp = left_pt.scenePos()
        rp = right_pt.scenePos()
        line.setLine(lp.x() + r, lp.y() + r, rp.x() + r, rp.y() + r)

    # ------------------------------------------------------------------
    # Qt Events
    # ------------------------------------------------------------------
    def showEvent(self, event):
        """
        Show event handler. Updates the layout.
        """
        super().showEvent(event)
        self.update_layout()
        QTimer.singleShot(0, self._fit_wizard_to_content)

    def resizeEvent(self, event):
        """
        Resize event handler. Updates the layout.
        """
        super().resizeEvent(event)
        self.update_layout()

    def on_request_validation(self):
        """
            Used to launch the validatePage remotely.
        """
        self.validatePage()

    def validatePage(self) -> bool:
        """
            Validate the page received.
        """
        state = self.get_state()
        return self.controller.validate_page(state)

    # ------------------------------------------------------------------
    # Point interaction
    # ------------------------------------------------------------------
    def point_clicked(self, point: ConnectionPoint):
        """
            Event handler for point clicked. Changes the color of the point if it's the first point pressed or if the last
            pressed was of the same column. If it's of a different column it tries to connect that point and the last one.
            :param point: Last ConnectionPoint pressed.
        """
        if not self.selected_point:
            self._select_point(point)
            return
        if point == self.selected_point:
            self._deselect_point()
            return
        if point.column == self.selected_point.column:
            self._deselect_point()
            self._select_point(point)
            return
        left_pt, right_pt = self._order_points(point, self.selected_point)
        self._try_connect(left_pt, right_pt)
        self._deselect_point()

    def _select_point(self, point: ConnectionPoint):
        """
            Set the given point as selected and highlight it.
            :param point: ConnectionPoint to be selected and highlighted.
        """
        self.selected_point = point
        point.highlight(True)

    def _deselect_point(self):
        """
            If there is a selected point, deselect it and dehighlight it.
        """
        if self.selected_point:
            self.selected_point.highlight(False)
        self.selected_point = None

    def _order_points(self, a: ConnectionPoint, b: ConnectionPoint):
        """
            Reordenate 2 given points of different columns, putting first the on in the left column and second the one in the right column.
        """
        return (a, b) if a.column == 2 else (b, a)

    def _try_connect(self, left_pt: ConnectionPoint, right_pt: ConnectionPoint):
        """
            Try to connect two given ConnectionPoints. They can connect automatically if they don't have a previous connection.
            If they have a previous connection then the connection still happens, but the previous connection changes to where
            the last connection where for these points.
            :param left_pt: left ConnectionPoint selected.
            :param right_pt: right ConnectionPoint selected.

        """
        left_idx = self._find_line_index(left=left_pt)
        right_idx = self._find_line_index(right=right_pt)
        left_item = self.search_item_by_point(left_pt,self.left_items)
        right_item = self.search_item_by_point(right_pt,self.right_items)

        can_continue = True
        if left_item and right_item and left_item.delete_button and right_item.delete_button:
            QMessageBox.warning(self,tr("Can't create connection."),tr("Can't create a connection between 2 non-permanent files. At least one"
                                                                       " of them should be permanent."), QMessageBox.Ok)
            self.wizard().logger.info(
                "Can't connect " + left_item.text.toPlainText() + " and " + right_item.text.toPlainText() + " because "
                "the 2 items are not permanent.")
            can_continue = False
        if can_continue:
            if left_idx is not None and left_idx == right_idx:
                return

            if left_idx is not None and right_idx is not None:
                _, left_line, old_right_of_left = self.lines[left_idx]
                old_left_of_right, right_line, _ = self.lines[right_idx]
                self.lines[left_idx] = (left_pt, left_line, right_pt)
                self.lines[right_idx] = (old_left_of_right, right_line, old_right_of_left)
                self._redraw_line(left_line, left_pt, right_pt)
                self._redraw_line(right_line, old_left_of_right, old_right_of_left)
            elif left_idx is not None:
                _, old_line, _ = self.lines[left_idx]
                self.lines[left_idx] = (left_pt, old_line, right_pt)
                self._redraw_line(old_line, left_pt, right_pt)
            elif right_idx is not None:
                _, old_line, _ = self.lines[right_idx]
                self.lines[right_idx] = (left_pt, old_line, right_pt)
                self._redraw_line(old_line, left_pt, right_pt)
            else:
                line = self.scene.addLine(0, 0, 0, 0, QPen(Qt.red, 3))
                self.lines.append((left_pt, line, right_pt))
                self._redraw_line(line, left_pt, right_pt)


    def search_item_by_point(self, point: ConnectionPoint, item_list:list[RowItem]):
        """
            Search a RowItem from it's ConnectionPoint.
            :param point: ConnectionPoint that will be used for the search.
            :param item_list: list of RowItem to search for.
            :return: RowItem if found, None otherwise.
        """
        for i, item_row in enumerate(item_list):
            if point is not None and item_row.point == point:
                return item_list[i]
        return None

    def _find_line_index(self, left: ConnectionPoint = None, right: ConnectionPoint = None) -> Optional[int]:
        """
            Find the index in the lines list that have the given ConnectionPoints as ConnectionPoints.
            :param left: left ConnectionPoint.
            :param right: right ConnectionPoint.
            :return: index if found, None otherwise.
        """
        for i, (lp, _, rp) in enumerate(self.lines):
            if left is not None and lp == left:
                return i
            if right is not None and rp == right:
                return i
        return None

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    def load_data(
        self,
        left_items: list[tuple[str, bool]],
        right_items: list[tuple[str, bool]],
        connections: list[tuple[str, str]] = None
    ):
        """
            Populates the scene with the left_items, right_items and connections.
            :param left_items:  [(name, deletable), ...]
            :param right_items: [(name, deletable), ...]
            :param connections: [(name_izq, name_der), ...] optional
        """
        self.scene.clear()
        self.left_header = None
        self.right_header = None
        self.left_items = []
        self.right_items = []
        self.lines = []
        self.separators = []
        self.selected_point = None
        self.left_add_button = None
        self.right_add_button = None

        self.left_items = self._create_row_items(left_items, column=2)
        self.right_items = self._create_row_items(right_items, column=3)

        self.left_add_button = AddButton()
        self.left_add_button.clicked.connect(lambda _: self._add_row(self.left_items, column=2))
        self.scene.addItem(self.left_add_button)

        self.right_add_button = AddButton()
        self.right_add_button.clicked.connect(lambda _: self._add_row(self.right_items, column=3))
        self.scene.addItem(self.right_add_button)

        if connections:
            left_by_name = {item.text.toPlainText(): item.point for item in self.left_items}
            right_by_name = {item.text.toPlainText(): item.point for item in self.right_items}
            for left_name, right_name in connections:
                left_pt = left_by_name.get(left_name)
                right_pt = right_by_name.get(right_name)
                if left_pt and right_pt:
                    line = self.scene.addLine(0, 0, 0, 0, QPen(Qt.red, 3))
                    self.lines.append((left_pt, line, right_pt))

        self._create_headers()
        self.update_layout()

    def get_state(self) -> dict:
        """
        Returns the current state of the Scene
        :return: {"left":  [(name, deletable), ...], "right": [(name, deletable), ...], "connections": [(name_izq, name_der), ...]}
        """
        point_to_name = {}
        left = []
        for item in self.left_items:
            name = item.text.toPlainText()
            point_to_name[id(item.point)] = name
            left.append((name, item.delete_button is not None))

        right = []
        for item in self.right_items:
            name = item.text.toPlainText()
            point_to_name[id(item.point)] = name
            right.append((name, item.delete_button is not None))

        connections = [
            (point_to_name[id(lp)], point_to_name[id(rp)])
            for lp, _, rp in self.lines
            if id(lp) in point_to_name and id(rp) in point_to_name
        ]

        return {"left": left, "right": right, "connections": connections}