from enum import IntEnum
from functools import partial
from typing import Any

from qtpy.QtCore import QPoint, QSize, Qt, Signal
from qtpy.QtGui import QFontMetrics, QValidator
from qtpy.QtWidgets import (
    QAbstractSlider,
    QApplication,
    QDoubleSpinBox,
    QHBoxLayout,
    QSlider,
    QSpinBox,
    QStyle,
    QStyleOptionSpinBox,
    QVBoxLayout,
    QWidget,
)

from ..utils import signals_blocked
from ._sliders import QDoubleRangeSlider, QDoubleSlider, QRangeSlider


class LabelPosition(IntEnum):
    NoLabel = 0
    LabelsAbove = 1
    LabelsBelow = 2
    LabelsRight = 1
    LabelsLeft = 2


class EdgeLabelMode(IntEnum):
    NoLabel = 0
    LabelIsRange = 1
    LabelIsValue = 2


class _SliderProxy:
    _slider: QSlider

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

    def setValue(self, value) -> None:
        self._slider.setValue(value)

    def sliderPosition(self):
        return self._slider.sliderPosition()

    def setSliderPosition(self, pos) -> None:
        self._slider.setSliderPosition(pos)

    def minimum(self):
        return self._slider.minimum()

    def setMinimum(self, minimum):
        self._slider.setMinimum(minimum)

    def maximum(self):
        return self._slider.maximum()

    def setMaximum(self, maximum):
        self._slider.setMaximum(maximum)

    def singleStep(self):
        return self._slider.singleStep()

    def setSingleStep(self, step):
        self._slider.setSingleStep(step)

    def pageStep(self):
        return self._slider.pageStep()

    def setPageStep(self, step) -> None:
        self._slider.setPageStep(step)

    def setRange(self, min, max) -> None:
        self._slider.setRange(min, max)

    def tickInterval(self):
        return self._slider.tickInterval()

    def setTickInterval(self, interval) -> None:
        self._slider.setTickInterval(interval)

    def tickPosition(self):
        return self._slider.tickPosition()

    def setTickPosition(self, pos) -> None:
        self._slider.setTickPosition(pos)

    def __getattr__(self, name) -> Any:
        return getattr(self._slider, name)


def _handle_overloaded_slider_sig(args, kwargs):
    parent = None
    orientation = Qt.Orientation.Vertical
    errmsg = (
        "TypeError: arguments did not match any overloaded call:\n"
        "  QSlider(parent: QWidget = None)\n"
        "  QSlider(Qt.Orientation, parent: QWidget = None)"
    )
    if len(args) > 2:
        raise TypeError(errmsg)
    elif len(args) == 2:
        if kwargs:
            raise TypeError(errmsg)
        orientation, parent = args
    elif args:
        if isinstance(args[0], QWidget):
            if kwargs:
                raise TypeError(errmsg)
            parent = args[0]
        else:
            orientation = args[0]
    parent = kwargs.get("parent", parent)
    return parent, orientation


class QLabeledSlider(_SliderProxy, QAbstractSlider):
    editingFinished = Signal()

    EdgeLabelMode = EdgeLabelMode
    _slider_class = QSlider
    _slider: QSlider

    def __init__(self, *args, **kwargs) -> None:
        parent, orientation = _handle_overloaded_slider_sig(args, kwargs)

        super().__init__(parent)

        self._slider = self._slider_class()
        self._label = SliderLabel(self._slider, connect=self._setValue)
        self._edge_label_mode: EdgeLabelMode = EdgeLabelMode.LabelIsValue

        self._rename_signals()
        self._slider.actionTriggered.connect(self.actionTriggered.emit)
        self._slider.rangeChanged.connect(self.rangeChanged.emit)
        self._slider.sliderMoved.connect(self.sliderMoved.emit)
        self._slider.sliderPressed.connect(self.sliderPressed.emit)
        self._slider.sliderReleased.connect(self.sliderReleased.emit)
        self._slider.valueChanged.connect(self._label.setValue)
        self._slider.valueChanged.connect(self.valueChanged.emit)
        self._label.editingFinished.connect(self.editingFinished)

        self.setOrientation(orientation)

    def _setValue(self, value: float):
        """
        Convert the value from float to int before
        setting the slider value
        """
        self._slider.setValue(int(value))

    def _rename_signals(self):
        # for subclasses
        pass

    def setOrientation(self, orientation):
        """Set orientation, value will be 'horizontal' or 'vertical'."""
        self._slider.setOrientation(orientation)
        marg = (0, 0, 0, 0)
        if orientation == Qt.Orientation.Vertical:
            layout = QVBoxLayout()
            layout.addWidget(self._slider, alignment=Qt.AlignmentFlag.AlignHCenter)
            layout.addWidget(self._label, alignment=Qt.AlignmentFlag.AlignHCenter)
            self._label.setAlignment(Qt.AlignmentFlag.AlignCenter)
            layout.setSpacing(1)
        else:
            if self._edge_label_mode == EdgeLabelMode.NoLabel:
                marg = (0, 0, 5, 0)

            layout = QHBoxLayout()
            layout.addWidget(self._slider)
            layout.addWidget(self._label)
            self._label.setAlignment(Qt.AlignmentFlag.AlignRight)
            layout.setSpacing(6)

        old_layout = self.layout()
        if old_layout is not None:
            QWidget().setLayout(old_layout)

        layout.setContentsMargins(*marg)
        self.setLayout(layout)

    def edgeLabelMode(self) -> EdgeLabelMode:
        """Return current `EdgeLabelMode`."""
        return self._edge_label_mode

    def setEdgeLabelMode(self, opt: EdgeLabelMode) -> None:
        """Set the `EdgeLabelMode`."""
        if opt is EdgeLabelMode.LabelIsRange:
            raise ValueError(
                "mode must be one of 'EdgeLabelMode.NoLabel' or "
                "'EdgeLabelMode.LabelIsValue'."
            )

        self._edge_label_mode = opt
        if not self._edge_label_mode:
            self._label.hide()
            w = 5 if self.orientation() == Qt.Orientation.Horizontal else 0
            self.layout().setContentsMargins(0, 0, w, 0)
        else:
            if self.isVisible():
                self._label.show()
            self._label.setMode(opt)
            self._label.setValue(self._slider.value())
            self.layout().setContentsMargins(0, 0, 0, 0)

        QApplication.processEvents()


