import numpy as np
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QApplication

from napari_layer_statistics._widget import (
    LayerStatisticsWidget,
    _compute_image_statistics,
    _compute_label_statistics,
)


def test_layer_statistics_widget_for_image(make_napari_viewer):
    viewer = make_napari_viewer()
    layer = viewer.add_image(
        np.arange(12, dtype=float).reshape((3, 4)),
        name='sample-image',
        scale=(1.5, 2.0),
        translate=(10.0, -2.0),
    )

    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('image', _compute_image_statistics(np.asarray(layer.data), False)),
    )

    assert widget._metadata_name_value.text() == 'sample-image'
    assert widget._metadata_type_value.text() == 'Image'
    assert widget._metadata_dtype_value.text() == 'float64'
    assert widget._shape_value.text() == '(3, 4)'
    assert widget._spacing_value.text() == '(1.5, 2)'
    assert widget._origin_value.text() == '(10, -2)'
    assert widget._direction_value.text() == '[(1, 0), (0, 1)]'
    assert not widget._image_group.isHidden()
    assert widget._image_table.rowCount() == 5
    assert widget._image_table.columnCount() == 2
    assert widget._image_table.horizontalHeaderItem(0).text() == 'Stats'
    assert widget._image_table.horizontalHeaderItem(1).text() == 'All'
    assert widget._image_table.item(0, 0).text() == 'Range'
    assert widget._image_table.item(0, 1).text() == '0 to 11'
    assert widget._image_table.item(1, 1).text() == '5.5 / 3.45205'
    assert widget._image_table.item(2, 1).text() == '5.5'
    assert widget._image_table.item(3, 1).text() == '0.11 / 10.89'
    assert widget._image_table.item(4, 1).text() == '11 (91.67%)'
    assert widget._background_value.text() == '-'
    assert widget._class_count_value.text() == '-'
    assert widget._foreground_value.text() == '-'
    assert widget._labels_group.isHidden()


def test_layer_statistics_widget_for_rgb_image(make_napari_viewer):
    viewer = make_napari_viewer()
    layer = viewer.add_image(
        np.array(
            [
                [[0, 10, 100], [1, 11, 101]],
                [[2, 12, 102], [3, 13, 103]],
            ],
            dtype=float,
        ),
        rgb=True,
    )

    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('image', _compute_image_statistics(np.asarray(layer.data), True)),
    )

    assert widget._image_table.rowCount() == 5
    assert widget._image_table.columnCount() == 4
    assert widget._image_table.horizontalHeaderItem(0).text() == 'Stats'
    assert widget._image_table.horizontalHeaderItem(1).text() == 'R'
    assert widget._image_table.horizontalHeaderItem(2).text() == 'G'
    assert widget._image_table.horizontalHeaderItem(3).text() == 'B'
    assert widget._image_table.item(0, 1).text() == '0 to 3'
    assert widget._image_table.item(0, 2).text() == '10 to 13'
    assert widget._image_table.item(0, 3).text() == '100 to 103'


def test_layer_statistics_widget_for_rgba_4d_image(make_napari_viewer):
    viewer = make_napari_viewer()
    layer = viewer.add_image(
        np.arange(2 * 3 * 4 * 4, dtype=float).reshape((2, 3, 4, 4)),
        rgb=True,
        scale=(1.0, 2.0, 3.0),
    )

    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('image', _compute_image_statistics(np.asarray(layer.data), True)),
    )

    assert widget._image_table.columnCount() == 5
    assert widget._image_table.horizontalHeaderItem(1).text() == 'R'
    assert widget._image_table.horizontalHeaderItem(2).text() == 'G'
    assert widget._image_table.horizontalHeaderItem(3).text() == 'B'
    assert widget._image_table.horizontalHeaderItem(4).text() == 'A'


