import json
from enum import Enum
from pathlib import Path
import PreCourlis

import numpy as np
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from qgis.core import (
    QgsApplication,
    QgsMapLayer,
    QgsMapLayerProxyModel,
    QgsProject,
    QgsWkbTypes,
    Qgis,
    QgsFeatureRequest,
)
from qgis.PyQt.QtWidgets import QComboBox, QItemDelegate
from qgis.PyQt.QtCore import Qt
from qgis.gui import QgsMessageBar, QgsDoubleSpinBox
from qgis.PyQt import QtCore, QtGui, QtWidgets, uic
from qgis.utils import iface
import processing

from PreCourlis.core.points_selection import PointsSelection
from PreCourlis.core.precourlis_file import PreCourlisFileLine, DEFAULT_LAYER_COLOR
from PreCourlis.core.settings import settings
from PreCourlis.widgets.delegates import FloatDelegate
from PreCourlis.widgets.new_point_dialog import NewPointDialog
from PreCourlis.widgets.points_table_model import PointsTableModel
from PreCourlis.widgets.section_item_model import SectionItemModel
from PreCourlis.widgets.sedimental_layer_model import SedimentalLayerModel

from PreCourlis.core.section_utils import (
    check_section,
    move_up_invalid_points_from_section,
    move_down_invalid_points_from_section,
)

# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer
FORM_CLASS, _ = uic.loadUiType(
    Path(PreCourlis.__file__).parent.resolve() / "ui" / "profile_dialog_base.ui"
)


class ImportZSourceType(Enum):
    DEM = 0
    Profiles = 1
    Points = 2


