from dataclasses import dataclass, field
from typing import List, Tuple

import numpy as np
import pytest

from napari.layers import Points
from napari.utils.interactions import (
    ReadOnlyWrapper,
    mouse_move_callbacks,
    mouse_press_callbacks,
    mouse_release_callbacks,
)


@dataclass
class Event:
    """Create a subclass for simulating vispy mouse events."""

    type: str
    is_dragging: bool = False
    modifiers: List[str] = field(default_factory=list)
    position: Tuple[int, int] = (0, 0)  # world coords
    pos: np.ndarray = np.zeros(2)  # canvas coords


@pytest.fixture
def create_known_points_layer():
    """Create points layer with known coordinates

    Returns
    -------
    layer : napari.layers.Points
        Points layer.
    n_points : int
        Number of points in the points layer
    known_non_point : list
        Data coordinates that are known to contain no points. Useful during
        testing when needing to guarantee no point is clicked on.
    """
    data = [[1, 3], [8, 4], [10, 10], [15, 4]]
    known_non_point = [20, 30]
    n_points = len(data)

    layer = Points(data, size=1)
    assert np.all(layer.data == data)
    assert layer.ndim == 2
    assert len(layer.data) == n_points
    assert len(layer.selected_data) == 0

    return layer, n_points, known_non_point


def test_not_adding_or_selecting_point(create_known_points_layer):
    """Don't add or select a point by clicking on one in pan_zoom mode."""
    layer, n_points, _ = create_known_points_layer
    layer.mode = 'pan_zoom'

    # Simulate click
    event = ReadOnlyWrapper(Event(type='mouse_press'))
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(Event(type='mouse_release'))
    mouse_release_callbacks(layer, event)

    # Check no new point added and non selected
    assert len(layer.data) == n_points
    assert len(layer.selected_data) == 0


def test_add_point(create_known_points_layer):
    """Add point by clicking in add mode."""
    layer, n_points, known_non_point = create_known_points_layer

    # Add point at location where non exists
    layer.mode = 'add'

    # Simulate click
    event = ReadOnlyWrapper(
        Event(type='mouse_press', position=known_non_point)
    )
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(
        Event(type='mouse_release', position=known_non_point)
    )
    mouse_release_callbacks(layer, event)

    # Check new point added at coordinates location
    assert len(layer.data) == n_points + 1
    np.testing.assert_allclose(layer.data[-1], known_non_point)


def test_drag_in_add_mode(create_known_points_layer):
    """Drag in add mode and make sure no point is added."""
    layer, n_points, known_non_point = create_known_points_layer

    # Add point at location where non exists
    layer.mode = 'add'
    layer.interactive = True

    # Simulate click
    event = ReadOnlyWrapper(
        Event(type='mouse_press', position=known_non_point)
    )
    mouse_press_callbacks(layer, event)

    known_non_point_end = [40, 60]

    # Simulate drag end
    event = ReadOnlyWrapper(
        Event(
            type='mouse_move', is_dragging=True, position=known_non_point_end
        )
    )
    mouse_move_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(
        Event(
            type='mouse_release',
            position=known_non_point_end,
            pos=np.array([4, 4]),
        )
    )
    mouse_release_callbacks(layer, event)

    # Check that no new point has been added
    assert len(layer.data) == n_points


def test_select_point(create_known_points_layer):
    """Select a point by clicking on one in select mode."""
    layer, n_points, _ = create_known_points_layer

    layer.mode = 'select'
    position = tuple(layer.data[0])

    # Simulate click
    event = ReadOnlyWrapper(Event(type='mouse_press', position=position))
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(Event(type='mouse_release', position=position))
    mouse_release_callbacks(layer, event)

    # Check clicked point selected
    assert len(layer.selected_data) == 1
    assert 0 in layer.selected_data


def test_after_in_add_mode_point(create_known_points_layer):
    """Don't add or select a point by clicking on one in pan_zoom mode."""
    layer, n_points, _ = create_known_points_layer

    layer.mode = 'add'
    layer.mode = 'pan_zoom'
    position = tuple(layer.data[0])

    # Simulate click
    event = ReadOnlyWrapper(Event(type='mouse_press', position=position))
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(Event(type='mouse_release', position=position))
    mouse_release_callbacks(layer, event)

    # Check no new point added and non selected
    assert len(layer.data) == n_points
    assert len(layer.selected_data) == 0


def test_after_in_select_mode_point(create_known_points_layer):
    """Don't add or select a point by clicking on one in pan_zoom mode."""
    layer, n_points, _ = create_known_points_layer

    layer.mode = 'select'
    layer.mode = 'pan_zoom'
    position = tuple(layer.data[0])

    # Simulate click
    event = ReadOnlyWrapper(Event(type='mouse_press', position=position))
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(Event(type='mouse_release', position=position))
    mouse_release_callbacks(layer, event)

    # Check no new point added and non selected
    assert len(layer.data) == n_points
    assert len(layer.selected_data) == 0