def test_layer_statistics_widget_can_edit_image_geometry(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    layer = viewer.add_image(np.zeros((3, 4), dtype=float))
    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer, force=True)

    assert not widget._spacing_value.isReadOnly()
    assert not widget._origin_value.isReadOnly()
    assert not widget._direction_value.isReadOnly()

    widget._spacing_value.setText('(2.5, 3.5)')
    widget._apply_spacing_from_input()
    widget._origin_value.setText('(10, -4)')
    widget._apply_origin_from_input()
    widget._direction_value.setText('[(0, 1), (1, 0)]')
    widget._apply_direction_from_input()

    np.testing.assert_allclose(np.asarray(layer.scale, dtype=float), [2.5, 3.5])
    np.testing.assert_allclose(
        np.asarray(layer.translate, dtype=float), [10.0, -4.0]
    )
    np.testing.assert_allclose(_compute_direction_from_layer(layer), [[0, 1], [1, 0]])


def _compute_direction_from_layer(layer) -> np.ndarray:
    affine_matrix = np.asarray(layer.affine.affine_matrix, dtype=float)
    return affine_matrix[:-1, :-1]


def test_layer_statistics_widget_for_labels(make_napari_viewer):
    viewer = make_napari_viewer()
    layer = viewer.add_labels(
        np.array([[0, 1, 1], [3, 5, 8]], dtype=int),
        name='sample-labels',
        scale=(2.0, 2.0),
        translate=(4.0, 6.0),
    )

    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('labels', _compute_label_statistics(np.asarray(layer.data))),
    )

    assert widget._metadata_name_value.text() == 'sample-labels'
    assert widget._metadata_type_value.text() == 'Labels'
    assert widget._metadata_dtype_value.text().startswith('int')
    assert widget._shape_value.text() == '(2, 3)'
    assert widget._spacing_value.text() == '(2, 2)'
    assert widget._origin_value.text() == '(4, 6)'
    assert widget._image_group.isHidden()
    assert widget._background_value.text() == '1 (16.67%)'
    assert widget._class_count_value.text() == '8'
    assert widget._classes_present_value.text() == '4'
    assert widget._foreground_value.text() == '5 (83.33%)'
    assert not widget._labels_group.isHidden()
    assert widget._classes_table.rowCount() == 4
    assert widget._classes_table.item(0, 0).text() == '1'
    assert widget._classes_table.item(0, 1).text() == '2 (40.00%)'
    assert widget._classes_table.item(1, 0).text() == '3'
    assert widget._classes_table.item(1, 1).text() == '1 (20.00%)'


def test_layer_statistics_widget_for_empty_foreground_labels(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    layer = viewer.add_labels(np.zeros((2, 3), dtype=int), name='empty-labels')

    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('labels', _compute_label_statistics(np.asarray(layer.data))),
    )

    assert widget._image_group.isHidden()
    assert widget._background_value.text() == '6 (100.00%)'
    assert widget._class_count_value.text() == '0'
    assert widget._classes_present_value.text() == '0'
    assert widget._foreground_value.text() == '0 (0.00%)'
    assert widget._classes_table.rowCount() == 1
    assert widget._classes_table.item(0, 0).text() == '-'
    assert (
        widget._classes_table.item(0, 1).text()
        == 'No non-zero labels present.'
    )


def test_layer_statistics_widget_class_color_picker_updates_selected_color(
    make_napari_viewer, monkeypatch
):
    viewer = make_napari_viewer()
    layer = viewer.add_labels(np.array([[0, 1, 1], [2, 0, 2]], dtype=int))
    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer, force=True)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('labels', _compute_label_statistics(np.asarray(layer.data))),
    )

    selected = QColor(25, 100, 230, 255)

    def _fake_get_color(*args, **kwargs):
        del args, kwargs
        return selected

    monkeypatch.setattr(
        'napari_layer_statistics._widget.QColorDialog.getColor',
        _fake_get_color,
    )

    before_class_1 = widget._get_label_qcolor(layer, 1)
    before_class_2 = widget._get_label_qcolor(layer, 2)
    widget._on_classes_table_cell_clicked(0, 0)

    override = widget._class_color_overrides[id(layer)][1]
    assert override.red() == selected.red()
    assert override.green() == selected.green()
    assert override.blue() == selected.blue()
    after_class_1 = widget._get_display_label_qcolor(layer, 1)
    after_other = widget._get_display_label_qcolor(layer, 2)
    if before_class_1 is not None and after_class_1 is not None:
        assert after_class_1.red() == selected.red()
        assert after_class_1.green() == selected.green()
        assert after_class_1.blue() == selected.blue()
    layer_class_1 = np.asarray(layer.get_color(1), dtype=float)
    assert np.isclose(layer_class_1[0], selected.redF())
    assert np.isclose(layer_class_1[1], selected.greenF())
    assert np.isclose(layer_class_1[2], selected.blueF())
    assert np.isclose(layer_class_1[3], selected.alphaF())
    if before_class_2 is not None and after_other is not None:
        assert after_other.red() == before_class_2.red()
        assert after_other.green() == before_class_2.green()
        assert after_other.blue() == before_class_2.blue()


