from qtpy.QtWidgets import QFrame, QStackedWidget

from napari._qt.layer_controls.dynamic.qt_dynamic_layer_controls import (
    QtDynamicLayerControls,
)
from napari._qt.layer_controls.qt_image_controls import QtImageControls
from napari._qt.layer_controls.qt_labels_controls import QtLabelsControls
from napari._qt.layer_controls.qt_points_controls import QtPointsControls
from napari._qt.layer_controls.qt_shapes_controls import QtShapesControls
from napari._qt.layer_controls.qt_surface_controls import QtSurfaceControls
from napari._qt.layer_controls.qt_tracks_controls import QtTracksControls
from napari._qt.layer_controls.qt_vectors_controls import QtVectorsControls
from napari.layers import (
    Image,
    Labels,
    Points,
    Shapes,
    Surface,
    Tracks,
    Vectors,
)
from napari.settings import get_settings

layer_to_controls = {
    Labels: QtLabelsControls,
    Image: QtImageControls,
    Points: QtPointsControls,
    Shapes: QtShapesControls,
    Surface: QtSurfaceControls,
    Vectors: QtVectorsControls,
    Tracks: QtTracksControls,
}


def create_qt_layer_controls(layer):
    """
    Create a qt controls widget for a layer based on its layer type.

    In case of a subclass, the type higher in the layer's method resolution
    order will be used.

    Parameters
    ----------
    layer : napari.layers.Layer
        Layer that needs its controls widget created.

    Returns
    -------
    controls : napari.layers.base.QtLayerControls
        Qt controls widget
    """
    candidates = []
    for layer_type in layer_to_controls:
        if isinstance(layer, layer_type):
            candidates.append(layer_type)

    if not candidates:
        raise TypeError(
            f'Could not find QtControls for layer of type {type(layer)}'
        )

    layer_cls = layer.__class__
    # Sort the list of candidates by 'lineage'
    candidates.sort(key=lambda layer_type: layer_cls.mro().index(layer_type))
    controls = layer_to_controls[candidates[0]]
    return controls(layer)


class QtLayerControlsContainer(QStackedWidget):
    """Container widget for QtLayerControl widgets.

    Parameters
    ----------
    viewer : napari.components.ViewerModel
        Napari viewer containing the rendered scene, layers, and controls.

    Attributes
    ----------
    empty_widget : qtpy.QtWidgets.QFrame
        Empty placeholder frame for when no layer is selected.
    viewer : napari.components.ViewerModel
        Napari viewer containing the rendered scene, layers, and controls.
    widgets : dict
        Dictionary of key value pairs matching layer with its widget controls.
        widgets[layer] = controls
    """

    def __init__(self, viewer) -> None:
        super().__init__()
        self.viewer = viewer

        self.setMouseTracking(True)
        self.empty_widget = QFrame()
        self.empty_widget.setObjectName('empty_controls_widget')
        self.widgets = {}
        self.panel = None  # dynamic controls
        self.addWidget(self.empty_widget)
        self.setCurrentWidget(self.empty_widget)

        self.viewer.layers.events.inserted.connect(self._add)
        self.viewer.layers.events.removed.connect(self._remove)
        viewer.layers.selection.events.changed.connect(self._populate)
        viewer.dims.events.ndisplay.connect(self._on_ndisplay_changed)
        viewer.events.theme.connect(self._on_viewer_theme_changed)

    def _on_ndisplay_changed(self, event):
        """Responds to a change in the dimensionality displayed in the canvas.

        Parameters
        ----------
        event : Event
            Event with the new dimensionality value at `event.value`.
        """
        for panel in self.widgets.values():
            if panel is not self.empty_widget:
                panel.ndisplay = event.value

        if self.panel is not None:
            self.panel.ndisplay = event.value

    def _on_viewer_theme_changed(self, event=None):
        """Respond to viewer.theme changes from keybindings (Ctrl+Shift+T).

        The ``toggle_theme`` keybinding sets ``viewer.theme`` directly
        without updating ``settings.appearance.theme``, so widgets that
        listen only to settings events miss the change. This bridges
        the gap by forwarding ``event.value`` (the new theme) to any
        histogram widgets that have been lazily created.
        """
        for widget in self.widgets.values():
            histogram_control = getattr(widget, '_histogram_control', None)
            if histogram_control is None:
                continue
            hist_widget = getattr(histogram_control, 'histogram_widget', None)
            if hist_widget is not None:
                hist_widget._on_theme_change(event)

        if self.panel is not None:
            for widget in self.panel.values():
                histogram_control = getattr(widget, '_histogram_control', None)
                if histogram_control is None:
                    continue
                hist_widget = getattr(
                    histogram_control, 'histogram_widget', None
                )
                if hist_widget is not None:
                    hist_widget._on_theme_change(event)

    def _populate(self):
        """Change the displayed controls to be those of the target layer.

        Parameters
        ----------
        event : Event
            Event with the target layer at `event.value`.
        """
        if self.panel is not None:
            self.removeWidget(self.panel)
            self.panel.hide()
            self.panel.deleteLater()
            self.panel = None

        selection = self.viewer.layers.selection
        always_dynamic = get_settings().experimental.dynamic_layer_controls
        if not selection:
            self.setCurrentWidget(self.empty_widget)
        elif selection.active is not None and not always_dynamic:
            controls = self.widgets[selection.active]
            self.setCurrentWidget(controls)
        else:
            # ordered list of layers in selection
            layers = [
                layer for layer in self.viewer.layers if layer in selection
            ]
            self.panel = QtDynamicLayerControls(layers)
            self.panel.ndisplay = self.viewer.dims.ndisplay
            self.addWidget(self.panel)
            self.setCurrentWidget(self.panel)

    def _add(self, event):
        """Add the controls target layer to the list of control widgets.

        Parameters
        ----------
        event : Event
            Event with the target layer at `event.value`.
        """
        always_dynamic = get_settings().experimental.dynamic_layer_controls
        if always_dynamic:
            return
        layer = event.value
        controls = create_qt_layer_controls(layer)
        controls.ndisplay = self.viewer.dims.ndisplay
        self.addWidget(controls)
        self.widgets[layer] = controls

    def _remove(self, event):
        """Remove the controls target layer from the list of control widgets.

        Parameters
        ----------
        event : Event
            Event with the target layer at `event.value`.
        """
        always_dynamic = get_settings().experimental.dynamic_layer_controls
        if always_dynamic:
            return
        layer = event.value
        controls = self.widgets[layer]
        self.removeWidget(controls)
        controls.hide()
        controls.deleteLater()
        controls = None
        del self.widgets[layer]
