#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Nov 24 10:33:40 2023.

@author: Maurizio D'Addona
"""
from __future__ import annotations

import os
import pickle
import sys
import json
import uuid
import webbrowser
from enum import Enum
from typing import Optional, Union, Tuple, List, Dict, Callable, Any, cast

import numpy as np
from astropy.nddata import VarianceUncertainty  # type: ignore
from astropy.nddata import StdDevUncertainty  # type: ignore
from astropy.nddata import InverseVariance  # type: ignore
from astropy import units  # type: ignore
from astropy.table import Table  # type: ignore

from specutils import Spectrum1D  # type: ignore

import redmost
from redmost import loaders
from redmost import utils
from redmost import lines
from redmost import backends

from redmost.qt_compat import qt_api, get_qapp


class SpectrumQChartView(qt_api.QtCharts.QChartView):
    """Subclass of qt_api.QtCharts.QChartView with advanced features."""
    onMouseMoveSeries = qt_api.Signal(object)
    onMousePressSeries = qt_api.Signal(object)
    onMouseReleaseSeries = qt_api.Signal(object)
    onMouseDoubleClickSeries = qt_api.Signal(object)
    onMouseWheelEvent = qt_api.Signal(object)

    def __init__(self, *args: Any) -> None:
        super().__init__(*args)

        self._last_mouse_pos: qt_api.QtCore.QPointF | None = None
        self._static_lines_list: List[Tuple[float, str, str]] = []
        self._lines_buffer_list: List[Tuple[float, str, str]] = []
        self._mouse_lambda: Union[None, qt_api.QtCore.QPointF] = None
        self._lines_type: Union[str, None] = None
        self.line_colors: Dict[str, str] = {
            "absorption": "#b02000",
            "both": "#d47800",
            "emission": "#61b000",
            "unknown": "#222222"
        }
        self.line_plot_options: Dict[str, Any] = {
            "alpha": 0.5,
            "dash_pattern": (8, 8),
            "width": 1.5
        }

        self.horizontal_lock: bool = False
        self.master: bool = False
        self.show_lines: bool = False
        self.sync_height: bool = False
        self.sync_width: bool = False
        self.redshift: float = 0
        self.vertical_lock: bool = False

        self.setDragMode(qt_api.QtCharts.QChartView.DragMode.NoDrag)
        self.setMouseTracking(True)
        self.setRubberBand(
            qt_api.QtCharts.QChartView.RubberBand.RectangleRubberBand
        )

        self.setViewportUpdateMode(
            qt_api.QtWidgets.
            QGraphicsView.
            ViewportUpdateMode.
            FullViewportUpdate
        )

        self.updateLines()

        # Pass events also to siblings
        self._sibling_locked: bool = False
        self.siblings: List[SpectrumQChartView] = []

        self.setSizePolicy(
            qt_api.QtWidgets.QSizePolicy(
                qt_api.QtWidgets.QSizePolicy.Policy.Expanding,
                qt_api.QtWidgets.QSizePolicy.Policy.Expanding
            )
        )
        self.chart().setSizePolicy(
            qt_api.QtWidgets.QSizePolicy(
                qt_api.QtWidgets.QSizePolicy.Policy.Expanding,
                qt_api.QtWidgets.QSizePolicy.Policy.Expanding
            )
        )

    def _getDataBounds(self) -> Union[
        Tuple[float, float, float, float],
        Tuple[None, None, None, None],
    ]:
        x_min = None
        x_max = None
        y_min = None
        y_max = None
        for series in self.chart().series():
            data: np.ndarray = np.array(
                [(p.x(), p.y()) for p in series.points()]
            )
            p1x, p1y = np.min(data, axis=0)[0:2]
            p2x, p2y = np.max(data, axis=0)[0:2]

            if x_min is None:
                x_min = p1x
                y_min = p1y
                x_max = p2x
                y_max = p2y
            else:
                x_min = min(x_min, p1x)
                y_min = min(y_min, p1y)
                x_max = max(x_max, p2x)
                y_max = max(y_max, p2y)
        return x_min, y_min, x_max, y_max

    def addSibling(self, sibling: SpectrumQChartView) -> None:
        """
        Add sibling to this widget.

        :param sibling: A new sibling SpectrumQChartView
        """
        if sibling not in self.siblings:
            self.siblings.append(sibling)
            sibling.addSibling(self)

    def drawForeground(
        self,
        painter: qt_api.QtGui.QPainter,
        rect: qt_api.QtCore.QRectF
    ) -> None:
        super().drawForeground(painter, rect)

        if len(self.chart().series()) == 0:
            return

        painter.save()
        plot_area: qt_api.QtCore.QRectF = self.chart().plotArea()
        pen: qt_api.QtGui.QPen = qt_api.QtGui.QPen()
        pen_color: qt_api.QtGui.QColor

        if self._mouse_lambda is not None:
            pen_color = qt_api.QtGui.QColor("#55555555")
            pen.setColor(pen_color)
            pen.setWidthF(1.5)
            pen.setDashPattern((2,2,2,2))
            painter.setPen(pen)

            mouse_line_x = self.chart().mapToPosition(
                qt_api.QtCore.QPointF(self._mouse_lambda, 0.0)
            ).x()

            if (
                    (mouse_line_x >= plot_area.left()) and
                    (mouse_line_x <= plot_area.right())
            ):
                p_line_top = qt_api.QtCore.QPointF(
                    mouse_line_x, plot_area.top()
                )
                p_line_bottom = qt_api.QtCore.QPointF(
                    mouse_line_x, plot_area.bottom()
                )
                painter.drawLine(p_line_bottom, p_line_top)

        if not self.show_lines:
            painter.restore()
            return

        for line_lambda, line_name, line_type in self._lines_buffer_list:
            pen_color = qt_api.QtGui.QColor(self.line_colors["unknown"])

            if ('AE' in line_type) or ('EA' in line_type):
                pen_color = qt_api.QtGui.QColor(self.line_colors["both"])
            elif 'A' in line_type:
                pen_color = qt_api.QtGui.QColor(self.line_colors["absorption"])
            elif 'E' in line_type:
                pen_color = qt_api.QtGui.QColor(self.line_colors["emission"])

            pen_color.setAlphaF(self.line_plot_options["alpha"])
            pen.setColor(pen_color)
            pen.setWidthF(self.line_plot_options["width"])
            pen.setDashPattern(self.line_plot_options["dash_pattern"])
            painter.setPen(pen)

            line_x = self.chart().mapToPosition(
                qt_api.QtCore.QPointF(line_lambda, 0)
            ).x()

            if (line_x >= plot_area.left()) and (line_x <= plot_area.right()):
                p_line_top = qt_api.QtCore.QPointF(line_x, plot_area.top())
                p_line_bottom = qt_api.QtCore.QPointF(line_x, plot_area.bottom())
                p_text_top = qt_api.QtCore.QPointF(line_x, plot_area.top() - 5)
                painter.drawLine(p_line_bottom, p_line_top)
                painter.drawText(p_text_top, line_name)

        painter.restore()

    def hideLines(self) -> None:
        self.setLinesVisible(False)

    def mouseDoubleClickEvent(self, event: qt_api.QtGui.QMouseEvent) -> None:
        """
        Handle mouse double click events.

        :param event: The input event.
        """
        if self._sibling_locked:
            return

        self._sibling_locked = True
        for sibling in self.siblings:
            pass
        self._sibling_locked = False

        super().mouseDoubleClickEvent(event)
        self.onMouseDoubleClickSeries.emit((self.toSeriesPos(event), event))

    def mouseMoveEvent(self, event: qt_api.QtGui.QMouseEvent, **kwargs) -> None:
        """
        Handle mouse move events.

        :param event: The input event.
        """
        if self._sibling_locked:
            return

        try:
            mouse_pos = event.position()
        except AttributeError:
            mouse_pos = event.pos()

        if event.buttons() & qt_api.QtCore.Qt.MouseButton.MiddleButton:
            if self._last_mouse_pos is None:
                return

            delta = mouse_pos - self._last_mouse_pos
            self._last_mouse_pos = mouse_pos

            self.scroll(-delta.x(), delta.y())

            event.accept()

        data_pos = self.toSeriesPos(event)

        self._mouse_lambda = data_pos.x()

        self._sibling_locked = True
        for sibling in self.siblings:
            sibling._mouse_lambda = self._mouse_lambda
            sibling.chart().update()
            sibling.update()
        self._sibling_locked = False

        self.chart().update()
        self.update()
        super().mouseMoveEvent(event)

        self.onMouseMoveSeries.emit((data_pos, event))

    def mousePressEvent(self, event: qt_api.QtGui.QMouseEvent, **kwargs) -> None:
        """
        Handle mouse button press events.

        :param event: The input event.
        """
        if self._sibling_locked:
            return

        if event.button() == qt_api.QtCore.Qt.MouseButton.MiddleButton:
            get_qapp().setOverrideCursor(
                qt_api.QtCore.Qt.CursorShape.SizeAllCursor
            )

            try:
                self._last_mouse_pos = event.position()
            except AttributeError:
                self._last_mouse_pos = event.pos()

            event.accept()

        super().mousePressEvent(event)
        self.onMousePressSeries.emit((self.toSeriesPos(event), event))

    def mouseReleaseEvent(self, event: qt_api.QtGui.QMouseEvent, **kwargs) -> None:
        """
        Handle mouse button release events.

        :param event: The input event.
        """
        if self._sibling_locked:
            return

        self._sibling_locked = True
        for sibling in self.siblings:
            sibling.mouseReleaseEvent(event)
        self._sibling_locked = False

        if event.button() == qt_api.QtCore.Qt.MouseButton.MiddleButton:
            get_qapp().restoreOverrideCursor()
            event.accept()
        elif event.button() == qt_api.QtCore.Qt.MouseButton.RightButton:
            qt_api.QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
        else:
            # Passtrough for rubber band handling
            super().mouseReleaseEvent(event)

        self.onMouseReleaseSeries.emit((self.toSeriesPos(event), event))
        if self.rubberBand() != qt_api.QtCharts.QChartView.RubberBand.NoRubberBand:
            self.syncSiblingAxes()

    def removeSibling(self, sibling: SpectrumQChartView) -> None:
        """
        Remove a sibling to this widget.

        :param sibling: The sibling to remove.
        """
        if sibling in self.siblings:
            self.siblings.pop(self.siblings.index(sibling))
            sibling.removeSibling(self)

    def scroll(self, dx: float, dy: float) -> None:
        if self._sibling_locked:
            return

        self._sibling_locked = True
        for sibling in self.siblings:
            sibling.scroll(dx, dy)
        self._sibling_locked = False

        chart = self.chart()

        if self.vertical_lock and self.horizontal_lock:
            return
        elif not (self.vertical_lock or self.horizontal_lock):
            chart.scroll(dx, dy)
        elif self.vertical_lock:
            chart.scroll(dx, 0)
        else:
            chart.scroll(0, dy)

    def setAxesRange(
        self,
        x_min: Optional[float],
        y_min: Optional[float],
        x_max: Optional[float],
        y_max: Optional[float]
    ) -> None:
        if self._sibling_locked:
            return

        axes = self.chart().axes()
        if len(axes) < 2:
            return
        x_axis, y_axis = axes[0:2]

        if not self.horizontal_lock:
            if x_min is not None:
                x_axis.setMin(x_min)
            if x_max is not None:
                x_axis.setMax(x_max)
        if not self.vertical_lock:
            if y_min is not None:
                y_axis.setMin(y_min)
            if y_max is not None:
                y_axis.setMax(y_max)

    def setLinesType(self, type: str) -> None:
        self._lines_type = type
        self.updateLines()

    def setLinesVisible(self, show: bool) -> None:
        self.show_lines = show
        self.chart().update()
        self.update()

    def setRedshift(self, redshift: float) -> None:
        self.redshift = redshift
        self.updateLines()

    def showLines(self) -> None:
        self.setLinesVisible(True)

    def syncSiblingAxes(self) -> None:
        if self._sibling_locked:
            return

        axes = self.chart().axes()
        if len(axes) < 2:
            return
        x_axis, y_axis = axes[0:2]

        self._sibling_locked = True
        # Get current range for x and y axes
        x_min = x_axis.min()
        y_min = y_axis.min()
        x_max = x_axis.max()
        y_max = y_axis.max()

        plot_area: qt_api.QtCore.QRectF = self.chart().plotArea()

        for sibling in self.siblings:

            sibling.setAxesRange(x_min, y_min, x_max, y_max)

            if sibling.sync_width or sibling.sync_height:
                sibling_chart: qt_api.QtCharts.QChart = sibling.chart()
                sibling_chart.setPlotArea(qt_api.QtCore.QRectF())
                sibling_plot_area: qt_api.QtCore.QRectF = sibling_chart.plotArea()
                if sibling.sync_width:
                    sibling_plot_area.setX(plot_area.x())
                    sibling_plot_area.setWidth(plot_area.width())

                if sibling.sync_height:
                    sibling_plot_area.setY(plot_area.y())
                    sibling_plot_area.setHeight(plot_area.height())

                sibling_chart.setPlotArea(sibling_plot_area)
                sibling_chart.update()
                sibling.update()

        self._sibling_locked = False

    def resizeEvent(self, event, QResizeEvent=None) -> None:
        super().resizeEvent(event)
        if self.master:
            self.syncSiblingAxes()

    def toSeriesPos(self, event: qt_api.QtGui.QMouseEvent):
        """
        Convert mouse position from event location to data values.

        :param event: The input event.
        """
        try:
            widget_pos = event.position()
        except AttributeError:
            widget_pos = event.pos()

        scene_pos = self.mapToScene(int(widget_pos.x()), int(widget_pos.y()))
        chart_item_pos = self.chart().mapFromScene(scene_pos)
        value_in_series = self.chart().mapToValue(chart_item_pos)
        return value_in_series

    def updateLines(self) -> None:
        known_lines: List[Tuple[float, str, str]] = lines.get_lines(
            line_type=self._lines_type,
            z=self.redshift
        )

        self._lines_buffer_list = known_lines + self._static_lines_list
        self.chart().update()
        self.update()

    def wheelEvent(self, event: qt_api.QtGui.QWheelEvent, **kwargs) -> None:
        """
        Handle mouse wheel events.

        :param event: The input event.
        """

        super().wheelEvent(event)
        if not event.angleDelta().isNull():
            delta_y = event.angleDelta().y() / 5
            delta_x = event.angleDelta().x() / 5
        elif not event.pixelDelta().isNull():
            delta_y = event.pixelDelta().y()
            delta_x = event.pixelDelta().x()
        else:
            delta_y = 0
            delta_x = 0

        modifiers = qt_api.QtWidgets.QApplication.keyboardModifiers()

        if modifiers == qt_api.QtCore.Qt.KeyboardModifier.ShiftModifier:
            # Vertical scroll
            self.scroll(delta_y, delta_x)
        elif modifiers == qt_api.QtCore.Qt.KeyboardModifier.ControlModifier:
            # Horizontal scroll
            if delta_y > 0:
                self.zoomIn()
            else:
                self.zoomOut()
        else:
            self.scroll(delta_x, delta_y)

        self.onMouseWheelEvent.emit(event)

    def zoom(
        self,
        value: float,
        x_center: Optional[float] = None,
        y_center: Optional[float] = None
    ) -> None:
        """
        Zoom.

        :param value: Zoom value.
        :param x_center: x of the zoom center
        :param y_center: y of the zoom center
        """
        if self._sibling_locked:
            return

        rect = self.chart().plotArea()

        self._sibling_locked = True
        for sibling in self.siblings:
            sibling.zoom(value, x_center, y_center)
        self._sibling_locked = False

        if x_center is None:
            x_center = rect.width()/2

        if y_center is None:
            y_center = rect.height()/2

        if not self.horizontal_lock:
            width_original = rect.width()
            rect.setWidth(width_original / value)
            center_scale_x = x_center / width_original
            left_offset = x_center - (rect.width() * center_scale_x)
            rect.moveLeft(rect.x() + left_offset)

        if not self.vertical_lock:
            height_original = rect.height()
            rect.setHeight(height_original / value)
            center_scale_y = y_center / height_original
            top_offset = y_center - (rect.height() * center_scale_y)
            rect.moveTop(rect.y() + top_offset)

        self.chart().zoomIn(rect)

    def zoomIn(
        self, value: float = 2.0,
        x_center: Optional[float] = None,
        y_center: Optional[float] = None
    ) -> None:
        """
        Zoom in.

        :param value: Zoom value. The default value is 2.0.
        :param x_center: x of the zoom center
        :param y_center: y of the zoom center
        """
        self.zoom(value, x_center, y_center)

    def zoomOut(
        self,
        value: float = 0.5,
        x_center: Optional[float] = None,
        y_center: Optional[float] = None
    ) -> None:
        """
        Zoom in.

        :param value: Zoom value. The default value is 0.5.
        :param x_center: x of the zoom center
        :param y_center: y of the zoom center
        """
        self.zoom(value, x_center, y_center)

    def zoomReset(self) -> None:
        """Reset the zoom to fit the chart content."""
        if self._sibling_locked:
            return

        self._sibling_locked = True
        for sibling in self.siblings:
            sibling.zoomReset()
        self._sibling_locked = False

        self.setAxesRange(*self._getDataBounds())


class QRedrockHandler(qt_api.QtCore.QObject):
    """Class to handle redrock subprocess."""

    progress: qt_api.Signal = qt_api.Signal(int)
    maximum: qt_api.Signal = qt_api.Signal(int)
    reset: qt_api.Signal = qt_api.Signal()
    message: qt_api.Signal = qt_api.Signal(str)
    finished: qt_api.Signal = qt_api.Signal()

    def __init__(self) -> None:
        super().__init__()
        self.qapp = get_qapp()
        self.targets: List[Any] = []
        self.results = None
        self._t_dict: Union[None, Dict[str, uuid.UUID]] = None

        self._target_dump_file = 'rr.targets'
        self._result_dump_file = 'rr.result'
        self.process: qt_api.QtCore.QProcess = qt_api.QtCore.QProcess(self)
        self.process.readyReadStandardOutput.connect(  # type: ignore
            self._process_output
        )
        self.process.finished.connect(  # type: ignore
            self.retrieve_results
        )

        self._total_progress = 0
        self._total_maximum = 0

    def _process_output(self) -> None:
        data_bytes: qt_api.QtCore.QByteArray = self.process.readAllStandardOutput()
        text = bytes(data_bytes).decode()  # type: ignore
        for line in text.splitlines():
            simplified = ' '.join(line.lower().strip().split())
            if ("finished in" in simplified):
                self._total_progress += 1
                self.progress.emit(self._total_progress)
            elif "computing redshifts took:" in simplified:
                self.progress.emit(self._total_maximum)

        self.message.emit(str(text))

    def retrieve_results(self) -> None:
        try:
            with open(self._result_dump_file, 'rb') as f:
                self.results = pickle.load(f)
        except Exception:
            self.results = None
        self.finished.emit()

    def run_redrock(self, spectra: Dict[uuid.UUID, Spectrum1D]) -> None:
        self.reset.emit()
        self.maximum.emit(len(spectra))
        self.message.emit(self.qapp.tr("Building targets"))
        self.targets, self._t_dict = backends.rrock.build_redrock_targets(
            spectra,
            progress_callback=self.progress.emit
        )

        self.reset.emit()
        self.maximum.emit(0)
        backends.rrock.dump_targets(self.targets, self._target_dump_file)

        self._total_maximum = 22
        self._total_progress = 0
        self.maximum.emit(self._total_maximum)
        self.message.emit(self.qapp.tr("Running redrock backend"))
        self.process.start(
            sys.executable,
            [
                backends.rrock.__file__,
                self._target_dump_file,
                self._result_dump_file
            ]
        )


class GlobalState(Enum):
    READY = 0
    WAITING = 1
    SELECT_LINE_MANUAL = 2
    SAVE_OBJECT_STATE = 3
    LOAD_OBJECT_STATE = 4
    REUQUEST_CANCEL = 5


class AboutDialog(qt_api.QtWidgets.QDialog):

    def __init__(
        self,
        parent: Optional[qt_api.QtWidgets.QWidget] = None,
        flags: qt_api.QtCore.Qt.WindowType = qt_api.QtCore.Qt.WindowType.Dialog
    ):
        super().__init__(parent, flags)

        qapp = get_qapp()

        properties: Dict[str, str] = {
            qapp.tr("PROGRAM VERSION"): redmost.__version__,
            qapp.tr("QT BACKEND"): qt_api.qt_api_name,
            qapp.tr("HAS REDROCK"): (
                qapp.tr("YES") if backends.HAS_REDROCK else qapp.tr("NO")
            )
        }

        button_box = qt_api.QtWidgets.QDialogButtonBox(self)
        button_box.setStandardButtons(
            qt_api.QtWidgets.QDialogButtonBox.StandardButton.Ok
        )

        text_edit: qt_api.QtWidgets.QTextEdit = qt_api.QtWidgets.QTextEdit()
        text_edit.setText(redmost.LICENSE)
        text_edit.setReadOnly(True)
        text_edit.setWordWrapMode(qt_api.QtGui.QTextOption.WrapMode.NoWrap)

        status_tbl = qt_api.QtWidgets.QTableWidget()
        status_tbl.setColumnCount(2)
        status_tbl.setRowCount(len(properties))
        status_tbl.setSizePolicy(
            qt_api.QtWidgets.QSizePolicy(
                qt_api.QtWidgets.QSizePolicy.Policy.Expanding,
                qt_api.QtWidgets.QSizePolicy.Policy.Expanding
            )
        )

        status_tbl.setHorizontalHeaderLabels(
            [qapp.tr("PROPERTY"), qapp.tr("VALUE")]
        )

        for j, (key, val) in enumerate(properties.items()):
            item_key = qt_api.QtWidgets.QTableWidgetItem(key)
            item_key.setFlags(qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled)

            item_val = qt_api.QtWidgets.QTableWidgetItem(val)
            item_val.setFlags(qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled)

            status_tbl.setItem(j, 0, item_key)
            status_tbl.setItem(j, 1, item_val)

        header: Union[qt_api.QtWidgets.QHeaderView, None]
        header = status_tbl.horizontalHeader()
        if header is None:
            header = qt_api.QtWidgets.QHeaderView(qt_api.QtCore.Qt.Orientation.Horizontal)
            status_tbl.setHorizontalHeader(header)

        header.setSectionResizeMode(qt_api.QtWidgets.QHeaderView.ResizeMode.Stretch)

        main_layout = qt_api.QtWidgets.QVBoxLayout()
        main_layout.addWidget(text_edit)
        main_layout.addWidget(status_tbl)
        main_layout.addStretch()
        main_layout.addWidget(button_box)

        self.setLayout(main_layout)
        self.setWindowTitle("About redmost")

        button_box.accepted.connect(
            self.close
        )

        self.setMinimumWidth(640)


class ImportZcatDialog(qt_api.QtWidgets.QDialog):

    def __init__(self) -> None:
        super().__init__()
        qapp: qt_api.QtWidgets.QApplication = get_qapp()

        self.zcat_tbl: Union[Table, None] = None

        button_box: qt_api.QtWidgets.QDialogButtonBox
        button_box = qt_api.QtWidgets.QDialogButtonBox(self)
        button_box.setStandardButtons(
            qt_api.QtWidgets.QDialogButtonBox.StandardButton.Ok |
            qt_api.QtWidgets.QDialogButtonBox.StandardButton.Cancel
        )

        main_layout: qt_api.QtWidgets.QVBoxLayout = qt_api.QtWidgets.QVBoxLayout()

        mapping_widgets_layout = qt_api.QtWidgets.QGridLayout()

        id_select_label: qt_api.QtWidgets.QLabel
        id_select_label = qt_api.QtWidgets.QLabel(
            qapp.tr("Match with catalogue:")
        )

        self.id_combo_box: qt_api.QtWidgets.QComboBox
        self.id_combo_box = qt_api.QtWidgets.QComboBox()

        z_select_label: qt_api.QtWidgets.QLabel
        z_select_label = qt_api.QtWidgets.QLabel(
            qapp.tr("Get redshift from column:")
        )

        self.z_combo_box: qt_api.QtWidgets.QComboBox
        self.z_combo_box = qt_api.QtWidgets.QComboBox()

        qf_select_label: qt_api.QtWidgets.QLabel
        qf_select_label = qt_api.QtWidgets.QLabel(
            qapp.tr("Quality flag (optional):")
        )

        self.qf_combo_box: qt_api.QtWidgets.QComboBox
        self.qf_combo_box = qt_api.QtWidgets.QComboBox()

        mapping_widgets_layout.addWidget(id_select_label)
        mapping_widgets_layout.addWidget(self.id_combo_box)
        mapping_widgets_layout.addWidget(z_select_label)
        mapping_widgets_layout.addWidget(self.z_combo_box)
        mapping_widgets_layout.addWidget(qf_select_label)
        mapping_widgets_layout.addWidget(self.qf_combo_box)

        main_layout.addLayout(mapping_widgets_layout)
        main_layout.addStretch()
        main_layout.addWidget(button_box)

        self.setLayout(main_layout)
        self.setWindowTitle(qapp.tr("Import Catalogue"))

        button_box.accepted.connect(
            self.accept
        )

        button_box.rejected.connect(
            self.reject
        )

    def setup(self, zcat_tbl: Table) -> None:

        self.zcat_tbl = zcat_tbl

        self.id_combo_box.clear()
        self.z_combo_box.clear()
        self.qf_combo_box.clear()

        self.qf_combo_box.addItem("None", None)
        for col_name in zcat_tbl.colnames:
            self.id_combo_box.addItem(col_name, col_name)
            self.z_combo_box.addItem(col_name, col_name)
            self.qf_combo_box.addItem(col_name, col_name)

    def get_mapping(self) -> Tuple[str, str, Union[str, None]]:
        id_col = str(
            self.id_combo_box.currentData(qt_api.QtCore.Qt.ItemDataRole.UserRole)
        )
        z_col = str(
            self.z_combo_box.currentData(qt_api.QtCore.Qt.ItemDataRole.UserRole)
        )

        qf_col = self.qf_combo_box.currentData(qt_api.QtCore.Qt.ItemDataRole.UserRole)
        if qf_col is not None:
            qf_col = str(qf_col)

        return id_col, z_col, qf_col


class MainWindow(qt_api.QtWidgets.QMainWindow):
    """Class definition for the QMainWindow created with the designer."""

    # actions
    action_about: qt_api.QtGui.QAction
    action_import_spectra: qt_api.QtGui.QAction
    action_zoom_in: qt_api.QtGui.QAction
    action_zoom_out: qt_api.QtGui.QAction
    action_zoom_fit: qt_api.QtGui.QAction
    action_save_project_as: qt_api.QtGui.QAction
    action_save_project: qt_api.QtGui.QAction
    action_online_user_manual: qt_api.QtGui.QAction
    action_open_project: qt_api.QtGui.QAction
    action_new_project: qt_api.QtGui.QAction

    # Widgets
    add_line_button: qt_api.QtWidgets.QPushButton
    container_tab_var: qt_api.QtWidgets.QWidget
    container_tab_wd: qt_api.QtWidgets.QWidget
    container_tab_sky: qt_api.QtWidgets.QWidget
    delete_line_button: qt_api.QtWidgets.QPushButton
    delete_lines_button: qt_api.QtWidgets.QPushButton
    export_zcat_button: qt_api.QtWidgets.QPushButton
    flux_group_box: qt_api.QtWidgets.QGroupBox
    flux_widget_layout: qt_api.QtWidgets.QHBoxLayout
    import_zcat_button: qt_api.QtWidgets.QPushButton
    info_group_box: qt_api.QtWidgets.QGroupBox
    lines_auto_button: qt_api.QtWidgets.QPushButton
    lines_tol_dspinbox: qt_api.QtWidgets.QDoubleSpinBox
    lines_match_list_widget: qt_api.QtWidgets.QListWidget
    lines_table_widget: qt_api.QtWidgets.QTableWidget
    match_lines_button: qt_api.QtWidgets.QPushButton
    obj_prop_table_widget: qt_api.QtWidgets.QTableWidget
    other_charts_tab_widget: qt_api.QtWidgets.QTabWidget
    plot_group_box: qt_api.QtWidgets.QGroupBox
    qflag_combo_box: qt_api.QtWidgets.QComboBox
    red_group_box: qt_api.QtWidgets.QGroupBox
    redrock_all_radio: qt_api.QtWidgets.QRadioButton
    redrock_selected_radio: qt_api.QtWidgets.QRadioButton
    redrock_run_button: qt_api.QtWidgets.QPushButton
    redrock_text_edit: qt_api.QtWidgets.QTextEdit
    redrock_progress_bar: qt_api.QtWidgets.QProgressBar
    redrock_current_radio: qt_api.QtWidgets.QRadioButton
    remove_selected_button: qt_api.QtWidgets.QPushButton
    remove_spec_button: qt_api.QtWidgets.QPushButton
    show_lines_check_box: qt_api.QtWidgets.QCheckBox
    show_lines_combo_box: qt_api.QtWidgets.QComboBox
    single_line_combo_box: qt_api.QtWidgets.QComboBox
    sky_widget_layout: qt_api.QtWidgets.QHBoxLayout
    smoothing_dspinbox: qt_api.QtWidgets.QDoubleSpinBox
    smoothing_check_box: qt_api.QtWidgets.QCheckBox
    spec_group_box: qt_api.QtWidgets.QGroupBox
    spec_list_widget: qt_api.QtWidgets.QListWidget
    splitter_main: qt_api.QtWidgets.QSplitter
    splitter_plots: qt_api.QtWidgets.QSplitter
    toggle_all_button: qt_api.QtWidgets.QPushButton
    toggle_done_button: qt_api.QtWidgets.QPushButton
    toggle_similar_button: qt_api.QtWidgets.QPushButton
    var_widget_layout: qt_api.QtWidgets.QHBoxLayout
    wdisp_widget_layout: qt_api.QtWidgets.QHBoxLayout
    z_dspinbox: qt_api.QtWidgets.QDoubleSpinBox
    z_min_dspinbox: qt_api.QtWidgets.QDoubleSpinBox
    z_max_dspinbox: qt_api.QtWidgets.QDoubleSpinBox


class GuiApp:
    """General class for the main GUI."""

    def __init__(self) -> None:
        self.qapp: qt_api.QtWidgets.QApplication = get_qapp()
        self.qapp.setApplicationName("Redmost")
        self.qapp.setOrganizationName("DAddona")
        self.qapp.setOrganizationDomain(
            "github.com/mauritiusdadd/redmost"
        )

        self.settings: qt_api.QtCore.QSettings = qt_api.QtCore.QSettings()

        self.open_spectra: Dict[uuid.UUID, Spectrum1D] = {}
        self.open_spectra_files: Dict[uuid.UUID, str] = {}
        self.open_spectra_items: Dict[
            uuid.UUID, qt_api.QtWidgets.QListWidgetItem
        ] = {}
        self.current_uuid: Optional[uuid.UUID] = None
        self.object_state_dict: Dict[uuid.UUID, Any] = {}
        self.current_project_file_path: Optional[str] = None
        self.current_open_dir: Optional[str] = None

        self.global_state: Enum = GlobalState.READY

        self.qf_color: Dict[int, str] = {
            0: "#FFFFFF",
            1: "#55940a00",
            2: "#55945400",
            3: "#55176601",
            4: "#55012d66"
        }

        self.main_wnd: MainWindow = cast(
            MainWindow,
            qt_api.loadUiWidget("main_window.ui")
        )
        setattr(self.main_wnd, "closeEvent", self.closeEvent)

        self.main_wnd.setWindowTitle("redmost")
        self.main_wnd.setWindowIcon(
            qt_api.QtGui.QIcon(utils.get_data_file("redmost.png"))
        )
        self.qapp.setWindowIcon(
            qt_api.QtGui.QIcon(utils.get_data_file("redmost.png"))
        )

        self.msgBox: qt_api.QtWidgets.QMessageBox = qt_api.QtWidgets.QMessageBox(
            parent=self.main_wnd
        )

        # About dialog

        self.about_wnd: AboutDialog = AboutDialog()

        # Status Bar
        self.mousePosLabel: qt_api.QtWidgets.QLabel = qt_api.QtWidgets.QLabel("")

        # Global cancel button
        self.cancel_button: qt_api.QtWidgets.QPushButton = qt_api.QtWidgets.QPushButton()
        self.cancel_button.setText("Cancel")
        self.cancel_button.hide()
        self.cancel_button.clicked.connect(
            self.requestCancelCurrentOperation
        )

        # Global progress bar
        self.pbar: qt_api.QtWidgets.QProgressBar = qt_api.QtWidgets.QProgressBar()
        self.pbar.hide()

        self.statusbar: qt_api.QtWidgets.QStatusBar = qt_api.QtWidgets.QStatusBar()
        self.main_wnd.setStatusBar(self.statusbar)

        self.statusbar.addPermanentWidget(self.pbar)
        self.statusbar.addPermanentWidget(self.cancel_button)
        self.statusbar.addPermanentWidget(self.mousePosLabel)

        # Fill single line combo box
        for lam, line_name, _ in lines.RESTFRAME_LINES:
            text = f"{line_name} - {lam:.2f} A"
            self.main_wnd.single_line_combo_box.addItem(text, lam)
        self.main_wnd.single_line_combo_box.currentIndexChanged.connect(
            self.setCurrentObjectRedshiftFromSingleLine
        )

        # Check for RedRock.
        if not backends.HAS_REDROCK:
            self.main_wnd.redrock_run_button.setEnabled(False)
            self.main_wnd.redrock_text_edit.setText(
                "*** WARNING ***\n"
                "\n"
                "Cannot load redrock python module.\n"
                "Please, check if redrock is correctly installed!\n"
            )

            self.redrock_handler = None
        else:
            self.main_wnd.redrock_run_button.clicked.connect(
                self.doStartRedrock
            )

            self.redrock_handler = QRedrockHandler()
            self.redrock_handler.reset.connect(
                self.main_wnd.redrock_progress_bar.reset
            )
            self.redrock_handler.maximum.connect(
                self.main_wnd.redrock_progress_bar.setMaximum
            )
            self.redrock_handler.progress.connect(
                self.main_wnd.redrock_progress_bar.setValue
            )
            self.redrock_handler.message.connect(
                self.main_wnd.redrock_text_edit.append
            )
            self.redrock_handler.finished.connect(
                self.collectRedrockResults
            )

        self.main_wnd.redrock_current_radio.setEnabled(False)

        # Dialog for importing zcat

        self.zcat_mapping_dialog = ImportZcatDialog()
        self.zcat_mapping_dialog.finished.connect(self.getZcatColumnMapping)

        # QChartView widget for flux

        self.flux_chart_view: SpectrumQChartView = SpectrumQChartView(
            self.main_wnd.flux_group_box
        )
        self.flux_chart_view.master = True
        self.flux_chart_view.setObjectName("flux_chart_view")
        self.flux_chart_view.setRenderHint(
            qt_api.QtGui.QPainter.RenderHint.Antialiasing
        )
        self.flux_chart_view.setHorizontalScrollBarPolicy(
            qt_api.QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.flux_chart_view.setVerticalScrollBarPolicy(
            qt_api.QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.flux_chart_view.setContentsMargins(0, 0, 0, 0)
        self.flux_chart_view.chart().setContentsMargins(0, 0, 0, 0)
        self.flux_chart_view.chart().layout().setContentsMargins(0, 0, 0, 0)
        self.flux_chart_view.setRubberBand(
            qt_api.QtCharts.QChartView.RubberBand.RectangleRubberBand
        )

        self.main_wnd.flux_widget_layout.addWidget(self.flux_chart_view)

        # QChartView widget for variance

        self.var_chart_view: SpectrumQChartView = SpectrumQChartView(
            self.main_wnd.other_charts_tab_widget
        )
        self.var_chart_view.vertical_lock = True
        self.var_chart_view.setObjectName("var_chart_view")
        self.var_chart_view.setRenderHint(
            qt_api.QtGui.QPainter.RenderHint.Antialiasing
        )
        self.var_chart_view.setHorizontalScrollBarPolicy(
            qt_api.QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.var_chart_view.setVerticalScrollBarPolicy(
            qt_api.QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.var_chart_view.setContentsMargins(0, 0, 0, 0)
        self.var_chart_view.chart().setContentsMargins(0, 0, 0, 0)
        self.var_chart_view.chart().layout().setContentsMargins(0, 0, 0, 0)
        self.var_chart_view.setRubberBand(
            qt_api.QtCharts.QChartView.RubberBand.NoRubberBand
        )
        self.main_wnd.var_widget_layout.addWidget(self.var_chart_view)

        # QChartView widget for resolution curve

        self.wdisp_chart_view: SpectrumQChartView = SpectrumQChartView(
            self.main_wnd.other_charts_tab_widget
        )
        self.wdisp_chart_view.vertical_lock = True
        self.wdisp_chart_view.setObjectName("var_chart_view")
        self.wdisp_chart_view.setRenderHint(
            qt_api.QtGui.QPainter.RenderHint.Antialiasing
        )
        self.wdisp_chart_view.setHorizontalScrollBarPolicy(
            qt_api.QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.wdisp_chart_view.setVerticalScrollBarPolicy(
            qt_api.QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.wdisp_chart_view.setContentsMargins(0, 0, 0, 0)
        self.wdisp_chart_view.chart().setContentsMargins(0, 0, 0, 0)
        self.wdisp_chart_view.chart().layout().setContentsMargins(0, 0, 0, 0)
        self.wdisp_chart_view.setRubberBand(
            qt_api.QtCharts.QChartView.RubberBand.NoRubberBand
        )
        self.main_wnd.wdisp_widget_layout.addWidget(self.wdisp_chart_view)

        # QChartView widget for sky

        self.sky_chart_view: SpectrumQChartView = SpectrumQChartView(
            self.main_wnd.other_charts_tab_widget
        )
        self.sky_chart_view.vertical_lock = True
        self.sky_chart_view.setObjectName("var_chart_view")
        self.sky_chart_view.setRenderHint(
            qt_api.QtGui.QPainter.RenderHint.Antialiasing
        )
        self.sky_chart_view.setHorizontalScrollBarPolicy(
            qt_api.QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.sky_chart_view.setVerticalScrollBarPolicy(
            qt_api.QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.sky_chart_view.setContentsMargins(0, 0, 0, 0)
        self.sky_chart_view.chart().setContentsMargins(0, 0, 0, 0)
        self.sky_chart_view.chart().layout().setContentsMargins(0, 0, 0, 0)
        self.sky_chart_view.setRubberBand(
            qt_api.QtCharts.QChartView.RubberBand.NoRubberBand
        )
        self.main_wnd.sky_widget_layout.addWidget(self.sky_chart_view)

        # Add siblings to main flux QChartView

        self.flux_chart_view.addSibling(self.var_chart_view)
        self.flux_chart_view.addSibling(self.wdisp_chart_view)
        self.flux_chart_view.addSibling(self.sky_chart_view)

        # Set QSplitters initial sizes

        self.main_wnd.splitter_main.setSizes([200, 1000, 200])
        self.main_wnd.splitter_plots.setSizes([500, 300])

        # Connect signals

        self.main_wnd.spec_list_widget.currentItemChanged.connect(
            self.currentSpecItemChanged
        )

        self.main_wnd.spec_list_widget.itemDoubleClicked.connect(
            self.toggleSimilarSpecItems
        )

        self.main_wnd.toggle_done_button.clicked.connect(
            self.doToggleDone
        )

        self.main_wnd.toggle_similar_button.clicked.connect(
            self.doToggleSimilar
        )

        self.main_wnd.toggle_all_button.clicked.connect(
            self.doToggleAll
        )

        self.main_wnd.remove_selected_button.clicked.connect(
            self.doRemoveSelectedSpecItems
        )

        self.main_wnd.remove_spec_button.clicked.connect(
            self.doRemoveCurrentSpecItems
        )

        self.main_wnd.export_zcat_button.clicked.connect(
            self.doExportZcat
        )

        self.main_wnd.import_zcat_button.clicked.connect(
            self.doImportZcat
        )

        self.main_wnd.action_import_spectra.triggered.connect(
            self.doImportSpectra
        )
        self.main_wnd.action_zoom_in.triggered.connect(
            self.doZoomIn
        )
        self.main_wnd.action_zoom_out.triggered.connect(
            self.doZoomOut
        )
        self.main_wnd.action_zoom_fit.triggered.connect(
            self.doZoomReset
        )
        self.main_wnd.action_save_project_as.triggered.connect(
            self.doSaveProjectAs
        )
        self.main_wnd.action_save_project.triggered.connect(
            self.doSaveProject
        )
        self.main_wnd.action_open_project.triggered.connect(
            self.doOpenProject
        )
        self.main_wnd.action_new_project.triggered.connect(
            self.doNewProject
        )

        self.flux_chart_view.onMouseMoveSeries.connect(
            self._updateMouseLabelFromEvent
        )
        self.flux_chart_view.onMousePressSeries.connect(self.mousePressedFlux)

        self.var_chart_view.onMouseMoveSeries.connect(
            self._updateMouseLabelFromEvent
        )

        self.main_wnd.smoothing_check_box.stateChanged.connect(
            self.toggleSmothing
        )
        self.main_wnd.smoothing_dspinbox.valueChanged.connect(
            self.setSmoothingFactor
        )

        self.main_wnd.lines_auto_button.clicked.connect(
            self.doIdentifyLines
        )

        self.main_wnd.lines_match_list_widget.currentRowChanged.connect(
            self.setCurrentObjectRedshiftFromLines
        )

        self.main_wnd.add_line_button.clicked.connect(
            self.doAddNewLine
        )

        self.main_wnd.delete_line_button.clicked.connect(
            self.doDeleteCurrentLine
        )

        self.main_wnd.delete_lines_button.clicked.connect(
            self.doDeleteAllLines
        )

        self.main_wnd.match_lines_button.clicked.connect(
            self.doRedshiftFromLines
        )

        self.main_wnd.show_lines_check_box.toggled.connect(
            self.flux_chart_view.setLinesVisible
        )

        self.main_wnd.z_dspinbox.valueChanged.connect(
            self.setCurrentObjectRedshift
        )

        self.main_wnd.qflag_combo_box.currentIndexChanged.connect(
            self.currentQualityFlagChanged
        )

        self.main_wnd.show_lines_combo_box.currentIndexChanged.connect(
            self.setShowLinesType
        )

        self.main_wnd.action_online_user_manual.triggered.connect(
            self._open_online_doc
        )

        self.main_wnd.action_about.triggered.connect(
            self.about_wnd.exec
        )

        # Final steps

        # This dictionary contains the global settings that should be saved
        # upon closing and restored when the application is started again.
        #
        # Keys are strings containing a unique name for the properties.
        # Values are tuples of three elements, and each element is a callable
        # object: the first one is used to retrieve the property value, the
        # second one is used to set the property value and the third one is
        # used to cast the valued read from the settings to the appropriate
        # type. This last callable is optional and can be replaced with a
        # None if no casting is required.
        self._globsal_setting: Dict[
            str,
            Tuple[Callable, Callable, Optional[Callable]]
        ] = {
            "geometry": (
                self.main_wnd.saveGeometry,
                self.main_wnd.restoreGeometry,
                None
            ),
            "smoothing_state": (
                self.main_wnd.smoothing_check_box.checkState,
                self.main_wnd.smoothing_check_box.setCheckState,
                qt_api.QtCore.Qt.CheckState
            ),
            "smoothing_value": (
                self.main_wnd.smoothing_dspinbox.value,
                self.main_wnd.smoothing_dspinbox.setValue,
                float,
            ),
            "show_lines": (
                self.main_wnd.show_lines_check_box.checkState,
                self.main_wnd.show_lines_check_box.setCheckState,
                qt_api.QtCore.Qt.CheckState
            ),
            "lines_type": (
                self.main_wnd.show_lines_combo_box.currentIndex,
                self.main_wnd.show_lines_combo_box.setCurrentIndex,
                int
            )
        }

        self.loadSettings()
        self.newProject()

        self.main_wnd.show()

        is_outdated, new_ver = utils.check_updates()
        if is_outdated:
            self._show_update_message(new_ver)

    def _backup_current_object_state(self) -> None:
        """Backup the program state for the current object."""
        if self.current_uuid is None:
            return

        self.global_state = GlobalState.SAVE_OBJECT_STATE

        lines_list = []
        for row_index in range(self.main_wnd.lines_table_widget.rowCount()):
            w_item: Union[None, qt_api.QtWidgets.QTableWidgetItem]
            w_item = self.main_wnd.lines_table_widget.item(row_index, 0)
            if w_item is None:
                continue

            line_info = {
                'row': row_index,
                'data': float(w_item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)),
                'text': w_item.text(),
                'checked': w_item.checkState()
            }
            lines_list.append(line_info)

        redshifts_form_lines = []
        for row_index in range(self.main_wnd.lines_match_list_widget.count()):
            z_item: Union[None, qt_api.QtWidgets.QListWidgetItem]
            z_item = self.main_wnd.lines_match_list_widget.item(row_index)
            if z_item is None:
                continue

            z_info = {
                'row': row_index,
                'text': z_item.text(),
                'data': z_item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)
            }
            redshifts_form_lines.append(z_info)

        obj_state = {
            'redshift': self.main_wnd.z_dspinbox.value(),
            'quality_flag': self.main_wnd.qflag_combo_box.currentIndex(),
            'lines': {
                'list': lines_list,
                'redshifts': redshifts_form_lines
            }
        }

        self.object_state_dict[self.current_uuid] = obj_state
        self.global_state = GlobalState.READY

    def _lock(self, *args, **kwargs) -> None:
        self.main_wnd.spec_group_box.setEnabled(False)
        self.main_wnd.red_group_box.setEnabled(False)
        self.main_wnd.info_group_box.setEnabled(False)
        self.flux_chart_view.setRubberBand(
            qt_api.QtCharts.QChartView.RubberBand.NoRubberBand
        )

    def _open_online_doc(self, *args: Any, **kwargs: Any) -> None:
        webbrowser.open(redmost.ONLINE_DOC_URL)

    def _restore_object_state(self, obj_uuid: uuid.UUID) -> None:
        """
        Restore the programs state for a given object.

        :param obj_uuid: The UUID of the object
        """
        self.global_state = GlobalState.LOAD_OBJECT_STATE

        # Clear current content of the widgets
        self.main_wnd.lines_table_widget.setRowCount(0)
        self.main_wnd.lines_match_list_widget.clear()

        # If object state has not been previously saved, then return
        if obj_uuid not in self.object_state_dict:
            self.global_state = GlobalState.READY
            return

        # otherwise restore any old previously saved data
        old_state = self.object_state_dict[obj_uuid]

        lines = old_state['lines']['list']
        self.main_wnd.lines_table_widget.setRowCount(len(lines))
        for line_info in lines:
            w_item = qt_api.QtWidgets.QTableWidgetItem(line_info['text'])
            w_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, line_info['data'])
            w_item.setCheckState(line_info['checked'])
            w_item.setFlags(
                qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
                qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled |
                qt_api.QtCore.Qt.ItemFlag.ItemIsUserCheckable
            )

            r_item = qt_api.QtWidgets.QTableWidgetItem("")
            r_item.setFlags(
                qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
                qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled
            )
            r_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, 0.0)

            m_item = qt_api.QtWidgets.QTableWidgetItem("")
            m_item.setFlags(
                qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
                qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled
            )

            self.main_wnd.lines_table_widget.setItem(
                line_info['row'], 0, w_item
            )

            self.main_wnd.lines_table_widget.setItem(
                line_info['row'], 1, r_item
            )

            self.main_wnd.lines_table_widget.setItem(
                line_info['row'], 2, m_item
            )

        redshifts_form_lines = old_state['lines']['redshifts']
        for z_info in redshifts_form_lines:
            z_item = qt_api.QtWidgets.QListWidgetItem(z_info['text'])
            z_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, z_info['data'])
            self.main_wnd.lines_match_list_widget.insertItem(
                z_info['row'], z_item
            )

        try:
           current_redshift = old_state['redshift']
        except KeyError:
            current_redshift = None

        if current_redshift:
            self.main_wnd.z_dspinbox.setValue(current_redshift)
        else:
            self.main_wnd.z_dspinbox.setValue(0)

        self.global_state = GlobalState.READY

        try:
            quality_flag = old_state['quality_flag']
        except KeyError:
            quality_flag = 0

        self.main_wnd.qflag_combo_box.setCurrentIndex(quality_flag)

        self._update_spec_item_qf(obj_uuid, quality_flag)

    def _show_update_message(self, new_version: Optional[str]) -> None:
        self.msgBox.setText(
            self.qapp.tr(
                "A new version of Redmost is available"
            )
            + f": {new_version}." if new_version else "."
        )
        self.msgBox.setWindowTitle(self.qapp.tr("New version available"))
        self.msgBox.setInformativeText(
            self.qapp.tr("Please update the program using pip or visit") +
            f" <a href='{redmost.PYPI_REPO_PAGE}'>{redmost.PYPI_REPO_PAGE}</a>"
        )
        self.msgBox.setDetailedText("")
        self.msgBox.setTextFormat(qt_api.QtCore.Qt.TextFormat.RichText)
        self.msgBox.setIcon(qt_api.QtWidgets.QMessageBox.Icon.Information)
        self.msgBox.setStandardButtons(
            qt_api.QtWidgets.QMessageBox.StandardButton.Ok
        )
        self.msgBox.exec()

    def _unlock(self, *args, **kwargs) -> None:
        self.main_wnd.spec_group_box.setEnabled(True)
        self.main_wnd.red_group_box.setEnabled(True)
        self.main_wnd.info_group_box.setEnabled(True)
        self.main_wnd.plot_group_box.setEnabled(True)
        self.flux_chart_view.setRubberBand(
            qt_api.QtCharts.QChartView.RubberBand.RectangleRubberBand
        )

    def _update_spec_item_qf(self, item_uuid: uuid.UUID, qf: int) -> None:
        item: qt_api.QtWidgets.QListWidgetItem = self.open_spectra_items[item_uuid]
        item.setBackground(qt_api.QtGui.QColor(self.qf_color[qf]))

    def _updateMouseLabelFromEvent(self, *args) -> None:
        self._updateMouseLabel(args[0][0])

    def _updateMouseLabel(self, mouse_pos: qt_api.QtCore.QPointF) -> None:
        self.mousePosLabel.setText(f"\u03BB = {mouse_pos.x():.2f}")

    def addLine(self, wavelength: float) -> None:
        """
        Add a new line by selecting its position with the mouse.

        :param wavelength: The wavelength of the line.
        """
        lam_item = qt_api.QtWidgets.QTableWidgetItem(f"{wavelength:.2f} A")
        lam_item.setFlags(
            qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
            qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled |
            qt_api.QtCore.Qt.ItemFlag.ItemIsUserCheckable
        )
        lam_item.setCheckState(qt_api.QtCore.Qt.CheckState.Checked)
        lam_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, wavelength)

        rest_lam = wavelength / (1 + self.main_wnd.z_dspinbox.value())
        rest_item = qt_api.QtWidgets.QTableWidgetItem(f"{rest_lam:.2f} A")
        rest_item.setFlags(
            qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
            qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled
        )
        rest_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, 0.0)

        best_matches = [
            x[1]
            for x in lines.get_lines(wrange=[rest_lam - 5, rest_lam + 5])
        ]

        match_item = qt_api.QtWidgets.QTableWidgetItem('; '.join(best_matches))
        match_item.setFlags(
            qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
            qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled
        )

        new_item_row: int = self.main_wnd.lines_table_widget.rowCount()

        self.main_wnd.lines_table_widget.setRowCount(new_item_row + 1)
        self.main_wnd.lines_table_widget.setItem(new_item_row, 0, lam_item)
        self.main_wnd.lines_table_widget.setItem(new_item_row, 1, rest_item)
        self.main_wnd.lines_table_widget.setItem(new_item_row, 2, match_item)

    def closeEvent(self, event: qt_api.QtGui.QCloseEvent) -> None:
        self.msgBox.setText(
            self.qapp.tr(
                "Do you want to save the current project before closing?"
            )
        )
        self.msgBox.setWindowTitle(self.qapp.tr("Closing..."))
        self.msgBox.setInformativeText("")
        self.msgBox.setDetailedText("")
        self.msgBox.setIcon(qt_api.QtWidgets.QMessageBox.Icon.Question)
        self.msgBox.setStandardButtons(
            qt_api.QtWidgets.QMessageBox.StandardButton.Yes |
            qt_api.QtWidgets.QMessageBox.StandardButton.No |
            qt_api.QtWidgets.QMessageBox.StandardButton.Cancel
        )
        res = self.msgBox.exec()

        if res == qt_api.QtWidgets.QMessageBox.StandardButton.Yes:
            # If Yes button is pressed, then accept the event only if the
            # project is correctly saved
            event.setAccepted(self.doSaveProject())
        elif res == qt_api.QtWidgets.QMessageBox.StandardButton.No:
            event.accept()
        else:
            # Ignore the close event if Cancel button is pressed
            event.ignore()

        self.saveSettings()

    def collectRedrockResults(self) -> None:
        if self.redrock_handler is None:
            return

        self.statusbar.showMessage(self.qapp.tr("Redrock results ready!"))

        res = self.redrock_handler.results

        if res is None:
            return

        zbest = res[1][res[1]['znum'] == 0]

        self._lock()
        self.global_state = GlobalState.LOAD_OBJECT_STATE
        self._backup_current_object_state()
        for row in zbest:
            obj_uuid = self.redrock_handler._t_dict[row['targetid']]

            try:
                info_dict = self.object_state_dict[obj_uuid]
            except KeyError:
                info_dict = {
                    'redshift': None,
                    'quality_flag': 0,
                    'lines': {
                        'list': [],
                        'redshifts': []
                    }
                }
                self.object_state_dict[obj_uuid] = info_dict

            info_dict['redshift'] = row['z']
            if row['zwarn'] != 0:
                self._update_spec_item_qf(obj_uuid, 1)
        self._restore_object_state(self.current_uuid)
        self.global_state = GlobalState.READY
        self._unlock()

    def currentQualityFlagChanged(self, df_index):
        if self.current_uuid is None:
            return
        self._update_spec_item_qf(self.current_uuid, df_index)
        self._backup_current_object_state()

    def currentSpecItemChanged(
        self,
        new_item: Optional[qt_api.QtWidgets.QListWidgetItem],
        *args: Any,
        **kwargs: Any
    ) -> None:
        """
        Update widgets when the current object changes.

        :param new_item: The new list item.
        """
        if new_item is None:
            self.main_wnd.redrock_current_radio.setEnabled(False)
            return
        else:
            self.main_wnd.redrock_current_radio.setEnabled(True)

        if self.global_state != GlobalState.READY:
            return

        self._unlock()

        spec_uuid: uuid.UUID = new_item.data(
            qt_api.QtCore.Qt.ItemDataRole.UserRole
        )

        self.showObjectInfo(spec_uuid)

        flux_chart = self.flux_chart_view.chart()
        flux_chart.removeAllSeries()

        var_chart = self.var_chart_view.chart()
        var_chart.removeAllSeries()

        wd_chart = self.wdisp_chart_view.chart()
        wd_chart.removeAllSeries()

        sky_chart = self.sky_chart_view.chart()
        sky_chart.removeAllSeries()

        self._backup_current_object_state()
        if spec_uuid != self.current_uuid:
            # If we actually change the spectrum, then reset the view
            self.current_uuid = spec_uuid
            self._restore_object_state(spec_uuid)

            for ax in flux_chart.axes():
                flux_chart.removeAxis(ax)
            for ax in var_chart.axes():
                var_chart.removeAxis(ax)
            for ax in wd_chart.axes():
                wd_chart.removeAxis(ax)
            for ax in sky_chart.axes():
                sky_chart.removeAxis(ax)

        sp: Spectrum1D = self.open_spectra[spec_uuid]

        wav: np.ndarray = sp.spectral_axis.value
        wav_unit: units.Unit = sp.spectral_axis.unit

        flux: np.ndarray = sp.flux.value
        flux_unit: units.Unit = sp.flux.unit

        var: Union[np.ndarray, None] = None
        var_unit: Union[units.Unit, None] = None

        wdisp: Union[np.ndarray, None] = None
        wdisp_unit: Union[units.Unit, None] = None

        sky: Union[np.ndarray, None] = None
        sky_unit: Union[units.Unit, None] = None

        if isinstance(sp.uncertainty, VarianceUncertainty):
            var = sp.uncertainty.array
            var_unit = sp.uncertainty.unit
        elif isinstance(sp.uncertainty, InverseVariance):
            var = 1 / sp.uncertainty.array
            var_unit = 1 / sp.uncertainty.unit
        elif isinstance(sp.uncertainty, StdDevUncertainty):
            var = sp.uncertainty.array ** 2
            var_unit = sp.uncertainty.unit ** 2

        try:
            wd_data = sp.wd  # type: ignore
        except AttributeError:
            pass
        else:
            if wd_data is not None:
                wdisp = wd_data.value
                wdisp_unit = wd_data.unit

        try:
            sky_data = sp.sky  # type: ignore
        except AttributeError:
            pass
        else:
            if sky_data is not None:
                sky = sky_data.value
                sky_unit = sky_data.unit

        flux_series = values2series(wav, flux, self.qapp.tr("Flux"))
        flux_chart.addSeries(flux_series)

        if not flux_chart.axes():
            flux_axis_x = qt_api.QtCharts.QValueAxis()
            flux_axis_y = qt_api.QtCharts.QValueAxis()
            var_axis_x = qt_api.QtCharts.QValueAxis()
            var_axis_y = qt_api.QtCharts.QValueAxis()
            wd_axis_x = qt_api.QtCharts.QValueAxis()
            wd_axis_y = qt_api.QtCharts.QValueAxis()
            sky_axis_x = qt_api.QtCharts.QValueAxis()
            sky_axis_y = qt_api.QtCharts.QValueAxis()

            flux_chart.addAxis(
                flux_axis_x, qt_api.QtCore.Qt.AlignmentFlag.AlignBottom
            )
            flux_chart.addAxis(
                flux_axis_y, qt_api.QtCore.Qt.AlignmentFlag.AlignLeft
            )
            var_chart.addAxis(
                var_axis_x, qt_api.QtCore.Qt.AlignmentFlag.AlignBottom
            )
            var_chart.addAxis(
                var_axis_y, qt_api.QtCore.Qt.AlignmentFlag.AlignLeft
            )
            wd_chart.addAxis(
                wd_axis_x, qt_api.QtCore.Qt.AlignmentFlag.AlignBottom
            )
            wd_chart.addAxis(
                wd_axis_y, qt_api.QtCore.Qt.AlignmentFlag.AlignLeft
            )
            sky_chart.addAxis(
                sky_axis_x, qt_api.QtCore.Qt.AlignmentFlag.AlignBottom
            )
            sky_chart.addAxis(
                sky_axis_y, qt_api.QtCore.Qt.AlignmentFlag.AlignLeft
            )
        else:
            flux_axis_x = flux_chart.axes()[0]
            flux_axis_y = flux_chart.axes()[1]
            var_axis_x = var_chart.axes()[0]
            var_axis_y = var_chart.axes()[1]
            wd_axis_x = wd_chart.axes()[0]
            wd_axis_y = wd_chart.axes()[1]
            sky_axis_x = sky_chart.axes()[0]
            sky_axis_y = sky_chart.axes()[1]

        flux_axis_x.setTickInterval(500)
        flux_axis_x.setLabelFormat("%.2f")
        flux_axis_x.setTitleText(str(wav_unit))

        flux_axis_y.setLabelFormat("%.2f")
        flux_axis_y.setTitleText(str(flux_unit))

        flux_series.attachAxis(flux_axis_x)
        flux_series.attachAxis(flux_axis_y)

        smoothing_check_state = self.main_wnd.smoothing_check_box.checkState()
        if smoothing_check_state == qt_api.QtCore.Qt.CheckState.Checked:
            smoothing_factor = self.main_wnd.smoothing_dspinbox.value()

            flux_series.setOpacity(0.2)
            smoothing_sigma = len(flux) / (1 + 2 * smoothing_factor)
            smoothed_flux = utils.smooth_fft(flux, sigma=smoothing_sigma)
            smoothed_flux_series = values2series(
                wav, smoothed_flux, self.qapp.tr("Smoothed flux")
            )

            pen: qt_api.QtGui.QPen = smoothed_flux_series.pen()
            pen.setColor(qt_api.QtGui.QColor("orange"))
            pen.setWidth(2)
            smoothed_flux_series.setPen(pen)

            flux_chart.addSeries(smoothed_flux_series)
            smoothed_flux_series.attachAxis(flux_axis_x)
            smoothed_flux_series.attachAxis(flux_axis_y)

        if var is None:
            self.main_wnd.container_tab_var.setEnabled(False)
        else:
            self.main_wnd.container_tab_var.setEnabled(True)
            var_series = values2series(wav, var, self.qapp.tr("Variance"))
            var_chart.addSeries(var_series)

            var_axis_x.setTickInterval(500)
            var_axis_x.setLabelFormat("%.2f")
            var_axis_x.setTitleText(str(wav_unit))

            var_axis_y.setTickCount(10)
            var_axis_y.setLabelFormat("%.2f")
            var_axis_y.setTitleText(str(var_unit))

            var_series.attachAxis(var_axis_x)
            var_series.attachAxis(var_axis_y)

        if wdisp is None:
            self.main_wnd.container_tab_wd.setEnabled(False)
        else:
            self.main_wnd.container_tab_wd.setEnabled(True)
            wd_series = values2series(wav, wdisp, self.qapp.tr("Reso. curve"))
            wd_chart.addSeries(wd_series)

            wd_axis_x.setTickInterval(500)
            wd_axis_x.setLabelFormat("%.2f")
            wd_axis_x.setTitleText(str(wav_unit))

            wd_axis_y.setTickCount(10)
            wd_axis_y.setLabelFormat("%.2f")
            wd_axis_y.setTitleText(str(wdisp_unit))

            wd_series.attachAxis(wd_axis_x)
            wd_series.attachAxis(wd_axis_y)

        if sky is None:
            self.main_wnd.container_tab_sky.setEnabled(False)
        else:
            self.main_wnd.container_tab_sky.setEnabled(True)
            sky_series = values2series(wav, sky, self.qapp.tr("Sky"))
            sky_chart.addSeries(sky_series)

            sky_axis_x.setTickInterval(500)
            sky_axis_x.setLabelFormat("%.2f")
            sky_axis_x.setTitleText(str(wav_unit))

            sky_axis_y.setTickCount(10)
            sky_axis_y.setLabelFormat("%.2f")
            sky_axis_y.setTitleText(str(sky_unit))

            sky_series.attachAxis(sky_axis_x)
            sky_series.attachAxis(sky_axis_y)

        flux_chart.setContentsMargins(0, 0, 0, 0)
        flux_chart.setBackgroundRoundness(0)
        flux_chart.legend().hide()

        var_chart.setContentsMargins(0, 0, 0, 0)
        var_chart.setBackgroundRoundness(0)
        var_chart.legend().hide()

        wd_chart.setContentsMargins(0, 0, 0, 0)
        wd_chart.setBackgroundRoundness(0)
        wd_chart.legend().hide()

        sky_chart.setContentsMargins(0, 0, 0, 0)
        sky_chart.setBackgroundRoundness(0)
        sky_chart.legend().hide()

    def doAddNewLine(self, *args: Any, **kwargs: Any) -> None:
        """Tell the main app to identify a new line."""
        # Do nothing if no spectrum is currently selected
        if self.current_uuid is None:
            return
        self._lock()
        self.main_wnd.spec_group_box.setEnabled(False)
        self.global_state = GlobalState.SELECT_LINE_MANUAL
        self.qapp.setOverrideCursor(qt_api.QtCore.Qt.CursorShape.CrossCursor)

    def doDeleteAllLines(self, *args: Any, **kwargs: Any) -> None:
        """Delete all identified lines."""
        self.main_wnd.lines_table_widget.setRowCount(0)

    def doDeleteCurrentLine(self, *args: Any, **kwargs: Any) -> None:
        """Delete the current selected line from the table."""
        # Do nothing if no spectrum is currently selected
        if self.current_uuid is None:
            return

        self.main_wnd.lines_table_widget.removeRow(
            self.main_wnd.lines_table_widget.currentRow()
        )

    def doExportZcat(self, *args, **kwargs) -> None:
        if self.current_open_dir is not None:
            cur_dir = self.current_open_dir
        elif self.current_project_file_path is not None:
            cur_dir = os.path.dirname(self.current_project_file_path)
        else:
            cur_dir = '.'

        open_list = qt_api.QtWidgets.QFileDialog.getSaveFileName(
            self.main_wnd,
            self.qapp.tr("Export redshift catalogue to file"),
            os.path.join(cur_dir, 'zcat.fits'),
            (
                f"{self.qapp.tr('FITS table')} (*.fits *.fit);;"
                f"{self.qapp.tr('CSV table')} (*.csv);;"
                f"{self.qapp.tr('VO table')} (*.votable)"
            ),
            f"{self.qapp.tr('FITS table')} (*.fits *.fit)"
        )

        try:
            dest_file_path, file_type = open_list
            self.exportZcat(dest_file_path, file_type)
        except Exception as exc:
            self.msgBox.setWindowTitle(self.qapp.tr("Error"))
            self.msgBox.setText(
                 self.qapp.tr(
                     "An error has occurred while saving the catalogue."
                 )
            )
            self.msgBox.setInformativeText('')
            self.msgBox.setDetailedText(str(exc))
            self.msgBox.setIcon(
                qt_api.QtWidgets.QMessageBox.Icon.Critical
            )
            self.msgBox.setStandardButtons(
                qt_api.QtWidgets.QMessageBox.StandardButton.Ok
            )
            self.msgBox.exec()
        else:
            self.current_open_dir = os.path.dirname(dest_file_path)
            self.statusbar.showMessage(
                self.qapp.tr("Redshift catalogue saved")
            )

    def doIdentifyLines(self,  *args: Any, **kwargs: Any) -> None:
        """Automagically identify lines in the current spectrum."""
        if self.current_uuid is None:
            return

        sp: Spectrum1D = self.open_spectra[self.current_uuid]

        wav: np.ndarray = sp.spectral_axis.value
        flux: np.ndarray = sp.flux.value
        var: np.ndarray | None = None

        if isinstance(sp.uncertainty, VarianceUncertainty):
            var = sp.uncertainty.array
        if isinstance(sp.uncertainty, InverseVariance):
            var = 1 / sp.uncertainty.array
        elif isinstance(sp.uncertainty, StdDevUncertainty):
            var = sp.uncertainty.array ** 2

        my_lines: List[
            Tuple[int, float, float, float]
        ] = lines.get_spectrum_lines(
            wavelengths=wav,
            flux=flux,
            var=var
        )

        self.main_wnd.lines_table_widget.setRowCount(0)
        self.main_wnd.lines_table_widget.setRowCount(len(my_lines))
        for j, (k, w, l, h) in enumerate(my_lines):
            new_item = qt_api.QtWidgets.QTableWidgetItem(f"{w:.2f} A")
            new_item.setFlags(
                qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
                qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled |
                qt_api.QtCore.Qt.ItemFlag.ItemIsUserCheckable
            )
            new_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, w)
            new_item.setCheckState(qt_api.QtCore.Qt.CheckState.Checked)
            self.main_wnd.lines_table_widget.setItem(j, 0, new_item)

    def doImportZcat(self, *args, **kwargs) -> str:
        """
        Open a catalogue file

        :param args: A placeholder
        :param kwargs: A placeholder
        :return: THe path of the catalogue file.
        """
        if self.current_open_dir is not None:
            cur_dir = self.current_open_dir
        elif self.current_project_file_path is not None:
            cur_dir = os.path.dirname(self.current_project_file_path)
        else:
            cur_dir = '.'
        catalogue_file, _ = qt_api.QtWidgets.QFileDialog.getOpenFileName(
            self.main_wnd,
            self.qapp.tr("Import a zcat"),
            cur_dir,
            (
                f"{self.qapp.tr('FITS table')} (*.fits *.fit);;"
                f"{self.qapp.tr('CSV table')} (*.csv);;"
                f"{self.qapp.tr('VO table')} (*.votable);;"
                f"{self.qapp.tr('All files')} (*.*)"
            ),
            f"{self.qapp.tr('FITS table')} (*.fits *.fit)"
        )
        self._doImportZcat(catalogue_file)


    def _doImportZcat(self, catalogue_file: Optional[str]) -> None:
        if not catalogue_file:
            return

        try:
            zcat_tbl = Table.read(catalogue_file)
        except Exception as exc:
            self.msgBox.setWindowTitle(self.qapp.tr("Error"))
            self.msgBox.setText(
                self.qapp.tr(
                    "An error has occurred while importing the catalogue."
                )
            )
            self.msgBox.setInformativeText('')
            self.msgBox.setDetailedText(str(exc))
            self.msgBox.setIcon(
                qt_api.QtWidgets.QMessageBox.Icon.Critical
            )
            self.msgBox.setStandardButtons(
                qt_api.QtWidgets.QMessageBox.StandardButton.Ok
            )
            self.msgBox.exec()
            return

        self.current_open_dir = os.path.dirname(catalogue_file)
        self.zcat_mapping_dialog.setup(zcat_tbl)
        self.zcat_mapping_dialog.open()

    def doImportSpectra(self,  *args: Any, **kwargs: Any) -> None:
        """Use QFileDialog to get spectra files."""
        if self.current_open_dir is not None:
            cur_dir = self.current_open_dir
        elif self.current_project_file_path is not None:
            cur_dir = os.path.dirname(self.current_project_file_path)
        else:
            cur_dir = '.'

        file_list, files_type = qt_api.QtWidgets.QFileDialog.getOpenFileNames(
            self.main_wnd,
            self.qapp.tr("Import Spectra"),
            cur_dir,
            (
                f"{self.qapp.tr('FITS')} (*.fit *.fits);;"
                f"{self.qapp.tr('ASCII')} (*.txt *.dat *.cat);;"
                f"{self.qapp.tr('All Files')} (*.*)"
            ),
            f"{self.qapp.tr('FITS')} (*.fit *.fits)"
        )

        exception_tracker: Dict[uuid.UUID, Tuple[str, str]] = {}

        self._lock()
        self.global_state = GlobalState.WAITING

        n_files = len(file_list)

        self.pbar.setMaximum(n_files)
        self.pbar.show()
        self.cancel_button.show()

        al_least_one: bool = False
        for j, file in enumerate(file_list):
            if self.global_state == GlobalState.REUQUEST_CANCEL:
                break

            self.pbar.setValue(j + 1)
            self.statusbar.showMessage(
                self.qapp.tr("Loading file") + f"{j + 1:d}/{n_files}..."
            )
            self.qapp.processEvents()

            item_uuid: uuid.UUID = uuid.uuid4()

            # Check for a possible collision, even if it should never happen
            while item_uuid in self.open_spectra_files.keys():
                item_uuid = uuid.uuid4()

            try:
                sp: Spectrum1D = loaders.read(file)
            except Exception as exc:
                exception_tracker[item_uuid] = (file, str(exc))
                continue
            else:
                if not al_least_one:
                    self.current_open_dir = os.path.dirname(file)
                    al_least_one = True

            new_item = qt_api.QtWidgets.QListWidgetItem(os.path.basename(file))
            new_item.setCheckState(qt_api.QtCore.Qt.CheckState.Checked)
            new_item.setToolTip(file)
            new_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, item_uuid)

            info_dict: Dict[str, Any] = {
                'redshift': 0,
                'quality_flag': 0,
                'lines': {
                    'list': [],
                    'redshifts': []
                }
            }

            self.open_spectra[item_uuid] = sp
            self.open_spectra_files[item_uuid] = os.path.abspath(file)
            self.open_spectra_items[item_uuid] = new_item
            self.object_state_dict[item_uuid] = info_dict
            self.main_wnd.spec_list_widget.addItem(new_item)
            self.qapp.processEvents()

        self.cancel_button.hide()
        self.pbar.hide()
        if len(self.open_spectra) > 0:
            self._unlock()
        self.global_state = GlobalState.READY

        if exception_tracker:
            self.msgBox.setWindowTitle(self.qapp.tr("Error"))
            self.msgBox.setText(
                self.qapp.tr(
                    "An error has occurred while loading the project: one or "
                    "more files could not be loaded."
                )
            )
            self.msgBox.setDetailedText(
                "\n\n".join(
                    [
                        f"uuid: {exc_uuid}\n"
                        f"file: {exc_data[0]}\n"
                        f"reason: {exc_data[1]}"
                        for exc_uuid, exc_data in exception_tracker.items()
                    ]
                )
            )
            self.msgBox.setIcon(qt_api.QtWidgets.QMessageBox.Icon.Critical)
            self.msgBox.setStandardButtons(
                qt_api.QtWidgets.QMessageBox.StandardButton.Ok
            )
            self.msgBox.exec()

    def doNewProject(self, *args, **kwargs) -> None:
        self.newProject()

    def doOpenProject(self, *args, **kwargs) -> bool:
        if self.current_project_file_path is not None:
            cur_dir = os.path.basename(self.current_project_file_path)
        elif self.current_open_dir is not None:
            cur_dir = self.current_open_dir
        else:
            cur_dir = '.'

        proj_file_path, _ = qt_api.QtWidgets.QFileDialog.getOpenFileName(
            self.main_wnd,
            self.qapp.tr("Load a project from file"),
            cur_dir,
            (
                f"{self.qapp.tr('Project file')} (*.json);;"
                f"{self.qapp.tr('All files')} (*.*)"
            ),
            f"{self.qapp.tr('Project file')} (*.json)"
        )

        if not proj_file_path:
            return False

        self.newProject()

        try:
            self.openProject(file_name=proj_file_path)
        except Exception as exc:
            self.msgBox.setWindowTitle(self.qapp.tr("Error"))
            self.msgBox.setText(
                 self.qapp.tr(
                     "An error has occurred while opening the project."
                 )
            )
            self.msgBox.setInformativeText('')
            self.msgBox.setDetailedText(str(exc))
            self.msgBox.setIcon(
                qt_api.QtWidgets.QMessageBox.Icon.Critical
            )
            self.msgBox.setStandardButtons(
                qt_api.QtWidgets.QMessageBox.StandardButton.Ok
            )
            self.msgBox.exec()
            return False

        self.current_project_file_path = proj_file_path
        return True

    def doRedshiftFromLines(self, *args: Any, **kwargs: Any) -> None:
        """Compute the redshift by matching identified lines."""
        line_table: qt_api.QtWidgets.QTableWidget = self.main_wnd.lines_table_widget
        z_list: qt_api.QtWidgets.QListWidget = self.main_wnd.lines_match_list_widget
        tol: float = self.main_wnd.lines_tol_dspinbox.value()
        z_min: float = self.main_wnd.z_min_dspinbox.value()
        z_max: float = self.main_wnd.z_max_dspinbox.value()

        # Build a list of selected lines to be used.
        # If no lines are selected, then use all lines.
        lines_lam = []
        for row_index in range(line_table.rowCount()):
            item = line_table.item(row_index, 0)
            if item is None:
                continue
            elif item.checkState() != qt_api.QtCore.Qt.CheckState.Checked:
                # Ignore lines that are not selected
                continue

            lines_lam.append(float(item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)))

        res = lines.get_redshift_from_lines(
            lines_lam, z_min=z_min, z_max=z_max, tol=tol
        )

        if res is None:
            return

        z_list.clear()
        for z, prob in zip(res[0], res[1]):
            new_z_item = qt_api.QtWidgets.QListWidgetItem(f"z={z:.4f} (p={prob:.4f})")
            new_z_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, (z, prob))
            z_list.addItem(new_z_item)

    def doRemoveCurrentSpecItems(self) -> None:
        if self.main_wnd.spec_list_widget.count() == 0:
            return

        current_row = self.main_wnd.spec_list_widget.currentRow()
        item = self.main_wnd.spec_list_widget.item(current_row)
        if item is None:
            return

        item_uuid = item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)
        self.main_wnd.spec_list_widget.takeItem(current_row)
        self.open_spectra_items.pop(item_uuid)
        self.open_spectra_files.pop(item_uuid)

        try:
            self.object_state_dict.pop(item_uuid)
        except KeyError:
            pass

        del item

        if self.main_wnd.spec_list_widget.count() == 0:
            self.newProject()

    def doRemoveSelectedSpecItems(self) -> None:
        if self.main_wnd.spec_list_widget.count() == 0:
            return

        for row in range(self.main_wnd.spec_list_widget.count(), 0, -1):
            item = self.main_wnd.spec_list_widget.item(row - 1)

            if item is None:
                continue
            elif item.checkState() != qt_api.QtCore.Qt.CheckState.Checked:
                continue

            item_uuid = item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)
            self.main_wnd.spec_list_widget.takeItem(row - 1)
            self.open_spectra_items.pop(item_uuid)
            self.open_spectra_files.pop(item_uuid)

            try:
                self.object_state_dict.pop(item_uuid)
            except KeyError:
                pass

            del item

        if self.main_wnd.spec_list_widget.count() == 0:
            self.newProject()

    def doSaveProject(self, *args: Any, **kwargs: Any) -> bool:
        """Save the current project."""
        return self.doSaveProjectAs(dest=self.current_project_file_path)

    def doSaveProjectAs(
            self,
            dest: Optional[str] = None,
            *args, **kwargs
    ) -> bool:
        """
        Save the current project to a file.

        :return: True if the project is saved correctly, False otherwise.
        """
        if (dest is None) or (not os.path.exists(str(dest))):
            if self.current_project_file_path is not None:
                cur_dir = self.current_project_file_path
            elif self.current_open_dir is not None:
                cur_dir = self.current_open_dir
            else:
                cur_dir = '.'

            dest_file_path, _ = qt_api.QtWidgets.QFileDialog.getSaveFileName(
                self.main_wnd,
                self.qapp.tr("Save project to file"),
                cur_dir,
                (
                    f"{self.qapp.tr('Project file')} (*.json);;"
                    f"{self.qapp.tr('All files')} (*.*)"
                ),
                f"{self.qapp.tr('Project file')} (*.json)"
            )

            if not dest_file_path:
                return False
        else:
            dest_file_path = str(dest)

        try:
            self.saveProject(dest_file_path)
        except Exception as exc:
            self.msgBox.setWindowTitle(self.qapp.tr("Error"))
            self.msgBox.setText(
                 self.qapp.tr(
                     "An error has occurred while saving the project."
                 )
            )
            self.msgBox.setInformativeText('')
            self.msgBox.setDetailedText(str(exc))
            self.msgBox.setIcon(
                qt_api.QtWidgets.QMessageBox.Icon.Critical
            )
            self.msgBox.setStandardButtons(
                qt_api.QtWidgets.QMessageBox.StandardButton.Ok
            )
            self.msgBox.exec()
            return False

        self.statusbar.showMessage(self.qapp.tr("Project saved"))
        self.current_project_file_path = dest_file_path
        return True

    def doStartRedrock(self, *args, **kwargs) -> None:
        if self.redrock_handler is None:
            # Should never happen since we lock the button to start this
            # action if the redrock backend is not found
            return

        self.main_wnd.redrock_text_edit.clear()
        self.statusbar.showMessage("Running redrock backend...")

        for_rr: Dict[uuid.UUID, Spectrum1D] = {}
        if self.main_wnd.redrock_all_radio.isChecked():
            for_rr = self.open_spectra
        elif self.main_wnd.redrock_selected_radio.isChecked():

            item: Union[None, qt_api.QtWidgets.QListWidgetItem]
            for row in range(self.main_wnd.spec_list_widget.count()):
                item = self.main_wnd.spec_list_widget.item(row)

                if item is None:
                    # should never happen!
                    continue

                if item.checkState() == qt_api.QtCore.Qt.CheckState.Checked:
                    item_uuid = item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)
                    for_rr[item_uuid] = self.open_spectra[item_uuid]
        else:
            if self.current_uuid is None:
                return
            for_rr[self.current_uuid] = self.open_spectra[self.current_uuid]

        self.redrock_handler.run_redrock(for_rr)

    def doToggleDone(self) -> None:
        check_state = None
        item: Union[qt_api.QtWidgets.QListWidgetItem, None]
        for row in range(self.main_wnd.spec_list_widget.count()):
            item = self.main_wnd.spec_list_widget.item(row)
            if item is None:
                continue

            item_uuid = item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)

            if item_uuid not in self.object_state_dict:
                continue

            if self.object_state_dict[item_uuid]['quality_flag'] == 0:
                continue

            if check_state is None:
                if item.checkState() == qt_api.QtCore.Qt.CheckState.Checked:
                    check_state = qt_api.QtCore.Qt.CheckState.Unchecked
                else:
                    check_state = qt_api.QtCore.Qt.CheckState.Checked

            item.setCheckState(check_state)

    def doToggleSimilar(self) -> None:
        self.toggleSimilarSpecItems(
            self.main_wnd.spec_list_widget.currentItem()
        )

    def doToggleAll(self) -> None:
        item = self.main_wnd.spec_list_widget.currentItem()
        if item is None:
            return

        if item.checkState() == qt_api.QtCore.Qt.CheckState.Checked:
            ref_check_state = qt_api.QtCore.Qt.CheckState.Unchecked
        else:
            ref_check_state = qt_api.QtCore.Qt.CheckState.Checked

        other_item: Union[None, qt_api.QtWidgets.QListWidgetItem]
        for row in range(self.main_wnd.spec_list_widget.count()):
            other_item = self.main_wnd.spec_list_widget.item(row)
            if other_item is None:
                continue
            other_item.setCheckState(ref_check_state)

    def doZoomIn(self, *args, **kwargs) -> None:
        """Zoom In flux and var plots."""
        self.flux_chart_view.zoomIn()

    def doZoomOut(self, *args, **kwargs) -> None:
        """Zoom Out flux and var plots."""
        self.flux_chart_view.zoomOut()

    def doZoomReset(self, *arg, **kwargs) -> None:
        """Reset the zoom for the flux chart and all its siblings."""
        self.flux_chart_view.zoomReset()

    def exportZcat(self, dest_file, file_type) -> None:
        zcat_tbl = Table(
            names=['INDEX', 'SPEC_FILE', 'Z', 'QF', 'UUID'],
            dtype=[int, str, float, int, str]
        )

        for j, (o_uuid, o_path) in enumerate(self.open_spectra_files.items()):
            spec_file = os.path.basename(o_path)

            try:
                info_dict = self.object_state_dict[o_uuid]
            except KeyError:
                redshift = -99.0
                quality_flag = 0
            else:
                if info_dict['redshift'] is None:
                    redshift = -99.0
                else:
                    redshift = info_dict['redshift']
                quality_flag = info_dict['quality_flag']

            new_row = (j, spec_file, redshift, quality_flag, o_uuid.hex)
            zcat_tbl.add_row(new_row)

        # Get valid file extensions
        valied_exts = (file_type.split('(')[1]).split(')')[0]
        valied_exts = valied_exts.replace('*', '').split()

        # get destination file extension
        dest_ext = os.path.splitext(dest_file)[1]

        # correct for mismatching extension
        if dest_ext not in valied_exts:
            dest_file = dest_file + valied_exts[0]

        zcat_tbl.write(dest_file, overwrite=True)

    def getZcatColumnMapping(self, result: int) -> None:

        try:
            self.importZcat(self.zcat_mapping_dialog.zcat_tbl)
        except Exception as exc:
            self.msgBox.setWindowTitle(self.qapp.tr("Error"))
            self.msgBox.setText(
                self.qapp.tr(
                    "An error has occurred while importing the catalogue."
                )
            )
            self.msgBox.setInformativeText('')
            self.msgBox.setDetailedText(str(exc))
            self.msgBox.setIcon(
                qt_api.QtWidgets.QMessageBox.Icon.Critical
            )
            self.msgBox.setStandardButtons(
                qt_api.QtWidgets.QMessageBox.StandardButton.Ok
            )
            self.msgBox.exec()

    def importZcat(self, zcat_tbl) -> None:
        id_col, z_col, qf_col = self.zcat_mapping_dialog.get_mapping()

        self._lock()
        self.global_state = GlobalState.LOAD_OBJECT_STATE
        self.pbar.show()
        self.cancel_button.show()
        self.pbar.setMaximum(len(zcat_tbl))

        matched_uuids = []

        for j, row in enumerate(zcat_tbl):
            self.pbar.setValue(j + 1)

            if self.global_state == GlobalState.REUQUEST_CANCEL:
                break

            spec_file = str(row[id_col]).strip()

            for obj_uuid, obj_file in self.open_spectra_files.items():
                if obj_uuid in matched_uuids:
                    continue
                elif os.path.basename(obj_file) == spec_file:
                    matched_uuids.append(obj_uuid)
                    try:
                        info_dict = self.object_state_dict[obj_uuid]
                    except KeyError:
                        info_dict = {
                            'redshift': None,
                            'quality_flag': 0,
                            'lines': {
                                'list': [],
                                'redshifts': []
                            }
                        }
                        self.object_state_dict[obj_uuid] = info_dict

                    if row[z_col] == -99:
                        info_dict["redshift"] = None
                    else:
                        info_dict["redshift"] = row[z_col]

                    try:
                        info_dict["quality_flag"] = row[qf_col]
                    except KeyError:
                        pass
                    else:
                        self._update_spec_item_qf(obj_uuid, row[qf_col])
                    break

        self.global_state = GlobalState.READY
        self.pbar.hide()
        self.cancel_button.hide()
        self._unlock()

    def loadSettings(self) -> None:
        for key, (_, set_func, cast_func) in self._globsal_setting.items():
            if self.settings.contains(key):
                value = self.settings.value(key)
                if value is None:
                    continue
                elif cast_func is not None:
                    value = cast_func(value)
                try:
                    set_func(value)
                except (TypeError, ValueError):
                    continue

    def mousePressedFlux(self, args) -> None:
        """Handle mouse button pressed events."""
        data_pos: qt_api.QtCore.QPointF = args[0]
        event: qt_api.QtGui.QMouseEvent = args[1]
        if self.global_state == GlobalState.SELECT_LINE_MANUAL:
            self.addLine(data_pos.x())
            self.global_state = GlobalState.READY
            self.qapp.restoreOverrideCursor()
            self._unlock()

    def newProject(self) -> None:
        self.open_spectra_files = {}
        self.open_spectra = {}
        self.object_state_dict = {}
        self.current_uuid = None
        self.current_project_file_path = None
        self.current_open_dir = None

        self.main_wnd.spec_list_widget.clear()
        self.main_wnd.lines_match_list_widget.clear()
        self.main_wnd.lines_table_widget.setRowCount(0)
        self.main_wnd.obj_prop_table_widget.setRowCount(0)

        flux_chart = self.flux_chart_view.chart()
        flux_chart.removeAllSeries()
        for ax in flux_chart.axes():
            flux_chart.removeAxis(ax)

        var_chart = self.var_chart_view.chart()
        var_chart.removeAllSeries()
        for ax in var_chart.axes():
            var_chart.removeAxis(ax)

        self._lock()
        self.global_state = GlobalState.READY
        self.statusbar.showMessage(self.qapp.tr("New project created"))

    def openProject(self, file_name: str) -> None:
        """Load the project from a file."""
        with open(file_name, 'r') as f:
            serialized_dict: Dict[str, Any] = json.load(f)

        self._lock()
        self.global_state = GlobalState.WAITING
        self.pbar.setMaximum(0)
        self.pbar.show()

        n_files = len(serialized_dict['open_files'])
        exception_tracker: Dict[uuid.UUID, Tuple[str, str]] = {}

        self.pbar.setMaximum(n_files)

        try:
            current_uuid = uuid.UUID(serialized_dict['current_uuid'])
        except Exception:
            current_uuid = None

        current_spec_list_item: Optional[qt_api.QtWidgets.QListWidgetItem] = None
        for j, file_info in enumerate(serialized_dict['open_files']):
            self.pbar.setValue(j + 1)
            self.statusbar.showMessage(
                self.qapp.tr("Loading project...")
            )
            self.qapp.processEvents()

            item_uuid = uuid.UUID(file_info['uuid'])
            item_row = int(file_info['index'])

            file_path = os.path.realpath(
                os.path.join(
                    os.path.dirname(file_name),
                    file_info['path']
                )
            )

            if not os.path.exists(file_path):
                if os.path.exists(file_info['path']):
                    file_path = file_info['path']
                else:
                    exception_tracker[item_uuid] = (
                        file_path,
                        f"File {file_path} does not exist!"
                    )
                    continue

            try:
                sp: Spectrum1D = loaders.read(file_path)
            except Exception as exc:
                exception_tracker[item_uuid] = (file_path, str(exc))
                continue

            new_item = qt_api.QtWidgets.QListWidgetItem(file_info['text'])
            new_item.setCheckState(qt_api.QtCore.Qt.CheckState(file_info['checked']))
            new_item.setToolTip(file_path)
            new_item.setData(qt_api.QtCore.Qt.ItemDataRole.UserRole, item_uuid)

            if item_uuid == current_uuid:
                current_spec_list_item = new_item

            self.main_wnd.spec_list_widget.addItem(new_item)
            self.open_spectra[item_uuid] = sp
            self.open_spectra_files[item_uuid] = file_path
            self.open_spectra_items[item_uuid] = new_item
            self.main_wnd.spec_list_widget.insertItem(item_row, new_item)
            self.qapp.processEvents()

        for h_uuid, obj_info in serialized_dict['objects_properties'].items():

            obj_uuid = uuid.UUID(h_uuid)

            lines_list = []
            for line_info in obj_info['lines']['list']:
                lines_list.append({
                    'row': int(line_info['row']),
                    'data': float(line_info['data']),
                    'text': str(line_info['text']),
                    'checked': qt_api.QtCore.Qt.CheckState(line_info['checked'])
                })

            z_list = []
            for z_info in obj_info['lines']['redshifts']:
                z_list.append({
                    'row': int(z_info['row']),
                    'text': str(z_info['text']),
                    'data': z_info['data']
                })

            try:
                redshift = obj_info['redshift']
            except KeyError:
                redshift = None

            try:
                quality_flag = obj_info['quality_flag']
            except KeyError:
                quality_flag = 0

            self._update_spec_item_qf(obj_uuid, quality_flag)
            self.object_state_dict[obj_uuid] = {
                'redshift': redshift,
                'quality_flag': quality_flag,
                'lines': {
                    'list': lines_list,
                    'redshifts': z_list,
                }
            }

        if current_spec_list_item is not None:
            self.main_wnd.spec_list_widget.setCurrentItem(
                current_spec_list_item
            )

        self.pbar.hide()
        self._unlock()
        self.global_state = GlobalState.READY

        self.redrawCurrentSpec()

        self.statusbar.showMessage(self.qapp.tr("Project loaded"))

        if exception_tracker:
            self.msgBox.setWindowTitle(self.qapp.tr("Error"))
            self.msgBox.setText(
                self.qapp.tr(
                    "An error has occurred while loading the project: one or "
                    "more files could not be loaded."
                )
            )
            self.msgBox.setDetailedText(
                "\n\n".join(
                    [
                        f"uuid: {exc_uuid}\n"
                        f"file: {exc_data[0]}\n"
                        f"reason: {exc_data[1]}"
                        for exc_uuid, exc_data in exception_tracker.items()
                    ]
                )
            )
            self.msgBox.setIcon(qt_api.QtWidgets.QMessageBox.Icon.Critical)
            self.msgBox.setStandardButtons(
                qt_api.QtWidgets.QMessageBox.StandardButton.Ok
            )
            self.msgBox.exec()

    def redrawCurrentSpec(self, *args, **kwargs) -> None:
        """Redraw the charts."""
        self.currentSpecItemChanged(
            self.main_wnd.spec_list_widget.currentItem()
        )

    def requestCancelCurrentOperation(self) -> None:
        """Set the global state of the program to REUQUEST_CANCEL."""
        self.global_state = GlobalState.REUQUEST_CANCEL

    def saveProject(self, file_name: str) -> None:
        """Save the project to a file."""
        # Store any pending information for the current object
        self._backup_current_object_state()

        # Serialize program state for json dumping
        serialized_open_file_list = []
        item: Union[qt_api.QtWidgets.QListWidgetItem, None]
        proj_path = os.path.dirname(file_name)
        for k in range(self.main_wnd.spec_list_widget.count()):
            item = self.main_wnd.spec_list_widget.item(k)
            if item is None:
                continue
            item_uuid: uuid.UUID = item.data(
                qt_api.QtCore.Qt.ItemDataRole.UserRole
            )

            file_info = {
                'index': k,
                'uuid': item_uuid.hex,
                'text': item.text(),
                'path': os.path.relpath(
                    self.open_spectra_files[item_uuid],
                    proj_path
                ),
                'checked': int(item.checkState().value)  # type: ignore
            }

            serialized_open_file_list.append(file_info)

        # Serialize objects info for json dumping
        serialized_object_info_dict: Dict[str, Any] = {}
        for obj_uuid, obj_info in self.object_state_dict.items():
            serialized_lines_list: List[Dict[str, Any]] = []
            serialized_z_list: List[Dict[str, Any]] = []

            for line_info in obj_info['lines']['list']:
                try:
                    serialized_lines_list.append({
                        'row': int(line_info['row']),
                        'data': float(line_info['data']),
                        'text': str(line_info['text']),
                        'checked': int(line_info['checked'].value)
                    })
                except (TypeError, ValueError):
                    continue

            for z_info in obj_info['lines']['redshifts']:
                try:
                    serialized_z_list.append({
                        'row': int(z_info['row']),
                        'text': str(z_info['text']),
                        'data': z_info['data']
                    })
                except (TypeError, ValueError):
                    continue

            serialized_info_dict: Dict[str, Any] = {
                'redshift': float(obj_info['redshift']),
                'quality_flag': int(obj_info['quality_flag']),
                'lines': {
                    'list': serialized_lines_list,
                    'redshifts': serialized_z_list,
                }
            }

            serialized_object_info_dict[obj_uuid.hex] = serialized_info_dict

        if self.current_uuid is None:
            serialized_current_uuid = None
        else:
            serialized_current_uuid = self.current_uuid.hex

        project_dict = {
            'current_uuid': serialized_current_uuid,
            'open_files': serialized_open_file_list,
            'objects_properties': serialized_object_info_dict
        }

        with open(file_name, 'w') as f:
            json.dump(project_dict, f, indent=2)


    def saveSettings(self) -> None:
        for key, (get_func, _, _) in self._globsal_setting.items():
            self.settings.setValue(key, get_func())

    def setCurrentObjectRedshift(self, redshift: Optional[float]) -> None:
        if redshift is None:
            redshift = 0

        self.flux_chart_view.setRedshift(redshift=redshift)

        item_col_0: Union[qt_api.QtWidgets.QTableWidgetItem, None]
        item_col_1: Union[qt_api.QtWidgets.QTableWidgetItem, None]
        item_col_2: Union[qt_api.QtWidgets.QTableWidgetItem, None]
        for j in range(self.main_wnd.lines_table_widget.rowCount()):
            item_col_0 = self.main_wnd.lines_table_widget.item(j, 0)
            item_col_1 = self.main_wnd.lines_table_widget.item(j, 1)
            item_col_2 = self.main_wnd.lines_table_widget.item(j, 2)

            if item_col_0 is None or item_col_1 is None or item_col_2 is None:
                continue

            line_lam: float = item_col_0.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)
            rest_lam = line_lam / (1 + redshift)
            item_col_1.setText(f"{rest_lam:.2f} A")

            best_matches = [
                x[1]
                for x in lines.get_lines(wrange=[rest_lam - 5, rest_lam + 5])
            ]

            item_col_2.setText('; '.join(best_matches))

    def setCurrentObjectRedshiftFromLines(self, row: int) -> None:
        if row < 0:
            return

        item: Union[qt_api.QtWidgets.QListWidgetItem, None]
        item = self.main_wnd.lines_match_list_widget.item(row)
        if item is None:
            return

        try:
            self.main_wnd.z_dspinbox.setValue(
                item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)[0]
            )
        except (IndexError, TypeError):
            return

    def setCurrentObjectRedshiftFromSingleLine(self, row: int) -> None:
        if row < 0:
            return

        lines_tbl_row = self.main_wnd.lines_table_widget.currentRow()
        if lines_tbl_row < 0:
            return

        w_item: Union[None, qt_api.QtWidgets.QTableWidgetItem]
        w_item = self.main_wnd.lines_table_widget.item(lines_tbl_row, 0)
        if w_item is None:
            return

        obs_lam = w_item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)

        rest_lam = self.main_wnd.single_line_combo_box.itemData(
            row, qt_api.QtCore.Qt.ItemDataRole.UserRole
        )

        redshift = (obs_lam/rest_lam) - 1

        self.main_wnd.z_dspinbox.setValue(redshift)

    def setShowLinesType(self, type_index: int) -> None:
        if type_index == 0:
            self.flux_chart_view.setLinesType('')
        elif type_index == 1:
            self.flux_chart_view.setLinesType('E')
        elif type_index == 2:
            self.flux_chart_view.setLinesType('A')

    def setSmoothingFactor(self, smoothing_value: float) -> None:
        self.redrawCurrentSpec()

    def showObjectInfo(self, object_uuid: uuid.UUID) -> None:
        sp: Spectrum1D = self.open_spectra[object_uuid]

        self.main_wnd.obj_prop_table_widget.setRowCount(0)

        if sp.meta is None:
            return

        try:
            header = sp.meta['header']
        except KeyError:
            return

        self.main_wnd.obj_prop_table_widget.setRowCount(len(header))
        self.main_wnd.obj_prop_table_widget.setVerticalHeaderLabels(
            list(header.keys())
        )
        for j, (key, val, comment) in enumerate(header.cards):

            val_item: qt_api.QtWidgets.QTableWidgetItem = qt_api.QtWidgets.QTableWidgetItem(
                str(val)
            )
            val_item.setFlags(
                qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
                qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled
            )

            com_item: qt_api.QtWidgets.QTableWidgetItem = qt_api.QtWidgets.QTableWidgetItem(
                str(comment)
            )
            com_item.setFlags(
                qt_api.QtCore.Qt.ItemFlag.ItemIsSelectable |
                qt_api.QtCore.Qt.ItemFlag.ItemIsEnabled
            )

            self.main_wnd.obj_prop_table_widget.setItem(j, 0, val_item)
            self.main_wnd.obj_prop_table_widget.setItem(j, 1, com_item)

    def toggleSimilarSpecItems(
        self,
        item: Optional[qt_api.QtWidgets.QListWidgetItem]
    ) -> None:
        if item is None:
            return

        ref_uuid = item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)
        if ref_uuid in self.object_state_dict:
            ref_qf = self.object_state_dict[ref_uuid]['quality_flag']
        else:
            ref_qf = 0

        if item.checkState() == qt_api.QtCore.Qt.CheckState.Checked:
            ref_check_state = qt_api.QtCore.Qt.CheckState.Unchecked
        else:
            ref_check_state = qt_api.QtCore.Qt.CheckState.Checked

        other_item: Union[None, qt_api.QtWidgets.QListWidgetItem]
        for row in range(self.main_wnd.spec_list_widget.count()):
            other_item = self.main_wnd.spec_list_widget.item(row)

            if other_item is None:
                continue

            other_uuid = other_item.data(qt_api.QtCore.Qt.ItemDataRole.UserRole)
            if other_uuid in self.object_state_dict:
                other_qf = self.object_state_dict[other_uuid]['quality_flag']
            else:
                other_qf = 0

            if other_qf == ref_qf:
                other_item.setCheckState(ref_check_state)

    def toggleSmothing(self, show_smoothing: int) -> None:
        self.redrawCurrentSpec()

    def run(self) -> None:
        """Run the main Qt Application."""
        self.main_wnd.show()
        sys.exit(self.qapp.exec())


def values2series(
    x_values: Union[List[float], np.ndarray],
    y_values: Union[List[float], np.ndarray],
    name: str
) -> qt_api.QtCharts.QLineSeries:
    """
    Convert point values to a QLineSeries object.

    :param x_values: x values of the points.
    :param y_values: y values of the points.
    :param name: The name of the series.
    :return series: The series object.
    """
    series: qt_api.QtCharts.QLineSeries = qt_api.QtCharts.QLineSeries()
    series.setName(name)

    for x, y in zip(x_values, y_values):
        series.append(x, y)

    return series


def main() -> None:
    """Run the main GUI application using PySide."""
    myapp: GuiApp = GuiApp()
    myapp.run()


if __name__ == '__main__':
    main()