class QLabeledDoubleSlider(QLabeledSlider):
    _slider_class = QDoubleSlider
    _slider: QDoubleSlider
    _fvalueChanged = Signal(float)
    _fsliderMoved = Signal(float)
    _frangeChanged = Signal(float, float)

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.setDecimals(2)

    def _rename_signals(self):
        self.valueChanged = self._fvalueChanged
        self.sliderMoved = self._fsliderMoved
        self.rangeChanged = self._frangeChanged

    def decimals(self) -> int:
        return self._label.decimals()

    def setDecimals(self, prec: int):
        self._label.setDecimals(prec)


class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
    _valueChanged = Signal(tuple)
    editingFinished = Signal()

    LabelPosition = LabelPosition
    EdgeLabelMode = EdgeLabelMode
    _slider_class = QRangeSlider
    _slider: QRangeSlider

    def __init__(self, *args, **kwargs) -> None:
        parent, orientation = _handle_overloaded_slider_sig(args, kwargs)
        super().__init__(parent)
        self._rename_signals()

        self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
        self._handle_labels = []
        self._handle_label_position: LabelPosition = LabelPosition.LabelsAbove

        # for fine tuning label position
        self.label_shift_x = 0
        self.label_shift_y = 0

        self._slider = self._slider_class()
        self._slider.valueChanged.connect(self.valueChanged.emit)
        self._slider.rangeChanged.connect(self.rangeChanged.emit)

        self._min_label = SliderLabel(
            self._slider,
            alignment=Qt.AlignmentFlag.AlignLeft,
            connect=self._min_label_edited,
        )
        self._max_label = SliderLabel(
            self._slider,
            alignment=Qt.AlignmentFlag.AlignRight,
            connect=self._max_label_edited,
        )
        self._min_label.editingFinished.connect(self.editingFinished)
        self._max_label.editingFinished.connect(self.editingFinished)
        self.setEdgeLabelMode(EdgeLabelMode.LabelIsRange)

        self._slider.valueChanged.connect(self._on_value_changed)
        self._slider.rangeChanged.connect(self._on_range_changed)

        self._on_value_changed(self._slider.value())
        self._on_range_changed(self._slider.minimum(), self._slider.maximum())
        self.setOrientation(orientation)

    def _rename_signals(self):
        self.valueChanged = self._valueChanged

    def handleLabelPosition(self) -> LabelPosition:
        """Return where/whether labels are shown adjacent to slider handles."""
        return self._handle_label_position

    def setHandleLabelPosition(self, opt: LabelPosition) -> LabelPosition:
        """Set where/whether labels are shown adjacent to slider handles."""
        self._handle_label_position = opt
        for lbl in self._handle_labels:
            if not opt:
                lbl.hide()
            else:
                lbl.show()
        self.setOrientation(self.orientation())

    def edgeLabelMode(self) -> EdgeLabelMode:
        """Return current `EdgeLabelMode`."""
        return self._edge_label_mode

    def setEdgeLabelMode(self, opt: EdgeLabelMode):
        """Set `EdgeLabelMode`, controls what is shown at the min/max labels."""
        self._edge_label_mode = opt
        if not self._edge_label_mode:
            self._min_label.hide()
            self._max_label.hide()
        else:
            if self.isVisible():
                self._min_label.show()
                self._max_label.show()
            self._min_label.setMode(opt)
            self._max_label.setMode(opt)
        if opt == EdgeLabelMode.LabelIsValue:
            v0, *_, v1 = self._slider.value()
            self._min_label.setValue(v0)
            self._max_label.setValue(v1)
        elif opt == EdgeLabelMode.LabelIsRange:
            self._min_label.setValue(self._slider.minimum())
            self._max_label.setValue(self._slider.maximum())
        QApplication.processEvents()
        self._reposition_labels()

    def _reposition_labels(self):
        if (
            not self._handle_labels
            or self._handle_label_position == LabelPosition.NoLabel
        ):
            return

        horizontal = self.orientation() == Qt.Orientation.Horizontal
        labels_above = self._handle_label_position == LabelPosition.LabelsAbove

        last_edge = None
        for i, label in enumerate(self._handle_labels):
            rect = self._slider._handleRect(i)
            dx = -label.width() / 2
            dy = -label.height() / 2
            if labels_above:
                if horizontal:
                    dy *= 3
                else:
                    dx *= -1
            else:
                if horizontal:
                    dy *= -1
                else:
                    dx *= 3
            pos = self._slider.mapToParent(rect.center())
            pos += QPoint(int(dx + self.label_shift_x), int(dy + self.label_shift_y))
            if last_edge is not None:
                # prevent label overlap
                if horizontal:
                    pos.setX(int(max(pos.x(), last_edge.x() + label.width() / 2 + 12)))
                else:
                    pos.setY(int(min(pos.y(), last_edge.y() - label.height() / 2 - 4)))
            label.move(pos)
            last_edge = pos
            label.clearFocus()
            label.show()
        self.update()

    def _min_label_edited(self, val):
        if self._edge_label_mode == EdgeLabelMode.LabelIsRange:
            self.setMinimum(val)
        else:
            v = list(self._slider.value())
            v[0] = val
            self.setValue(v)
        self._reposition_labels()

    def _max_label_edited(self, val):
        if self._edge_label_mode == EdgeLabelMode.LabelIsRange:
            self.setMaximum(val)
        else:
            v = list(self._slider.value())
            v[-1] = val
            self.setValue(v)
        self._reposition_labels()

    def _on_value_changed(self, v):
        if self._edge_label_mode == EdgeLabelMode.LabelIsValue:
            self._min_label.setValue(v[0])
            self._max_label.setValue(v[-1])

        if len(v) != len(self._handle_labels):
            for lbl in self._handle_labels:
                lbl.setParent(None)
                lbl.deleteLater()
            self._handle_labels.clear()
            for n, val in enumerate(self._slider.value()):
                _cb = partial(self._slider.setSliderPosition, index=n)
                s = SliderLabel(self._slider, parent=self, connect=_cb)
                s.editingFinished.connect(self.editingFinished)
                s.setValue(val)
                self._handle_labels.append(s)
        else:
            for val, label in zip(v, self._handle_labels):
                label.setValue(val)
        self._reposition_labels()

    def _on_range_changed(self, min, max):
        if (min, max) != (self._slider.minimum(), self._slider.maximum()):
            self._slider.setRange(min, max)
        for lbl in self._handle_labels:
            lbl.setRange(min, max)
        if self._edge_label_mode == EdgeLabelMode.LabelIsRange:
            self._min_label.setValue(min)
            self._max_label.setValue(max)
        self._reposition_labels()

    # def setValue(self, value) -> None:
    #     super().setValue(value)
    #     self.sliderChange(QSlider.SliderValueChange)

    def setRange(self, min, max) -> None:
        self._on_range_changed(min, max)

    def setOrientation(self, orientation):
        """Set orientation, value will be 'horizontal' or 'vertical'."""

        self._slider.setOrientation(orientation)
        if orientation == Qt.Orientation.Vertical:
            layout = QVBoxLayout()
            layout.setSpacing(1)
            layout.addWidget(self._max_label)
            layout.addWidget(self._slider)
            layout.addWidget(self._min_label)
            # TODO: set margins based on label width
            if self._handle_label_position == LabelPosition.LabelsLeft:
                marg = (30, 0, 0, 0)
            elif self._handle_label_position == LabelPosition.NoLabel:
                marg = (0, 0, 0, 0)
            else:
                marg = (0, 0, 20, 0)
            layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
        else:
            layout = QHBoxLayout()
            layout.setSpacing(7)
            if self._handle_label_position == LabelPosition.LabelsBelow:
                marg = (0, 0, 0, 25)
            elif self._handle_label_position == LabelPosition.NoLabel:
                marg = (0, 0, 0, 0)
            else:
                marg = (0, 25, 0, 0)
            layout.addWidget(self._min_label)
            layout.addWidget(self._slider)
            layout.addWidget(self._max_label)

        # remove old layout
        old_layout = self.layout()
        if old_layout is not None:
            QWidget().setLayout(old_layout)

        self.setLayout(layout)
        layout.setContentsMargins(*marg)
        super().setOrientation(orientation)
        QApplication.processEvents()
        self._reposition_labels()

    def resizeEvent(self, a0) -> None:
        super().resizeEvent(a0)
        self._reposition_labels()


