from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
    from napari.viewer import Viewer

# This is the minimum size of the zoom box (in pixels) that will
# trigger a zoom when the user drags the mouse while holding Alt.
MIN_ZOOMBOX_SIZE = 5


def _pan_zoom_enabled(viewer: Viewer) -> bool:
    active_layer = viewer.layers.selection.active
    return active_layer is None or active_layer.mode == 'pan_zoom'


def dims_scroll(viewer, event):
    """Scroll the dimensions slider."""
    if 'Control' not in event.modifiers:
        return
    # always scroll by 1 at most, even if scroll wheel is set to
    # scroll many lines at once.
    delta = np.clip(event.delta[1], -1, 1)
    forward = 1 if event.native.inverted() else -1
    viewer.dims._scroll_progress += delta * forward
    while abs(viewer.dims._scroll_progress) >= 1:
        if viewer.dims._scroll_progress < 0:
            viewer.dims._increment_dims_left()
            viewer.dims._scroll_progress += 1
        else:
            viewer.dims._increment_dims_right()
            viewer.dims._scroll_progress -= 1


def layers_scroll(viewer, event):
    """Scroll through the layer list."""
    if 'Alt' not in event.modifiers:
        return

    # Use the sum of both axes to handle axis flipping
    delta = np.sum(event.delta)

    # Clip delta to +/- 1.0 to prevent skipping layers
    delta = np.clip(delta, -1, 1)

    if event.native.inverted():
        viewer._layer_list_scroll_progress -= delta
    else:
        viewer._layer_list_scroll_progress += delta

    while abs(viewer._layer_list_scroll_progress) >= 1:
        if viewer._layer_list_scroll_progress < 0:
            # previous is down the list
            viewer.layers.select_previous()
            viewer._layer_list_scroll_progress += 1
        else:
            # next is up the list
            viewer.layers.select_next()
            viewer._layer_list_scroll_progress -= 1


def double_click_to_zoom(viewer, event):
    """Zoom in on double click by zoom_factor; zoom out with Alt.

    Note: only active when there is no selected layer or the active
    layer is in pan/zoom mode.
    """
    if not _pan_zoom_enabled(viewer):
        return
    # if Alt held down, zoom out instead
    zoom_factor = 0.5 if 'Alt' in event.modifiers else 2
    viewer.scene.camera.zoom *= zoom_factor
    if viewer.dims.ndisplay == 3 and viewer.dims.ndim == 3:
        viewer.scene.camera.center = np.asarray(viewer.scene.camera.center) + (
            np.asarray(event.position)[np.asarray(viewer.dims.displayed)]
            - np.asarray(viewer.scene.camera.center)
        ) * (1 - 1 / zoom_factor)
    else:
        viewer.scene.camera.center = np.asarray(viewer.scene.camera.center)[
            -2:
        ] + (
            np.asarray(event.position)[-2:]
            - np.asarray(viewer.scene.camera.center)[-2:]
        ) * (1 - 1 / zoom_factor)


def drag_to_zoom(viewer, event):
    """While holding Alt, drag mouse to select a region to zoom.

    This function allows the user to click and drag the mouse while
    holding the `Alt` key to create a zoom box. When the mouse is released,
    the camera zooms into the selected region.

    Note: only active when there is no selected layer or the active
    layer is in pan/zoom mode.
    """
    if (
        'Alt' not in event.modifiers
        or event.type != 'mouse_press'
        or not _pan_zoom_enabled(viewer)
    ):
        return

    # on mouse press
    viewer.canvas.overlays._zoom_box.visible = True
    press_pos = event.pos[::-1]
    press_position = event.position
    move_pos = press_pos
    move_position = press_position
    viewer.canvas.overlays._zoom_box.position = (press_pos, press_pos)
    yield
    event.handled = True

    # on mouse move
    while event.type == 'mouse_move':
        if 'Alt' not in event.modifiers:
            viewer.canvas.overlays._zoom_box.visible = False
            yield
            return

        move_pos = event.pos[::-1]
        viewer.canvas.overlays._zoom_box.position = (press_pos, move_pos)
        move_position = event.position
        yield

    # on mouse release
    viewer.canvas.overlays._zoom_box.visible = False

    # only trigger zoom if the box is larger than a MIN_ZOOMBOX_SIZE in pixels
    distance = np.abs(np.array(press_pos) - np.array(move_pos))
    if distance.min() > MIN_ZOOMBOX_SIZE:
        # Slice to the last two coordinates (displayed axes) for cases where
        # ndim>2 and ndisplay=2
        viewer.canvas.overlays._zoom_box.zoom_area = (
            press_position[-2:],
            move_position[-2:],
        )
    yield
