from subprocess import Popen

from typing import Optional, List
from PyQt5.QtWidgets import (
    QMessageBox,
    QComboBox,
    QFrame,
    QCheckBox,
    QGridLayout,
    QLineEdit,
    QVBoxLayout,
    QLabel,
    QHBoxLayout,
    QPushButton,
    QMainWindow,
)
from PyQt5.QtCore import Qt, QSize, QTimer

from celldetective.gui.base.utils import center_window
from celldetective.gui.gui_utils import (
    GeometryChoice,
    OperationChoice,
)
from celldetective.gui.base.list_widget import ListWidget
from celldetective.gui.base.feature_choice import FeatureChoice
from celldetective.gui.base.components import QHSeperationLine
from superqt import QLabeledDoubleSlider
from superqt.fonticon import icon
from fonticon_mdi6 import MDI6

from celldetective.utils.experiment import extract_experiment_channels
from celldetective.utils.image_loaders import auto_load_number_of_frames, load_frames

import numpy as np
import json
import os

from glob import glob
from natsort import natsorted

from pathlib import Path

from celldetective.gui.layouts import (
    ProtocolDesignerLayout,
    BackgroundFitCorrectionLayout,
    LocalCorrectionLayout,
)
from celldetective.gui.gui_utils import PreprocessingLayout2
from celldetective.gui.settings._settings_base import CelldetectiveSettingsPanel


