import logging

from PyQt5 import QtGui, QtCore, QtWidgets
import numpy as np

from .ticker import Ticker


logger = logging.getLogger(__name__)


class ScanWidget(QtWidgets.QWidget):
    startChanged = QtCore.pyqtSignal(float)
    stopChanged = QtCore.pyqtSignal(float)
    numChanged = QtCore.pyqtSignal(int)

    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.zoomMargin = .1
        self.zoomFactor = 1.05
        self.dynamicRange = 1e9
        self.suffix = ""

        self.ticker = Ticker()

        self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
        action = QtWidgets.QAction("V&iew range", self)
        action.setShortcut(QtGui.QKeySequence("CTRL+i"))
        action.setShortcutContext(QtCore.Qt.WidgetShortcut)
        action.triggered.connect(self.viewRange)
        self.addAction(action)
        action = QtWidgets.QAction("Sna&p range", self)
        action.setShortcut(QtGui.QKeySequence("CTRL+p"))
        action.setShortcutContext(QtCore.Qt.WidgetShortcut)
        action.triggered.connect(self.snapRange)
        self.addAction(action)

        qfm = QtGui.QFontMetrics(self.font())
        self._labelSize = QtCore.QSize(
            (self.ticker.precision + 5)*qfm.averageCharWidth(),
            qfm.lineSpacing())

        self._start, self._stop, self._num = None, None, None
        self._min, self._max = float("-inf"), float("inf")
        self._axisView = None
        self._offset, self._drag, self._rubber = None, None, None

    def sizeHint(self):
        return self.minimumSizeHint()

    def minimumSizeHint(self):
        return QtCore.QSize(int(2.5*3*self._labelSize.width()),
                            4*self._labelSize.height())

    def _axisToPixel(self, val):
        a, b = self._axisView
        return a + val*b

    def _pixelToAxis(self, val):
        a, b = self._axisView
        return (val - a)/b

    def _setView(self, left, scale):
        self._axisView = left, scale
        self.update()

    def _setViewAxis(self, center, scale):
        if center:
            scale = min(scale, self.dynamicRange/abs(center))
        left = self.width()/2 - center*scale
        self._setView(left, scale)

    def _clamp(self, v):
        if v is None:
            return None
        v = max(self._min, v)
        v = min(self._max, v)
        return v

    def setStart(self, val):
        val = self._clamp(val)
        if self._start == val:
            return
        self._start = val
        self.update()
        self.startChanged.emit(val)

    def setStop(self, val):
        val = self._clamp(val)
        if self._stop == val:
            return
        self._stop = val
        self.update()
        self.stopChanged.emit(val)

    def setNum(self, val):
        if self._num == val:
            return
        self._num = val
        self.update()
        self.numChanged.emit(val)

    def setMinimum(self, v):
        self._min = v
        self.setStart(self._start)
        self.setStop(self._stop)

    def setMaximum(self, v):
        self._max = v
        self.setStart(self._start)
        self.setStop(self._stop)

    def setDecimals(self, n):
        # TODO
        # the axis should always use the most compact notation
        # do not:
        # self.ticker.precision = n
        pass

    def setSingleStep(self, v):
        # TODO
        # use this (and/or maybe decimals) to snap to "nice" values when
        # dragging
        pass

    def setSuffix(self, v):
        self.suffix = v

    def viewRange(self):
        center = (self._stop + self._start)/2
        scale = self.width()*(1 - 2*self.zoomMargin)
        if self._stop != self._start:
            scale /= abs(self._stop - self._start)
        else:
            scale = self.dynamicRange
        self._setViewAxis(center, scale)

    def snapRange(self):
        self.setStart(self._pixelToAxis(self.zoomMargin*self.width()))
        self.setStop(self._pixelToAxis((1 - self.zoomMargin)*self.width()))

    def mousePressEvent(self, ev):
        if ev.buttons() ^ ev.button():  # buttons changed
            ev.ignore()
            return
        if ev.modifiers() & QtCore.Qt.ShiftModifier:
            self._drag = "select"
            self.setStart(self._pixelToAxis(ev.x()))
            self.setStop(self._start)
        elif ev.modifiers() & QtCore.Qt.ControlModifier:
            self._drag = "zoom"
            self._offset = QtCore.QPoint(ev.x(), 0)
            self._rubber = QtWidgets.QRubberBand(
                QtWidgets.QRubberBand.Rectangle, self)
            self._rubber.setGeometry(QtCore.QRect(
                self._offset, QtCore.QPoint(ev.x(), self.height() - 1)))
            self._rubber.show()
        else:
            qfm = QtGui.QFontMetrics(self.font())
            if ev.y() <= 2.5*qfm.lineSpacing():
                self._drag = "axis"
                self._offset = ev.x() - self._axisView[0]
            # testing should match inverse drawing order for start/stop
            elif abs(self._axisToPixel(self._stop) -
                     ev.x()) < qfm.lineSpacing()/2:
                self._drag = "stop"
                self._offset = ev.x() - self._axisToPixel(self._stop)
            elif abs(self._axisToPixel(self._start) -
                     ev.x()) < qfm.lineSpacing()/2:
                self._drag = "start"
                self._offset = ev.x() - self._axisToPixel(self._start)
            else:
                self._drag = "both"
                self._offset = (ev.x() - self._axisToPixel(self._start),
                                ev.x() - self._axisToPixel(self._stop))

    def mouseMoveEvent(self, ev):
        if not self._drag:
            ev.ignore()
            return
        if self._drag == "select":
            self.setStop(self._pixelToAxis(ev.x()))
        elif self._drag == "zoom":
            self._rubber.setGeometry(QtCore.QRect(
                self._offset, QtCore.QPoint(ev.x(), self.height() - 1)
            ).normalized())
        elif self._drag == "axis":
            self._setView(ev.x() - self._offset, self._axisView[1])
        elif self._drag == "start":
            self.setStart(self._pixelToAxis(ev.x() - self._offset))
        elif self._drag == "stop":
            self.setStop(self._pixelToAxis(ev.x() - self._offset))
        elif self._drag == "both":
            self.setStart(self._pixelToAxis(ev.x() - self._offset[0]))
            self.setStop(self._pixelToAxis(ev.x() - self._offset[1]))

    def mouseReleaseEvent(self, ev):
        if self._drag == "zoom":
            self._rubber.hide()
            if self._rubber.geometry().width():
                scale = self._axisView[1]
                scale *= self.width()/self._rubber.geometry().width()
                center = self._pixelToAxis(
                    self._rubber.geometry().center().x())
                self._setViewAxis(center, scale)
        self._drag = None

    def _zoom(self, z, x):
        a, b = self._axisView
        scale = z*b
        left = x + z*(a - x)
        if z > 1 and abs(left - self.width()/2) > self.dynamicRange:
            return
        self._setView(left, scale)

    def wheelEvent(self, ev):
        y = round(ev.angleDelta().y()/120.)
        if not y:
            return
        if ev.modifiers() & QtCore.Qt.ShiftModifier:
            self.setNum(max(1, self._num + y))
        else:
            self._zoom(self.zoomFactor**y, ev.x())

    def resizeEvent(self, ev):
        if not ev.oldSize().isValid() or not ev.oldSize().width():
            self.viewRange()
            return
        self.ticker.min_ticks = max(
            3, int(ev.size().width()/(2.5*self._labelSize.width())))
        scale = self._axisView[1]
        scale *= ev.size().width()/ev.oldSize().width()
        center = self._pixelToAxis(ev.oldSize().width()/2)
        self._setViewAxis(center, scale)

    def paintEvent(self, ev):
        painter = QtGui.QPainter(self)
        qfm = QtGui.QFontMetrics(painter.font())
        avgCharWidth = qfm.averageCharWidth()
        lineSpacing = qfm.lineSpacing()
        descent = qfm.descent()
        ascent = qfm.ascent()
        painter.translate(0, ascent)

        ticks, prefix, labels = self.ticker(self._pixelToAxis(0),
                                            self._pixelToAxis(self.width()))
        rect = QtCore.QRect(0, 0, self.width(), lineSpacing)
        painter.drawText(rect, QtCore.Qt.AlignLeft, prefix)
        painter.drawText(rect, QtCore.Qt.AlignRight, self.suffix)

        painter.translate(0, lineSpacing + ascent)

        for t, l in zip(ticks, labels):
            t = self._axisToPixel(t)
            painter.drawText(int(t - len(l)/2*avgCharWidth), 0, l)
            painter.drawLine(int(t), descent, int(t), int(lineSpacing/2))
        painter.translate(0, int(lineSpacing/2))

        painter.drawLine(0, 0, self.width(), 0)

        for p in np.linspace(self._axisToPixel(self._start),
                             self._axisToPixel(self._stop),
                             self._num):
            painter.drawLine(int(p), 0, int(p), int(lineSpacing/2))
        painter.translate(0, int(lineSpacing/2))

        for x, c in (self._start, QtCore.Qt.blue), (self._stop, QtCore.Qt.red):
            x = self._axisToPixel(x)
            painter.setPen(c)
            painter.setBrush(c)
            painter.drawPolygon(*(QtCore.QPointF(*i) for i in [
                (int(x), 0), (int(x - lineSpacing/2), lineSpacing),
                (int(x + lineSpacing/2), lineSpacing)]))
