"""
Qt Widget for displaying VNC framebuffer using RFB protocol

(c) zocker-160 2024
licensed under GPLv3
"""

import logging
import time

from PyQt6.QtCore import (
    QSize,
    Qt,
    pyqtSignal,
    QSemaphore
)
from PyQt6.QtGui import (
    QImage,
    QPaintEvent,
    QPainter,
    QColor,
    QBrush,
    QPixmap,
    QResizeEvent,
    QKeyEvent,
    QMouseEvent
)

from PyQt6.QtWidgets import (
    QWidget,
    QLabel,
    QWidget
)

from PyQt6.QtOpenGLWidgets import QOpenGLWidget

from qvncwidget6.rfb import RFBClient
from qvncwidget6.rfbhelpers import RFBPixelformat, RFBInput

log = logging.getLogger("QVNCWidget")

class QVNCWidget(QWidget, RFBClient):

    onInitialResize = pyqtSignal(QSize)

    def __init__(self, parent: QWidget,
                 host: str, port = 5900, password: str = None,
                 readOnly = False):
        super().__init__(
            parent=parent,
            host=host, port=port, password=password
        )
        self.readOnly = readOnly

        self.backbuffer: QImage = None
        self.frontbuffer: QImage = None

        self.setMouseTracking(not self.readOnly)
        self.setMinimumSize(1, 1) # make window scalable

        self.mouseButtonMask = 0

    def start(self):
        self.startConnection()

    def stop(self):
        self.closeConnection()

    def onConnectionMade(self):
        log.info("VNC handshake done")

        self.setPixelFormat(RFBPixelformat.getRGB32())
        self.PIX_FORMAT = QImage.Format.Format_RGB32

        #self.setPixelFormat(RFBPixelformat.getRGB24())
        #self.PIX_FORMAT = QImage.Format.Format_RGB888

        #self.setPixelFormat(RFBPixelformat.getRGB16())
        #self.PIX_FORMAT = QImage.Format.Format_RGB16

        self.backbuffer = QImage(self.vncWidth, self.vncHeight, self.PIX_FORMAT)
        self.onInitialResize.emit(QSize(self.vncWidth, self.vncHeight))

    def onRectangleUpdate(self,
            x: int, y: int, width: int, height: int, data: bytes):

        if self.backbuffer is None:
            log.warning("backbuffer is None")
            return
        else:
            log.debug("drawing backbuffer")

        #with open(f"{width}x{height}.data", "wb") as f:
        #    f.write(data)

        t1 = time.time()

        painter = QPainter(self.backbuffer)
        painter.drawImage(x, y, QImage(data, width, height, self.PIX_FORMAT))
        painter.end()

        log.debug(f"painting took: {(time.time() - t1)*1e3} ms")

        del painter
        del data

    def onFramebufferUpdateFinished(self):
        log.debug("FB Update finished")
        self.update()

    def paintEvent(self, a0: QPaintEvent):
        #log.debug("Paint event")
        painter = QPainter(self)

        if self.backbuffer is None:
            log.debug("backbuffer is None")
            painter.fillRect(0, 0, self.width(), self.height(), Qt.GlobalColor.black)

        else:
            self.frontbuffer = self.backbuffer.scaled(
                    self.width(), self.height(),
                    Qt.AspectRatioMode.KeepAspectRatio,
                    Qt.TransformationMode.SmoothTransformation
                )
            painter.drawImage(0, 0, self.frontbuffer)

        painter.end()

    # Mouse events

    def mousePressEvent(self, ev: QMouseEvent):
        if self.readOnly or not self.frontbuffer: return
        self.mouseButtonMask = RFBInput.fromQMouseEvent(ev, True, self.mouseButtonMask)
        self.pointerEvent(*self._getRemoteRel(ev), self.mouseButtonMask)

    def mouseReleaseEvent(self, ev: QMouseEvent):
        if self.readOnly or not self.frontbuffer: return
        self.mouseButtonMask = RFBInput.fromQMouseEvent(ev, False, self.mouseButtonMask)
        self.pointerEvent(*self._getRemoteRel(ev), self.mouseButtonMask)

    def mouseMoveEvent(self, ev: QMouseEvent):
        if self.readOnly or not self.frontbuffer: return
        self.pointerEvent(*self._getRemoteRel(ev), self.mouseButtonMask)

    def _getRemoteRel(self, ev: QMouseEvent) -> tuple:
        xPos = (ev.position().x() / self.frontbuffer.width()) * self.vncWidth
        yPos = (ev.position().y() / self.frontbuffer.height()) * self.vncHeight

        return int(xPos), int(yPos)

    # Key events

    def keyPressEvent(self, ev: QKeyEvent):
        if self.readOnly: return
        self.keyEvent(RFBInput.fromQKeyEvent(ev.key(), ev.text()), down=1)

    def keyReleaseEvent(self, ev: QKeyEvent):
        if self.readOnly: return
        self.keyEvent(RFBInput.fromQKeyEvent(ev.key(), ev.text()), down=0)


