import pyqtgraph as pg
from pyqtgraph.graphicsItems.GraphicsWidgetAnchor import GraphicsWidgetAnchor, Point
from pyqtgraph.graphicsItems.PlotItem import PlotItem
from qgis.PyQt import Qt, QtCore, QtGui


class CustomRegionItem(pg.LinearRegionItem):
    """
    pg.LinearRegionItem abstract subclass.
    Lines are not individually movable.
    mouseDragEvent should be overriden to not zooming when the user drag region above or below data bounds.
    Finally, region size stay constant when dragged.
    """

    def __init__(self, **kwargs):
        pg.LinearRegionItem.__init__(self, **kwargs)
        self.blocked_at = None
        self.is_upper_line_blocked = None
        self.tolerance = 0.9999
        for line in self.lines:
            line.setMovable(False)

    def _setBounds(self, bounds):
        super()._setBounds(bounds)
        self.blocked_at = None
        self.is_upper_line_blocked = None

    def setRegion(self, rgn):
        super().setRegion(rgn)
        self.blocked_at = None
        self.is_upper_line_blocked = None

    def moveRegion(self, ev):
        self.lines[0].blockSignals(True)  # only want to update once

        for i, l in enumerate(self.lines):
            l.setPos(self.cursorOffsets[i] + ev.pos())

        self.lines[0].blockSignals(False)
        self.prepareGeometryChange()


class DepthRegionItem(CustomRegionItem):
    def mouseDragEvent(self, ev):

        if not self.movable or ev.button() != QtCore.Qt.MouseButton.LeftButton:
            return
        ev.accept()

        if ev.isStart():
            bdp = ev.buttonDownPos()
            self.cursorOffsets = [l.pos() - bdp for l in self.lines]
            self.startPositions = [l.pos() for l in self.lines]
            self.region_size = self.startPositions[1] - self.startPositions[0]
            self.threshold = abs(self.region_size.y())
            self.moving = True

        if not self.moving:
            return

        # current region size
        delta = abs(((self.lines[1].pos()) - (self.lines[0].pos())).y())

        moved = False

        # small variation of region size is tolered because of numerical precision
        # normal behavior
        if delta >= self.threshold * self.tolerance and self.blocked_at is None:
            moved = True
            self.moveRegion(ev)

        # if region have been blocked to a bound, let the user drag it again on the opposite direction
        elif self.blocked_at is not None:
            if (ev.pos().y() < self.blocked_at and self.is_upper_line_blocked) or (
                ev.pos().y() > self.blocked_at and not self.is_upper_line_blocked
            ):

                self.moveRegion(ev)

                self.blocked_at = None

        # if a line is outside bounds, i.e. region decrease, set region to the bounding value
        else:
            # get upperline
            lower_line, upper_line = sorted(self.lines, key=lambda x: x.pos().y())
            lower_line_index = [i for i, l in enumerate(self.lines) if l == lower_line][
                0
            ]

            # get blocked line
            self.is_upper_line_blocked = upper_line.pos().y() in upper_line.bounds()
            self.lines[0].blockSignals(True)
            if self.is_upper_line_blocked:
                lower_line.setPos(upper_line.pos() - self.region_size * self.tolerance)
                upper_line.setPos(upper_line.bounds()[1])
            else:
                lower_line.setPos(lower_line.bounds()[0])
                upper_line.setPos(lower_line.pos() + self.region_size * self.tolerance)

            self.lines[0].blockSignals(False)
            self.prepareGeometryChange()
            moved = True
            self.blocked_at = lower_line.pos().y() + abs(
                self.cursorOffsets[lower_line_index].y()
            )

        if ev.isFinish():
            self.sigRegionChanged.emit(self)

            self.moving = False
            self.sigRegionChangeFinished.emit(self)
        elif moved:
            self.sigRegionChanged.emit(self)


