"""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

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()

    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