def test_unselect_select_point(create_known_points_layer):
    """Select a point by clicking on one in select mode."""
    layer, n_points, _ = create_known_points_layer

    layer.mode = 'select'
    position = tuple(layer.data[0])
    layer.selected_data = {2, 3}

    # Simulate click
    event = ReadOnlyWrapper(Event(type='mouse_press', position=position))
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(Event(type='mouse_release', position=position))
    mouse_release_callbacks(layer, event)

    # Check clicked point selected
    assert len(layer.selected_data) == 1
    assert 0 in layer.selected_data


def test_add_select_point(create_known_points_layer):
    """Add to a selection of points point by shift-clicking on one."""
    layer, n_points, _ = create_known_points_layer

    layer.mode = 'select'
    position = tuple(layer.data[0])
    layer.selected_data = {2, 3}

    # Simulate click
    event = ReadOnlyWrapper(
        Event(type='mouse_press', modifiers=['Shift'], position=position)
    )
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(
        Event(type='mouse_release', modifiers=['Shift'], position=position)
    )
    mouse_release_callbacks(layer, event)

    # Check clicked point selected
    assert len(layer.selected_data) == 3
    assert layer.selected_data == {2, 3, 0}


def test_remove_select_point(create_known_points_layer):
    """Remove from a selection of points point by shift-clicking on one."""
    layer, n_points, _ = create_known_points_layer

    layer.mode = 'select'
    position = tuple(layer.data[0])
    layer.selected_data = {0, 2, 3}

    # Simulate click
    event = ReadOnlyWrapper(
        Event(type='mouse_press', modifiers=['Shift'], position=position)
    )
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(
        Event(type='mouse_release', modifiers=['Shift'], position=position)
    )
    mouse_release_callbacks(layer, event)

    # Check clicked point selected
    assert len(layer.selected_data) == 2
    assert layer.selected_data == {2, 3}


def test_not_selecting_point(create_known_points_layer):
    """Don't select a point by not clicking on one in select mode."""
    layer, n_points, known_non_point = create_known_points_layer

    layer.mode = 'select'

    # Simulate click
    event = ReadOnlyWrapper(
        Event(type='mouse_press', position=known_non_point)
    )
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(
        Event(type='mouse_release', position=known_non_point)
    )
    mouse_release_callbacks(layer, event)

    # Check clicked point selected
    assert len(layer.selected_data) == 0


def test_unselecting_points(create_known_points_layer):
    """Unselect points by not clicking on one in select mode."""
    layer, n_points, known_non_point = create_known_points_layer

    layer.mode = 'select'
    layer.selected_data = {2, 3}
    assert len(layer.selected_data) == 2

    # Simulate click
    event = ReadOnlyWrapper(
        Event(type='mouse_press', position=known_non_point)
    )
    mouse_press_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(
        Event(type='mouse_release', position=known_non_point)
    )
    mouse_release_callbacks(layer, event)

    # Check clicked point selected
    assert len(layer.selected_data) == 0


def test_selecting_all_points_with_drag(create_known_points_layer):
    """Select all points when drag box includes all of them."""
    layer, n_points, known_non_point = create_known_points_layer

    layer.mode = 'select'

    # Simulate click
    event = ReadOnlyWrapper(
        Event(type='mouse_press', position=known_non_point)
    )
    mouse_press_callbacks(layer, event)

    # Simulate drag start
    event = ReadOnlyWrapper(
        Event(type='mouse_move', is_dragging=True, position=known_non_point)
    )
    mouse_move_callbacks(layer, event)

    # Simulate drag end
    event = ReadOnlyWrapper(Event(type='mouse_move', is_dragging=True))
    mouse_move_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(Event(type='mouse_release', is_dragging=True))
    mouse_release_callbacks(layer, event)

    # Check all points selected as drag box contains them
    assert len(layer.selected_data) == n_points


def test_selecting_no_points_with_drag(create_known_points_layer):
    """Select all points when drag box includes all of them."""
    layer, n_points, known_non_point = create_known_points_layer

    layer.mode = 'select'

    # Simulate click
    event = ReadOnlyWrapper(
        Event(type='mouse_press', position=known_non_point)
    )
    mouse_press_callbacks(layer, event)

    # Simulate drag start
    event = ReadOnlyWrapper(
        Event(type='mouse_move', is_dragging=True, position=known_non_point)
    )
    mouse_move_callbacks(layer, event)

    # Simulate drag end
    event = ReadOnlyWrapper(
        Event(type='mouse_move', is_dragging=True, position=(50, 60))
    )
    mouse_move_callbacks(layer, event)

    # Simulate release
    event = ReadOnlyWrapper(
        Event(type='mouse_release', is_dragging=True, position=(50, 60))
    )
    mouse_release_callbacks(layer, event)

    # Check no points selected as drag box doesn't contain them
    assert len(layer.selected_data) == 0