def test_layer_statistics_widget_can_copy_colors_to_clipboard(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    layer = viewer.add_labels(np.array([[0, 1, 1], [2, 0, 2]], dtype=int))
    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer, force=True)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('labels', _compute_label_statistics(np.asarray(layer.data))),
    )

    widget._apply_label_class_color(layer, 2, QColor(8, 16, 24, 255))
    widget._copy_colors_to_clipboard()
    copied = QApplication.clipboard().text()

    assert copied.startswith('Colors\n')
    assert '1: (' in copied
    assert '2: (8, 16, 24, 255)' in copied
    assert 'Labels Statistics' not in copied
    assert 'Background:' not in copied
    assert 'Foreground:' not in copied


def test_layer_statistics_widget_can_paste_colors_from_clipboard(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    layer = viewer.add_labels(np.array([[0, 1, 1], [2, 0, 2]], dtype=int))
    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer, force=True)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('labels', _compute_label_statistics(np.asarray(layer.data))),
    )

    QApplication.clipboard().setText(
        '\n'.join(
            [
                'Colors',
                '1: (17, 34, 51, 255)',
                '2: (90, 80, 70, 255)',
            ]
        )
    )

    widget._paste_colors_from_clipboard()

    class_1 = widget._get_display_label_qcolor(layer, 1)
    class_2 = widget._get_display_label_qcolor(layer, 2)
    assert class_1 is not None
    assert class_2 is not None
    assert (class_1.red(), class_1.green(), class_1.blue(), class_1.alpha()) == (
        17,
        34,
        51,
        255,
    )
    assert (class_2.red(), class_2.green(), class_2.blue(), class_2.alpha()) == (
        90,
        80,
        70,
        255,
    )
    assert widget._status_indicator.toolTip() == 'Pasted class colors from clipboard.'


def test_layer_statistics_widget_global_summary_excludes_camera_and_colors(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    layer = viewer.add_labels(np.array([[0, 1, 1], [2, 0, 2]], dtype=int))
    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer, force=True)
    widget._on_data_statistics_completed(
        widget._stats_request_id,
        ('labels', _compute_label_statistics(np.asarray(layer.data))),
    )

    widget._copy_statistics_to_clipboard()
    copied = QApplication.clipboard().text()

    assert 'Metadata' in copied
    assert 'Geometry' in copied
    assert 'Labels Statistics' in copied
    assert '\nCamera\n' not in f'\n{copied}\n'
    assert '\nColors\n' not in f'\n{copied}\n'


def test_layer_statistics_widget_empty_state(make_napari_viewer):
    viewer = make_napari_viewer()

    widget = LayerStatisticsWidget(viewer)

    assert widget._compute_data_checkbox.isChecked()
    assert not widget._metadata_group.isHidden()
    assert widget._metadata_name_value.text() == '-'
    assert widget._metadata_type_value.text() == '-'
    assert not widget._geometry_group.isHidden()
    assert widget._geometry_group.isChecked()
    assert widget._geometry_body.isVisible()
    assert not widget._camera_group.isHidden()
    assert not widget._camera_group.isChecked()
    assert widget._camera_center_value.text() != '-'
    assert widget._image_group.isHidden()
    assert widget._labels_group.isHidden()