class QLabeledDoubleRangeSlider(QLabeledRangeSlider):
    _slider_class = QDoubleRangeSlider
    _slider: QDoubleRangeSlider
    _frangeChanged = Signal(float, float)

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.setDecimals(2)

    def _rename_signals(self):
        super()._rename_signals()
        self.rangeChanged = self._frangeChanged

    def decimals(self) -> int:
        return self._min_label.decimals()

    def setDecimals(self, prec: int):
        self._min_label.setDecimals(prec)
        self._max_label.setDecimals(prec)
        for lbl in self._handle_labels:
            lbl.setDecimals(prec)


class SliderLabel(QDoubleSpinBox):
    def __init__(
        self,
        slider: QSlider,
        parent=None,
        alignment=Qt.AlignmentFlag.AlignCenter,
        connect=None,
    ) -> None:
        super().__init__(parent=parent)
        self._slider = slider
        self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        self.setMode(EdgeLabelMode.LabelIsValue)
        self.setDecimals(0)

        self.setRange(slider.minimum(), slider.maximum())
        slider.rangeChanged.connect(self._update_size)
        self.setAlignment(alignment)
        self.setButtonSymbols(QSpinBox.ButtonSymbols.NoButtons)
        self.setStyleSheet("background:transparent; border: 0;")
        if connect is not None:
            self.editingFinished.connect(lambda: connect(self.value()))
        self.editingFinished.connect(self._silent_clear_focus)
        self._update_size()

    def _silent_clear_focus(self):
        with signals_blocked(self):
            self.clearFocus()

    def setDecimals(self, prec: int) -> None:
        super().setDecimals(prec)
        self._update_size()

    def _update_size(self, *_):
        # fontmetrics to measure the width of text
        fm = QFontMetrics(self.font())
        h = self.sizeHint().height()
        fixed_content = self.prefix() + self.suffix() + " "

        if self._mode == EdgeLabelMode.LabelIsValue:
            # determine width based on min/max/specialValue
            mintext = self.textFromValue(self.minimum())[:18] + fixed_content
            maxtext = self.textFromValue(self.maximum())[:18] + fixed_content
            w = max(0, _fm_width(fm, mintext))
            w = max(w, _fm_width(fm, maxtext))
            if self.specialValueText():
                w = max(w, _fm_width(fm, self.specialValueText()))
        else:
            w = max(0, _fm_width(fm, self.textFromValue(self.value()))) + 3

        w += 3  # cursor blinking space
        # get the final size hint
        opt = QStyleOptionSpinBox()
        self.initStyleOption(opt)
        size = self.style().sizeFromContents(
            QStyle.ContentsType.CT_SpinBox, opt, QSize(w, h), self
        )
        self.setFixedSize(size)

    def setValue(self, val: Any) -> None:
        super().setValue(val)
        if self._mode == EdgeLabelMode.LabelIsRange:
            self._update_size()

    def setMaximum(self, max: int) -> None:
        super().setMaximum(max)
        if self._mode == EdgeLabelMode.LabelIsValue:
            self._update_size()

    def setMinimum(self, min: int) -> None:
        super().setMinimum(min)
        if self._mode == EdgeLabelMode.LabelIsValue:
            self._update_size()

    def setMode(self, opt: EdgeLabelMode):
        # when the edge labels are controlling slider range,
        # we want them to have a big range, but not have a huge label
        self._mode = opt
        if opt == EdgeLabelMode.LabelIsRange:
            self.setMinimum(-9999999)
            self.setMaximum(9999999)
            try:
                self._slider.rangeChanged.disconnect(self.setRange)
            except Exception:
                pass
        else:
            self.setMinimum(self._slider.minimum())
            self.setMaximum(self._slider.maximum())
            self._slider.rangeChanged.connect(self.setRange)
        self._update_size()

    def validate(self, input: str, pos: int):
        # fake like an integer spinbox
        if "." in input and self.decimals() < 1:
            return QValidator.Invalid, input, len(input)
        return super().validate(input, pos)


def _fm_width(fm, text):
    if hasattr(fm, "horizontalAdvance"):
        return fm.horizontalAdvance(text)
    return fm.width(text)