class SettingsMeasurements(CelldetectiveSettingsPanel):
    """
    UI to set measurement instructions.

    """

    def __init__(self, parent_window: Optional[QMainWindow] = None) -> None:
        """
        Initialize the SettingsMeasurements widget.

        Parameters
        ----------
        parent_window : QMainWindow, optional
            The parent window.
        """

        self.parent_window = parent_window
        self.mode = self.parent_window.mode
        self.exp_dir = self.parent_window.exp_dir
        self.background_correction = []
        self.config_name = f"btrack_config_{self.mode}.json"
        self.measure_instructions_path = (
            self.parent_window.exp_dir
            + f"configs/measurement_instructions_{self.mode}.json"
        )
        self.clear_previous = False
        self.config_path = self.exp_dir + self.config_name
        self.channel_names, self.channels = extract_experiment_channels(self.exp_dir)
        self.channel_names = np.array(self.channel_names)
        self.channels = np.array(self.channels)

        super().__init__("Configure measurements")

        self._add_to_layout()
        self._load_previous_instructions()

        self._adjust_size()
        self.resize(int(self.width()), int(self._screen_height * 0.8))

    def _create_widgets(self):
        """
        Create the multibox design.

        """
        super()._create_widgets()

        self.iso_lbl: QLabel = QLabel("Position-based measurements".upper())
        self.feature_lbl: QLabel = QLabel("Mask-based measurements".upper())
        self.radii_lbl: QLabel = QLabel("Measurement radii (from center):")
        self.op_lbl: QLabel = QLabel("Operation to perform:")

        self.normalisation_frame = QFrame()
        self.normalisation_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)

        self.local_correction_layout = LocalCorrectionLayout(self)
        self.fit_correction_layout = BackgroundFitCorrectionLayout(self)

        self.protocol_layout = ProtocolDesignerLayout(
            parent_window=self,
            tab_layouts=[self.local_correction_layout, self.fit_correction_layout],
            tab_names=["Local", "Fit"],
            title="BACKGROUND CORRECTION",
            list_title="Corrections to apply:",
        )

        self.normalisation_frame.setLayout(self.protocol_layout)

        # self.populate_normalisation_tabs()

        # first frame for FEATURES
        self.features_frame = QFrame()
        self.features_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        self.populate_features_frame()

        # second frame for ISOTROPIC MEASUREMENTS
        self.iso_frame = QFrame()
        self.iso_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        self.populate_iso_frame()

        self.spot_detection_frame = QFrame()
        self.spot_detection_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        self.populate_spot_detection()

        self.clear_previous_btn = QCheckBox("clear previous measurements")

    def _add_to_layout(self):
        """Add widgets to the layout."""
        self._layout.addWidget(self.normalisation_frame)
        self._layout.addWidget(self.features_frame)
        self._layout.addWidget(self.iso_frame)
        self._layout.addWidget(self.spot_detection_frame)
        self._layout.addWidget(self.clear_previous_btn)
        self._layout.addWidget(self.submit_btn)

    def populate_iso_frame(self):
        """
        Add widgets and layout in the POST-PROCESSING frame.
        """

        grid = QGridLayout(self.iso_frame)

        self.iso_lbl.setStyleSheet(
            """
			font-weight: bold;
			padding: 0px;
			"""
        )
        grid.addWidget(self.iso_lbl, 0, 0, 1, 4, alignment=Qt.AlignCenter)
        self.generate_iso_contents()
        grid.addWidget(self.ContentsIso, 1, 0, 1, 4, alignment=Qt.AlignTop)

    def populate_features_frame(self):
        """
        Add widgets and layout in the FEATURES frame.
        """

        grid = QGridLayout(self.features_frame)

        self.feature_lbl.setStyleSheet(
            """
			font-weight: bold;
			padding: 0px;
			"""
        )
        grid.addWidget(self.feature_lbl, 0, 0, 1, 4, alignment=Qt.AlignCenter)

        self.generate_feature_panel_contents()
        grid.addWidget(self.ContentsFeatures, 1, 0, 1, 4, alignment=Qt.AlignTop)

    def generate_iso_contents(self):
        """Generate the isotropic measurements panel contents."""
        self.ContentsIso = QFrame()
        layout = QVBoxLayout(self.ContentsIso)
        layout.setContentsMargins(0, 0, 0, 0)

        radii_layout = QHBoxLayout()
        self.radii_lbl.setToolTip(
            "Define radii or donughts for intensity measurements."
        )
        radii_layout.addWidget(self.radii_lbl, 90)

        self.del_radius_btn = QPushButton("")
        self.del_radius_btn.setStyleSheet(self.button_select_all)
        self.del_radius_btn.setIcon(icon(MDI6.trash_can, color="black"))
        self.del_radius_btn.setToolTip("Remove radius")
        self.del_radius_btn.setIconSize(QSize(20, 20))
        radii_layout.addWidget(self.del_radius_btn, 5)

        self.add_radius_btn = QPushButton("")
        self.add_radius_btn.setStyleSheet(self.button_select_all)
        self.add_radius_btn.setIcon(icon(MDI6.plus, color="black"))
        self.add_radius_btn.setToolTip("Add radius")
        self.add_radius_btn.setIconSize(QSize(20, 20))
        radii_layout.addWidget(self.add_radius_btn, 5)
        layout.addLayout(radii_layout)

        self.radii_list = ListWidget(GeometryChoice, initial_features=["10"], dtype=int)
        layout.addWidget(self.radii_list)

        self.del_radius_btn.clicked.connect(self.radii_list.removeSel)
        self.add_radius_btn.clicked.connect(self.radii_list.addItem)

        # Operation
        operation_layout = QHBoxLayout()
        self.op_lbl.setToolTip("Set the operations to perform inside the ROI.")
        operation_layout.addWidget(self.op_lbl, 90)

        self.del_op_btn = QPushButton("")
        self.del_op_btn.setStyleSheet(self.button_select_all)
        self.del_op_btn.setIcon(icon(MDI6.trash_can, color="black"))
        self.del_op_btn.setToolTip("Remove operation")
        self.del_op_btn.setIconSize(QSize(20, 20))
        operation_layout.addWidget(self.del_op_btn, 5)

        self.add_op_btn = QPushButton("")
        self.add_op_btn.setStyleSheet(self.button_select_all)
        self.add_op_btn.setIcon(icon(MDI6.plus, color="black"))
        self.add_op_btn.setToolTip("Add operation")
        self.add_op_btn.setIconSize(QSize(20, 20))
        operation_layout.addWidget(self.add_op_btn, 5)
        layout.addLayout(operation_layout)

        self.operations_list = ListWidget(OperationChoice, initial_features=["mean"])
        layout.addWidget(self.operations_list)

        self.del_op_btn.clicked.connect(self.operations_list.removeSel)
        self.add_op_btn.clicked.connect(self.operations_list.addItem)

    def generate_feature_panel_contents(self):
        """Generate the feature panel contents."""
        self.ContentsFeatures = QFrame()
        layout = QVBoxLayout(self.ContentsFeatures)
        layout.setContentsMargins(0, 0, 0, 0)

        feature_layout = QHBoxLayout()
        feature_layout.setContentsMargins(0, 0, 0, 0)

        self.feature_lbl = QLabel("Add features:")
        self.del_feature_btn = QPushButton("")
        self.del_feature_btn.setStyleSheet(self.button_select_all)
        self.del_feature_btn.setIcon(icon(MDI6.trash_can, color="black"))
        self.del_feature_btn.setToolTip("Remove feature")
        self.del_feature_btn.setIconSize(QSize(20, 20))

        self.add_feature_btn = QPushButton("")
        self.add_feature_btn.setStyleSheet(self.button_select_all)
        self.add_feature_btn.setIcon(icon(MDI6.filter_plus, color="black"))
        self.add_feature_btn.setToolTip("Add feature")
        self.add_feature_btn.setIconSize(QSize(20, 20))

        self.features_list = ListWidget(
            FeatureChoice,
            initial_features=[
                "area",
                "intensity_mean",
            ],
        )

        self.create_feature_btn = QPushButton("")
        self.create_feature_btn.setStyleSheet(self.button_select_all)
        self.create_feature_btn.setIcon(icon(MDI6.file_cog, color="black"))
        self.create_feature_btn.setToolTip("Create new feature")
        self.create_feature_btn.setIconSize(QSize(20, 20))

        self.del_feature_btn.clicked.connect(self.features_list.removeSel)
        self.add_feature_btn.clicked.connect(self.features_list.addItem)
        self.create_feature_btn.clicked.connect(self.go_to_extraprops)

        feature_layout.addWidget(self.feature_lbl, 90)
        feature_layout.addWidget(self.del_feature_btn, 5)
        feature_layout.addWidget(self.add_feature_btn, 5)
        feature_layout.addWidget(self.create_feature_btn, 5)

        layout.addLayout(feature_layout)
        layout.addWidget(self.features_list)

        self.feat_sep2 = QHSeperationLine()
        layout.addWidget(self.feat_sep2)

        contour_layout = QHBoxLayout()
        self.border_dist_lbl = QLabel("Contour measurements (from edge of mask):")
        self.border_dist_lbl.setToolTip(
            "Apply the intensity measurements defined above\nto a slice of each cell mask, defined as distance\nfrom the edge."
        )
        contour_layout.addWidget(self.border_dist_lbl, 90)

        self.del_contour_btn = QPushButton("")
        self.del_contour_btn.setStyleSheet(self.button_select_all)
        self.del_contour_btn.setIcon(icon(MDI6.trash_can, color="black"))
        self.del_contour_btn.setToolTip("Remove distance")
        self.del_contour_btn.setIconSize(QSize(20, 20))
        contour_layout.addWidget(self.del_contour_btn, 5)

        self.add_contour_btn = QPushButton("")
        self.add_contour_btn.setStyleSheet(self.button_select_all)
        self.add_contour_btn.setIcon(icon(MDI6.plus, color="black"))
        self.add_contour_btn.setToolTip("Add distance")
        self.add_contour_btn.setIconSize(QSize(20, 20))
        contour_layout.addWidget(self.add_contour_btn, 5)

        self.view_contour_btn = QPushButton("")
        self.view_contour_btn.setStyleSheet(self.button_select_all)
        self.view_contour_btn.setIcon(icon(MDI6.eye_plus_outline, color="black"))
        self.view_contour_btn.setToolTip("View contour")
        self.view_contour_btn.setIconSize(QSize(20, 20))
        contour_layout.addWidget(self.view_contour_btn, 5)

        layout.addLayout(contour_layout)

        self.contours_list = ListWidget(GeometryChoice, initial_features=[], dtype=int)
        layout.addWidget(self.contours_list)

        self.del_contour_btn.clicked.connect(self.contours_list.removeSel)
        self.add_contour_btn.clicked.connect(self.contours_list.addItem)
        self.view_contour_btn.clicked.connect(self.view_selected_contour)

        self.feat_sep3 = QHSeperationLine()
        layout.addWidget(self.feat_sep3)

        # Haralick features parameters
        self.activate_haralick_btn = QCheckBox("Measure Haralick texture features")
        self.activate_haralick_btn.toggled.connect(self.show_haralick_options)

        self.haralick_channel_choice = QComboBox()
        self.haralick_channel_choice.addItems(self.channel_names)
        self.haralick_channel_lbl = QLabel("Target channel: ")

        self.haralick_distance_le = QLineEdit("1")
        self.haralick_distance_lbl = QLabel("Distance: ")

        self.haralick_n_gray_levels_le = QLineEdit("256")
        self.haralick_n_gray_levels_lbl = QLabel("# gray levels: ")

        # Slider to set vmin & vmax
        self.haralick_scale_slider = QLabeledDoubleSlider()
        self.haralick_scale_slider.setSingleStep(0.05)
        self.haralick_scale_slider.setTickInterval(0.05)
        self.haralick_scale_slider.setSingleStep(1)
        self.haralick_scale_slider.setOrientation(Qt.Horizontal)
        self.haralick_scale_slider.setRange(0, 1)
        self.haralick_scale_slider.setValue(0.5)
        self.haralick_scale_lbl = QLabel("Scale: ")

        self.haralick_percentile_min_le = QLineEdit("0.01")
        self.haralick_percentile_max_le = QLineEdit("99.9")
        self.haralick_normalization_mode_btn = QPushButton()
        self.haralick_normalization_mode_btn.setStyleSheet(self.button_select_all)
        self.haralick_normalization_mode_btn.setIcon(
            icon(MDI6.percent_circle, color="black")
        )
        self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
        self.haralick_normalization_mode_btn.setToolTip(
            "Switch to absolute normalization values."
        )
        self.percentile_mode = True

        min_percentile_hbox = QHBoxLayout()
        min_percentile_hbox.addWidget(self.haralick_percentile_min_le, 90)
        min_percentile_hbox.addWidget(self.haralick_normalization_mode_btn, 10)
        min_percentile_hbox.setContentsMargins(0, 0, 0, 0)

        self.haralick_percentile_min_lbl = QLabel("Min percentile: ")
        self.haralick_percentile_max_lbl = QLabel("Max percentile: ")

        self.haralick_hist_btn = QPushButton()
        self.haralick_hist_btn.clicked.connect(
            self.control_haralick_intensity_histogram
        )
        self.haralick_hist_btn.setIcon(icon(MDI6.poll, color="k"))
        self.haralick_hist_btn.setStyleSheet(self.button_select_all)

        self.haralick_digit_btn = QPushButton()
        self.haralick_digit_btn.clicked.connect(self.control_haralick_digitalization)
        self.haralick_digit_btn.setIcon(icon(MDI6.image_check, color="k"))
        self.haralick_digit_btn.setStyleSheet(self.button_select_all)

        self.haralick_layout = QVBoxLayout()
        self.haralick_layout.setContentsMargins(20, 20, 20, 20)

        activate_layout = QHBoxLayout()
        activate_layout.addWidget(self.activate_haralick_btn, 80)
        activate_layout.addWidget(self.haralick_hist_btn, 10)
        activate_layout.addWidget(self.haralick_digit_btn, 10)
        self.haralick_layout.addLayout(activate_layout)

        channel_layout = QHBoxLayout()
        channel_layout.addWidget(self.haralick_channel_lbl, 40)
        channel_layout.addWidget(self.haralick_channel_choice, 60)
        self.haralick_layout.addLayout(channel_layout)

        distance_layout = QHBoxLayout()
        distance_layout.addWidget(self.haralick_distance_lbl, 40)
        distance_layout.addWidget(self.haralick_distance_le, 60)
        self.haralick_layout.addLayout(distance_layout)

        gl_layout = QHBoxLayout()
        gl_layout.addWidget(self.haralick_n_gray_levels_lbl, 40)
        gl_layout.addWidget(self.haralick_n_gray_levels_le, 60)
        self.haralick_layout.addLayout(gl_layout)

        slider_layout = QHBoxLayout()
        slider_layout.addWidget(self.haralick_scale_lbl, 40)
        slider_layout.addWidget(self.haralick_scale_slider, 60)
        self.haralick_layout.addLayout(slider_layout)

        slider_min_percentile_layout = QHBoxLayout()
        slider_min_percentile_layout.addWidget(self.haralick_percentile_min_lbl, 40)
        # slider_min_percentile_layout.addWidget(self.haralick_percentile_min_le,55)
        slider_min_percentile_layout.addLayout(min_percentile_hbox, 60)
        # slider_min_percentile_layout.addWidget(self.haralick_normalization_mode_btn, 5)
        self.haralick_layout.addLayout(slider_min_percentile_layout)

        slider_max_percentile_layout = QHBoxLayout()
        slider_max_percentile_layout.addWidget(self.haralick_percentile_max_lbl, 40)
        slider_max_percentile_layout.addWidget(self.haralick_percentile_max_le, 60)
        self.haralick_layout.addLayout(slider_max_percentile_layout)

        self.haralick_to_hide = [
            self.haralick_hist_btn,
            self.haralick_digit_btn,
            self.haralick_channel_lbl,
            self.haralick_channel_choice,
            self.haralick_distance_le,
            self.haralick_distance_lbl,
            self.haralick_n_gray_levels_le,
            self.haralick_n_gray_levels_lbl,
            self.haralick_scale_lbl,
            self.haralick_scale_slider,
            self.haralick_percentile_min_lbl,
            self.haralick_percentile_min_le,
            self.haralick_percentile_max_lbl,
            self.haralick_percentile_max_le,
            self.haralick_normalization_mode_btn,
        ]

        self.features_to_disable = [
            self.feature_lbl,
            self.del_feature_btn,
            self.add_feature_btn,
            self.features_list,
            self.activate_haralick_btn,
        ]

        self.activate_haralick_btn.setChecked(False)
        for f in self.haralick_to_hide:
            f.setEnabled(False)

        self.haralick_normalization_mode_btn.clicked.connect(
            self.switch_to_absolute_normalization_mode
        )
        layout.addLayout(self.haralick_layout)

    def go_to_extraprops(self):
        """Open the extra properties file."""
        path = os.sep.join(
            [self._software_path, "celldetective", os.sep, "extra_properties.py"]
        )
        try:
            Popen(f"explorer {os.path.realpath(path)}")
        except:

            try:
                os.system('xdg-open "%s"' % path)
            except:
                return None

    def switch_to_absolute_normalization_mode(self):
        """Switch between percentile and absolute normalization for Haralick features."""
        if self.percentile_mode:
            self.percentile_mode = False
            self.haralick_normalization_mode_btn.setIcon(
                icon(MDI6.percent_circle_outline, color="black")
            )
            self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
            self.haralick_normalization_mode_btn.setToolTip(
                "Switch to percentile normalization values."
            )
            self.haralick_percentile_min_lbl.setText("Min value: ")
            self.haralick_percentile_max_lbl.setText("Max value: ")
            self.haralick_percentile_min_le.setText("0")
            self.haralick_percentile_max_le.setText("10000")

        else:
            self.percentile_mode = True
            self.haralick_normalization_mode_btn.setIcon(
                icon(MDI6.percent_circle, color="black")
            )
            self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
            self.haralick_normalization_mode_btn.setToolTip(
                "Switch to absolute normalization values."
            )
            self.haralick_percentile_min_lbl.setText("Min percentile: ")
            self.haralick_percentile_max_lbl.setText("Max percentile: ")
            self.haralick_percentile_min_le.setText("0.01")
            self.haralick_percentile_max_le.setText("99.99")

    def show_haralick_options(self):
        """
        Show the Haralick texture options.
        """

        if self.activate_haralick_btn.isChecked():
            for element in self.haralick_to_hide:
                element.setEnabled(True)
        else:
            for element in self.haralick_to_hide:
                element.setEnabled(False)

    def adjustScrollArea(self):
        """
        Auto-adjust scroll area to fill space
        (from https://stackoverflow.com/questions/66417576/make-qscrollarea-use-all-available-space-of-qmainwindow-height-axis)
        """

        step = 5
        while (
            self.scroll_area.verticalScrollBar().isVisible()
            and self.height() < self.maximumHeight()
        ):
            self.resize(self.width(), self.height() + step)

    def _write_instructions(self):
        """
        Write the selected options in a json file for later reading by the software.
        """

        print(f"{self.spot_preprocessing.list.items=}")

        print("Writing instructions...")
        measurement_options = {}
        background_correction = self.protocol_layout.protocols
        if not background_correction:
            background_correction = None
        measurement_options.update({"background_correction": background_correction})
        features = self.features_list.getItems()
        if not features:
            features = None
        measurement_options.update({"features": features})

        border_distances = self.contours_list.getItems()
        if not border_distances:
            border_distances = None
        measurement_options.update({"border_distances": border_distances})

        self.extract_haralick_options()
        measurement_options.update({"haralick_options": self.haralick_options})

        intensity_measurement_radii = self.radii_list.getItems()
        if not intensity_measurement_radii:
            intensity_measurement_radii = None

        isotropic_operations = self.operations_list.getItems()
        if not isotropic_operations:
            isotropic_operations = None
            intensity_measurement_radii = None
        measurement_options.update(
            {
                "intensity_measurement_radii": intensity_measurement_radii,
                "isotropic_operations": isotropic_operations,
            }
        )
        spot_detection = None
        if self.spot_check.isChecked():
            image_preprocessing = self.spot_preprocessing.list.items
            if not image_preprocessing:
                image_preprocessing = None
            spot_detection = {
                "channel": self.spot_channel.currentText(),
                "diameter": float(self.diameter_value.text().replace(",", ".")),
                "threshold": float(self.threshold_value.text().replace(",", ".")),
                "image_preprocessing": image_preprocessing,
            }
        measurement_options.update({"spot_detection": spot_detection})
        if self.clear_previous_btn.isChecked():
            self.clear_previous = True
        else:
            self.clear_previous = False
        measurement_options.update({"clear_previous": self.clear_previous})

        print("Measurement instructions: ", measurement_options)
        file_name = self.measure_instructions_path
        with open(file_name, "w") as f:
            json.dump(measurement_options, f, indent=4)

        print("Done.")
        self.close()

    def extract_haralick_options(self):
        """Extract Haralick features options from the GUI."""
        if self.activate_haralick_btn.isChecked():
            self.haralick_options = {
                "target_channel": self.haralick_channel_choice.currentIndex(),
                "scale_factor": float(self.haralick_scale_slider.value()),
                "n_intensity_bins": int(self.haralick_n_gray_levels_le.text()),
                "distance": int(self.haralick_distance_le.text()),
            }
            if self.percentile_mode:
                self.haralick_options.update(
                    {
                        "percentiles": (
                            float(self.haralick_percentile_min_le.text()),
                            float(self.haralick_percentile_max_le.text()),
                        ),
                        "clip_values": None,
                    }
                )
            else:
                self.haralick_options.update(
                    {
                        "percentiles": None,
                        "clip_values": (
                            float(self.haralick_percentile_min_le.text()),
                            float(self.haralick_percentile_max_le.text()),
                        ),
                    }
                )

        else:
            self.haralick_options = None

    def _load_previous_instructions(self):
        """
        Read the measurmeent options from a previously written json file and format properly for the UI.
        """

        print("Reading instructions..")
        if os.path.exists(self.measure_instructions_path):
            with open(self.measure_instructions_path, "r") as f:
                measurement_instructions = json.load(f)
                print(measurement_instructions)
                if "background_correction" in measurement_instructions:
                    self.protocol_layout.protocols = measurement_instructions[
                        "background_correction"
                    ]
                    if self.protocol_layout.protocols is None:
                        self.protocol_layout.protocols = []
                    if (self.protocol_layout.protocols is not None) and len(
                        self.protocol_layout.protocols
                    ) > 0:
                        self.protocol_layout.protocol_list.clear()
                        for norm_params in self.protocol_layout.protocols:
                            normalisation_description = ""
                            for index, (key, value) in enumerate(norm_params.items()):
                                if index > 0:
                                    normalisation_description += ", "
                                normalisation_description += (
                                    str(key) + " : " + str(value)
                                )
                            self.protocol_layout.protocol_list.addItem(
                                normalisation_description
                            )
                    else:
                        self.protocol_layout.protocol_list.clear()
                if "features" in measurement_instructions:
                    features = measurement_instructions["features"]
                    if (features is not None) and len(features) > 0:
                        self.features_list.list_widget.clear()
                        self.features_list.list_widget.addItems(features)
                    else:
                        self.features_list.list_widget.clear()

                if "spot_detection" in measurement_instructions:
                    spot_detection = measurement_instructions["spot_detection"]
                    if spot_detection is not None:
                        self.spot_check.setChecked(True)
                        if "channel" in spot_detection:
                            idx = spot_detection["channel"]
                            self.spot_channel.setCurrentText(idx)
                        self.diameter_value.setText(str(spot_detection["diameter"]))
                        self.threshold_value.setText(str(spot_detection["threshold"]))
                        if "image_preprocessing" in spot_detection:
                            items = spot_detection["image_preprocessing"]
                            if items is not None:
                                items_for_list = [a[0] for a in items]
                                for it in items_for_list:
                                    self.spot_preprocessing.list.addItemToList(it)
                                self.spot_preprocessing.list.items = items

                if "border_distances" in measurement_instructions:
                    border_distances = measurement_instructions["border_distances"]
                    if border_distances is not None:
                        if isinstance(border_distances, int):
                            distances = [str(border_distances)]
                        elif isinstance(border_distances, list):
                            distances = []
                            for d in border_distances:
                                if isinstance(d, (int, float)):
                                    distances.append(str(int(d)))
                                elif isinstance(d, list):
                                    # Use (min,max) tuple format to match CellEdgeVisualizer output
                                    distances.append(f"({int(d[0])},{int(d[1])})")
                        self.contours_list.list_widget.clear()
                        self.contours_list.list_widget.addItems(distances)

                if "haralick_options" in measurement_instructions:
                    haralick_options = measurement_instructions["haralick_options"]
                    if haralick_options is None:
                        self.activate_haralick_btn.setChecked(False)
                        self.show_haralick_options()
                    else:
                        self.activate_haralick_btn.setChecked(True)
                        self.show_haralick_options()
                        if "target_channel" in haralick_options:
                            idx = haralick_options["target_channel"]
                            self.haralick_channel_choice.setCurrentIndex(idx)
                        if "scale_factor" in haralick_options:
                            self.haralick_scale_slider.setValue(
                                float(haralick_options["scale_factor"])
                            )
                        if ("percentiles" in haralick_options) and (
                            haralick_options["percentiles"] is not None
                        ):
                            perc = list(haralick_options["percentiles"])
                            self.haralick_percentile_min_le.setText(str(perc[0]))
                            self.haralick_percentile_max_le.setText(str(perc[1]))
                        if ("clip_values" in haralick_options) and (
                            haralick_options["clip_values"] is not None
                        ):
                            values = list(haralick_options["clip_values"])
                            self.percentile_mode = True
                            self.switch_to_absolute_normalization_mode()
                            self.haralick_percentile_min_le.setText(str(values[0]))
                            self.haralick_percentile_max_le.setText(str(values[1]))
                        if "n_intensity_bins" in haralick_options:
                            self.haralick_n_gray_levels_le.setText(
                                str(haralick_options["n_intensity_bins"])
                            )
                        if "distance" in haralick_options:
                            self.haralick_distance_le.setText(
                                str(haralick_options["distance"])
                            )

                if "intensity_measurement_radii" in measurement_instructions:
                    intensity_measurement_radii = measurement_instructions[
                        "intensity_measurement_radii"
                    ]
                    if intensity_measurement_radii is not None:
                        if isinstance(intensity_measurement_radii, int):
                            radii = [intensity_measurement_radii]
                        elif isinstance(intensity_measurement_radii, list):
                            radii = []
                            for r in intensity_measurement_radii:
                                if isinstance(r, int) | isinstance(r, float):
                                    radii.append(str(int(r)))
                                elif isinstance(r, list):
                                    radii.append(str(int(r[0])) + "-" + str(int(r[1])))
                        self.radii_list.list_widget.clear()
                        self.radii_list.list_widget.addItems(radii)
                    else:
                        self.radii_list.list_widget.clear()

                if "isotropic_operations" in measurement_instructions:
                    isotropic_operations = measurement_instructions[
                        "isotropic_operations"
                    ]
                    if (isotropic_operations is not None) and len(
                        isotropic_operations
                    ) > 0:
                        self.operations_list.list_widget.clear()
                        self.operations_list.list_widget.addItems(isotropic_operations)
                    else:
                        self.operations_list.list_widget.clear()

                if "clear_previous" in measurement_instructions:
                    self.clear_previous = measurement_instructions["clear_previous"]
                    self.clear_previous_btn.setChecked(self.clear_previous)

    def locate_image(self):
        """
        Load the first frame of the first movie found in the experiment folder as a sample.
        """

        movies = glob(
            self.parent_window.parent_window.pos
            + os.sep.join(["movie", f"{self.parent_window.movie_prefix}*.tif"])
        )

        if len(movies) == 0:
            msgBox = QMessageBox()
            msgBox.setIcon(QMessageBox.Warning)
            msgBox.setText("Please select a position containing a movie...")
            msgBox.setWindowTitle("Warning")
            msgBox.setStandardButtons(QMessageBox.Ok)
            returnValue = msgBox.exec()
            if returnValue == QMessageBox.Ok:
                self.current_stack = None
                self.test_frame = None
                return None
        else:
            self.current_stack = movies[0]
            self.stack_length = auto_load_number_of_frames(self.current_stack)
            self.mid_time = self.stack_length // 2
            indices = len(self.channel_names) * self.mid_time + np.arange(
                len(self.channel_names)
            )
            self.test_frame = load_frames(
                list(indices.astype(int)), self.current_stack, normalize_input=False
            )

    def control_haralick_digitalization(self):
        """
        Load an image for the first experiment movie found.
        Apply the Haralick parameters and check the result of the digitization (normalization + binning of intensities).

        """

        self.locate_image()  # pb here
        self.extract_haralick_options()
        if self.test_frame is not None:
            import matplotlib.pyplot as plt
            from mpl_toolkits.axes_grid1 import make_axes_locatable
            from celldetective.gui.base.figure_canvas import FigureCanvas
            from celldetective.measure import compute_haralick_features

            digitized_img = compute_haralick_features(
                self.test_frame,
                np.zeros(self.test_frame.shape[:2]),
                channels=list(self.channel_names),
                return_digit_image_only=True,
                **self.haralick_options,
            )

            self.fig, self.ax = plt.subplots()
            divider = make_axes_locatable(self.ax)
            cax = divider.append_axes("right", size="5%", pad=0.05)

            self.imshow_digit_window = FigureCanvas(
                self.fig, title="Haralick: control digitization"
            )
            self.ax.clear()
            im = self.ax.imshow(digitized_img, cmap="gray")
            self.fig.colorbar(im, cax=cax, orientation="vertical")
            self.ax.set_xticks([])
            self.ax.set_yticks([])
            self.fig.set_facecolor("none")  # or 'None'
            self.fig.canvas.setStyleSheet("background-color: transparent;")
            self.imshow_digit_window.canvas.draw()
            self.imshow_digit_window.show()

    def control_haralick_intensity_histogram(self):
        """
        Load an image for the first experiment movie found.
        Apply the Haralick normalization parameters and check the normalized intensity histogram.

        """

        self.locate_image()
        self.extract_haralick_options()
        if self.test_frame is not None:
            import matplotlib.pyplot as plt
            from celldetective.gui.base.figure_canvas import FigureCanvas
            from celldetective.measure import compute_haralick_features

            norm_img = compute_haralick_features(
                self.test_frame,
                np.zeros(self.test_frame.shape[:2]),
                channels=list(self.channel_names),
                return_norm_image_only=True,
                **self.haralick_options,
            )
            self.fig, self.ax = plt.subplots(1, 1, figsize=(4, 3))
            self.hist_window = FigureCanvas(
                self.fig, title="Haralick: control digitized histogram"
            )
            self.ax.clear()
            flat = norm_img.flatten()
            self.ax.hist(
                flat[flat == flat], bins=self.haralick_options["n_intensity_bins"]
            )
            self.ax.set_xlabel("gray level value")
            self.ax.set_ylabel("#")
            plt.tight_layout()
            self.fig.set_facecolor("none")  # or 'None'
            self.fig.canvas.setStyleSheet("background-color: transparent;")
            self.hist_window.canvas.draw()
            self.hist_window.show()

            try:
                QTimer.singleShot(100, lambda: center_window(self.hist_window))
            except Exception as e:
                pass

    def view_selected_contour(self):
        """
        Show the ROI for the selected contour measurement on experimental data.

        """
        self.locate_image()

        if self.current_stack is not None:

            self.locate_mask()
            if self.test_mask is None:
                return

            from celldetective.gui.viewers.contour_viewer import CellEdgeVisualizer

            self.viewer = CellEdgeVisualizer(
                cell_type=self.mode,
                stack_path=self.current_stack,
                parent_list_widget=self.contours_list.list_widget,
                n_channels=len(self.channel_names),
                target_channel=0,
                window_title="Set an edge measurement",
                channel_cb=True,
                channel_names=self.channel_names,
                PxToUm=1,
            )
            self.viewer.show()

    def locate_mask(self):
        """
        Load the first mask of the detected movie.
        """

        labels_path = (
            str(Path(self.current_stack).parent.parent)
            + os.sep
            + f"labels_{self.mode}"
            + os.sep
        )
        masks = natsorted(glob(labels_path + "*.tif"))
        if len(masks) == 0:
            msgBox = QMessageBox()
            msgBox.setIcon(QMessageBox.Warning)
            msgBox.setText(
                f"No mask found in {labels_path}\nPlease segment your data first."
            )
            msgBox.setWindowTitle("Warning")
            msgBox.setStandardButtons(QMessageBox.Ok)
            msgBox.exec()
            self.test_mask = None
        else:
            from tifffile import imread

            self.test_mask = imread(masks[self.mid_time])

    def switch_channel_contour(self, value: int) -> None:
        """
        Adjust intensity values when changing channels in the contour visualizer.

        Parameters
        ----------
        value : int
            The index of the selected channel.
        """

        self.im_contour.set_array(self.test_frame[:, :, value])
        self.im_contour.set_clim(
            vmin=np.percentile(self.test_frame[:, :, value].flatten(), 1),
            vmax=np.percentile(self.test_frame[:, :, value].flatten(), 99.99),
        )
        self.contrast_slider_contour.setRange(
            np.amin(self.test_frame[:, :, value]), np.amax(self.test_frame[:, :, value])
        )
        self.contrast_slider_contour.setValue(
            [
                np.percentile(self.test_frame[:, :, value].flatten(), 1),
                np.percentile(self.test_frame[:, :, value].flatten(), 99.99),
            ]
        )
        self.fig_contour.canvas.draw_idle()

    def contrast_im_contour(self, value: List[float]) -> None:
        """
        Contrast the image for contour visualization.

        Parameters
        ----------
        value : list
            A list containing [vmin, vmax].
        """
        vmin = value[0]
        vmax = value[1]
        self.im_contour.set_clim(vmin=vmin, vmax=vmax)
        self.fig_contour.canvas.draw_idle()

    def make_contour_transparent(self, value: float) -> None:
        """
        Make the contour transparent.

        Parameters
        ----------
        value : float
            Transparency value (alpha).
        """

        self.im_mask.set_alpha(value)
        self.fig_contour.canvas.draw_idle()

    def remove_item_from_list(self):
        """Remove the selected item from the list."""
        current_item = self.normalisation_list.currentRow()
        if current_item > -1:
            del self.background_correction[current_item]
            self.normalisation_list.takeItem(current_item)

    def check_the_information(self):
        """
        Check if the provided information is valid.

        Returns
        -------
        bool
            True if information is valid or confirmed, False otherwise.
        """

        if self.tabs.currentIndex() == 0:
            if self.background_correction is None:
                self.background_correction = []
            for index, normalisation_opt in enumerate(self.background_correction):
                if (
                    self.tab1_channel_dropdown.currentText()
                    in normalisation_opt["target channel"]
                ):
                    result = self.channel_already_in_list()
                    if result != QMessageBox.Yes:
                        return False
                    else:
                        self.background_correction.remove(normalisation_opt)
                        self.normalisation_list.takeItem(index)
                        return True

    def display_message_box(self, missing_info: str) -> None:
        """
        Display a warning message box.

        Parameters
        ----------
        missing_info : str
            The missing information description.
        """
        QMessageBox.about(
            self,
            "Message Box Title",
            "Please " + missing_info + " for background correction",
        )

    def channel_already_in_list(self):
        """
        Check if a channel is already in the list.

        Returns
        -------
        int
            QMessageBox response (Yes/No).
        """
        response = QMessageBox.question(
            self,
            "Message Box Title",
            "The background correction parameters for this channel already exist, "
            "continuing will erase the previous configurations. Are you sure you want to "
            "proceed?",
            QMessageBox.No | QMessageBox.Yes,
            QMessageBox.No,
        )
        return response

    def view_normalisation_contour(self):
        """
        Show the ROI for the selected contour measurement on experimental data.

        """

        self.locate_image()

        if self.current_stack is not None:
            self.locate_mask()
            if self.test_mask is None:
                return

            from celldetective.gui.viewers.contour_viewer import CellEdgeVisualizer

            self.viewer = CellEdgeVisualizer(
                cell_type=self.mode,
                stack_path=self.current_stack,
                parent_le=self.tab1_txt_distance,
                n_channels=len(self.channel_names),
                target_channel=self.tab1_channel_dropdown.currentIndex(),
                edge_range=(0, 30),
                initial_edge=self.tab1_txt_distance.get_threshold(),
                invert=True,
                window_title="Set an edge distance to estimate local intensity",
                channel_cb=False,
                PxToUm=1,
            )
            self.viewer.show()

    def populate_spot_detection(self):
        """Populate the spot detection frame."""

        layout = QVBoxLayout(self.spot_detection_frame)

        self.spot_detection_lbl = QLabel("SPOT DETECTION")
        self.spot_detection_lbl.setStyleSheet("""font-weight: bold;padding: 0px;""")
        layout.addWidget(self.spot_detection_lbl, alignment=Qt.AlignCenter)

        perform_hbox = QHBoxLayout()
        self.spot_check = QCheckBox("Perform spot detection")
        self.spot_check.toggled.connect(self.enable_spot_detection)
        perform_hbox.addWidget(self.spot_check, 95)

        self.spot_viewer_btn = QPushButton()
        self.spot_viewer_btn.clicked.connect(self.spot_preview)
        self.spot_viewer_btn.setIcon(icon(MDI6.image_check, color="k"))
        self.spot_viewer_btn.setStyleSheet(self.button_select_all)
        self.spot_viewer_btn.setToolTip("Set detection parameters visually.")
        perform_hbox.addWidget(self.spot_viewer_btn, 5)
        layout.addLayout(perform_hbox)

        channel_hbox = QHBoxLayout()
        self.spot_channel_lbl = QLabel("Channel: ")
        self.spot_channel = QComboBox()
        self.spot_channel.addItems(self.channel_names)
        channel_hbox.addWidget(self.spot_channel_lbl, 30)
        channel_hbox.addWidget(self.spot_channel, 70)
        layout.addLayout(channel_hbox)

        self.spot_preprocessing = PreprocessingLayout2(fraction=30, parent_window=self)
        layout.addLayout(self.spot_preprocessing)

        # continue switching to VBox + HBox down
        diam_hbox = QHBoxLayout()
        self.diameter_lbl = QLabel("Spot diameter: ")
        self.diameter_value = QLineEdit()
        self.diameter_value.setValidator(self._floatValidator)
        self.diameter_value.setText("7")
        self.diameter_value.textChanged.connect(self.enable_spot_preview)

        diam_hbox.addWidget(self.diameter_lbl, 30)
        diam_hbox.addWidget(self.diameter_value, 70)
        layout.addLayout(diam_hbox)

        thresh_hbox = QHBoxLayout()
        self.threshold_lbl = QLabel("Spot threshold: ")
        self.threshold_value = QLineEdit()
        self.threshold_value.setValidator(self._floatValidator)
        self.threshold_value.setText("0")
        self.threshold_value.textChanged.connect(self.enable_spot_preview)

        thresh_hbox.addWidget(self.threshold_lbl, 30)
        thresh_hbox.addWidget(self.threshold_value, 70)
        layout.addLayout(thresh_hbox)

        self.spot_detection_widgets = [
            self.spot_channel,
            self.spot_channel_lbl,
            self.diameter_value,
            self.diameter_lbl,
            self.threshold_value,
            self.threshold_lbl,
            self.spot_viewer_btn,
            # self.invert_check,
            # self.invert_value_le,
            self.spot_preprocessing.list,
            self.spot_preprocessing.add_filter_btn,
            self.spot_preprocessing.delete_filter_btn,
            self.spot_preprocessing.preprocess_lbl,
        ]
        for wg in self.spot_detection_widgets:
            wg.setEnabled(False)

    def enable_spot_preview(self):
        """Enable or disable the spot preview button."""

        diam = self.diameter_value.text().replace(",", "").replace(".", "")
        thresh = self.threshold_value.text().replace(",", "").replace(".", "")
        if diam.isnumeric() and thresh.isnumeric():
            self.spot_viewer_btn.setEnabled(True)
        else:
            self.spot_viewer_btn.setEnabled(False)

    def spot_preview(self):
        """Preview the spot detection."""
        self.locate_image()
        if self.test_frame is not None:
            self.locate_mask()
            if self.test_mask is not None:

                # invert_value = self.invert_value_le.text().replace(',','.')
                # if invert_value != '':
                # 	invert_value = float(invert_value)
                # else:
                # 	invert_value = None

                from celldetective.gui.viewers.spot_detection_viewer import (
                    SpotDetectionVisualizer,
                )

                self.spot_visual = SpotDetectionVisualizer(
                    frame_slider=True,
                    contrast_slider=True,
                    cell_type=self.mode,
                    channel_cb=True,
                    channel_names=self.channel_names,
                    stack_path=self.current_stack,
                    n_channels=len(self.channel_names),
                    target_channel=self.spot_channel.currentIndex(),
                    window_title="Detect spots",
                    parent_channel_cb=self.spot_channel,
                    parent_diameter_le=self.diameter_value,
                    parent_threshold_le=self.threshold_value,
                    parent_preprocessing_list=self.spot_preprocessing.list,
                    initial_diameter=self.diameter_value.text(),
                    initial_threshold=self.threshold_value.text(),
                    initial_preprocessing=(
                        self.spot_preprocessing.list.items
                        if self.spot_preprocessing.list.items
                        else None
                    ),
                    # parent_invert_check=self.invert_check,
                    # invert = self.invert_check.isChecked(),
                    # invert_value = self.invert_value_le.text().replace(',','.'),
                    PxToUm=1,
                )
                self.spot_visual.show()
                # self.spot_visual = ThresholdSpot(current_channel=self.spot_channel.currentIndex(), img=self.test_frame,
                # 								 mask=self.test_mask, parent_window=self)

    def enable_spot_detection(self):
        """Enable or disable spot detection based on input validity."""

        if self.spot_check.isChecked():
            for wg in self.spot_detection_widgets:
                wg.setEnabled(True)
        else:
            for wg in self.spot_detection_widgets:
                wg.setEnabled(False)