class TimeRegionItem(CustomRegionItem):
    def mouseDragEvent(self, ev):

        if not self.movable or ev.button() != QtCore.Qt.MouseButton.LeftButton:
            return
        ev.accept()

        if ev.isStart():
            bdp = ev.buttonDownPos()
            self.cursorOffsets = [l.pos() - bdp for l in self.lines]
            self.startPositions = [l.pos() for l in self.lines]
            self.region_size = self.startPositions[1] - self.startPositions[0]
            self.threshold = abs(self.region_size.x())
            self.moving = True

        if not self.moving:
            return

        # current region size
        delta = abs(((self.lines[1].pos()) - (self.lines[0].pos())).x())

        moved = False

        # 1% variation of region size is tolered because of numerical precision
        # normal behavior
        if delta >= self.threshold * self.tolerance and self.blocked_at is None:
            moved = True
            self.moveRegion(ev)

        # if region have been blocked to a bound, let the user drag it again on the opposite direction
        elif self.blocked_at is not None:
            if (ev.pos().x() < self.blocked_at and self.is_upper_line_blocked) or (
                ev.pos().x() > self.blocked_at and not self.is_upper_line_blocked
            ):

                self.moveRegion(ev)

                self.blocked_at = None

        # if a line is outside bounds, i.e. region decrease, set region to the bounding value
        else:
            # get upperline
            lower_line, upper_line = sorted(self.lines, key=lambda x: x.pos().x())
            lower_line_index = [i for i, l in enumerate(self.lines) if l == lower_line][
                0
            ]

            # get blocked line
            self.is_upper_line_blocked = upper_line.pos().x() in upper_line.bounds()
            self.lines[0].blockSignals(True)
            if self.is_upper_line_blocked:
                lower_line.setPos(upper_line.pos() - self.region_size * self.tolerance)
                upper_line.setPos(upper_line.bounds()[1])
            else:
                lower_line.setPos(lower_line.bounds()[0])
                upper_line.setPos(lower_line.pos() + self.region_size * self.tolerance)

            self.lines[0].blockSignals(False)
            self.prepareGeometryChange()
            moved = True
            self.blocked_at = lower_line.pos().x() + abs(
                self.cursorOffsets[lower_line_index].x()
            )

        if ev.isFinish():
            self.sigRegionChanged.emit(self)

            self.moving = False
            self.sigRegionChangeFinished.emit(self)
        elif moved:
            self.sigRegionChanged.emit(self)


class MiniMap(GraphicsWidgetAnchor, PlotItem):
    """
    Abstract class defining a minimap.
    """

    def __init__(
        self,
        parent=None,
        name=None,
        labels=None,
        title=None,
        viewBox=None,
        axisItems=None,
        enableMenu=True,
        **kargs
    ):

        self.lr = None

        PlotItem.__init__(
            self,
            parent=None,
            name=None,
            labels=None,
            title=None,
            viewBox=None,
            axisItems=None,
            enableMenu=True,
            **kargs
        )
        GraphicsWidgetAnchor.__init__(self)

        self.plotwidget = parent
        self.getViewBox().setMouseEnabled(False, False)
        self.getAxis("bottom").setWidth(0)
        self.getAxis("top").setWidth(0)
        self.hideAxis("left")
        self.hideAxis("right")
        self.hideAxis("top")
        self.hideAxis("bottom")
        self.getViewBox().setBorder(pg.mkPen(color="black", width=1))
        self.getViewBox().setBackgroundColor("white")
        self.plotwidget.geometryChanged.connect(self._setGeom)
        self.hideButtons()

    def mouseClickEvent(self, ev):
        if ev.buttons() == Qt.RightButton:
            ev.ignore()

    def onMove(self):
        raise NotImplementedError

    def copy_item(self, item):
        new_item = None
        if hasattr(item, "copy_item"):
            new_item = item.copy_item()
        return new_item

    def addItem(self, item, *args, **kargs):
        raise NotImplementedError

    def _setRange(self, range: list = None):
        raise NotImplementedError

    def _setGeom(self):
        raise NotImplementedError

    def setOffset(self):
        raise NotImplementedError

    def set_stacked_items(self, items: list) -> None:
        """
        For stacked plot, we allow multiple items.
        """
        self.clear()

        for item in items:
            new_item = self.copy_item(item)
            if new_item is not None:
                pg.PlotItem.addItem(self, new_item)

        pg.PlotItem.addItem(self, self.lr)
        self._setRange()


