from __future__ import annotations

from silx.gui import qt

from ewoks3dxrd.models import GridIndexParameters

from ..common.tolerance_selector import ToleranceSelector
from ..common.tuple_input_widget import TupleInputWidget
from .constants import Symmetry


class GridIndexParameterGroupBox(qt.QGroupBox):
    sigGridSettingsChanged = qt.Signal()

    def __init__(self, parent: qt.QWidget | None = None, **kwargs) -> None:
        super().__init__("Grid Index Settings", parent=parent, **kwargs)
        layout = qt.QFormLayout(self)

        self._nbPeaksSpinBox = qt.QSpinBox()
        self._nbPeaksSpinBox.setRange(1, 1000)
        self._nbPeaksSpinBox.setValue(70)
        self._nbPeaksSpinBox.setToolTip("""
            Minimum number of peaks needed to index a grain.
            Set this to ensure only grains with a sensible number of peaks are kept. A reliable starting value is:
            180° Rotation: 65-80% of the total expected peaks for Ring 1.
            360° Rotation: 130-160% of the total expected peaks for Ring 1.
            If this number is too low, you will index grains that might be fake (some peaks will always "accidentally" work with a grain).
            If this number is too high, you will incorrectly discard real grains.
            In general, after indexing and refining, all your grains should have a similar number of peaks.
        """)

        self._dstol = qt.QDoubleSpinBox()
        self._dstol.setRange(0.0001, 10.0)
        self._dstol.setSingleStep(0.001)
        self._dstol.setDecimals(5)
        self._dstol.setToolTip("""
            Distance in reciprocal space to consider a peak as being on/off a HKL ring.
            Typical values: 1e-4 to 1e-3
            This value can be quite small, because we correct for the grain centre-of-mass position in the grid indexing before assigning to rings.
        """)
        self._dstol.setValue(0.004)

        self._ring1 = TupleInputWidget(placeHolderText="e.g. 1, 0", Type=int)
        self._ring1.setToolTip(
            "Ring 1 indices as comma-separated integers. Must be a subset of the ring indices picked in indexing filtering."
        )

        self._ring2 = TupleInputWidget(placeHolderText="e.g. 0", Type=int)
        self._ring2.setToolTip(
            "Ring 2 indices as comma-separated integers. Must be a subset of the ring indices picked in indexing filtering."
        )

        self._tolangle = qt.QDoubleSpinBox()
        self._tolangle.setRange(0.0001, 100.0)
        self._tolangle.setSingleStep(0.01)
        self._tolangle.setToolTip("""
            Tolerance angle in degrees.
            During grid indexing, we can find the same grain many times at neighbouring grid points (indexing in parallel).
            We handle this via a grain de-duplication step, that checks to see if we already found each grain before.
            We de-duplicate based on centre-of-mass distance and (symmetry-aware) misorientation angle
            tolangle and toldist control the de-duplication tolerances.
            Typical values: 0.1 - 2.0, depending on how textured your sample is.
        """)
        self._tolangle.setValue(0.50)
        degSymbol = "\u00b0"
        self._tolangle.setSuffix(f" {degSymbol}")

        self._toldist = qt.QDoubleSpinBox()
        self._toldist.setRange(0.0, 1000.0)
        self._toldist.setToolTip("""
            Tolerance Distance, usually in range of 0.5 to 2 times detector pixel width.
            See Tolerance Angle for more information
        """)
        self._toldist.setValue(100.0)
        self._toldist.setSuffix(" µm")

        self._toleranceGroup = ToleranceSelector()

        self._symmetry = qt.QComboBox()
        self._symmetry.addItems([e.value for e in Symmetry])
        self._symmetry.setToolTip("Optional. Symmetry group.")

        layout.addRow("Number of Peaks", self._nbPeaksSpinBox)
        layout.addRow("Reciprocal Tolerance", self._dstol)
        layout.addRow("Ring 1", self._ring1)
        layout.addRow("Ring 2", self._ring2)
        layout.addRow("Tolerance Angle", self._tolangle)
        layout.addRow("Tolerance Distance", self._toldist)
        layout.addRow("Crystal Symmetry", self._symmetry)
        layout.addRow("Tolerance Tuning", self._toleranceGroup)

        self._ring1.sigTupleChanged.connect(self.sigGridSettingsChanged)
        self._ring2.sigTupleChanged.connect(self.sigGridSettingsChanged)

    def getGridIndexParameters(self) -> GridIndexParameters:
        try:
            self._validate()
            return GridIndexParameters(
                NPKS=self._nbPeaksSpinBox.value(),
                DSTOL=self._dstol.value(),
                RING1=self._ring1.getValue(),
                RING2=self._ring2.getValue(),
                tolangle=self._tolangle.value(),
                toldist=self._toldist.value(),
                TOLSEQ=self._toleranceGroup.getValue(),
                SYMMETRY=self._symmetry.currentText(),
            )
        except Exception as e:
            raise ValueError(e)

    def setGridIndexParameters(self, params: GridIndexParameters):
        self._nbPeaksSpinBox.setValue(params.NPKS)
        self._dstol.setValue(params.DSTOL)
        self._ring1.setValue(params.RING1)
        self._ring2.setValue(params.RING2)
        self._tolangle.setValue(params.tolangle)
        self._toleranceGroup.setValue(params.TOLSEQ)
        self._symmetry.setCurrentText(params.SYMMETRY)

    def _validate(self):
        if not self._ring1.getValue():
            raise ValueError("Ring1 must not be empty")
        if not self._ring2.getValue():
            raise ValueError("Ring2 must not be empty")
