#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Custom Mini Canvas Preview, a custom Qt widget
# Copyright (C) 2011-2020 Filipe Coelho <falktx@falktx.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# For a full copy of the GNU General Public License see the doc/GPL.txt file.

# ---------------------------------------------------------------------------------------------------------------------
# Imports (Global)

from math import floor, ceil

from PyQt5.QtCore import pyqtSignal, Qt, QRectF, QTimer, QEvent, QPoint
from PyQt5.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QPixmap
from PyQt5.QtWidgets import QFrame, QGraphicsScene

# ---------------------------------------------------------------------------------------------------------------------
# Antialiasing settings

from patchcanvas import options, ANTIALIASING_FULL

# ---------------------------------------------------------------------------------------------------------------------
# Widget Class

class CanvasPreviewFrame(QFrame):
    miniCanvasMoved = pyqtSignal(float, float)

    # x = 2
    # y = 2
    # w = width - 4
    # h = height - 3
    # bounds -1 px

    _kRectX = 0
    _kRectY = 1
    _kRectWidth = 2
    _kRectHeight = 3

    _kCursorName = 0
    _kCursorZoom = 1
    _kCursorZoomIn = 2
    _kCursorZoomOut = 3
    _kCursorZoomCount = 4

    _MOUSE_MODE_NONE = 0
    _MOUSE_MODE_MOVE = 1
    _MOUSE_MODE_SCALE = 2

    _RUBBERBAND_BLENDING_DEFAULT = 0
    _RUBBERBAND_BLENDING_PLUS = 1
    _RUBBERBAND_BLENDING_DIFF = 2

    # -----------------------------------------------------------------------------------------------------------------

    def __init__(self, parent):
        QFrame.__init__(self, parent)

        #self.setMinimumWidth(210)
        #self.setMinimumHeight(162)
        #self.setMaximumHeight(162)

        self.fRealParent = None
        self.fInternalWidth  = 210-6 # -4 for width + -1px*2 bounds
        self.fInternalHeight = 162-5 # -3 for height + -1px*2 bounds
        self.fInternalRatio  = self.fInternalWidth / self.fInternalHeight

        self.fScene = None
        self.fRenderSource   = QRectF(0.0, 0.0, 0.0, 0.0)
        self.fRenderTarget   = QRectF(0.0, 0.0, 0.0, 0.0)
        self.fUseCustomPaint = False
        self.fFrameWidth = 0.0

        self.fInitialX = 0.0
        self.fInitialY = 0.0
        self.fScale    = 1.0

        self.fViewBg    = QColor(0, 0, 0)
        self.fViewBrush = QBrush(QColor(75, 75, 255, 30))
        self.fViewPen   = QPen(Qt.blue, 1)
        self.fViewRect  = [3.0, 3.0, 10.0, 10.0]

        self.fMouseMode = self._MOUSE_MODE_NONE
        self.fMouseLeftDown = False
        self.fMouseRightDown = False
        self.fMouseInitialZoomPos = None

        self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DEFAULT

        self.fZoomCursors = [None for _ in range(self._kCursorZoomCount)]
        self.fZoomCursors[self._kCursorName] = "black"
        self.fZoomCursors[self._kCursorZoom] = QCursor(QPixmap(":/cursors/zoom_black.png"), 8, 7)
        self.fZoomCursors[self._kCursorZoomIn] = QCursor(QPixmap(":/cursors/zoom-in_black.png"), 8, 7)
        self.fZoomCursors[self._kCursorZoomOut] = QCursor(QPixmap(":/cursors/zoom-out_black.png"), 8, 7)

    def init(self, scene: QGraphicsScene, realWidth: float, realHeight: float, useCustomPaint: bool = False):
        self.fScene = scene
        self.fRenderSource = QRectF(0.0, 0.0, realWidth, realHeight)
        self.fInternalRatio = realWidth / realHeight
        self._updateStyle()

        if self.fUseCustomPaint != useCustomPaint:
            self.fUseCustomPaint = useCustomPaint
            self.repaint()

    def setRealParent(self, parent):
        self.fRealParent = parent

    # -----------------------------------------------------------------------------------------------------------------

    def setViewPosX(self, xp: float):
        self.fViewRect[self._kRectX] = xp * (self.fInternalWidth - self.fViewRect[self._kRectWidth]/self.fScale)
        self.update()

    def setViewPosY(self, yp: float):
        self.fViewRect[self._kRectY] = yp * (self.fInternalHeight - self.fViewRect[self._kRectHeight]/self.fScale)
        self.update()

    def setViewScale(self, scale: float):
        self.fScale = scale

        if self.fRealParent is not None:
            QTimer.singleShot(0, self.fRealParent.slot_miniCanvasCheckAll)

    def setViewSize(self, width: float, height: float):
        self.fViewRect[self._kRectWidth] = width * self.fInternalWidth
        self.fViewRect[self._kRectHeight] = height * self.fInternalHeight
        self.update()

    def setViewTheme(self, bgColor, brushColor, penColor):
        bg_black      = bgColor.blackF()
        brush_black   = brushColor.blackF()
        r0, g0, b0, _ = bgColor.getRgb()
        r1, g1, b1, _ = brushColor.getRgb()

        if brush_black < bg_black:
            self.fRubberBandBlending = self._RUBBERBAND_BLENDING_PLUS
            brushColor = QColor(r1-r0, g1-g0, b1-b0, 40)
        elif bg_black < brush_black:
            self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DIFF
            brushColor = QColor(r0-r1, g0-g1, b0-b1, 40)
        else:
            bgColor.setAlpha(40)
            self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DEFAULT

        penColor.setAlpha(100)
        self.fViewBg    = bgColor
        self.fViewBrush = QBrush(brushColor)
        self.fViewPen   = QPen(penColor, 1)

        cursorName = "black" if bg_black < 0.5 else "white"
        if self.fZoomCursors[self._kCursorName] != cursorName:
            prefix = ":/cursors/zoom"
            self.fZoomCursors[self._kCursorName] = cursorName
            self.fZoomCursors[self._kCursorZoom] = QCursor(QPixmap(f"{prefix}_{cursorName}.png"), 8, 7)
            self.fZoomCursors[self._kCursorZoomIn] = QCursor(QPixmap(f"{prefix}-in_{cursorName}.png"), 8, 7)
            self.fZoomCursors[self._kCursorZoomOut] = QCursor(QPixmap(f"{prefix}-out_{cursorName}.png"), 8, 7)

    # -----------------------------------------------------------------------------------------------------------------

    def changeEvent(self, event):
        if event.type() in (QEvent.StyleChange, QEvent.PaletteChange):
            self._updateStyle()
        QFrame.changeEvent(self, event)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            event.accept()
            self.fMouseLeftDown = True
            self._updateMouseMode(event)
            return
        if event.button() == Qt.RightButton:
            event.accept()
            self.fMouseRightDown = True
            self._updateMouseMode(event)
            return
        if event.button() == Qt.MidButton:
            event.accept()
            self.fMouseLeftDown = True
            self.fMouseRightDown = True
            self._updateMouseMode(event)
            return
        QFrame.mouseMoveEvent(self, event)

    def mouseMoveEvent(self, event):
        if self.fMouseMode == self._MOUSE_MODE_MOVE:
            event.accept()
            self._moveViewRect(event.x(), event.y())
            return
        if self.fMouseMode == self._MOUSE_MODE_SCALE:
            event.accept()
            self._scaleViewRect(event.globalY())
            return
        QFrame.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            event.accept()
            self.fMouseLeftDown = False
            self._updateMouseMode(event)
            return
        if event.button() == Qt.RightButton:
            event.accept()
            self.fMouseRightDown = False
            self._updateMouseMode(event)
            return
        if event.button() == Qt.MidButton:
            event.accept()
            self.fMouseLeftDown = event.buttons() & Qt.LeftButton
            self.fMouseRightDown = event.buttons() & Qt.RightButton
            self._updateMouseMode(event)
            return
        QFrame.mouseReleaseEvent(self, event)

    def wheelEvent(self, event):
        if self.fScene is None:
            QFrame.wheelEvent(self, event)
            return

        event.accept()
        self.fScene.zoom_wheel(event.angleDelta().y())

    def paintEvent(self, event):
        if self.fScene is None:
            QFrame.paintEvent(self, event)
            return

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing == ANTIALIASING_FULL))

        # Brightness-aware out-of-canvas shading
        bg_color = self.fViewBg
        bg_black = bg_color.black()
        bg_shade = -12 if bg_black < 127 else 12
        r,g,b,_ = bg_color.getRgb()
        bg_color = QColor(r+bg_shade, g+bg_shade, b+bg_shade)

        frameWidth = self.fFrameWidth
        if self.fUseCustomPaint:
            # Inner shadow
            color = QColor.fromHsv(40, 0, 255-max(210, bg_color.black(), bg_black))
            painter.setBrush(Qt.transparent)
            painter.setPen(color)
            painter.drawRect(QRectF(0.5, 0.5, self.width()-1, self.height()-1))

            # Background
            painter.setBrush(bg_color)
            painter.setPen(bg_color)
            painter.drawRect(QRectF(1.5, 1.5, self.width()-3, self.height()-3))
        else:
            use_rounding = int(frameWidth > 1)

            rounding = 0.5 * use_rounding
            painter.setBrush(bg_color)
            painter.setPen(bg_color)
            painter.drawRoundedRect(QRectF(0.5+frameWidth,
                                           0.5+frameWidth,
                                           self.width()-1-frameWidth*2,
                                           self.height()-1-frameWidth*2), rounding, rounding)

            clipPath = QPainterPath()
            rounding = 1.0 * use_rounding
            clipPath.addRoundedRect(QRectF(frameWidth,
                                           frameWidth,
                                           self.width()-frameWidth*2,
                                           self.height()-frameWidth*2), rounding, rounding)
            painter.setClipPath(clipPath)

        self.fScene.render(painter, self.fRenderTarget, self.fRenderSource, Qt.KeepAspectRatio)

        # Allow cursor frame to look joined with minicanvas frame
        painter.setClipping(False)

        width  = self.fViewRect[self._kRectWidth]/self.fScale
        height = self.fViewRect[self._kRectHeight]/self.fScale

        # cursor
        lineHinting = self.fViewPen.widthF() / 2
        x = self.fViewRect[self._kRectX]+self.fInitialX
        y = self.fViewRect[self._kRectY]+self.fInitialY
        scr_x = floor(x)
        scr_y = floor(y)
        rect = QRectF(
            scr_x+lineHinting,
            scr_y+lineHinting,
            ceil(width+x-scr_x)-lineHinting*2,
            ceil(height+y-scr_y)-lineHinting*2 )

        if self.fRubberBandBlending == self._RUBBERBAND_BLENDING_PLUS:
            painter.setCompositionMode(QPainter.CompositionMode_Plus)
        elif self.fRubberBandBlending == self._RUBBERBAND_BLENDING_DIFF:
            painter.setCompositionMode(QPainter.CompositionMode_Difference)

        painter.setBrush(self.fViewBrush)
        painter.setPen(Qt.NoPen)
        painter.drawRect(rect)

        painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
        painter.setBrush(Qt.NoBrush)
        painter.setPen(self.fViewPen)
        painter.drawRect(rect)

        if self.fUseCustomPaint:
            event.accept()
        else:
            QFrame.paintEvent(self, event)

    def resizeEvent(self, event):
        size = event.size()
        width = size.width()
        height = size.height()
        extRatio = (width - self.fFrameWidth * 2) / (height - self.fFrameWidth * 2)
        if extRatio >= self.fInternalRatio:
            self.fInternalHeight = floor(height - self.fFrameWidth * 2)
            self.fInternalWidth  = floor(self.fInternalHeight * self.fInternalRatio)
            self.fInitialX       = floor(float(width - self.fInternalWidth) / 2.0)
            self.fInitialY       = self.fFrameWidth
        else:
            self.fInternalWidth  = floor(width - self.fFrameWidth * 2)
            self.fInternalHeight = floor(self.fInternalWidth / self.fInternalRatio)
            self.fInitialX       = self.fFrameWidth
            self.fInitialY       = floor(float(height - self.fInternalHeight) / 2.0)

        self.fRenderTarget = QRectF(self.fInitialX,
                                    self.fInitialY,
                                    float(self.fInternalWidth),
                                    float(self.fInternalHeight))

        if self.fRealParent is not None:
            QTimer.singleShot(0, self.fRealParent.slot_miniCanvasCheckAll)

        QFrame.resizeEvent(self, event)

    # -----------------------------------------------------------------------------------------------------------------

    def _moveViewRect(self, x: float, y: float):
        if self.fScene is None:
            return

        x -= self.fInitialX
        y -= self.fInitialY

        fixPos = False
        rCentX = self.fViewRect[self._kRectWidth] / self.fScale / 2
        rCentY = self.fViewRect[self._kRectHeight] / self.fScale / 2
        if x < rCentX:
            x = rCentX
            fixPos = True
        elif x > self.fInternalWidth - rCentX:
            x = self.fInternalWidth - rCentX
            fixPos = True

        if y < rCentY:
            y = rCentY
            fixPos = True
        elif y > self.fInternalHeight - rCentY:
            y = self.fInternalHeight - rCentY
            fixPos = True

        if fixPos:
            globalPos = self.mapToGlobal(QPoint(int(self.fInitialX + x), int(self.fInitialY + y)))
            self.cursor().setPos(globalPos)

        x = self.fRenderSource.width() * x / self.fInternalWidth
        y = self.fRenderSource.height() * y / self.fInternalHeight
        self.fScene.m_view.centerOn(x, y)

    def _scaleViewRect(self, globalY: int):
        if self.fScene is None:
            return

        dy = self.fMouseInitialZoomPos.y() - globalY
        if dy != 0:
            self.setCursor(self.fZoomCursors[self._kCursorZoomIn if dy > 0 else self._kCursorZoomOut])
            self.fScene.zoom_wheel(dy)

        self.cursor().setPos(self.fMouseInitialZoomPos)

    def _updateMouseMode(self, event):
        if self.fMouseLeftDown and self.fMouseRightDown:
            self.fMouseInitialZoomPos = event.globalPos()
            self.setCursor(self.fZoomCursors[self._kCursorZoom])
            self.fMouseMode = self._MOUSE_MODE_SCALE

        elif self.fMouseLeftDown:
            self.setCursor(QCursor(Qt.SizeAllCursor))
            if self.fMouseMode == self._MOUSE_MODE_NONE:
                self._moveViewRect(event.x(), event.y())
            self.fMouseMode = self._MOUSE_MODE_MOVE

        else:
            self.unsetCursor()
            self.fMouseMode = self._MOUSE_MODE_NONE

    def _updateStyle(self):
        self.fFrameWidth = 1 if self.fUseCustomPaint else self.frameWidth()

# ---------------------------------------------------------------------------------------------------------------------
