"""
Dialog panel to explore the P(r) inversion results for a range
of D_max value. User picks a number of points and a range of
distances, then can toggle between inversion outputs and see
their distribution as a function of D_max.
"""

# global
import logging

import numpy as np
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import QSize
from PySide6.QtGui import QIcon

import sas.qtgui.Utilities.GuiUtils as GuiUtils
from sas.qtgui.Plotting import PlotUtilities
from sas.qtgui.Plotting.Plotter import PlotterWidget

# sas-global
from sas.qtgui.Plotting.PlotterData import Data1D
from sas.qtgui.Utilities.GuiUtils import enum

# local
from .UI.DMaxExplorer import Ui_DmaxExplorer

logger = logging.getLogger(__name__)


W = enum(
    "NPTS",
    "DMIN",
    "DMAX",
    "VARIABLE",
)


class DmaxWindow(QtWidgets.QDialog, Ui_DmaxExplorer):
    """
    The controller which is responsible for managing signal slot connections
    for the GUI and providing an interface to the data model.
    """

    name = "Dmax Explorer"  # For displaying in the combo box

    def __init__(self, pr_state: "Invertor", nfunc: int, parent=None):
        super().__init__()
        self.setupUi(self)
        self.parent = parent

        self.setWindowTitle("Dmax Explorer")

        icon = QIcon()
        icon.addFile(":/res/ball.ico", QSize(), QIcon.Normal, QIcon.Off)
        self.setWindowIcon(icon)

        self.pr_state = pr_state
        self.nfunc: int = nfunc
        self.communicator = GuiUtils.communicator

        self.plot = PlotterWidget(self, self)
        self.hasPlot: bool = False
        self.plot.showLegend = False
        self.verticalLayout.insertWidget(0, self.plot)

        # Let's choose the Standard Item Model.
        self.model = QtGui.QStandardItemModel(self)
        self.mapper = QtWidgets.QDataWidgetMapper(self)

        # Add validators on line edits
        self.setupValidators()

        # Connect buttons to slots.
        # Needs to be done early so default values propagate properly.
        self.setupSlots()

        # Set up the model.
        self.setupModel()

        # Set up the mapper
        self.setupMapper()

    def setupValidators(self) -> None:
        """Add validators on relevant line edits."""
        self.Npts.setValidator(QtGui.QIntValidator())
        self.minDist.setValidator(GuiUtils.DoubleValidator())
        self.maxDist.setValidator(GuiUtils.DoubleValidator())

    def setupSlots(self) -> None:
        """Connect UI signals to their corresponding slots."""
        self.closeButton.clicked.connect(self.close)
        self.model.itemChanged.connect(self.modelChanged)
        self.dependentVariable.currentIndexChanged.connect(lambda: self.modelChanged(None))

    def setupModel(self) -> None:
        """Populate the model with initial values derived from the current P(r) state."""
        self.model.blockSignals(True)
        self.model.setItem(W.NPTS, QtGui.QStandardItem(str(self.nfunc)))
        self.model.blockSignals(False)
        self.model.blockSignals(True)
        self.model.setItem(W.DMIN, QtGui.QStandardItem(f"{0.9 * self.pr_state.dmax:.1f}"))
        self.model.blockSignals(False)
        self.model.blockSignals(True)
        self.model.setItem(W.DMAX, QtGui.QStandardItem(f"{1.1 * self.pr_state.dmax:.1f}"))
        self.model.blockSignals(False)
        self.model.blockSignals(True)
        self.model.setItem(W.VARIABLE, QtGui.QStandardItem("χ²/dof"))
        self.model.blockSignals(False)

    def setupMapper(self) -> None:
        """Configure the data widget mapper to bind model items to UI widgets."""
        self.mapper.setOrientation(QtCore.Qt.Vertical)
        self.mapper.setModel(self.model)

        self.mapper.addMapping(self.Npts, W.NPTS)
        self.mapper.addMapping(self.minDist, W.DMIN)
        self.mapper.addMapping(self.maxDist, W.DMAX)
        self.mapper.addMapping(self.dependentVariable, W.VARIABLE)

        self.mapper.toFirst()

    def modelChanged(self, item: QtGui.QStandardItem | None) -> None:
        """Recompute and replot the D_max exploration results when the model changes."""
        if not self.mapper:
            return

        iq0: list[float] = []
        rg: list[float] = []
        pos: list[float] = []
        pos_err: list[float] = []
        osc: list[float] = []
        bck: list[float] = []
        chi2: list[float] = []
        # Introducing this to make sure size of x and y for plotting is the same.
        plotable_xs: list[float] = []

        try:
            dmin = float(self.model.item(W.DMIN).text())
            dmax = float(self.model.item(W.DMAX).text())
            npts = int(self.model.item(W.NPTS).text())
            xs = np.linspace(dmin, dmax, npts)
        except ValueError as e:
            msg = f"An input value is not correctly formatted. Please check {e}"
            logger.error(msg)

        original = self.pr_state.dmax

        for x in xs:
            self.pr_state.dmax = x
            try:
                out, cov = self.pr_state.invert(self.pr_state.nfunc)

                iq0.append(self.pr_state.iq0(out))
                rg.append(self.pr_state.rg(out))
                pos.append(self.pr_state.get_positive(out))
                pos_err.append(self.pr_state.get_pos_err(out, cov))
                osc.append(self.pr_state.oscillations(out))
                bck.append(self.pr_state.background)
                chi2.append(self.pr_state.chi2)
                plotable_xs.append(x)
            except Exception as ex:
                # This inversion failed, skip this D_max value
                msg = (
                    f"ExploreDialog: inversion failed for D_max={x}\n"
                    f"{ex}\n"
                    f"Please adapt Inversion parameters"
                )
                logger.error(msg)

        # Return the invertor to its original state
        self.pr_state.d_max = original
        try:
            self.pr_state.invert(self.nfunc)
        except RuntimeError as ex:
            msg = "ExploreDialog: inversion failed "
            msg += "for D_max=%s\n%s" % (str(x), ex)
            logger.error(msg)

        plotter = self.dependentVariable.currentText()
        x_label = "D_{max}"
        x_unit = "A"
        if plotter == "χ²/dof":
            ys = chi2
            y_label = "\\chi^2/dof"
            y_unit = "a.u."
        elif plotter == "I(Q=0)":
            ys = iq0
            y_label = "I(q=0)"
            y_unit = "cm^{-1}"
        elif plotter == "Rg":
            ys = rg
            y_label = "R_g"
            y_unit = "\\AA"
        elif plotter == "Oscillation parameter":
            ys = osc
            y_label = "Osc"
            y_unit = "a.u."
        elif plotter == "Background":
            ys = bck
            y_label = "Bckg"
            y_unit = "cm^{-1}"
        elif plotter == "Positive Fraction":
            ys = pos
            y_label = "P^+"
            y_unit = "a.u."
        else:
            ys = pos_err
            y_label = "P^{+}_{1\\sigma}"
            y_unit = "a.u."

        data = Data1D(plotable_xs, ys)
        # Store the line symbol so re-renders (e.g. 'Reset original view') keep the line style
        data.symbol = list(PlotUtilities.SHAPES.keys()).index('Line')
        if self.hasPlot:
            self.plot.removePlot(data.name)
        self.hasPlot = True
        data.title = plotter
        data._xaxis = x_label
        data._xunit = x_unit
        data._yaxis = y_label
        data._yunit = y_unit
        self.plot.plot(data=data, marker="-")

    def closeEvent(self, event: QtCore.QEvent) -> None:
        """Override close event to clear the parent's reference to this window."""
        self.parent.dmaxWindow = None
        event.accept()