class ProfileDialog(QtWidgets.QDialog, FORM_CLASS):

    CLEAR_VALUE = -99999999.0

    def __init__(self, parent=None):
        """Constructor."""
        super().__init__(parent, QtCore.Qt.Window)
        # Set up the user interface from Designer through FORM_CLASS.
        # After self.setupUi() you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)

        self.file = None
        self.editing = False
        self.interpolation = False
        self.current_section = None
        self.selected_color = None
        self.splitter.setSizes([200, 400])
        self.new_point_dialog = None

        self.addPointButton.setIcon(QgsApplication.getThemeIcon("/mActionAdd.svg"))
        self.addPointButton.clicked.connect(self.add_point)

        self.removePointsButton.setIcon(
            QgsApplication.getThemeIcon("/mActionRemove.svg")
        )
        self.removePointsButton.clicked.connect(self.remove_points)

        self.moveUpInvalidPointsFromSectionButton.setIcon(
            QgsApplication.getThemeIcon("/mActionArrowUp.svg")
        )
        self.moveUpInvalidPointsFromSectionButton.setToolTip(
            "Move up invalid points from current section"
        )
        self.moveUpInvalidPointsFromSectionButton.clicked.connect(
            self.moveUpInvalidPointsFromSection
        )

        self.moveDownInvalidPointsFromSectionButton.setIcon(
            QgsApplication.getThemeIcon("/mActionArrowDown.svg")
        )
        self.moveDownInvalidPointsFromSectionButton.setToolTip(
            "Move down invalid points from current section"
        )
        self.moveDownInvalidPointsFromSectionButton.clicked.connect(
            self.moveDownInvalidPointsFromSection
        )

        # Create QTransform to rotate icon
        rotate_up = QtGui.QTransform().rotate(90)
        move_icon = QgsApplication.getThemeIcon("/mActionDoubleArrowLeft.svg")
        rotated_up_icon = QtGui.QIcon(move_icon.pixmap(32, 32).transformed(rotate_up))
        self.moveUpInvalidPointsFromLayerButton.setIcon(rotated_up_icon)
        self.moveUpInvalidPointsFromLayerButton.setToolTip(
            "Move up invalid points from current layer"
        )
        self.moveUpInvalidPointsFromLayerButton.clicked.connect(
            self.moveUpInvalidPointsFromLayer
        )

        rotate_down = QtGui.QTransform().rotate(-90)
        rotated_down_icon = QtGui.QIcon(
            move_icon.pixmap(32, 32).transformed(rotate_down)
        )
        self.moveDownInvalidPointsFromLayerButton.setIcon(rotated_down_icon)
        self.moveDownInvalidPointsFromLayerButton.setToolTip(
            "Move down invalid points from current layer"
        )
        self.moveDownInvalidPointsFromLayerButton.clicked.connect(
            self.moveDownInvalidPointsFromLayer
        )

        self.undoButton.setIcon(QgsApplication.getThemeIcon("/mActionUndo.svg"))
        self.undoButton.clicked.connect(self.undo)
        undo_shortcut = QtWidgets.QShortcut(
            QtGui.QKeySequence(QtGui.QKeySequence.Undo), self
        )
        undo_shortcut.activated.connect(self.undo)

        self.VALID_LAYER_ICON = QtGui.QIcon(
            str(
                Path(PreCourlis.__file__).parent.resolve()
                / "resources"
                / "images"
                / "greenCircleIcon.svg"
            )
        ).pixmap(20, 20)
        self.VALID_LAYER_TOOLTIP = "Layer is valid!"

        self.UNVALID_LAYER_ICON = QtGui.QIcon(
            str(
                Path(PreCourlis.__file__).parent.resolve()
                / "resources"
                / "images"
                / "redCircleIcon.svg"
            )
        ).pixmap(20, 20)
        self.UNVALID_LAYER_TOOLTIP = "Layer is unvalid!"

        self.layerValidityIndicator.setPixmap(self.VALID_LAYER_ICON)
        self.layerValidityIndicator.setToolTip(self.VALID_LAYER_TOOLTIP)

        self.redoButton.setIcon(QgsApplication.getThemeIcon("/mActionRedo.svg"))
        self.redoButton.clicked.connect(self.redo)
        redo_shortcut = QtWidgets.QShortcut(
            QtGui.QKeySequence(QtGui.QKeySequence.Redo), self
        )
        redo_shortcut.activated.connect(self.redo)

        self.saveButton.setIcon(QgsApplication.getThemeIcon("/mActionSaveAllEdits.svg"))
        self.saveButton.clicked.connect(self.save)
        save_shortcut = QtWidgets.QShortcut(
            QtGui.QKeySequence(QtGui.QKeySequence.Save), self
        )
        save_shortcut.activated.connect(self.save)

        self.deleteButton.setIcon(
            QgsApplication.getThemeIcon("/mActionDeleteSelected.svg")
        )
        self.deleteButton.clicked.connect(self.delete_current_profile)

        self.previousAndNextSectionsVisibilityPushButton.clicked.connect(
            self.graphWidget.set_previous_and_next_sections_visibility
        )
        self.previousAndNextSectionsVisibilityPushButton.setIcon(
            QgsApplication.getThemeIcon("/mIconSnappingActiveLayer.svg")
        )

        self.nav_toolbar = NavigationToolbar(self.graphWidget, self)
        self.sectionSelectionLayout.insertWidget(10, self.nav_toolbar)

        self.message_bar = QgsMessageBar(self)
        self.layout().insertWidget(1, self.message_bar)

        self.sectionItemModel = SectionItemModel(self)
        self.pointsTableModel = PointsTableModel(self)
        self.pointsTableModel.dataChanged.connect(self.data_changed)
        self.sedimentalLayerModel = SedimentalLayerModel(self)

        self.init_layer_combo_box()
        self.init_secondary_layer_combo_box()
        self.init_buffer_size_spin_box()
        self.init_sections_combo_box()
        self.init_points_table_view()
        self.init_graph_widget()
        self.init_sedimental_layers_group_box()
        self.init_import_z_group_box()
        self.init_interpolation_group_box()

        self.points_selection = PointsSelection()
        self.points_selection.set_selection_model(self.pointsTableView.selectionModel())
        self.points_selection.open()

        # Secondary sections offset slider and spin boxinit
        self.secondarySectionsOffsetSlider.setRange(-100000, 100000)
        self.secondarySectionsOffsetSpinBox.setRange(-1000.0, 1000.0)

        self.secondarySectionsOffsetSlider.setSingleStep(1)
        self.secondarySectionsOffsetSlider.setPageStep(10)

        self.secondarySectionsOffsetSlider.setValue(
            round(self.graphWidget.secondary_sections_offset)
        )
        self.secondarySectionsOffsetSpinBox.setValue(
            round(self.graphWidget.secondary_sections_offset)
        )
        self.secondarySectionsOffsetSpinBox.setClearValue(0)

        self.secondarySectionsOffsetSlider.valueChanged.connect(
            self.handle_secondary_sections_offset_slider_value_changed
        )
        self.secondarySectionsOffsetSpinBox.valueChanged.connect(
            self.handle_secondary_sections_offset_spin_box_value_changed
        )

        self.secondarySectionsOffsetSlider.setEnabled(False)
        self.secondarySectionsOffsetSpinBox.setEnabled(False)

        self.layer_changed(self.layer())

    def closeEvent(self, event):
        # In QGIS 3.40+ removeMapLayer can destruct the C++ layer object
        # before (or while) emitting layerWillBeRemoved.  If the dialog is
        # still alive at that point, QgsMapLayerComboBox fires layerChanged →
        # layer_changed → layerModified.disconnect on a dying object → SIGABRT.
        # Sever all layer-watching connections here so the dialog is inert
        # after close(), regardless of when Qt actually destroys the widget.
        try:
            self.layerComboBox.layerChanged.disconnect(self.layer_changed)
        except RuntimeError:
            pass
        try:
            self.secondaryLayerComboBox.layerChanged.disconnect(
                self.secondary_layer_changed
            )
        except RuntimeError:
            pass
        if self.file is not None and self.file.layer() is not None:
            try:
                self.file.layer().layerModified.disconnect(self.layer_modified)
            except RuntimeError:
                pass
        self.points_selection.close()
        super().closeEvent(event)

    def init_layer_combo_box(self):
        self.layerComboBox.setAllowEmptyLayer(True)
        self.layerComboBox.setFilters(QgsMapLayerProxyModel.LineLayer)

        def accept_layer(layer):
            if layer.type() != QgsMapLayer.VectorLayer:
                return False
            if layer.geometryType() != QgsWkbTypes.LineGeometry:
                return False
            if layer.fields().indexFromName("sec_id") == -1:
                return False
            return True

        excluded_layers = []
        for layer in QgsProject.instance().mapLayers().values():
            if not accept_layer(layer):
                excluded_layers.append(layer)
        self.layerComboBox.setExceptedLayerList(excluded_layers)

        # Select active layer
        layer = iface.activeLayer()
        if layer not in excluded_layers:
            self.layerComboBox.setLayer(layer)
        else:
            self.layerComboBox.setLayer(None)

        self.layerComboBox.layerChanged.connect(self.layer_changed)

    def init_secondary_layer_combo_box(self):
        self.secondaryLayerComboBox.setAllowEmptyLayer(True)
        self.secondaryLayerComboBox.setFilters(QgsMapLayerProxyModel.LineLayer)
        self.secondaryLayerComboBox.setEnabled(False)

        self.secondaryLayerComboBox.layerChanged.connect(self.secondary_layer_changed)

    def init_buffer_size_spin_box(self):
        self.bufferSizeDoubleSpinBox.setMinimum(0.0)
        self.bufferSizeDoubleSpinBox.setClearValueMode(
            QgsDoubleSpinBox.ClearValueMode.MinimumValue
        )
        self.bufferSizeDoubleSpinBox.setValue(0.0)
        self.bufferSizeDoubleSpinBox.setEnabled(False)

        self.bufferSizeDoubleSpinBox.valueChanged.connect(
            self.compute_buffered_sections
        )

    def init_sections_combo_box(self):
        self.sectionComboBox.setModel(self.sectionItemModel)
        self.sectionComboBox.currentIndexChanged.connect(self.section_changed)

        self.previousSectionButton.clicked.connect(self.previous_section)
        self.nextSectionButton.clicked.connect(self.next_section)

    def init_points_table_view(self):
        self.pointsTableView.setItemDelegate(FloatDelegate(self))
        self.pointsTableView.setModel(self.pointsTableModel)

        self.topo_bath_combobox_delegate = ComboBoxDelegate(["T", "B"], self)
        self.pointsTableView.setItemDelegateForColumn(
            1, self.topo_bath_combobox_delegate
        )

    def init_graph_widget(self):
        self.graphWidget.set_selection_model(self.pointsTableView.selectionModel())
        self.graphWidget.editing_finished.connect(self.graph_editing_finished)

    def init_sedimental_layers_group_box(self):
        self.sedimentalLayerComboBox.setModel(self.sedimentalLayerModel)
        self.sedimentalLayerComboBox.currentIndexChanged.connect(
            self.sedimental_layer_changed
        )
        self.addLayerColorButton.clicked.connect(self.addLayerColorButton_clicked)
        self.moveLayerUpButton.setIcon(
            QgsApplication.getThemeIcon("/mActionArrowUp.svg")
        )
        self.moveLayerUpButton.clicked.connect(self.move_layer_up)
        self.moveLayerDownButton.setIcon(
            QgsApplication.getThemeIcon("/mActionArrowDown.svg")
        )
        self.moveLayerDownButton.clicked.connect(self.move_layer_down)
        self.addLayerButton.clicked.connect(self.add_layer)
        self.applyLayerButton.clicked.connect(self.apply_layer)
        self.deleteLayerButton.clicked.connect(self.delete_layer)

    def init_import_z_group_box(self):
        self.importZFromComboBox.addItem(self.tr("DEM"), ImportZSourceType.DEM)
        self.importZFromComboBox.addItem(
            self.tr("Profiles"), ImportZSourceType.Profiles
        )
        self.importZFromComboBox.addItem(self.tr("Points"), ImportZSourceType.Points)
        self.importZFromComboBox.currentIndexChanged.connect(
            self.importZFromComboBox_currentIndexChanged
        )
        self.importZFromComboBox_currentIndexChanged(0)

        self.importZMaskComboBox.setAllowEmptyLayer(True)
        self.importZMaskComboBox.setCurrentIndex(-1)
        self.importZMaskComboBox.setFilters(QgsMapLayerProxyModel.PolygonLayer)

        self.importZDefaultElevationSpinBox.setMinimum(self.CLEAR_VALUE)
        self.importZDefaultElevationSpinBox.setClearValue(
            self.CLEAR_VALUE, self.tr("Not set")
        )
        self.setDefaultElevation(settings.default_elevation)

        self.importZButton.clicked.connect(self.importZButton_clicked)

    def init_interpolation_group_box(self):
        self.applyInterpolationButton.clicked.connect(self.apply_interpolation)

    def importZFromComboBox_currentIndexChanged(self, index):
        if self.importZFrom() == ImportZSourceType.DEM:
            self.importZSourceComboBox.setFilters(QgsMapLayerProxyModel.RasterLayer)
        elif self.importZFrom() == ImportZSourceType.Profiles:
            self.importZSourceComboBox.setFilters(QgsMapLayerProxyModel.LineLayer)
        elif self.importZFrom() == ImportZSourceType.Points:
            self.importZSourceComboBox.setFilters(QgsMapLayerProxyModel.PointLayer)

    def importZFrom(self) -> ImportZSourceType:
        return self.importZFromComboBox.currentData()

    def importZButton_clicked(self):
        import_z_from = self.importZFrom()
        if import_z_from == ImportZSourceType.DEM:
            return self.import_z_from_dem()
        if import_z_from == ImportZSourceType.Profiles:
            return self.import_z_from_profiles()
        if import_z_from == ImportZSourceType.Points:
            return self.import_z_from_points()

    def importZDefaultElevation(self):
        if self.importZDefaultElevationSpinBox.value() == self.CLEAR_VALUE:
            return None
        else:
            return self.importZDefaultElevationSpinBox.value()

    def setDefaultElevation(self, value):
        if value is None:
            self.importZDefaultElevationSpinBox.setValue(self.CLEAR_VALUE)
        else:
            self.importZDefaultElevationSpinBox.setValue(value)

    def layer(self):
        return self.layerComboBox.currentLayer()

    def secondaryLayer(self):
        return self.secondaryLayerComboBox.currentLayer()

    def layer_changed(self, layer):
        if self.file is not None and self.file.layer() is not None:
            self.file.layer().layerModified.disconnect(self.layer_modified)

        # Check if topo_bath field is present
        if layer is not None:
            if layer.fields().indexFromName("topo_bat") == -1:
                self.message_bar.pushMessage(
                    self.tr("Missing field"),
                    self.tr(
                        "The field 'topo_bat' is missing in the layer. "
                        "Please add it with the repair profiles algorithm before using this tool."
                    ),
                    Qgis.MessageLevel.Critical,
                    duration=0,
                )

                self.layerComboBox.setLayer(None)
                return

        self.file = PreCourlisFileLine(layer)
        self.sectionItemModel.setLayer(layer)
        self.sectionComboBox.setCurrentIndex(0)
        self.sedimentalLayerModel.setLayer(layer)
        self.sedimentalLayerComboBox.setCurrentIndex(0)

        if layer is not None:
            layer.layerModified.connect(self.layer_modified)
            self.points_selection.set_profiles_layers(layer)

            self.update_sections_validity_indicators()

            # Secondary layer
            exceptedLayers = self.layerComboBox.exceptedLayerList()
            exceptedLayers.append(self.layerComboBox.currentLayer())
            self.secondaryLayerComboBox.setExceptedLayerList(exceptedLayers)

            self.secondaryLayerComboBox.setEnabled(True)
            self.bufferSizeDoubleSpinBox.setEnabled(True)
        else:
            self.secondaryLayerComboBox.setEnabled(False)
            self.bufferSizeDoubleSpinBox.setEnabled(False)

        self.secondaryLayerComboBox.setLayer(None)

    def secondary_layer_changed(self, layer):
        if layer is not None:
            self.secondarySectionsOffsetSlider.setEnabled(True)
            self.secondarySectionsOffsetSpinBox.setEnabled(True)
            self.bufferSizeDoubleSpinBox.setEnabled(True)
        else:
            self.secondarySectionsOffsetSlider.setEnabled(False)
            self.secondarySectionsOffsetSpinBox.setEnabled(False)
            self.bufferSizeDoubleSpinBox.setEnabled(False)

        self.compute_buffered_sections()

    def compute_buffered_sections(self):
        layer = self.secondaryLayer()
        if layer is None:
            self.graphWidget.set_secondary_sections([], None)
            return

        geometry = PreCourlisFileLine.geometry_from_section(self.current_section)
        file_secondary_layer = PreCourlisFileLine(layer)

        if self.bufferSizeDoubleSpinBox.value() == 0.0:
            buffered_geometry = geometry
        else:
            buffered_geometry = geometry.buffer(self.bufferSizeDoubleSpinBox.value(), 5)

        buffered_sections = []
        buffered_feature_ids = []
        for feature in self.secondaryLayer().getFeatures():
            if buffered_geometry.intersects(feature.geometry()):
                buffered_sections.append(
                    file_secondary_layer.section_from_feature(feature)
                )
                buffered_feature_ids.append(feature.id())

        layer.selectByIds(buffered_feature_ids)
        self.graphWidget.set_secondary_sections(
            buffered_sections,
            layer.renderer()
            .symbol()
            .color()
            .name(),  # TODO: use reference to layer color for auto-update
        )

    def layer_modified(self):
        if not self.editing:
            self.sedimental_layers_update()
            self.section_changed(self.sectionComboBox.currentIndex())

    def section_from_feature_id(self, f_id):
        if f_id is None:
            return None
        f = self.layer().getFeature(f_id)
        section = self.file.section_from_feature(f)
        section.feature = f
        return section

    def section_changed(self, index):
        self.set_section(index)
        self.compute_buffered_sections()

        spinBoxOffset = 2 * round(
            self.current_section.distances[-1] - self.current_section.distances[0]
        )
        sliderOffset = spinBoxOffset * 100

        self.secondarySectionsOffsetSlider.setRange(-sliderOffset, sliderOffset)
        self.secondarySectionsOffsetSpinBox.setRange(-spinBoxOffset, spinBoxOffset)

    def set_section(self, index):
        if index == -1:
            self.graphWidget.clear()
            self.points_selection.clear()
            return

        item = self.sectionItemModel.item(index)

        # Select current feature in vector layer
        self.layer().selectByIds([item.current_f_id])

        self.current_section = self.section_from_feature_id(item.current_f_id)

        self.pointsTableModel.set_section(self.current_section)

        self.graphWidget.set_sections(
            self.layer(),
            self.layer().getFeature(item.current_f_id),
            self.section_from_feature_id(item.previous_f_id),
            self.current_section,
            self.section_from_feature_id(item.next_f_id),
        )

        self.points_selection.set_section(self.current_section)

        # Reset navigation history
        self.nav_toolbar.update()
        if self.nav_toolbar._nav_stack() is None:
            self.nav_toolbar.push_current()  # set the home button to this view

        self.update_sections_validity_indicators()

    def previous_section(self):
        if self.sectionComboBox.currentIndex() < 1:
            return
        self.sectionComboBox.setCurrentIndex(self.sectionComboBox.currentIndex() - 1)

    def next_section(self):
        if self.sectionComboBox.currentIndex() > self.sectionItemModel.rowCount() - 2:
            return
        self.sectionComboBox.setCurrentIndex(self.sectionComboBox.currentIndex() + 1)

    def data_changed(self, topLeft, bottomRight, roles):
        if self.graphWidget.selection_tool.editing:
            return
        elif self.interpolation:
            return
        else:
            self.update_feature("Profile dialog table edit")

    def graph_editing_finished(self):
        model = self.pointsTableModel
        model.dataChanged.emit(
            model.index(0, 1),
            model.index(model.rowCount() - 1, model.columnCount() - 1),
        )
        self.update_feature("Profile dialog graph translation")

    def update_feature(self, title):
        self.layer().startEditing()
        self.editing = True
        self.file.update_feature(
            self.current_section.feature.id(),
            self.current_section,
            title,
        )
        self.editing = False
        self.graphWidget.refresh_current_section()

        self.pointsTableModel.set_validation_data(check_section(self.current_section))
        self.update_sections_validity_indicators()

    def update_sections_validity_indicators(self):
        layer_is_inconsistent = False

        # Update items color from sections combobox
        for i, section in enumerate(self.file.get_sections()):
            validation_datas = check_section(section)

            # Check if there is at least one False element in the entire array
            has_false_element = bool(np.any(~validation_datas))
            layer_is_inconsistent = layer_is_inconsistent or has_false_element

            if i == self.sectionComboBox.currentIndex():
                if has_false_element:
                    palette = self.sectionComboBox.palette()
                    palette.setColor(QtGui.QPalette.Button, QtGui.QColor("red"))
                    self.sectionComboBox.setPalette(palette)
                else:
                    palette = self.sectionComboBox.palette()
                    palette.setColor(QtGui.QPalette.Button, QtGui.QColor("white"))
                    self.sectionComboBox.setPalette(palette)

            item = self.sectionComboBox.model().item(i)

            if item:
                if has_false_element:
                    item.setBackground(QtGui.QColor("red"))
                else:
                    item.setBackground(QtGui.QColor("white"))

        # Update move up and down from section buttons visibility
        current_section_is_inconsistent = bool(
            np.any(~check_section(self.current_section))
        )
        self.moveUpInvalidPointsFromSectionButton.setEnabled(
            current_section_is_inconsistent
        )
        self.moveDownInvalidPointsFromSectionButton.setEnabled(
            current_section_is_inconsistent
        )

        # Update unvalid layer indicator visibility
        if layer_is_inconsistent:
            self.layerValidityIndicator.setPixmap(self.UNVALID_LAYER_ICON)
            self.layerValidityIndicator.setToolTip(self.UNVALID_LAYER_TOOLTIP)
        else:
            self.layerValidityIndicator.setPixmap(self.VALID_LAYER_ICON)
            self.layerValidityIndicator.setToolTip(self.VALID_LAYER_TOOLTIP)

        # and move up/down from layer buttons visibility
        self.moveUpInvalidPointsFromLayerButton.setEnabled(layer_is_inconsistent)
        self.moveDownInvalidPointsFromLayerButton.setEnabled(layer_is_inconsistent)

    def add_point(self):
        selection = self.pointsTableView.selectionModel().selection()

        rows = set()
        for index in selection.indexes():
            rows.add(index.row())
        if len(rows) != 1:
            self.message_bar.pushMessage(
                self.tr("Add new point"),
                self.tr("Please select one and only one point"),
                Qgis.MessageLevel.Critical,
                duration=5,
            )
            return
        row = next(rows.__iter__())

        self.new_point_dialog = NewPointDialog(self.current_section, row)
        result = self.new_point_dialog.exec_()
        self.new_point_dialog = None

        if result == QtWidgets.QDialog.Accepted:
            self.pointsTableModel.set_section(self.current_section)
            self.update_feature(self.tr("Point added to profile"))

            index = self.pointsTableModel.index(row + 1, 0)
            self.pointsTableView.selectionModel().select(
                index,
                QtCore.QItemSelectionModel.ClearAndSelect
                | QtCore.QItemSelectionModel.Rows,
            )

    def remove_points(self):
        section = self.current_section

        selection = self.pointsTableView.selectionModel().selection()

        rows = set()
        for index in selection.indexes():
            rows.add(index.row())

        if len(rows) == 0:
            self.message_bar.pushMessage(
                self.tr("Remove points"),
                self.tr("No points selected"),
                Qgis.MessageLevel.Critical,
                duration=5,
            )
            return

        # Use reversed order when removing by index
        for index in reversed(sorted(rows)):

            section.distances = np.delete(section.distances, index)
            section.x = np.delete(section.x, index)
            section.y = np.delete(section.y, index)
            section.topo_bath.pop(index)
            section.z = np.delete(section.z, index)
            if section.layers_elev is not None and len(section.layers_elev) > 0:
                section.layers_elev = np.vstack(
                    [np.delete(layer_elev, index) for layer_elev in section.layers_elev]
                )

        self.pointsTableModel.set_section(self.current_section)
        self.update_feature(self.tr("Points removed from profile"))

    def moveUpInvalidPointsFromSection(self):
        move_up_invalid_points_from_section(self.current_section)
        self.update_feature(self.tr("Invalid points from section moved up"))

    def moveDownInvalidPointsFromSection(self):
        move_down_invalid_points_from_section(self.current_section)
        self.update_feature(self.tr("Invalid points from section moved down"))

    def moveUpInvalidPointsFromLayer(self):
        self.layer().startEditing()
        self.editing = True

        fids = []
        sections = []

        for feature in self.layer().getFeatures():
            section = self.file.section_from_feature(feature)
            move_up_invalid_points_from_section(section)

            fids.append(feature.id())
            sections.append(section)

        self.editing = False
        self.graphWidget.refresh_current_section()

        self.pointsTableModel.set_validation_data(check_section(self.current_section))
        self.update_sections_validity_indicators()

        self.file.update_features(
            fids,
            sections,
            self.tr("Invalid points from layer moved up"),
        )

    def moveDownInvalidPointsFromLayer(self):
        self.layer().startEditing()
        self.editing = True

        fids = []
        sections = []

        for feature in self.layer().getFeatures():
            section = self.file.section_from_feature(feature)
            move_down_invalid_points_from_section(section)

            fids.append(feature.id())
            sections.append(section)

        self.editing = False
        self.graphWidget.refresh_current_section()

        self.pointsTableModel.set_validation_data(check_section(self.current_section))
        self.update_sections_validity_indicators()

        self.file.update_features(
            fids,
            sections,
            self.tr("Invalid points from layer moved down"),
        )

    def undo(self):
        self.layer().undoStack().undo()

    def redo(self):
        self.layer().undoStack().redo()

    def save(self):
        layer = self.layer()
        ok = layer.commitChanges()
        if not ok:
            self.message_bar.pushCritical(
                self.tr("Save Layer Edits"),
                self.tr("Could not commit changes to layer {}\n\nErrors: {}\n").format(
                    layer.name(), "\n".join(layer.commitErrors())
                ),
            )

    def delete_current_profile(self):
        if self.layer() is None:
            return

        layer = self.layer()
        self.layerComboBox.setLayer(None)

        layer.startEditing()
        layer.beginEditCommand("Delete profile")

        layer.deleteFeature(self.current_section.feature.id())

        # Update sec_id
        features_ordered_by_abs_long_request = QgsFeatureRequest().addOrderBy(
            '"abs_long"', ascending=True
        )
        features_ordered_by_abs_long_it = layer.getFeatures(
            features_ordered_by_abs_long_request
        )
        for current, feature in enumerate(features_ordered_by_abs_long_it, start=1):
            feature["sec_id"] = current
            layer.updateFeature(feature)

        layer.endEditCommand()

        self.layerComboBox.setLayer(layer)

    def sedimental_layers_update(self, name=None):
        if name is None:
            name = self.sedimental_layer()
        self.sedimentalLayerModel.setLayer(self.layer())
        self.sedimentalLayerComboBox.setCurrentText(name)

    def sedimental_layer(self):
        return self.sedimentalLayerComboBox.currentText()

    def sedimental_layer_changed(self, index):
        layer = self.sedimental_layer()
        self.addLayerNameLineEdit.setText(layer)
        self.set_layer_color_button_color(self.file.layer_color(layer))
        self.graphWidget.set_current_layer(layer)

    def addLayerColorButton_clicked(self):
        self.set_layer_color_button_color(
            QtWidgets.QColorDialog.getColor(self.selected_color or DEFAULT_LAYER_COLOR)
        )

    def handle_secondary_sections_offset_slider_value_changed(self, value):
        self.secondarySectionsOffsetSpinBox.setValue(round(value / 100.0, 2))

    def handle_secondary_sections_offset_spin_box_value_changed(self, value):
        if round(self.secondarySectionsOffsetSlider.value() / 100.0, 2) != value:
            self.secondarySectionsOffsetSlider.setValue(round(value * 100))

        self.graphWidget.set_secondary_sections_offset(value)

    def set_layer_color_button_color(self, color):
        stylesheet = ""
        if color is not None:
            if isinstance(color, str):
                color = QtGui.QColor(color)
            stylesheet = "background-color: rgba({}, {}, {}, 1);".format(
                color.red(),
                color.green(),
                color.blue(),
            )
        self.selected_color = color
        self.addLayerColorButton.setStyleSheet(stylesheet)

    def move_layer_up(self):
        name = self.sedimental_layer()
        try:
            self.file.move_layer_up(name)
        except (KeyError, ValueError) as e:
            self.message_bar.pushCritical("Impossible to move layer", str(e))
            return
        self.sedimentalLayerComboBox.setCurrentText(name)

    def move_layer_down(self):
        name = self.sedimental_layer()
        try:
            self.file.move_layer_down(name)
        except (KeyError, ValueError) as e:
            self.message_bar.pushCritical("Impossible to move layer", str(e))
            return
        self.sedimentalLayerComboBox.setCurrentText(name)

    def add_layer(self):
        name = self.addLayerNameLineEdit.text()
        color = self.selected_color
        try:
            self.file.add_sedimental_layer(
                name,
                self.sedimental_layer(),
                self.addLayerDeltaBox.value(),
            )
        except (KeyError, ValueError) as e:
            self.message_bar.pushCritical("Impossible to add layer", str(e))
            return
        self.file.set_layer_color(name, color)
        self.sedimentalLayerComboBox.setCurrentText(name)

    def apply_layer(self):
        self.file.set_layer_color(self.sedimental_layer(), self.selected_color)
        self.graphWidget.refresh()

    def import_z_points_filter(self):
        selection = self.pointsTableView.selectionModel().selection()
        current_sec_id = self.current_section.feature["sec_id"]

        points_filter = []
        for index in selection.indexes():
            f = self.layer().getFeature(
                self.sectionItemModel.item(index.row(), 0).current_f_id
            )
            p_id = int(f.attribute("p_id").split(",")[index.row()])

            points_filter.append([current_sec_id, p_id])

        points_filter_parameter_value = ""
        if len(points_filter) > 0:
            points_filter_parameter_value = json.dumps(points_filter)

        return points_filter_parameter_value

    def import_z_mask_layer(self):
        return self.importZMaskComboBox.currentLayer()

    def import_z_from_dem(self):
        processing.execAlgorithmDialog(
            "precourlis:import_layer_from_dem",
            {
                "INPUT": self.layer(),
                "LAYER_NAME": self.sedimental_layer(),
                "DEM": self.importZSourceComboBox.currentLayer(),
                "POINTS_FILTER": self.import_z_points_filter(),
                "MASK": self.import_z_mask_layer(),
                "BAND": 1,
                "DEFAULT_ELEVATION": self.importZDefaultElevation(),
            },
        )

    def import_z_from_profiles(self):
        processing.execAlgorithmDialog(
            "precourlis:import_layer_from_profiles",
            {
                "TARGET_PROFILES": self.layer(),
                "TARGET_LAYER_NAME": self.sedimental_layer(),
                "SOURCE_POINTS": self.importZSourceComboBox.currentLayer(),
                # "SOURCE_FIELD"
                # "IMPORT_SOURCE_POINTS"
                "POINTS_FILTER": self.import_z_points_filter(),
                "MASK": self.import_z_mask_layer(),
                "DEFAULT_ELEVATION": self.importZDefaultElevation(),
            },
        )

    def import_z_from_points(self):
        processing.execAlgorithmDialog(
            "precourlis:import_layer_from_points",
            {
                "TARGET_PROFILES": self.layer(),
                "TARGET_LAYER_NAME": self.sedimental_layer(),
                "SOURCE_POINTS": self.importZSourceComboBox.currentLayer(),
                # "SOURCE_FIELD_SEC_NAME" : "sec_id",
                # "SOURCE_FIELD_Z"
                "POINTS_FILTER": self.import_z_points_filter(),
                "MASK": self.import_z_mask_layer(),
                "DEFAULT_ELEVATION": self.importZDefaultElevation(),
            },
        )

    def delete_layer(self):
        self.layer().startEditing()
        layer = self.sedimental_layer()
        self.sedimentalLayerComboBox.setCurrentIndex(
            self.sedimentalLayerComboBox.currentIndex() - 1
        )
        self.file.delete_sedimental_layer(layer)

    def apply_interpolation(self):
        section = self.current_section

        dz0 = self.leftSpinBox.value()
        dz1 = self.rightSpinBox.value()

        index0 = None
        index1 = None
        model = self.pointsTableModel
        sel_model = self.pointsTableView.selectionModel()
        for index in sel_model.selection().indexes():
            if index0 is None:
                index0 = index.row()
            else:
                index0 = min(index0, index.row())

            if index1 is None:
                index1 = index.row()
            else:
                index1 = max(index1, index.row())

        x0 = section.distances[index0]
        x1 = section.distances[index1]

        columns = set([])
        for index in sel_model.selection().indexes():
            x = section.distances[index.row()]
            dz = dz0 + (x - x0) * (dz1 - dz0) / (x1 - x0)
            column = index.column()
            if column <= 1:
                # abs_lat, topo_bath
                continue
            elif column == 2:
                # zfond
                values = section.z
            else:
                # layers
                values = section.layers_elev[column - 3]
            values[index.row()] += dz
            columns.add(column)

        self.interpolation = True
        self.pointsTableModel.dataChanged.emit(
            model.index(0, min(columns)),
            model.index(model.rowCount() - 1, max(columns)),
        )
        self.interpolation = False

        self.update_feature("Profile dialog interpolation")


class ComboBoxDelegate(QItemDelegate):
    """
    A generic QComboBox delegate
    """

    def __init__(self, items, parent):
        super(ComboBoxDelegate, self).__init__(parent)
        self.parent = parent
        self._items = items

    def createEditor(self, parent, option, index):
        combobox = QComboBox(parent)

        for item in self._items:
            combobox.addItem(
                item,  # displayed value
                item,  # model value
            )

        return combobox

    def setEditorData(self, editor, index):
        data = index.data(Qt.EditRole)
        cbIndex = editor.findData(data)
        if cbIndex >= 0:
            editor.setCurrentIndex(cbIndex)

    def setModelData(self, editor, model, index):
        model.setData(index, editor.currentData(), Qt.EditRole)
