from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
    QComboBox,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QSlider,
    QWidget,
)
from superqt import QLabeledDoubleSlider

from ...layers.image._image_constants import (
    ImageRendering,
    Interpolation,
    Interpolation3D,
    VolumeDepiction,
)
from ...utils.action_manager import action_manager
from ...utils.translations import trans
from .qt_image_controls_base import QtBaseImageControls
from .qt_layer_controls_base import LayerListGridLayout


class QtImageControls(QtBaseImageControls):
    """Qt view and controls for the napari Image layer.

    Parameters
    ----------
    layer : napari.layers.Image
        An instance of a napari Image layer.

    Attributes
    ----------
    attenuationSlider : qtpy.QtWidgets.QSlider
        Slider controlling attenuation rate for `attenuated_mip` mode.
    attenuationLabel : qtpy.QtWidgets.QLabel
        Label for the attenuation slider widget.
    grid_layout : qtpy.QtWidgets.QGridLayout
        Layout of Qt widget controls for the layer.
    interpComboBox : qtpy.QtWidgets.QComboBox
        Dropdown menu to select the interpolation mode for image display.
    interpLabel : qtpy.QtWidgets.QLabel
        Label for the interpolation dropdown menu.
    isoThresholdSlider : qtpy.QtWidgets.QSlider
        Slider controlling the isosurface threshold value for rendering.
    isoThresholdLabel : qtpy.QtWidgets.QLabel
        Label for the isosurface threshold slider widget.
    layer : napari.layers.Image
        An instance of a napari Image layer.
    renderComboBox : qtpy.QtWidgets.QComboBox
        Dropdown menu to select the rendering mode for image display.
    renderLabel : qtpy.QtWidgets.QLabel
        Label for the rendering mode dropdown menu.
    """

    def __init__(self, layer):
        super().__init__(layer)

        self.layer.events.interpolation.connect(self._on_interpolation_change)
        self.layer.events.rendering.connect(self._on_rendering_change)
        self.layer.events.iso_threshold.connect(self._on_iso_threshold_change)
        self.layer.events.attenuation.connect(self._on_attenuation_change)
        self.layer.events._ndisplay.connect(self._on_ndisplay_change)
        self.layer.events.depiction.connect(self._on_depiction_change)
        self.layer.plane.events.thickness.connect(
            self._on_plane_thickness_change
        )

        self.interpComboBox = QComboBox(self)
        self.interpComboBox.activated[str].connect(self.changeInterpolation)
        self.interpLabel = QLabel(trans._('interpolation:'))

        renderComboBox = QComboBox(self)
        rendering_options = [i.value for i in ImageRendering]
        renderComboBox.addItems(rendering_options)
        index = renderComboBox.findText(
            self.layer.rendering, Qt.MatchFixedString
        )
        renderComboBox.setCurrentIndex(index)
        renderComboBox.activated[str].connect(self.changeRendering)
        self.renderComboBox = renderComboBox
        self.renderLabel = QLabel(trans._('rendering:'))

        self.depictionComboBox = QComboBox(self)
        depiction_options = [d.value for d in VolumeDepiction]
        self.depictionComboBox.addItems(depiction_options)
        index = self.depictionComboBox.findText(
            self.layer.depiction, Qt.MatchFixedString
        )
        self.depictionComboBox.setCurrentIndex(index)
        self.depictionComboBox.activated[str].connect(self.changeDepiction)
        self.depictionLabel = QLabel(trans._('depiction:'))

        self.planeControls = QtPlaneControls()
        self.planeControls.planeThicknessSlider.setValue(
            self.layer.plane.thickness
        )
        self.planeControls.planeThicknessSlider.valueChanged.connect(
            self.changePlaneThickness
        )
        action_manager.bind_button(
            'napari:orient_plane_normal_along_z',
            self.planeControls.planeNormalButtons.zButton,
        )
        action_manager.bind_button(
            'napari:orient_plane_normal_along_y',
            self.planeControls.planeNormalButtons.yButton,
        )
        action_manager.bind_button(
            'napari:orient_plane_normal_along_x',
            self.planeControls.planeNormalButtons.xButton,
        )
        action_manager.bind_button(
            'napari:orient_plane_normal_along_view_direction',
            self.planeControls.planeNormalButtons.obliqueButton,
        )

        sld = QSlider(Qt.Horizontal, parent=self)
        sld.setFocusPolicy(Qt.NoFocus)
        sld.setMinimum(0)
        sld.setMaximum(100)
        sld.setSingleStep(1)
        sld.setValue(int(self.layer.iso_threshold * 100))
        sld.valueChanged.connect(self.changeIsoThreshold)
        self.isoThresholdSlider = sld
        self.isoThresholdLabel = QLabel(trans._('iso threshold:'))

        sld = QSlider(Qt.Horizontal, parent=self)
        sld.setFocusPolicy(Qt.NoFocus)
        sld.setMinimum(0)
        sld.setMaximum(100)
        sld.setSingleStep(1)
        sld.setValue(int(self.layer.attenuation * 200))
        sld.valueChanged.connect(self.changeAttenuation)
        self.attenuationSlider = sld
        self.attenuationLabel = QLabel(trans._('attenuation:'))
        self._on_ndisplay_change()

        colormap_layout = QHBoxLayout()
        if hasattr(self.layer, 'rgb') and self.layer.rgb:
            colormap_layout.addWidget(QLabel("RGB"))
            self.colormapComboBox.setVisible(False)
            self.colorbarLabel.setVisible(False)
        else:
            colormap_layout.addWidget(self.colorbarLabel)
            colormap_layout.addWidget(self.colormapComboBox)
        colormap_layout.addStretch(1)

        # grid_layout created in QtLayerControls
        # addWidget(widget, row, column, [row_span, column_span])
        self.grid_layout.addWidget(QLabel(trans._('opacity:')), 0, 0)
        self.grid_layout.addWidget(self.opacitySlider, 0, 1)
        self.grid_layout.addWidget(QLabel(trans._('contrast limits:')), 1, 0)
        self.grid_layout.addWidget(self.contrastLimitsSlider, 1, 1)
        self.grid_layout.addWidget(QLabel(trans._('auto-contrast:')), 2, 0)
        self.grid_layout.addWidget(self.autoScaleBar, 2, 1)
        self.grid_layout.addWidget(QLabel(trans._('gamma:')), 3, 0)
        self.grid_layout.addWidget(self.gammaSlider, 3, 1)
        self.grid_layout.addWidget(QLabel(trans._('colormap:')), 4, 0)
        self.grid_layout.addLayout(colormap_layout, 4, 1)
        self.grid_layout.addWidget(QLabel(trans._('blending:')), 5, 0)
        self.grid_layout.addWidget(self.blendComboBox, 5, 1)
        self.grid_layout.addWidget(self.interpLabel, 6, 0)
        self.grid_layout.addWidget(self.interpComboBox, 6, 1)
        self.grid_layout.addWidget(self.renderLabel, 7, 0)
        self.grid_layout.addWidget(self.renderComboBox, 7, 1)
        self.grid_layout.addWidget(self.depictionLabel, 8, 0)
        self.grid_layout.addWidget(self.depictionComboBox, 8, 1)
        self.grid_layout.addWidget(self.planeControls, 9, 0, 2, 2)
        self.grid_layout.addWidget(self.isoThresholdLabel, 11, 0)
        self.grid_layout.addWidget(self.isoThresholdSlider, 11, 1)
        self.grid_layout.addWidget(self.attenuationLabel, 12, 0)
        self.grid_layout.addWidget(self.attenuationSlider, 12, 1)
        self.grid_layout.setRowStretch(13, 1)
        self.grid_layout.setColumnStretch(1, 1)
        self.grid_layout.setSpacing(4)

    def changeInterpolation(self, text):
        """Change interpolation mode for image display.

        Parameters
        ----------
        text : str
            Interpolation mode used by vispy. Must be one of our supported
            modes:
            'bessel', 'bicubic', 'bilinear', 'blackman', 'catrom', 'gaussian',
            'hamming', 'hanning', 'hermite', 'kaiser', 'lanczos', 'mitchell',
            'nearest', 'spline16', 'spline36'
        """
        self.layer.interpolation = text

    def changeRendering(self, text):
        """Change rendering mode for image display.

        Parameters
        ----------
        text : str
            Rendering mode used by vispy.
            Selects a preset rendering mode in vispy that determines how
            volume is displayed:
            * translucent: voxel colors are blended along the view ray until
              the result is opaque.
            * mip: maximum intensity projection. Cast a ray and display the
              maximum value that was encountered.
            * additive: voxel colors are added along the view ray until
              the result is saturated.
            * iso: isosurface. Cast a ray until a certain threshold is
              encountered. At that location, lighning calculations are
              performed to give the visual appearance of a surface.
            * attenuated_mip: attenuated maximum intensity projection. Cast a
              ray and attenuate values based on integral of encountered values,
              display the maximum value that was encountered after attenuation.
              This will make nearer objects appear more prominent.
        """
        self.layer.rendering = text
        self._toggle_rendering_parameter_visbility()

    def changeDepiction(self, text):
        self.layer.depiction = text
        self._toggle_plane_parameter_visibility()

    def changePlaneThickness(self, value: float):
        self.layer.plane.thickness = value

    def changeIsoThreshold(self, value):
        """Change isosurface threshold on the layer model.

        Parameters
        ----------
        value : float
            Threshold for isosurface.
        """
        with self.layer.events.blocker(self._on_iso_threshold_change):
            self.layer.iso_threshold = value / 100

    def _on_iso_threshold_change(self):
        """Receive layer model isosurface change event and update the slider."""
        with self.layer.events.iso_threshold.blocker():
            self.isoThresholdSlider.setValue(
                int(self.layer.iso_threshold * 100)
            )

    def changeAttenuation(self, value):
        """Change attenuation rate for attenuated maximum intensity projection.

        Parameters
        ----------
        value : Float
            Attenuation rate for attenuated maximum intensity projection.
        """
        with self.layer.events.blocker(self._on_attenuation_change):
            self.layer.attenuation = value / 200

    def _on_attenuation_change(self):
        """Receive layer model attenuation change event and update the slider."""
        with self.layer.events.attenuation.blocker():
            self.attenuationSlider.setValue(int(self.layer.attenuation * 200))

    def _on_interpolation_change(self, event):
        """Receive layer interpolation change event and update dropdown menu.

        Parameters
        ----------
        event : napari.utils.event.Event
            The napari event that triggered this method.
        """
        interp_string = event.value.value

        with self.layer.events.interpolation.blocker():
            if self.interpComboBox.findText(interp_string) == -1:
                self.interpComboBox.addItem(interp_string)
            self.interpComboBox.setCurrentText(interp_string)

    def _on_rendering_change(self):
        """Receive layer model rendering change event and update dropdown menu."""
        with self.layer.events.rendering.blocker():
            index = self.renderComboBox.findText(
                self.layer.rendering, Qt.MatchFixedString
            )
            self.renderComboBox.setCurrentIndex(index)
            self._toggle_rendering_parameter_visbility()

    def _on_depiction_change(self):
        """Receive layer model depiction change event and update combobox."""
        with self.layer.events.depiction.blocker():
            index = self.depictionComboBox.findText(
                self.layer.depiction, Qt.MatchFixedString
            )
            self.depictionComboBox.setCurrentIndex(index)
            self._toggle_plane_parameter_visibility()

    def _on_plane_thickness_change(self):
        with self.layer.plane.events.blocker():
            self.planeControls.planeThicknessSlider.setValue(
                self.layer.plane.thickness
            )

    def _toggle_rendering_parameter_visbility(self):
        """Hide isosurface rendering parameters if they aren't needed."""
        rendering = ImageRendering(self.layer.rendering)
        if rendering == ImageRendering.ISO:
            self.isoThresholdSlider.show()
            self.isoThresholdLabel.show()
        else:
            self.isoThresholdSlider.hide()
            self.isoThresholdLabel.hide()
        if rendering == ImageRendering.ATTENUATED_MIP:
            self.attenuationSlider.show()
            self.attenuationLabel.show()
        else:
            self.attenuationSlider.hide()
            self.attenuationLabel.hide()

    def _toggle_plane_parameter_visibility(self):
        """Hide plane rendering controls if they aren't needed."""
        depiction = VolumeDepiction(self.layer.depiction)
        if depiction == VolumeDepiction.VOLUME or self.layer._ndisplay == 2:
            self.planeControls.hide()
        if depiction == VolumeDepiction.PLANE and self.layer._ndisplay == 3:
            self.planeControls.show()

    def _update_interpolation_combo(self):
        self.interpComboBox.clear()
        interp_names = (
            Interpolation3D.keys()
            if self.layer._ndisplay == 3
            else [i.value for i in Interpolation.view_subset()]
        )
        self.interpComboBox.addItems(interp_names)
        index = self.interpComboBox.findText(
            self.layer.interpolation, Qt.MatchFixedString
        )
        self.interpComboBox.setCurrentIndex(index)

    def _on_ndisplay_change(self):
        """Toggle between 2D and 3D visualization modes."""
        self._update_interpolation_combo()
        self._toggle_plane_parameter_visibility()
        if self.layer._ndisplay == 2:
            self.isoThresholdSlider.hide()
            self.isoThresholdLabel.hide()
            self.attenuationSlider.hide()
            self.attenuationLabel.hide()
            self.renderComboBox.hide()
            self.renderLabel.hide()
            self.depictionComboBox.hide()
            self.depictionLabel.hide()
        else:
            self.renderComboBox.show()
            self.renderLabel.show()
            self._toggle_rendering_parameter_visbility()
            self.depictionComboBox.show()
            self.depictionLabel.show()


