from ngSkinTools2.api import PaintMode
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
from ngSkinTools2.api.python_compatibility import Object
from ngSkinTools2.signal import Signal
from ngSkinTools2.ui import qt
from ngSkinTools2.ui.layout import scale_multiplier


def curve_mapping(x, s, t):
    """
    provides a linear-to smooth curve mapping

    based on a paper https://arxiv.org/abs/2010.09714
    """
    epsilon = 0.000001

    if x < 0:
        return 0
    if x > 1:
        return 1
    if x < t:
        return (t * x) / (x + s * (t - x) + epsilon)

    return ((1 - t) * (x - 1)) / (1 - x - s * (t - x) + epsilon) + 1


class NumberSliderGroup(Object):
    """
    float spinner is the "main control" while the slider acts as complementary way to change value
    """

    slider_resolution = 1000.0
    infinity_max = 65535

    def __init__(self, value_type=float, min_value=0, max_value=1, soft_max=True, tooltip="", expo=None, decimals=3):
        self.value_range = 0
        self.min_value = 0
        self.max_value = 0

        self.float_mode = value_type == float

        self.__layout = layout = QtWidgets.QHBoxLayout()
        self.valueChanged = Signal("sliderGroupValueChanged")

        self.spinner = spinner = QtWidgets.QDoubleSpinBox() if self.float_mode else QtWidgets.QSpinBox()
        spinner.setKeyboardTracking(False)

        self.expo = expo
        self.expo_coefficient = 1.0

        spinner.setFixedWidth(80 * scale_multiplier)
        if self.float_mode:
            spinner.setDecimals(decimals)

        spinner.setToolTip(tooltip)

        self.slider = slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        slider.setMinimum(0)
        slider.setMaximum(self.slider_resolution)
        slider.setToolTip(tooltip)

        layout.addWidget(spinner)
        layout.addWidget(slider)

        self.set_range(min_value, max_value, soft_max=soft_max)

        @qt.on(spinner.valueChanged)
        def update_slider():
            slider_value = self.__to_slider_value(spinner.value())
            if slider.value() == slider_value:
                return
            with qt.signals_blocked(slider):
                slider.setValue(slider_value)
            self.valueChanged.emit()

        @qt.on(slider.valueChanged)
        def slider_dragging():
            slider_value = self.__from_slider_value(slider.value())
            with qt.signals_blocked(spinner):
                spinner.setValue(slider_value)

        @qt.on(slider.sliderReleased)
        def slider_drag_finished():
            self.valueChanged.emit()

        self.update_slider = update_slider

    def set_range(self, min_value, max_value, soft_max=True):
        with qt.signals_blocked(self.spinner):
            self.spinner.setMaximum(self.infinity_max if soft_max else max_value)
        self.min_value = min_value
        self.max_value = max_value
        self.value_range = max_value - min_value
        single_step = self.value_range / 100.0
        if not self.float_mode and single_step < 1:
            single_step = 1
        self.spinner.setSingleStep(single_step)

    def __to_slider_value(self, v):
        # formulas: https://www.desmos.com/calculator/gjwk5t3wmn

        x = float(v - self.min_value) / self.value_range

        y = x
        if self.expo == 'start':
            y = curve_mapping(x, self.expo_coefficient, 0)
        if self.expo == 'end':
            y = curve_mapping(x, self.expo_coefficient, 1)

        return y * self.slider_resolution

    def __from_slider_value(self, v):
        x = v / self.slider_resolution
        if self.expo == 'start':
            x = curve_mapping(x, self.expo_coefficient, 1)
        if self.expo == 'end':
            x = curve_mapping(x, self.expo_coefficient, 0)

        return self.min_value + self.value_range * x

    def layout(self):
        return self.__layout

    def value(self):
        return self.spinner.value()

    def value_trimmed(self):
        value = self.value()
        if self.min_value is not None and value < self.min_value:
            return self.min_value
        if self.max_value is not None and value > self.max_value:
            return self.max_value
        return value

    def set_value(self, value):
        if self.value == value:
            return
        self.spinner.setValue(value)
        self.update_slider()

    def set_enabled(self, enabled):
        self.spinner.setEnabled(enabled)
        self.slider.setEnabled(enabled)

    # noinspection PyPep8Naming
    def blockSignals(self, block):
        """
        a mimic of qt's blockSignals for both inner widgets
        """
        result = self.spinner.blockSignals(block)
        self.slider.blockSignals(block)
        self.valueChanged.enabled = not block
        return result

    def set_expo(self, expo, coefficient=3):
        self.expo = expo
        self.expo_coefficient = coefficient
        self.update_slider()


def set_paint_expo(number_group, paint_mode):
    """
    Sets number slider group expo according to paint mode.

    :type paint_mode: int
    :type number_group: NumberSliderGroup
    """
    intensity_expo = {
        PaintMode.add: ("start", 3),
        PaintMode.scale: ("end", 8),
        PaintMode.smooth: ("start", 3),
        PaintMode.sharpen: ("start", 3),
    }
    expo, c = intensity_expo.get(paint_mode, (None, 1))
    if number_group.expo == expo and number_group.expo_coefficient == c:
        return

    number_group.set_expo(expo=expo, coefficient=c)


def button_row(button_defs, side_menu=None):
    result = QtWidgets.QHBoxLayout()

    stretch_marker = "Marker"

    for i in (side_menu or []) + [stretch_marker] + button_defs:
        if i == stretch_marker:
            result.addStretch()
            continue
        label, handler = i
        btn = QtWidgets.QPushButton(label, minimumWidth=100)
        qt.on(btn.clicked)(handler)
        result.addWidget(btn)

    return result


class ColorButton(QtWidgets.QPushButton):
    def __init__(self):
        QtWidgets.QPushButton.__init__(self)
        self.color = None
        qt.on(self.clicked)(self.__pick_color)

        self.color_changed = Signal("color changed")

    def set_color(self, color):
        if isinstance(color, (list, tuple)):
            color = QtGui.QColor.fromRgb(color[0] * 255, color[1] * 255, color[2] * 255, 255)
        self.color = color
        self.setStyleSheet("background-color: %s;" % color.name())
        self.color_changed.emit()

    def get_color_3f(self):
        return [i / 255.0 for i in self.color.getRgb()[:3]]

    def __pick_color(self):
        color = QtWidgets.QColorDialog.getColor(initial=self.color, options=QtWidgets.QColorDialog.DontUseNativeDialog)
        if color.isValid():
            self.set_color(color)
