import platform
from unittest.mock import patch

import numpy as np
import pytest
from qtpy import API_NAME

try:
    from cmap import Colormap
except ImportError:
    pytest.skip("cmap not installed", allow_module_level=True)

from qtpy.QtCore import QRect
from qtpy.QtGui import QPainter, QPixmap
from qtpy.QtWidgets import QStyleOptionViewItem, QWidget

from superqt import QColormapComboBox
from superqt.cmap import (
    CmapCatalogComboBox,
    QColormapItemDelegate,
    QColormapLineEdit,
    _cmap_combo,
    draw_colormap,
)
from superqt.utils import qimage_to_array


def test_draw_cmap(qtbot):
    # draw into a QWidget
    wdg = QWidget()
    qtbot.addWidget(wdg)
    draw_colormap(wdg, "viridis")
    # draw into any QPaintDevice
    draw_colormap(QPixmap(), "viridis")
    # pass a painter an explicit colormap and a rect
    draw_colormap(QPainter(), Colormap(("red", "yellow", "blue")), QRect())
    # test with a border
    draw_colormap(wdg, "viridis", border_color="red", border_width=2)

    with pytest.raises(TypeError, match="Expected a QPainter or QPaintDevice instance"):
        draw_colormap(QRect(), "viridis")  # type: ignore

    with pytest.raises(TypeError, match="Expected a Colormap instance or something"):
        draw_colormap(QPainter(), "not a recognized string or cmap", QRect())


def test_cmap_draw_result():
    """Test that the image drawn actually looks correct."""
    # draw into any QPaintDevice
    w = 100
    h = 20
    pix = QPixmap(w, h)
    cmap = Colormap("viridis")
    draw_colormap(pix, cmap)

    ary1 = cmap(np.tile(np.linspace(0, 1, w), (h, 1)), bytes=True)
    ary2 = qimage_to_array(pix.toImage())

    # there are some subtle differences between how qimage draws and how
    # cmap draws, so we can't assert that the arrays are exactly equal.
    # they are visually indistinguishable, and numbers are close within 4 (/255) values
    # and linux, for some reason, is a bit more different``
    atol = 8 if platform.system() == "Linux" else 4
    np.testing.assert_allclose(ary1, ary2, atol=atol)

    cmap2 = Colormap(("#230777",), name="MyMap")
    draw_colormap(pix, cmap2)  # include transparency


def test_catalog_combo(qtbot):
    wdg = CmapCatalogComboBox()
    qtbot.addWidget(wdg)
    wdg.show()

    wdg.setCurrentText("viridis")
    assert wdg.currentColormap() == Colormap("viridis")


@pytest.mark.parametrize("filterable", [False, True])
def test_cmap_combo(qtbot, filterable):
    wdg = QColormapComboBox(allow_user_colormaps=True, filterable=filterable)
    qtbot.addWidget(wdg)
    wdg.show()
    assert wdg.userAdditionsAllowed()

    with qtbot.waitSignal(wdg.currentColormapChanged):
        wdg.addColormaps([Colormap("viridis"), "magma", ("red", "blue", "green")])
    assert wdg.currentColormap().name.split(":")[-1] == "viridis"

    with pytest.raises(ValueError, match="Invalid colormap"):
        wdg.addColormap("not a recognized string or cmap")

    assert wdg.currentColormap().name.split(":")[-1] == "viridis"
    assert wdg.currentIndex() == 0
    assert wdg.count() == 4  # includes "Add Colormap..."
    wdg.setCurrentColormap("magma")
    assert wdg.count() == 4  # make sure we didn't duplicate
    assert wdg.currentIndex() == 1

    if API_NAME == "PySide2":
        return  # the rest fails on CI... but works locally

    # click the Add Colormap... item
    with qtbot.waitSignal(wdg.currentColormapChanged):
        with patch.object(_cmap_combo._CmapNameDialog, "exec", return_value=True):
            wdg._on_activated(wdg.count() - 1)

    assert wdg.count() == 5
    # this could potentially fail in the future if cmap catalog changes
    # but mocking the return value of the dialog is also annoying
    assert wdg.itemColormap(3).name.split(":")[-1] == "accent"

    # click the Add Colormap... item, but cancel the dialog
    with patch.object(_cmap_combo._CmapNameDialog, "exec", return_value=False):
        wdg._on_activated(wdg.count() - 1)


def test_cmap_item_delegate(qtbot):
    wdg = CmapCatalogComboBox()
    qtbot.addWidget(wdg)
    view = wdg.view()
    delegate = view.itemDelegate()
    assert isinstance(delegate, QColormapItemDelegate)

    # smoke tests:
    painter = QPainter()
    option = QStyleOptionViewItem()
    index = wdg.model().index(0, 0)
    delegate._colormap_fraction = 1
    delegate.paint(painter, option, index)
    delegate._colormap_fraction = 0.33
    delegate.paint(painter, option, index)

    assert delegate.sizeHint(option, index) == delegate._item_size


def test_cmap_line_edit(qtbot, qapp):
    wdg = QColormapLineEdit()
    qtbot.addWidget(wdg)
    wdg.show()

    wdg.setColormap("viridis")
    assert wdg.colormap() == Colormap("viridis")
    wdg.setText("magma")  # also works if the name is recognized
    assert wdg.colormap() == Colormap("magma")
    qapp.processEvents()
    qtbot.wait(10)  # force the paintEvent

    wdg.setFractionalColormapWidth(1)
    assert wdg.fractionalColormapWidth() == 1
    wdg.update()
    qapp.processEvents()
    qtbot.wait(10)  # force the paintEvent

    wdg.setText("not-a-cmap")
    assert wdg.colormap() is None
    # or

    wdg.setFractionalColormapWidth(0.3)
    wdg.setColormap(None)
    assert wdg.colormap() is None
    qapp.processEvents()
    qtbot.wait(10)  # force the paintEvent