# other experimental implementations

class QVNCWidgetGL(QOpenGLWidget, RFBClient):

    IMG_FORMAT = QImage.Format.Format_RGB32

    onInitialResize = pyqtSignal(QSize)
    #onUpdatePixmap = pyqtSignal(int, int, int, int, bytes)
    onUpdatePixmap = pyqtSignal()
    onSetPixmap = pyqtSignal()

    onKeyPress = pyqtSignal(QKeyEvent)
    onKeyRelease = pyqtSignal(QKeyEvent)

    def __init__(self, parent, 
            host, port=5900, password: str=None,
            mouseTracking=False):

        #super(QOpenGLWidget, self).__init__()
        #super(RFBClient, self).__init__(
        super().__init__(
            parent=parent,
            host=host,
            port=port,
            password=password,
            daemonThread=True
        )

        #self.setAlignment(Qt.AlignCenter)

        #self.onUpdatePixmap.connect(self._updateImage)
        #self.onSetPixmap.connect(self._setImage)
        self.onSetPixmap.connect(self._updateImage)

        self.acceptMouseEvents = False # mouse events are not accepted at first
        self.setMouseTracking(mouseTracking)
        
        # Allow Resizing
        self.setMinimumSize(1, 1)

        self.data = list(tuple())
        self.dataMonitor = QSemaphore(0)

    def start(self):
        self.startConnection()

    def stop(self):
        self.closeConnection()

    def onConnectionMade(self):
        log.info("VNC handshake done")

        self.setPixelFormat(RFBPixelformat.getRGB32())
        self.onInitialResize.emit(QSize(self.vncWidth, self.vncHeight))
        self._initKeypress()
        self._initMouse()

    def onRectangleUpdate(self,
            x: int, y: int, width: int, height: int, data: bytes):
        #img = QImage(data, width, height, self.IMG_FORMAT)
        #self.onUpdatePixmap.emit(x, y, width, height, data)

        #self.dataMonitor.acquire(1)

        self.data.append((x, y, width, height, data))
        #self.data = (x, y, width, height, data)
        #self.dataMonitor.release(1)

        #self.onUpdatePixmap.emit()
        
        #else:
        #    print("AAAAAAAAAAAAAA", "MONITOR AQUIRE FAILED")

    def onFramebufferUpdateFinished(self):
        self.onSetPixmap.emit()
        return

        if self.pixmap:
            #self.setPixmap(QPixmap.fromImage(self.image))
            self.resizeEvent(None)

    def onFatalError(self, error: Exception):
        log.error(str(error))
        #logging.exception(str(error))
        #self.reconnect()

    #def _updateImage(self, x: int, y: int, width: int, height: int, data: bytes):
    def _updateImage(self):
        print("update image")
        self.update()

        #if not self.screen:
        #    self.screen = QImage(width, height, self.IMG_FORMAT)
        #    self.screen.fill(Qt.red)
        #    self.screenPainter = QPainter(self.screen)

        #self.painter.beginNativePainting()
        #self.painter.drawPixmapFragments()

        #with open("/tmp/images/test.raw", "wb") as f:
        #    f.write(data)
        
        #p = QPainter(self.screen)

        #self.screenPainter.drawImage(
        #    x, y, QImage(data, width, height, self.IMG_FORMAT))

        #p.end()

        #self.repaint()
        #self.update()

    def _setPixmap(self):
        if self.pixmap:
            self.setPixmap(
                self.pixmap.scaled(
                    self.width(), self.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                )
            )

    def _setImage(self):
        if self.screen:
            self.setPixmap(QPixmap.fromImage(
                self.screen.scaled(
                    self.width(), self.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                )
            ))
        self.acceptMouseEvents = True  # mouse events are getting accepted

    # Passed events

    def _keyPress(self, ev: QKeyEvent):
        self.keyEvent(
            RFBInput.fromQKeyEvent(ev.key(), ev.text()), down=1)

    def _keyRelease(self, ev: QKeyEvent):
        self.keyEvent(
            RFBInput.fromQKeyEvent(ev.key(), ev.text()), down=0)

    # Window events

    def paintEvent(self, e: QPaintEvent):
        print("paint event")

        #self.dataMonitor.acquire(1)

        #while self.dataMonitor.tryAcquire(1):
        while len(self.data) > 0:
            x, y, w, h, data = self.data.pop(0)

            p = QPainter(self)

        #p.setPen(QColor(255, 0, 0))
        #p.drawText(e.rect(), Qt.AlignCenter, str(self.dataMonitor.available()))
        
            p.drawImage(x, y, QImage(data, w, h, self.IMG_FORMAT))
            p.end()

        #self.dataMonitor.release(1)

        return

        p = QPainter(self)
        p.fillRect(e.rect(), QBrush(QColor(255, 255, 255)))
        p.end()

        return

        if self.dataMonitor.tryAcquire(1):
            x, y, w, h, data = self.data

            p = QPainter(self)
            p.drawImage(x, y, QImage(data, w, h, self.IMG_FORMAT))
            p.end()

            self.dataMonitor.release(1)

            print("CCCCC", "Image painted diggah")

        else:
            print("BBBBBBBBB", "aquire monitor failed")

        #return super().paintEvent(a0)
        return

        if not self.screen:
            self.screen = QImage(self.size(), self.IMG_FORMAT)
            self.screen.fill(Qt.red)
            self.screenPainter = QPainter(self.screen)

        p = QPainter()
        p.begin(self)
        p.drawImage(0, 0,
            self.screen.scaled(
                self.width(), self.height(),
                Qt.KeepAspectRatio,
                Qt.SmoothTransformation
            ))
        p.end()

    def resizeEvent(self, e: QResizeEvent):
        return super().resizeEvent(e)

    def resizeGL(self, w: int, h: int):
        print("RESIZE THAT BITCH!!!", w, h)
        #return super().resizeGL(w, h)

    def resizeEvent_(self, a0: QResizeEvent):
        #print("RESIZE!", self.width(), self.height())
        #return super().resizeEvent(a0)
        if self.screen:
            self.setPixmap(QPixmap.fromImage(
                self.screen.scaled(
                    self.width(), self.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                ))
            )
        return super().resizeEvent(a0)

    def mousePressEvent(self, ev: QMouseEvent):
        #print(ev.position(), ev.button())
        #print(self.height() - self.pixmap().height())

        if self.acceptMouseEvents: # need pixmap instance
            self.buttonMask = RFBInput.fromQMouseEvent(ev, True, self.buttonMask)
            self.pointerEvent(*self._getRemoteRel(ev), self.buttonMask)

        return super().mousePressEvent(ev)

    def mouseReleaseEvent(self, ev: QMouseEvent):
        if self.acceptMouseEvents: # need pixmap instance
            self.buttonMask = RFBInput.fromQMouseEvent(ev, False, self.buttonMask)
            self.pointerEvent(*self._getRemoteRel(ev), self.buttonMask)

        return super().mouseReleaseEvent(ev)

    def mouseMoveEvent(self, ev: QMouseEvent):
        if self.acceptMouseEvents: # need pixmap instance
            self.pointerEvent(*self._getRemoteRel(ev), self.buttonMask)

    # FIXME: The pixmap is assumed to be aligned center.
    def _getRemoteRel(self, ev: QMouseEvent) -> tuple:
        # FIXME: this code is ugly as fk

        # y coord is kinda fucked up
        yDiff = (self.height() - self.pixmap().height()) / 2
        yPos = ev.position().y() - yDiff
        if yPos < 0: yPos = 0
        if yPos > self.pixmap().height(): yPos = self.pixmap().height()

        yPos = self._calcRemoteRel(
            yPos, self.pixmap().height(), self.vncHeight)

        # x coord is kinda fucked up, too
        xDiff = (self.width() - self.pixmap().width()) / 2
        xPos = ev.position().x() - xDiff
        if xPos < 0: xPos = 0
        if xPos > self.pixmap().width(): xPos = self.pixmap().width()

        xPos = self._calcRemoteRel(
            xPos, self.pixmap().width(), self.vncWidth)
        
        return xPos, yPos

    def _calcRemoteRel(self, locRel, locMax, remoteMax) -> int:
        return int( (locRel / locMax) * remoteMax )

    def _initMouse(self):
        self.buttonMask = 0 # pressed buttons (bit fields)

    def _initKeypress(self):
        self.onKeyPress.connect(self._keyPress)
        self.onKeyRelease.connect(self._keyRelease)

    def __del__(self):
        self.stop()
    
    def __exit__(self, *args):
        self.stop()
        self.deleteLater()

