"""Application for TDO analysis."""

import importlib.metadata
from collections.abc import Sequence
from pathlib import Path

from PyQt6 import QtGui, QtWidgets
from PyQt6.QtCore import pyqtSignal, pyqtSlot

from pymagnetos.core.gui import BaseMainWindow
from pymagnetos.core.gui.widgets import BaseBatchProcessingWidget, FileBrowserWidget

from .widgets import ButtonsWidget, ConfigurationWidget, GraphsWidget, ParamContent
from .worker import Worker

ICON_PATH = str(Path(__file__).parent / "assets" / "icon.png")
REGEXP_EXPID_SEPARATORS = r"[_-]"
PROGRAM_NAME = "pymagnetos"
OUT_FORMAT = (".txt", ".csv", ".tsv", ".out")
LOG_LEVEL = "INFO"
VERSION = importlib.metadata.version(PROGRAM_NAME)

# List of parameter names that should trigger an analysis
PARAM_ANALYSIS_TRIGGER = [
    "poly_deg",
    "npoints_interp_inverse",
    "fft_pad_mult",
    "max_bfreq",
]


class MainWindow(BaseMainWindow):
    """
    A graphical user interface application for TDO analysis.

    The `MainWindow` class defines the main thread with the front-end user interface.
    """

    sig_worker_crop = pyqtSignal()
    sig_worker_offset = pyqtSignal()
    sig_worker_tdocsv = pyqtSignal(str)
    sig_worker_rescsv = pyqtSignal(str)
    sig_worker_loadcsv = pyqtSignal(str)

    worker: Worker

    def __init__(self):
        # Register the widgets
        self._type_wbatch = BaseBatchProcessingWidget
        self._type_wbuttons = ButtonsWidget
        self._type_wconfiguration = ConfigurationWidget
        self._type_wfiles = FileBrowserWidget
        self._type_wgraphs = GraphsWidget
        self._param_content = ParamContent
        self._type_worker = Worker

        super().__init__()

        # Initialize window
        self.setGeometry(300, 300, 900, 450)
        self.setWindowTitle("TDO Analyzer")

        self.logger.info(f"Running pymagnetos.pytdo v{VERSION}")

    def init_buttons(self):
        """Create the main buttons."""
        super().init_buttons()

        # Connect
        self.wbuttons.sig_extract.connect(self.preprocess)
        self.wbuttons.sig_analyse.connect(self.run_analysis)
        self.wbuttons.sig_tdocsv.connect(self.save_tdo_csv)
        self.wbuttons.sig_rescsv.connect(self.save_results_csv)

    def init_plots(self):
        """Create the graphs area."""
        super().init_plots()

        # Connect ROIs
        self.wgraphs.sig_roi1_changed.connect(self.handle_roi1_changed)
        self.wgraphs.sig_roi2_changed.connect(self.handle_roi2_changed)

    def init_log(self):
        """Setup logging."""
        super()._init_log(PROGRAM_NAME, log_level=LOG_LEVEL)

    def connect_worker(self):
        """Connect signals to the worker slots."""
        super().connect_worker()

        # Cropping
        self.sig_worker_crop.connect(self.worker.crop_signal)
        self.worker.sig_crop_finished.connect(self.crop_signal_finished)

        # Time offset
        self.sig_worker_offset.connect(self.worker.time_offset)
        self.worker.sig_offset_finished.connect(self.time_offset_finished)

        # Save TDO signal as CSV
        self.sig_worker_tdocsv.connect(self.worker.save_tdo_csv)
        self.worker.sig_tdocsv_finished.connect(self.save_tdo_csv_finished)

        # Save results as CSV
        self.sig_worker_rescsv.connect(self.worker.save_results_csv)
        self.worker.sig_rescsv_finished.connect(self.save_results_csv_finished)

        # Load from CSV
        self.sig_worker_loadcsv.connect(self.worker.load_csv_file)
        self.worker.sig_load_csv_finished.connect(self.load_csv_file_finished)

    def connect_parameter_tree(self) -> None:
        """Connect signals from the Configuration widget."""
        self.wconfiguration.sig_syncroi_changed.connect(self.syncroi_changed)

    def run_after_tree_change(
        self, context: str, param_name: str, value: float | int | list | bool | str
    ) -> None:
        """Run additional actions after a parameter change in the parameter tree."""
        if context != "settings":
            return

        match param_name:
            case "poly_window":
                if isinstance(value, list):
                    self.update_roi_from_poly(value)
            case "fft_window":
                if isinstance(value, list):
                    self.update_roi_from_fft(value)
            case "max_time":
                self.crop_signal()
            case "time_offset":
                self.apply_time_offset()
            case "offset":
                self.plot_tdo_detrended()
            case param_name if param_name in PARAM_ANALYSIS_TRIGGER:
                self.run_analysis()

    @pyqtSlot()
    def load_file(self):
        """
        Load a configuration or NeXus file and start a worker.

        Callback for when the "File" parameter changes, or a file is drag & dropped in
        the window.
        """
        super().load_file()

        # Set the ROIs
        # Turn off analysis because we need to extract the TDO signal first
        self._do_analysis = False
        region1 = self.worker.proc.cfg.settings.poly_window
        region2 = self.worker.proc.cfg.settings.fft_window
        if isinstance(region1, Sequence) and len(region1) == 2:
            self.update_roi_from_poly(region1)
        if isinstance(region2, Sequence) and len(region2) == 2:
            self.update_roi_from_fft(region2)

        if self.worker.is_data_loaded:
            # If data was loaded automatically, we can show it
            self.load_data_finished()

        if self.worker.proc.is_nexus_file:
            # If NeXus file, data and results can be shown
            self.load_data_finished()
            self.preprocess_finished()
            self.run_analysis_finished()

        self._do_analysis = True  # turn analysis back on

    @pyqtSlot()
    def crop_signal(self):
        """Crop the signal."""
        self.disable_buttons()
        self.sig_worker_crop.emit()

    def crop_signal_finished(self):
        """Re-enable buttons and print a message."""
        self.enable_buttons()
        self.logger.info(
            "[GUI] Signal was cropped, please re-run the TDO signal extraction"
        )

    @pyqtSlot()
    def preprocess_finished(self):
        """Plot the TDO signal."""
        # Clear plots since source signal was changed
        self.wgraphs.tdo_field.clearPlots()
        self.wgraphs.tdo_inverse_field.clearPlots()
        self.wgraphs.fft.clearPlots()

        self.align_field_finished()
        self.plot_tdo()
        self.enable_buttons()

    def apply_time_offset(self):
        """Apply an offset between the pickup and the signal time bases."""
        if not self.check_data_loaded():
            return
        self.disable_buttons()
        self.sig_worker_offset.emit()

    @pyqtSlot()
    def time_offset_finished(self):
        """Re-plot signals after applying the offset."""
        self.align_field_finished()
        self.plot_tdo()
        self.run_analysis()

    @pyqtSlot()
    def run_analysis(self):
        """Fit, detrend, oversample in 1/B and compute the FFT."""
        if not self.check_tdo_extracted():
            self.logger.warning("[GUI] TDO signal was not extracted")
            return
        
        super().run_analysis()

    @pyqtSlot()
    def run_analysis_finished(self):
        """Plot the detrended signals and the FFT after analysis."""
        self.plot_tdo_detrended()
        self.plot_fft()
        self.enable_buttons()

    @pyqtSlot()
    def save_tdo_csv(self):
        """Save extracted TDO signal as CSV."""
        if not self.check_data_loaded():
            return

        self.disable_buttons()

        default_fname = self.worker.proc.get_csv_filename(suffix="-tdo")
        fname, _ = QtWidgets.QFileDialog.getSaveFileName(
            self,
            "Save TDO signals as...",
            default_fname,
            "Text files (*.txt, *.csv, *.tsv, *.out)",
        )

        if fname:
            self.sig_worker_tdocsv.emit(fname)
        else:
            self.logger.error("[GUI] Invalid output file name for CSV file")
            self.save_tdo_csv_finished()

    @pyqtSlot()
    def save_tdo_csv_finished(self):
        """Re-enable buttons."""
        self.enable_buttons()

    @pyqtSlot()
    def save_results_csv(self):
        """Save final results as CSV."""
        if not self.check_data_loaded():
            return

        self.disable_buttons()

        default_fname = self.worker.proc.get_csv_filename(suffix="-results")
        fname, _ = QtWidgets.QFileDialog.getSaveFileName(
            self,
            "Save TDO signals as...",
            default_fname,
            "Text files (*.txt, *.csv, *.tsv)",
        )

        if fname:
            self.sig_worker_rescsv.emit(fname)
        else:
            self.logger.error("[GUI] Invalid output file name for CSV file")
            self.save_tdo_csv_finished()

    @pyqtSlot()
    def save_results_csv_finished(self):
        """Re-enable buttons."""
        self.enable_buttons()

    def load_csv_file(self, file_path: str):
        """Load a CSV file."""
        if not self.check_config_loaded():
            self.logger.error(
                "[GUI] Can't load a CSV file without loading a configuration file first"
            )
            return

        self.disable_buttons()
        self.sig_worker_loadcsv.emit(file_path)

    @pyqtSlot()
    def load_csv_file_finished(self):
        """Trigger action as if the TDO signal was extracted (but it was loaded)."""
        self.preprocess_finished()
        self.run_analysis_finished()

    @pyqtSlot()
    def batch_process(self):
        """Required for compatibility with the base app, but it is not implemented."""
        self.logger.warning("[GUI] Not implemented")

    @pyqtSlot()
    def handle_roi1_changed(self):
        """
        Define what to do when the ROI in the TDO panel is changed.

        The "poly_window" setting is updated in the tree, a new analysis is requested.
        """
        self._handle_roi_changed(self.wgraphs.roi, "poly_window")

        if self.check_tdo_extracted():
            self.run_analysis()

    @pyqtSlot()
    def handle_roi2_changed(self):
        """
        Define what to do when the ROI in the TDO panel is changed.

        The "fft_window" setting is updated in the tree, a new analysis is requested.
        """
        self._handle_roi_changed(self.wgraphs.roi2, "fft_window")

        if self.check_tdo_extracted():
            self.run_analysis()

    def update_roi_from_poly(self, new_region: Sequence[float | int]):
        """Update the ROI in the graph from "Fit: field window" in the parameter tree."""
        self._update_roi(self.wgraphs.roi, new_region)

    def update_roi_from_fft(self, new_region: Sequence[float | int]):
        """Update the ROI in the graph from "FFT: field window" in the parameter tree."""
        self._update_roi(self.wgraphs.roi2, new_region)

    def plot_tdo(self):
        """Plot TDO signal versus field and time."""
        if not self.check_field_aligned():
            self.worker.align_field()

        ## TDO signal versus field
        self.wgraphs.sig_field.clearPlots()

        # Decreasing magnetic field
        self.wgraphs.sig_field.plot(
            self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
            self.worker.proc.get_data_processed(self.worker.proc._tdo_name)[
                self.ind_bdown
            ],
            pen=self.wgraphs.pen_bdown,
            name="B down",
        )
        # Increasing magnetic field
        self.wgraphs.sig_field.plot(
            self.worker.proc.get_data_processed("magfield")[self.ind_bup],
            self.worker.proc.get_data_processed(self.worker.proc._tdo_name)[
                self.ind_bup
            ],
            pen=self.wgraphs.pen_bup,
            name="B up",
        )

        ## TDO signal versus time
        self.wgraphs.sig_time.clearPlots()

        # Decreasing magnetic field
        self.wgraphs.sig_time.plot(
            self.worker.proc.get_data_processed("time_exp")[self.ind_bdown],
            self.worker.proc.get_data_processed(self.worker.proc._tdo_name)[
                self.ind_bdown
            ],
            pen=self.wgraphs.pen_bdown,
            name="B down",
        )
        # Increasing magnetic field
        self.wgraphs.sig_time.plot(
            self.worker.proc.get_data_processed("time_exp")[self.ind_bup],
            self.worker.proc.get_data_processed(self.worker.proc._tdo_name)[
                self.ind_bup
            ],
            pen=self.wgraphs.pen_bup,
            name="B up",
        )

    @pyqtSlot()
    def plot_tdo_detrended(self):
        """Plot TDO signal with background removed."""
        if not self.check_tdo_detrended():
            return
        # Get parameters
        offset = self.worker.proc.cfg.settings.offset
        lower_b, upper_b = self.worker.proc.cfg.settings.fft_window

        # Sync fit and FFT windows
        if lower_b == -1:
            lower_b = self.worker.proc.cfg.settings.poly_window[0]
        if upper_b == -1:
            upper_b = self.worker.proc.cfg.settings.poly_window[1]

        ## TDO detrended versus field
        self.wgraphs.tdo_field.clearPlots()
        self.wgraphs.sig_field.clearPlots()
        self.plot_tdo()  # to make sure data shown is synced

        # Decreasing magnetic field
        if self.worker.proc.get_data_processed(
            self.worker.proc._tdo_det_dec_name, checkonly=True
        ):
            self.wgraphs.tdo_field.plot(
                self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
                self.worker.proc.get_data_processed(self.worker.proc._tdo_det_dec_name)
                - offset / 2,
                pen=self.wgraphs.pen_bdown,
                name="B down",
            )
        # Increasing magnetic field
        if self.worker.proc.get_data_processed(
            self.worker.proc._tdo_det_inc_name, checkonly=True
        ):
            self.wgraphs.tdo_field.plot(
                self.worker.proc.get_data_processed("magfield")[self.ind_bup],
                self.worker.proc.get_data_processed(self.worker.proc._tdo_det_inc_name)
                + offset / 2,
                pen=self.wgraphs.pen_bup,
                name="B up",
            )

        # Fit decreasing magnetic field
        if self.worker.proc.get_data_processed(
            self.worker.proc._tdo_name + "_fit_dec", checkonly=True
        ):
            self.wgraphs.sig_field.plot(
                self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
                self.worker.proc.get_data_processed(
                    self.worker.proc._tdo_name + "_fit_dec"
                ),
                pen=self.wgraphs.pen_fitdown,
                name="Fit B down",
            )
        # Fit increasing magnetic field
        if self.worker.proc.get_data_processed(
            self.worker.proc._tdo_name + "_fit_inc", checkonly=True
        ):
            self.wgraphs.sig_field.plot(
                self.worker.proc.get_data_processed("magfield")[self.ind_bup],
                self.worker.proc.get_data_processed(
                    self.worker.proc._tdo_name + "_fit_inc"
                ),
                pen=self.wgraphs.pen_fitbup,
                name="Fit B up",
            )

        ## TDO detrended versus 1/B
        self.wgraphs.tdo_inverse_field.clearPlots()

        # Plot the whole non-interpolated signal
        # Increasing magnetic field
        if self.worker.proc.get_data_processed(
            self.worker.proc._tdo_det_inc_name, checkonly=True
        ):
            self.wgraphs.tdo_inverse_field.plot(
                1 / self.worker.proc.get_data_processed("magfield")[self.ind_bup],
                self.worker.proc.get_data_processed(self.worker.proc._tdo_det_inc_name)
                + offset / 2,
                pen=self.wgraphs.pen_tdo,
            )
        # Decreasing magnetic field
        if self.worker.proc.get_data_processed(
            self.worker.proc._tdo_det_dec_name, checkonly=True
        ):
            self.wgraphs.tdo_inverse_field.plot(
                1 / self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
                self.worker.proc.get_data_processed(self.worker.proc._tdo_det_dec_name)
                - offset / 2,
                pen=self.wgraphs.pen_tdo,
            )

        # Now, the oversampled signal
        # Decreasing magnetic field
        if self.worker.proc.get_data_processed(
            self.worker.proc._tdo_inv_dec_name, checkonly=True
        ):
            p0 = self.wgraphs.tdo_inverse_field.plot(
                self.worker.proc.get_data_processed("magfield_inverse_dec"),
                self.worker.proc.get_data_processed(self.worker.proc._tdo_inv_dec_name)
                - offset / 2,
                pen=self.wgraphs.pen_bdown,
                name="B down",
            )
        # Increasing magnetic field
        if self.worker.proc.get_data_processed(
            self.worker.proc._tdo_inv_inc_name, checkonly=True
        ):
            p1 = self.wgraphs.tdo_inverse_field.plot(
                self.worker.proc.get_data_processed("magfield_inverse_inc"),
                self.worker.proc.get_data_processed(self.worker.proc._tdo_inv_inc_name)
                + offset / 2,
                pen=self.wgraphs.pen_bup,
                name="B up",
            )
        # Adjust limits
        self.wgraphs.tdo_inverse_field.getViewBox().autoRange(
            padding=0.1, items=[p0, p1]
        )

    def plot_fft(self):
        """Plot FFT in 1/B."""
        self.wgraphs.fft.clearPlots()

        # Decreasing magnetic field
        if self.worker.proc.get_data_processed("fft_dec", checkonly=True):
            self.wgraphs.fft.plot(
                self.worker.proc.get_data_processed("bfreq_dec"),
                self.worker.proc.get_data_processed("fft_dec"),
                pen=self.wgraphs.pen_bdown,
                name="B down",
            )
        # Increasing magnetic field
        if self.worker.proc.get_data_processed("fft_inc", checkonly=True):
            self.wgraphs.fft.plot(
                self.worker.proc.get_data_processed("bfreq_inc"),
                self.worker.proc.get_data_processed("fft_inc"),
                pen=self.wgraphs.pen_bup,
                name="B up",
            )

    @pyqtSlot()
    def syncroi_changed(self):
        """Update fit and FFT windows sync. status."""
        self.wgraphs._sync_roi = self.wconfiguration.syncroi_parameter.value()

    @pyqtSlot(bool, str)
    def select_file_in_browser(self, is_toml: bool, filepath: str):
        """
        Select a file and set it as a configuration file or a new experiment ID.

        Callback for when the user double-click on a file in the "Files" tab.
        """
        if filepath.endswith(".csv"):
            self.load_csv_file(filepath)
        else:
            super().select_file_in_browser(is_toml, filepath)

    def check_tdo_extracted(self) -> bool:
        """Check if the TDO signal was extracted."""
        if not self.check_data_loaded():
            return False
        return self.worker.proc._check_barycenters_computed()

    def check_tdo_detrended(self) -> bool:
        """Check if the detrending was performed."""
        if not self.check_tdo_extracted():
            return False
        return self.worker.proc._check_tdo_detrended()

    def dropEvent(self, a0: QtGui.QDropEvent | None = None) -> None:
        """Load a file when it is dropped in the main window."""
        if a0 is None:
            return
        mime_data = a0.mimeData()
        if mime_data is None:
            return
        for url in mime_data.urls():
            file_path = url.toLocalFile()
            if file_path.endswith(OUT_FORMAT):
                self.load_csv_file(file_path)
                return

        super().dropEvent(a0)