def test_layer_statistics_widget_camera_values_follow_viewer_camera(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    viewer.add_image(np.zeros((4, 5, 6), dtype=float))
    widget = LayerStatisticsWidget(viewer)

    viewer.dims.order = (2, 0, 1)
    viewer.dims.ndisplay = 2
    viewer.camera.center = (10.0, 20.0, 30.0)
    viewer.camera.zoom = 3.5
    viewer.camera.angles = (5.0, 10.0, 15.0)

    displayed_text = '(' + ', '.join(str(i) for i in viewer.dims.displayed) + ')'

    assert widget._camera_ndisplay_value.text() == '2'
    assert widget._camera_displayed_axis_value.text() == displayed_text
    assert widget._camera_center_value.text() == '(10, 20, 30)'
    assert widget._camera_zoom_value.text() == '3.5'
    assert widget._camera_angle_value.text() == '(5, 10, 15)'
    assert widget._camera_canvas_size_value.text().startswith('(')


def test_layer_statistics_widget_can_edit_camera_values(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    viewer.add_image(np.zeros((4, 5, 6), dtype=float))
    widget = LayerStatisticsWidget(viewer)

    widget._camera_ndisplay_value.setText('2')
    widget._apply_camera_ndisplay_from_input()
    widget._camera_displayed_axis_value.setText('(0, 2)')
    widget._apply_camera_displayed_axis_from_input()
    widget._camera_zoom_value.setText('2.25')
    widget._apply_camera_zoom_from_input()
    widget._camera_center_value.setText('(1, 2, 3)')
    widget._apply_camera_center_from_input()
    widget._camera_angle_value.setText('(4, 5, 6)')
    widget._apply_camera_angle_from_input()

    assert viewer.dims.ndisplay == 2
    assert tuple(viewer.dims.displayed) == (0, 2)
    assert np.isclose(viewer.camera.zoom, 2.25)
    np.testing.assert_allclose(np.asarray(viewer.camera.center, dtype=float), [1, 2, 3])
    np.testing.assert_allclose(np.asarray(viewer.camera.angles, dtype=float), [4, 5, 6])


def test_layer_statistics_widget_can_paste_camera_settings_from_clipboard(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    viewer.add_image(np.zeros((4, 5, 6), dtype=float))
    widget = LayerStatisticsWidget(viewer)

    clipboard = QApplication.clipboard()
    clipboard.setText(
        '\n'.join(
            [
                'Camera',
                'NDisplay: 2',
                'Displayed axis: (1, 2)',
                'Zoom: 1.75',
                'Center: (6, 7, 8)',
                'Angle: (9, 10, 11)',
            ]
        )
    )

    widget._paste_camera_from_clipboard()

    assert viewer.dims.ndisplay == 2
    assert tuple(viewer.dims.displayed) == (1, 2)
    assert np.isclose(viewer.camera.zoom, 1.75)
    np.testing.assert_allclose(np.asarray(viewer.camera.center, dtype=float), [6, 7, 8])
    np.testing.assert_allclose(
        np.asarray(viewer.camera.angles, dtype=float), [9, 10, 11]
    )


def test_layer_statistics_widget_can_copy_camera_settings_to_clipboard(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    viewer.add_image(np.zeros((4, 5, 6), dtype=float))
    widget = LayerStatisticsWidget(viewer)

    viewer.dims.ndisplay = 2
    viewer.camera.zoom = 2.5
    viewer.camera.center = (1.0, 2.0, 3.0)
    viewer.camera.angles = (4.0, 5.0, 6.0)
    widget._update_camera_statistics()

    widget._copy_camera_to_clipboard()
    copied = QApplication.clipboard().text()

    assert copied.startswith('Camera\n')
    assert 'NDisplay: 2' in copied
    assert 'Zoom: 2.5' in copied
    assert 'Center: (1, 2, 3)' in copied
    assert 'Angle: (4, 5, 6)' in copied
    assert 'Canvas (w, h): ' in copied


def test_layer_statistics_widget_shows_geometry_for_other_layer_types(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    layer = viewer.add_points(np.array([[1, 2], [3, 4]]), name='points')

    widget = LayerStatisticsWidget(viewer)
    widget._on_layer_changed(layer)

    assert widget._metadata_name_value.text() == 'points'
    assert widget._metadata_type_value.text() == 'Points'
    assert not widget._spacing_value.isReadOnly()
    assert not widget._origin_value.isReadOnly()
    assert not widget._direction_value.isReadOnly()
    widget._spacing_value.setText('(1.25, 2.5)')
    widget._apply_spacing_from_input()
    widget._origin_value.setText('(3, 4)')
    widget._apply_origin_from_input()
    assert widget._shape_value.text() != '-'
    assert widget._spacing_value.text() != '-'
    assert widget._origin_value.text() != '-'
    np.testing.assert_allclose(np.asarray(layer.scale, dtype=float), [1.25, 2.5])
    np.testing.assert_allclose(np.asarray(layer.translate, dtype=float), [3, 4])
    assert not widget._geometry_group.isHidden()
    assert widget._image_group.isHidden()
    assert widget._background_value.text() == '-'
    assert widget._class_count_value.text() == '-'
    assert widget._classes_present_value.text() == '-'
    assert widget._foreground_value.text() == '-'
    assert widget._labels_group.isHidden()


def test_layer_statistics_widget_follows_active_layer_by_default(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    image_layer = viewer.add_image(np.zeros((4, 4)), name='image')
    label_layer = viewer.add_labels(np.zeros((4, 4), dtype=int), name='labels')

    widget = LayerStatisticsWidget(viewer)

    assert widget._current_layer is label_layer

    viewer.layers.selection.active = image_layer

    assert widget._current_layer is image_layer
    assert widget._metadata_name_value.text() == 'image'
    assert widget._metadata_type_value.text() == 'Image'


def test_layer_statistics_widget_can_fix_to_current_layer(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    image_layer = viewer.add_image(np.zeros((4, 4)), name='image')
    label_layer = viewer.add_labels(np.zeros((4, 4), dtype=int), name='labels')

    widget = LayerStatisticsWidget(viewer)
    viewer.layers.selection.active = image_layer
    widget._fix_layer_checkbox.setChecked(True)

    viewer.layers.selection.active = label_layer

    assert widget._current_layer is image_layer


def test_layer_statistics_widget_fix_toggle_does_not_refresh_same_layer(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    viewer.add_image(np.zeros((4, 4)), name='image')
    widget = LayerStatisticsWidget(viewer)

    calls: list[int] = []

    def _track_update_statistics(event=None):
        del event
        calls.append(1)

    widget._update_statistics = _track_update_statistics

    widget._on_fix_layer_toggled(True)
    widget._on_fix_layer_toggled(False)

    assert calls == []


def test_layer_statistics_widget_can_disable_heavy_computation(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    layer = viewer.add_image(np.arange(12, dtype=float).reshape((3, 4)))

    widget = LayerStatisticsWidget(viewer)
    widget._compute_data_checkbox.setChecked(False)
    widget._on_layer_changed(layer, force=True)

    assert widget._status_indicator.toolTip() == 'Idle'
    assert widget._image_group.isVisible()
    assert widget._image_table.rowCount() == 1
    assert widget._image_table.item(0, 1).text() == 'Disabled'


def test_layer_statistics_widget_labels_keep_class_overview_when_disabled(
    make_napari_viewer,
):
    viewer = make_napari_viewer()
    layer = viewer.add_labels(np.array([[0, 1], [2, 0]], dtype=int))

    widget = LayerStatisticsWidget(viewer)
    widget._compute_data_checkbox.setChecked(False)
    widget._on_layer_changed(layer, force=True)

    assert widget._status_indicator.toolTip() == 'Idle'
    assert widget._labels_group.isVisible()
    assert widget._background_value.text() == '-'
    assert widget._class_count_value.text() == '2'
    assert widget._classes_present_value.text() == '2'
    assert widget._foreground_value.text() == '-'
    assert widget._classes_table.rowCount() == 2
    assert widget._classes_table.item(0, 0).text() == '1'
    assert widget._classes_table.item(0, 1).text() == '-'
    assert widget._classes_table.item(1, 0).text() == '2'
    assert widget._classes_table.item(1, 1).text() == '-'
