"""Application for digital lock-in."""

import importlib.metadata

from PyQt6 import QtWidgets
from PyQt6.QtCore import pyqtSignal as Signal
from PyQt6.QtCore import pyqtSlot as Slot

from pymagnetos.core.gui import BaseMainWindow
from pymagnetos.core.gui.widgets import (
    BaseBatchProcessingWidget,
    FileBrowserWidget,
    PopupProgressBar,
)

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

LOG_LEVEL = "INFO"
PROGRAM_NAME = "pymagnetos"
VERSION = importlib.metadata.version(PROGRAM_NAME)


class MainWindow(BaseMainWindow):
    """A graphical user interface to perform digital lock-in."""

    sig_worker_auto_phase = Signal()
    sig_worker_export_csv = Signal(str, int, bool)

    wbuttons: ButtonsWidget
    wconfiguration: ConfigurationWidget
    wfiles: FileBrowserWidget
    wgraphs: GraphsWidget
    worker: Worker

    def __init__(self) -> None:
        # 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("LINX")

        # Internal flags
        self._do_load_data = False

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

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

        # Connect
        self.wbuttons.sig_find_f0.connect(self.preprocess)
        self.wbuttons.sig_auto_phase.connect(self.auto_phase)
        self.wbuttons.sig_export_csv.connect(self.export_csv)

    def init_log(self) -> None:
        """Initialize logging."""
        self._init_log(PROGRAM_NAME, log_level=LOG_LEVEL)

    def load_file(self) -> None:
        """
        Load a configuration or NeXus file.

        Populate the signal and reference names settings with the ones specified in the
        "measurements" section of the configuration file.
        """
        # Disable loading data before ref_name and signal_names are properly set
        self._do_load_data = False
        self._ignore_tree_change = True
        super().load_file()

        # Populate the available ref and signal names in the tree, without updating the
        # worker
        available_names = self.worker.proc._signal_names
        ref_name = self.worker.proc.ref_name
        signal_name = self.worker.proc.signal_name
        self.wconfiguration.settings_parameters.child("ref_name").setLimits(
            available_names
        )
        self.wconfiguration.settings_parameters.child("signal_name").setLimits(
            available_names
        )
        self.wconfiguration.settings_parameters["ref_name"] = ref_name
        self.wconfiguration.settings_parameters["signal_name"] = signal_name
        self._ignore_tree_change = False
        self._do_load_data = True

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

        # Now, data can be loaded
        if (
            self.wconfiguration.files_parameters["autoload"]
            and not self.worker.is_data_loaded
        ):
            self.load_data()

    def connect_worker(self) -> None:
        """Connect signals to worker slots."""
        super().connect_worker()

        self.sig_worker_auto_phase.connect(self.worker.auto_phase)
        self.sig_worker_export_csv.connect(self.worker.export_csv)
        self.worker.sig_export_csv_finished.connect(self.export_csv_finished)

    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 "ref_name":
                if isinstance(value, str):
                    self.update_ref_name(value)
            case "signal_name":
                if isinstance(value, str):
                    self.update_signal_name(value)
            case "time_offset":
                if isinstance(value, (float, int)):
                    self.update_time_offset(value)
            case "f0":
                if isinstance(value, (float, int)):
                    self.update_f0(value)
            case "phase_offset":
                if isinstance(value, (float, int)):
                    self.update_phase_offset(value)
            case "auto_phase_method":
                self.auto_phase()
            case _:
                self.run_analysis()

    @Slot()
    def load_data_finished(self) -> None:
        """Plot the raw data."""
        self.plot_signal()
        self.run_analysis_finished()  # the worker ran the analysis after loading data
        super().load_data_finished()

    @Slot()
    def compute_field_finished(self) -> None:
        """Plot the new field, run demodulation."""
        super().compute_field_finished()
        self.run_analysis()

    @Slot()
    def run_analysis_finished(self) -> None:
        """Plot demodulation results."""
        # Update f0 and phase offset in case it changed
        self._do_analysis = False
        self.wconfiguration.settings_parameters["f0"] = self.worker.proc.f0
        self.wconfiguration.settings_parameters["phase_offset"] = (
            self.worker.proc.phase_offset
        )
        self._do_analysis = True
        self.plot_resistance()
        self.enable_buttons()

    @Slot()
    def auto_phase(self) -> None:
        """Find the phase offset that maximises the in-phase signal."""
        if not self.check_config_loaded():
            return
        self.disable_buttons()
        self.worker.proc.cfg.settings.auto_phase_method = (
            self.wconfiguration.settings_parameters["auto_phase_method"]
        )
        self.sig_worker_auto_phase.emit()

    @Slot()
    def export_csv(self):
        """Save results as CSV."""
        if not self.check_data_loaded():
            return

        self.disable_buttons()

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

        # Get saving parameters
        downsample_factor = self.wbuttons.spinbox_downsample.value()
        averaging = self.wbuttons.checkbox_downsample.isChecked()

        if fname:
            self.sig_worker_export_csv.emit(fname, downsample_factor, averaging)
        else:
            self.logger.error("[GUI] Invalid output file name for CSV file")
            self.export_csv_finished()

    @Slot()
    def export_csv_finished(self) -> None:
        """Enable buttons."""
        self.enable_buttons()

    @Slot()
    def batch_process(self) -> None:
        """Run batch-processing."""
        super().batch_process()

        # Get settings
        to_csv = self.wbatch.checkbox_save_as_csv.isChecked()
        downsample_factor = self.wbuttons.spinbox_downsample.value()
        averaging = self.wbuttons.checkbox_downsample.isChecked()

        # Set up progress bar
        self.pbar_batch = PopupProgressBar(
            min=0, max=len(self.batch_expids), title="Batch processing"
        )
        self.worker.sig_batch_progress.connect(self.batch_process_step_finished)
        self.pbar_batch.start_progress()

        # Run the batch processing
        self.disable_buttons()
        self.sig_worker_batch.emit(
            (
                self.batch_expids,
                to_csv,
                {"downsample": downsample_factor, "average": averaging},
            )
        )

    @Slot(int)
    def batch_process_step_finished(self, idx: int):
        """Track progress of the batch processing."""
        self.pbar_batch.update_progress(idx)
        super().batch_process_step_finished(idx)

    @Slot()
    def batch_process_finished(self):
        """
        Cleanup after batch-processing.

        Callback for when the worker has finished the batch-processing.
        """
        self.pbar_batch.close()
        super().batch_process_finished()

    def plot_signal(self) -> None:
        """Plot raw signal."""
        if not self.check_data_loaded():
            self.logger.warning("[GUI] Data not loaded")

        # Plot raw signal versus time
        self.wgraphs.raw_signal.clearPlots()
        self.wgraphs.raw_signal.plot(
            self.worker.proc.get_data_raw("time_exp"),
            self.worker.proc.get_data_raw(self.worker.proc.signal_name),
            pen=self.wgraphs.pen_raw_signal,
        )

        # Plot reference signal versus time
        self.wgraphs.reference.clearPlots()
        self.wgraphs.reference.plot(
            self.worker.proc.get_data_raw("time_exp"),
            self.worker.proc.get_data_raw(self.worker.proc.ref_name),
            pen=self.wgraphs.pen_reference,
        )

    def plot_resistance(self) -> None:
        """Plot the resistance after demodulation."""
        if not self.check_resistance_computed():
            self.logger.warning("[GUI] Resistance not computed, can't plot resistance")
            return

        self.wgraphs.res_time.clearPlots()
        # Plot resistance in phase versus time
        self.wgraphs.res_time.plot(
            self.worker.proc.get_data_processed("time_exp"),
            self.worker.proc.get_data_serie("resistance_in_phase"),
            pen=self.wgraphs.pen_in_phase_bup,
            name="in phase",
        )
        # Plot resistance out phase versus time
        self.wgraphs.res_time.plot(
            self.worker.proc.get_data_processed("time_exp"),
            self.worker.proc.get_data_serie("resistance_out_phase"),
            pen=self.wgraphs.pen_out_phase_bup,
            name="out phase",
        )

        self.wgraphs.res_field.clearPlots()
        if not self.check_field_aligned():
            self.logger.warning(
                "[GUI] Magnetic field not aligned, can't plot resistance versus field"
            )
            return
        # Plot resistance in phase versus field, field down
        self.wgraphs.res_field.plot(
            self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
            self.worker.proc.get_data_serie("resistance_in_phase")[self.ind_bdown],
            pen=self.wgraphs.pen_in_phase_bdown,
            name="in phase, B down",
        )
        # Plot resistance in phase versus field, field up
        self.wgraphs.res_field.plot(
            self.worker.proc.get_data_processed("magfield")[self.ind_bup],
            self.worker.proc.get_data_serie("resistance_in_phase")[self.ind_bup],
            pen=self.wgraphs.pen_in_phase_bup,
            name="out phase, B up",
        )
        # Plot resistance out phase versus field, field down
        self.wgraphs.res_field.plot(
            self.worker.proc.get_data_processed("magfield")[self.ind_bdown],
            self.worker.proc.get_data_serie("resistance_out_phase")[self.ind_bdown],
            pen=self.wgraphs.pen_out_phase_bdown,
            name="out phase, B down",
        )
        # Plot resistance out phase versus field, field up
        self.wgraphs.res_field.plot(
            self.worker.proc.get_data_processed("magfield")[self.ind_bup],
            self.worker.proc.get_data_serie("resistance_out_phase")[self.ind_bup],
            pen=self.wgraphs.pen_out_phase_bup,
            name="out phase, B up",
        )

    @Slot(str)
    def update_ref_name(self, ref_name: str) -> None:
        """Update reference name in the worker."""
        if not self.check_config_loaded():
            return
        self.worker.proc.ref_name = ref_name
        self.load_data()

    @Slot(str)
    def update_signal_name(self, signal_name: str) -> None:
        """Update signal name in the worker."""
        if not self.check_config_loaded():
            return
        self.worker.proc.signal_name = signal_name
        self.load_data()

    @Slot(float)
    def update_f0(self, f0: float) -> None:
        """Update f0 in the worker."""
        if not self.check_config_loaded():
            return
        self.worker.proc.f0 = f0
        self.run_analysis()

    @Slot(float)
    def update_phase_offset(self, phase_offset: float) -> None:
        """Update phase offset in the worker."""
        if not self.check_config_loaded():
            return
        self.worker.proc.phase_offset = phase_offset
        self.run_analysis()

    @Slot(float)
    def update_time_offset(self, time_offset: float) -> None:
        """Update time offset in the worker and re-compute magnetic field."""
        if not self.check_config_loaded():
            return
        self.worker.proc.cfg.settings.time_offset = time_offset
        self.compute_field()

    def check_demodulated(self) -> bool:
        """Check if the demodulation was performed."""
        if self.check_config_loaded():
            return self.worker.proc._check_demodulated()
        return False

    def check_resistance_computed(self) -> bool:
        """Check if the resistance was computed."""
        if self.check_config_loaded():
            return self.worker.proc._check_resistance_computed()
        return False