class DepthMiniMap(MiniMap):
    def __init__(
        self,
        parent=None,
        name=None,
        labels=None,
        title=None,
        viewBox=None,
        axisItems=None,
        enableMenu=True,
        **kargs
    ):

        super().__init__(
            parent, name, labels, title, viewBox, axisItems, enableMenu, **kargs
        )

        self.parent = parent.getViewBox()
        self.setParentItem(self.parent)
        self.getViewBox().invertY(True)
        self.setOffset()
        self._setGeom()

    def onMove(self):
        widget = self.parent.parentWidget()
        range = sorted(widget.viewRange()[1])
        if self.lr:
            # block signal to avoid infinite loop
            blocker = QtCore.QSignalBlocker(self.lr)
            self.lr.setRegion(range)
            blocker.unblock()

    def addItem(self, item, *args, **kargs):
        """
        Must construct a copy of item
        """
        # create LinearRegionItem after 1st item insertion
        if not self.lr:
            self.lr = DepthRegionItem(
                orientation="horizontal", movable=True, swapMode="block"
            )
            # mouseDragEvent is sent to this item in priority
            self.lr.setZValue(100)

            self.lr.sigRegionChanged.connect(self.plotwidget.onMinimapMove)

        new_item = self.copy_item(item)
        if new_item is not None:
            self.clear()
            pg.PlotItem.addItem(self, new_item, *args, **kargs)

        pg.PlotItem.addItem(self, self.lr)
        self._setRange()

    def _setRange(self, range: list = None):

        if self.lr:
            if range is None:
                range = sorted(self.plotwidget.getViewBox().state["limits"]["yLimits"])

            self.setRange(yRange=range, padding=0)
            self.lr.setBounds(range)

    def _setGeom(self):
        """
        Set minimap contours
        """
        axe_rect = self.parent.geometry()
        axe_rect.setWidth(self.parent.width() * 0.10)
        self.setGeometry((axe_rect))

    def setOffset(self):
        """
        Set the offset position relative to the parent.
        Should be anchored at graph origin.

        """
        # offset is the lag relative to parent selected corner

        offset = (0, 0)
        offset = Point(offset)
        # Corner reference selection. 0 is left/up. 1 is right/bottom
        self.anchor(itemPos=(0, 0), parentPos=(0, 0), offset=offset)


class TimeMiniMap(MiniMap):
    def __init__(
        self,
        parent=None,
        name=None,
        labels=None,
        title=None,
        viewBox=None,
        axisItems=None,
        enableMenu=True,
        **kargs
    ):

        super().__init__(
            parent, name, labels, title, viewBox, axisItems, enableMenu, **kargs
        )

        self.parent = parent.getViewBox()
        self.setParentItem(self.parent)
        self.getViewBox().invertY(False)
        self.setOffset()
        self._setGeom()

    def onMove(self):
        widget = self.parent.parentWidget()
        range = sorted(widget.viewRange()[0])
        if self.lr:
            # block signal to avoid infinite loop
            blocker = QtCore.QSignalBlocker(self.lr)
            self.lr.setRegion(range)
            blocker.unblock()

    def addItem(self, item, *args, **kargs):
        """
        Must construct a copy of item
        """
        # create LinearRegionItem after 1st item insertion
        if not self.lr:
            self.lr = TimeRegionItem(
                orientation="vertical", movable=True, swapMode="block"
            )
            self.lr.setZValue(100)
            self.lr.sigRegionChanged.connect(self.plotwidget.onMinimapMove)

        new_item = self.copy_item(item)
        if new_item is not None:
            self.clear()
            pg.PlotItem.addItem(self, new_item, *args, **kargs)
            pg.PlotItem.addItem(self, self.lr)

        self._setRange()

    def _setRange(self, range: list = None):

        if self.lr:
            if range is None:
                range = sorted(self.plotwidget.getViewBox().state["limits"]["xLimits"])

            self.setRange(xRange=range, padding=0)
            self.lr.setBounds(range)

    def _setGeom(self):
        """
        Set minimap contours
        """
        axe_rect = self.parent.geometry()
        axe_rect.setHeight(self.parent.height() * 0.10)
        self.setGeometry((axe_rect))

    def setOffset(self):
        """
        Set the offset position relative to the parent.
        Should be anchored at graph origin.

        """
        # offset is the lag relative to parent selected corner.
        offset = (0, 0)
        offset = Point(offset)
        # Corner reference selection. 0 is left/up. 1 is right/bottom
        self.anchor(itemPos=(0, 1), parentPos=(0, 1), offset=offset)