class QVNCWidget_old(QLabel, RFBClient):
    
    IMG_FORMAT = QImage.Format.Format_RGB32

    onInitialResize = pyqtSignal(QSize)
    onUpdatePixmap = pyqtSignal(int, int, int, int, bytes)
    onSetPixmap = pyqtSignal()

    onKeyPress = pyqtSignal(QKeyEvent)
    onKeyRelease = pyqtSignal(QKeyEvent)

    def __init__(self, parent, 
                host, port=5900, password: str=None,
                mouseTracking=False):
        super().__init__(
            parent=parent,
            host=host,
            port=port,
            password=password,
            daemonThread=True
        )
        #import faulthandler
        #faulthandler.enable()
        self.screen: QImage = None

        # FIXME: The pixmap is assumed to be aligned center.
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.onUpdatePixmap.connect(self._updateImage)
        self.onSetPixmap.connect(self._setImage)

        self.acceptMouseEvents = False # mouse events are not accepted at first
        self.setMouseTracking(mouseTracking)
        
        # Allow Resizing
        self.setMinimumSize(1,1)

    def _initMouse(self):
        self.buttonMask = 0 # pressed buttons (bit fields)

    def _initKeypress(self):
        self.onKeyPress.connect(self._keyPress)
        self.onKeyRelease.connect(self._keyRelease)

    def start(self):
        self.startConnection()

    def stop(self):
        self.closeConnection()
        if self.screenPainter: self.screenPainter.end()

    def onConnectionMade(self):
        self.onInitialResize.emit(QSize(self.vncWidth, self.vncHeight))
        self.setPixelFormat(RFBPixelformat.getRGB32())
        self._initKeypress()
        self._initMouse()

    def onRectangleUpdate(self,
            x: int, y: int, width: int, height: int, data: bytes):
        #img = QImage(data, width, height, self.IMG_FORMAT)
        self.onUpdatePixmap.emit(x, y, width, height, data)

    def onFramebufferUpdateFinished(self):
        self.onSetPixmap.emit()
        return

        if self.pixmap:
            #self.setPixmap(QPixmap.fromImage(self.image))
            self.resizeEvent(None)

    def onFatalError(self, error: Exception):
        log.error(str(error))
        #logging.exception(str(error))
        #self.reconnect()

    def _updateImage(self, x: int, y: int, width: int, height: int, data: bytes):
        if not self.screen:
            self.screen = QImage(width, height, self.IMG_FORMAT)
            self.screen.fill(Qt.red)
            self.screenPainter = QPainter(self.screen)

        #self.painter.beginNativePainting()
        #self.painter.drawPixmapFragments()

        #with open("/tmp/images/test.raw", "wb") as f:
        #    f.write(data)
        
        #p = QPainter(self.screen)
        self.screenPainter.drawImage(
            x, y, QImage(data, width, height, self.IMG_FORMAT))
        #p.end()

        #self.repaint()
        #self.update()

    def _drawPixmap(self, x: int, y: int, pix: QPixmap):
        #self.paintLock.acquire()
        self.pixmap = pix

        if not self.painter:
            self.painter = QPainter(self.pixmap)
        else:
            print("DRAW PIXMAP:", x, y, self.pixmap, self.painter, pix, pix.isNull())
            self.painter.drawPixmap(x, y, self.pixmap)
        #self.paintLock.release()

    def _drawPixmap2(self, x: int, y: int, pix: QPixmap, data: bytes):
        if not self.pixmap or (
            x == 0 and y == 0 and
            pix.width() == self.pixmap.width() and pix.height() == self.pixmap.height()):

            self.pixmap = pix.copy()
            self._setPixmap()
            return
        
        import time
        print("DRAW PIXMAP:", x, y, self.pixmap.width(), self.pixmap.height(), pix.width(), pix.height())
        _t = time.time()
        #self.pixmap.save(f"/tmp/images/imgP_{_t}", "jpg")
        #with open(f"/tmp/images/img_{_t}.raw", "wb") as f:
        #    f.write(data)
        #pix.save(f"/tmp/images/img_{_t}", "jpg")

        painter = QPainter(self.pixmap)
        painter.drawPixmap(x, y, pix)
        painter.end()
        #self._setPixmap()

    def _setPixmap(self):
        if self.pixmap:
            self.setPixmap(
                self.pixmap.scaled(
                    self.width(), self.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                )
            )

    def _setImage(self):
        if self.screen:
            self.setPixmap(QPixmap.fromImage(
                self.screen.scaled(
                    self.width(), self.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                )
            ))
        self.acceptMouseEvents = True  # mouse events are getting accepted

    # Passed events

    def _keyPress(self, ev: QKeyEvent):
        self.keyEvent(
            RFBInput.fromQKeyEvent(ev.key(), ev.text()), down=1)

    def _keyRelease(self, ev: QKeyEvent):
        self.keyEvent(
            RFBInput.fromQKeyEvent(ev.key(), ev.text()), down=0)

    # Window events

    def paintEvent(self, a0: QPaintEvent):
        return super().paintEvent(a0)
        if not self.screen:
            self.screen = QImage(self.size(), self.IMG_FORMAT)
            self.screen.fill(Qt.red)
            self.screenPainter = QPainter(self.screen)

        p = QPainter()
        p.begin(self)
        p.drawImage(0, 0,
            self.screen.scaled(
                self.width(), self.height(),
                Qt.KeepAspectRatio,
                Qt.SmoothTransformation
            ))
        p.end()

    def resizeEvent(self, a0: QResizeEvent):
        #print("RESIZE!", self.width(), self.height())
        #return super().resizeEvent(a0)
        if self.screen:
            self.setPixmap(QPixmap.fromImage(
                self.screen.scaled(
                    self.width(), self.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                ))
            )
        return super().resizeEvent(a0)

    def mousePressEvent(self, ev: QMouseEvent):
        #print(ev.position(), ev.button())
        #print(self.height() - self.pixmap().height())

        if self.acceptMouseEvents: # need pixmap instance
            self.buttonMask = RFBInput.fromQMouseEvent(ev, True, self.buttonMask)
            self.pointerEvent(*self._getRemoteRel(ev), self.buttonMask)

        return super().mousePressEvent(ev)

    def mouseReleaseEvent(self, ev: QMouseEvent):
        if self.acceptMouseEvents: # need pixmap instance
            self.buttonMask = RFBInput.fromQMouseEvent(ev, False, self.buttonMask)
            self.pointerEvent(*self._getRemoteRel(ev), self.buttonMask)

        return super().mouseReleaseEvent(ev)

    def mouseMoveEvent(self, ev: QMouseEvent):
        if self.acceptMouseEvents: # need pixmap instance
            self.pointerEvent(*self._getRemoteRel(ev), self.buttonMask)

    # FIXME: The pixmap is assumed to be aligned center.
    def _getRemoteRel(self, ev: QMouseEvent) -> tuple:
        # FIXME: this code is ugly as fk

        # y coord is kinda fucked up
        yDiff = (self.height() - self.pixmap().height()) / 2
        yPos = ev.position().y() - yDiff
        if yPos < 0: yPos = 0
        if yPos > self.pixmap().height(): yPos = self.pixmap().height()

        yPos = self._calcRemoteRel(
            yPos, self.pixmap().height(), self.vncHeight)

        # x coord is kinda fucked up, too
        xDiff = (self.width() - self.pixmap().width()) / 2
        xPos = ev.position().x() - xDiff
        if xPos < 0: xPos = 0
        if xPos > self.pixmap().width(): xPos = self.pixmap().width()

        xPos = self._calcRemoteRel(
            xPos, self.pixmap().width(), self.vncWidth)
        
        return xPos, yPos

    def _calcRemoteRel(self, locRel, locMax, remoteMax) -> int:
        return int( (locRel / locMax) * remoteMax )


    def __del__(self):
        self.stop()
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop()
        self.deleteLater()