class PlaneNormalButtons(QWidget):
    """Qt buttons for controlling plane orientation.

        Attributes
    ----------
    xButton : qtpy.QtWidgets.QPushButton
        Button which orients a plane normal along the x axis.
    yButton : qtpy.QtWidgets.QPushButton
        Button which orients a plane normal along the y axis.
    zButton : qtpy.QtWidgets.QPushButton
        Button which orients a plane normal along the z axis.
    obliqueButton : qtpy.QtWidgets.QPushButton
        Button which orients a plane normal along the camera view direction.
    """

    def __init__(self, parent=None) -> None:
        super().__init__(parent=parent)
        self.setLayout(QHBoxLayout())
        self.layout().setSpacing(2)
        self.layout().setContentsMargins(0, 0, 0, 0)

        self.xButton = QPushButton('x')
        self.yButton = QPushButton('y')
        self.zButton = QPushButton('z')
        self.obliqueButton = QPushButton(trans._('oblique'))

        self.layout().addWidget(self.xButton)
        self.layout().addWidget(self.yButton)
        self.layout().addWidget(self.zButton)
        self.layout().addWidget(self.obliqueButton)


class QtPlaneControls(QWidget):
    """Qt widget encapsulating plane controls for an image layer."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.grid_layout = LayerListGridLayout(self)
        self.setLayout(self.grid_layout)

        self.planeNormalLabel = QLabel(trans._('plane normal:'))
        self.planeNormalButtons = PlaneNormalButtons(parent=self)

        self.planeThicknessSlider = QLabeledDoubleSlider(Qt.Horizontal, self)
        self.planeThicknessSlider.setFocusPolicy(Qt.NoFocus)
        self.planeThicknessSlider.setMinimum(1)
        self.planeThicknessSlider.setMaximum(50)
        self.planeThicknessLabel = QLabel(trans._('plane thickness:'))

        self.grid_layout.addWidget(self.planeNormalLabel, 1, 0)
        self.grid_layout.addWidget(self.planeNormalButtons, 1, 1)
        self.grid_layout.addWidget(self.planeThicknessLabel, 2, 0)
        self.grid_layout.addWidget(self.planeThicknessSlider, 2, 1)
