#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Opinsys Oy
#
# 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
# (at your option) 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.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os.path
import sys

import dbus
import dbus.service
import dbus.mainloop.pyqt5

dbus.mainloop.pyqt5.DBusQtMainLoop(set_as_default=True)

from PyQt5.QtCore    import *
from PyQt5.QtGui     import *
from PyQt5.QtWidgets import *

import puavoblackboard.icons

from PyQt5.QtMultimedia import (QCamera, QCameraImageCapture, QImageEncoderSettings, QMultimedia)
from PyQt5.QtMultimediaWidgets import QCameraViewfinder

class WebcamImageCapture(QDialog):
    # ------------------------------------------------------
    def __init__(self, parent = None):
        super(WebcamImageCapture, self).__init__(parent)

        self.camera = None
        self.capture = None
        self.capturedImage = None

        if len(QCamera.availableDevices()) == 0:
            QMessageBox.critical(self, "No Cameras", "This system has no webcams available, or they cannot be accessed.")
            #sys.exit()
            self.reject()
            return

        sourceLabel = QLabel("Which camera to use:")
        sourceLabel.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        sourceBox = QComboBox()
        #sourceBox.setToolTip("Choose the camera to use")

        # put available cameras in the combobox
        for deviceName in QCamera.availableDevices():
            description = QCamera.deviceDescription(deviceName)
            sourceBox.addItem(description)

        # select the first available camera
        cameraName = QCamera.availableDevices()[0]
        self.camera = QCamera(cameraName)
        self.camera.CaptureMode = QCamera.CaptureStillImage
        #self.camera.stateChanged.connect(self.cameraStateChanged)
        self.camera.error.connect(self.cameraError)

        # the capture object actually captures the image
        self.capture = QCameraImageCapture(self.camera)
        #self.capture.setCaptureDestination(QCameraImageCapture.CaptureToFile)
        self.capture.setCaptureDestination(QCameraImageCapture.CaptureToBuffer)
        self.capture.readyForCaptureChanged.connect(self.readyForCapture)
        self.capture.imageCaptured.connect(self.storeCapturedImage)

        # image encoder
        self.encoder = QImageEncoderSettings()
        self.encoder.setCodec("image/jpeg")
        self.encoder.setQuality(QMultimedia.VeryHighQuality)
        #self.encoder.setResolution(640, 480)
        self.capture.setEncodingSettings(self.encoder)

        # create a viewfinder for the camera
        self.viewfinder = QCameraViewfinder()
        self.camera.setViewfinder(self.viewfinder)

        # buttons
        self.cancelButton = QPushButton("Close")
        self.cancelButton.setToolTip("Closes the window without taking an image")
        self.cancelButton.clicked.connect(self.reject)
        self.captureButton = QPushButton("Capture image")
        self.captureButton.setToolTip("Captures an image and closes the window")
        self.captureButton.clicked.connect(self.takeImage)
        self.captureButton.setEnabled(False)

        # layout
        grid = QGridLayout()
        grid.setSpacing(10)
        grid.addWidget(sourceLabel, 0, 0)
        grid.addWidget(sourceBox, 0, 1)
        grid.addWidget(self.viewfinder, 1, 0, 1, 2)
        grid.addWidget(self.cancelButton, 2, 0)
        grid.addWidget(self.captureButton, 2, 1)

        # setup the window
        self.setLayout(grid)
        self.setMinimumSize(640, 480)
        self.setGeometry(0, 0, 640, 480)
        self.setWindowTitle("Import image from webcam")

        # center the window
        fg = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        fg.moveCenter(cp)
        self.move(fg.topLeft())

        self.camera.start()

    # ------------------------------------------------------
    def stopCamera(self):
        if self.camera is not None:
            self.camera.stop()

    # ------------------------------------------------------
    def closeEvent(self, event):
        self.stopCamera()

    # ------------------------------------------------------
    def cameraError(self, error):
        QMessageBox.critical(self, "Error", "The camera returned the following error message:\n\n\t" +
            self.camera.errorString())

    # ------------------------------------------------------
    def readyForCapture(self, ready):
        self.captureButton.setEnabled(True)
        self.captureButton.setDefault(True)

    # ------------------------------------------------------
    def takeImage(self, state):
        self.captureButton.setEnabled(False)
        self.cancelButton.setEnabled(False)
        self.capture.capture()

    # ------------------------------------------------------
    def storeCapturedImage(self, requestId, img):
        #kuva = QImage(img)
        #name = datetime.today().strftime("/home/jarmo/%Y%m%d-%H%M%S.png")
        #kuva.save(name)
        #print("Tallennettu kuva \"" + name + "\"")
        self.capturedImage = QImage(img)
        self.accept()

def penWidthIcon(width, iconWidth=16, iconHeight=16):
    pixmap = QPixmap(iconWidth, iconHeight)
    painter = QPainter(pixmap)
    painter.setRenderHint(QPainter.Antialiasing)
    painter.setPen(Qt.NoPen)
    painter.setBrush(Qt.white)
    painter.drawRect(0, 0, iconWidth, iconHeight)
    painter.setBrush(Qt.black)
    x = y = int((max(iconWidth, iconHeight) - width) / 2)
    painter.drawEllipse(x, y, width, width)
    painter.end()

    return QIcon(pixmap)

def colorIcon(colorName, iconWidth=16, iconHeight=16):
    pixmap = QPixmap(iconWidth, iconHeight)
    painter = QPainter(pixmap)
    painter.setPen(Qt.NoPen)
    painter.setBrush(QColor(colorName))
    painter.drawRect(0, 0, iconWidth, iconHeight)
    painter.end()

    return QIcon(pixmap)

class LineTool:

    def __init__(self):
        self.__line  = None
        self.__canvas = None

    def start(self, canvas, pos, pen):
        if self.__canvas is not None:
            return False

        self.__line = QGraphicsLineItem()
        self.__line.setPen(pen)
        self.__line.setPos(pos)
        canvas.addItem(self.__line)
        self.__canvas = canvas

        return True

    def move(self, pos):
        if self.__canvas is None:
            return False

        self.__line.setLine(0, 0,
                            pos.x() - self.__line.pos().x(),
                            pos.y() - self.__line.pos().y())

        return True

    def stop(self, pos):
        if self.__canvas is None:
            return False

        self.__line  = None
        self.__canvas = None

        return True

class FreehandTool:

    def __init__(self):
        self.__path  = None
        self.__canvas = None

    def start(self, canvas, pos, pen):
        if self.__canvas is not None:
            return False

        self.__canvas = canvas

        self.__path = QGraphicsPathItem()
        self.__path.setPen(pen)
        painterPath = QPainterPath()
        painterPath.moveTo(pos)
        self.__path.setPath(painterPath)
        canvas.addItem(self.__path)
        self.__canvas = canvas

        return True

    def move(self, pos):
        if self.__canvas is None:
            return False

        painterPath = self.__path.path()
        painterPath.lineTo(pos)
        self.__path.setPath(painterPath)

        return True

    def stop(self, pos):
        self.__path  = None
        self.__canvas = None

class MoveTool:

    def __init__(self):
        self.__canvas = None

    def start(self, canvas, pos, pen):
        self.__canvas = canvas

        for item in canvas.items():
            item.setFlag(QGraphicsItem.ItemIsMovable, True)
            item.setFlag(QGraphicsItem.ItemIsSelectable, True)

    def move(self, pos):
        pass

    def stop(self, pos):
        if self.__canvas is None:
            return

        for item in self.__canvas.items():
            item.setFlag(QGraphicsItem.ItemIsMovable, False)
            item.setFlag(QGraphicsItem.ItemIsSelectable, False)

        self.__canvas = None

class Artist:

    TOOL_LINE     = LineTool()
    TOOL_FREEHAND = FreehandTool()
    TOOL_MOVE     = MoveTool()

    def __init__(self):
        self.__tool = None
        self.__pen  = QPen(Qt.white, 5, Qt.SolidLine, Qt.RoundCap)

    def setPenColor(self, colorName):
        self.__pen.setColor(QColor(colorName))

    def setPenWidth(self, width):
        self.__pen.setWidth(width)

    def setTool(self, tool):
        self.__tool = tool

    def start(self, canvas, pos):
        self.__tool.start(canvas, pos, self.__pen)

    def move(self, pos):
        self.__tool.move(pos)

    def stop(self, pos):
        self.__tool.stop(pos)

class Canvas(QGraphicsScene):

    def __init__(self, *args):
        super().__init__(*args)

        self.__artist = None
        self.resetBackground()

    def resetBackground(self):
        self.setBackgroundBrush(QBrush(QColor(27, 51, 40), Qt.SolidPattern))

    def setBackgroundImage(self, fileName):
        pixmap = QPixmap(fileName)
        if not pixmap.isNull():
            self.setBackgroundBrush(QBrush(pixmap))

    def mousePressEvent(self, event):
        if self.__artist is not None:
            self.__artist.start(self, event.scenePos())

        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self.__artist is not None:
            self.__artist.move(event.scenePos())

        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if self.__artist is not None:
            self.__artist.stop(event.scenePos())

        super().mouseReleaseEvent(event)

    def setArtist(self, artist):
        self.__artist = artist

class CanvasBrowser(QWidget):

    currentChanged = pyqtSignal()

    def __init__(self, *args):
        super().__init__(*args)

        self.__listWidget = QListWidget()
        self.__listWidget.setDragDropMode(QAbstractItemView.InternalMove)
        self.__listWidget.currentItemChanged.connect(self.currentChanged)

        newButton = QPushButton("New")
        newButton.clicked.connect(self.addNewCanvas)

        layout = QVBoxLayout(self)
        layout.addWidget(self.__listWidget)
        layout.addWidget(newButton)

        self.addNewCanvas()

    def addNewCanvas(self):
        thumbnailSize = QSize(192, 108)

        canvas = Canvas(0, 0, 1920, 1080, self)

        pixmap = QPixmap(thumbnailSize)
        painter = QPainter(pixmap)
        canvas.render(painter)
        painter.end()

        label = QLabel()
        label.setMargin(5)
        label.setPixmap(pixmap)
        label.setAlignment(Qt.AlignHCenter)

        item = QListWidgetItem(self.__listWidget)
        item.setData(Qt.UserRole, canvas)
        item.setSizeHint(thumbnailSize)

        def updateThumbnail():
            pixmap = QPixmap(thumbnailSize)
            painter = QPainter(pixmap)
            canvas.render(painter)
            painter.end()
            label.setPixmap(pixmap)

        timer = QTimer(self)
        timer.setInterval(250)
        timer.timeout.connect(updateThumbnail)

        canvas.changed.connect(timer.start)

        self.__listWidget.setItemWidget(item, label)
        self.__listWidget.setCurrentItem(item)

        return canvas

    def currentCanvas(self):
        currentItem = self.__listWidget.currentItem()
        if currentItem is not None:
            return currentItem.data(Qt.UserRole)

        return None

class CanvasBrowserService(dbus.service.Object):

    def __init__(self, busName, canvasBrowser):
        dbus.service.Object.__init__(self, busName, '/canvasbrowser')
        self.__canvasBrowser = canvasBrowser

    @dbus.service.method('org.puavo.blackboard.CanvasBrowser', in_signature='s')
    def addNewCanvas(self, backgroundImageFileName):
        canvas = self.__canvasBrowser.addNewCanvas()
        canvas.setBackgroundImage(backgroundImageFileName)

class MainWindow(QMainWindow):

    def __init__(self, busName, *args):
        super().__init__(*args)

        self.__artist = Artist()

        view = QGraphicsView()
        view.setRenderHints(QPainter.Antialiasing)
        self.setCentralWidget(view)

        self.__canvasBrowser = CanvasBrowser(self)
        self.__canvasBrowserService = CanvasBrowserService(busName, self.__canvasBrowser)

        self.__changeCanvas()
        self.__canvasBrowser.currentChanged.connect(self.__changeCanvas)

        self.__canvasBrowserDockWidget = QDockWidget(self)
        self.__canvasBrowserDockWidget.setFeatures(QDockWidget.NoDockWidgetFeatures)
        self.__canvasBrowserDockWidget.setWidget(self.__canvasBrowser)
        self.__canvasBrowserDockWidget.setWindowTitle("Boards")

        self.addDockWidget(Qt.LeftDockWidgetArea, self.__canvasBrowserDockWidget)

        self.__toolBar = QToolBar("Tools")
        self.addToolBar(Qt.BottomToolBarArea, self.__toolBar)

        self.__lineAction = self.__toolBar.addAction(
            QIcon(":icons/line.svg"), "Line")
        self.__freehandAction = self.__toolBar.addAction(
            QIcon(":icons/freehand.svg"), "Freehand")
        self.__moveAction = self.__toolBar.addAction("Move")

        self.__lineAction.toggled.connect(
            lambda: self.__artist.setTool(Artist.TOOL_LINE))
        self.__freehandAction.toggled.connect(
            lambda: self.__artist.setTool(Artist.TOOL_FREEHAND))
        self.__moveAction.toggled.connect(
            lambda: self.__artist.setTool(Artist.TOOL_MOVE))

        self.__toolActionGroup = QActionGroup(self)
        self.__toolActionGroup.setExclusive(True)
        self.__toolActionGroup.addAction(self.__lineAction)
        self.__toolActionGroup.addAction(self.__freehandAction)
        self.__toolActionGroup.addAction(self.__moveAction)

        self.__lineAction.setCheckable(True)
        self.__moveAction.setCheckable(True)
        self.__freehandAction.setCheckable(True)

        self.__colorActionGroup = QActionGroup(self)
        self.__colorActionGroup.setExclusive(True)
        self.__colorActionGroup.setEnabled(False)

        self.__toolBar.addSeparator()

        colorSignalMapper = QSignalMapper(self)
        for colorName in ("black", "maroon", "green", "olive",
                          "navy", "purple", "teal", "silver",
                          "gray", "red", "lime", "yellow",
                          "blue", "fuchsia", "aqua", "white"):
            colorAction = self.__colorActionGroup.addAction(colorIcon(colorName),
                                                            colorName.capitalize())
            colorAction.setCheckable(True)
            colorAction.toggled.connect(colorSignalMapper.map)
            colorSignalMapper.setMapping(colorAction, colorName)
            self.__toolBar.addAction(colorAction)
        colorSignalMapper.mapped[str].connect(self.__artist.setPenColor)
        colorAction.setChecked(True)

        self.__penWidthActionGroup = QActionGroup(self)
        self.__penWidthActionGroup.setExclusive(True)
        self.__penWidthActionGroup.setEnabled(False)

        self.__toolBar.addSeparator()

        penWidthSignalMapper = QSignalMapper(self)
        for penWidth in (2, 5, 8, 11, 14):
            penWidthAction = self.__penWidthActionGroup.addAction(penWidthIcon(penWidth),
                                                                  str(penWidth).capitalize())
            penWidthAction.setCheckable(True)
            penWidthAction.toggled.connect(penWidthSignalMapper.map)
            penWidthSignalMapper.setMapping(penWidthAction, penWidth)
            self.__toolBar.addAction(penWidthAction)
        penWidthSignalMapper.mapped[int].connect(self.__artist.setPenWidth)

        self.__lineAction.toggled.connect(self.__colorActionGroup.setEnabled)
        self.__freehandAction.toggled.connect(self.__colorActionGroup.setEnabled)
        self.__lineAction.toggled.connect(self.__penWidthActionGroup.setEnabled)
        self.__freehandAction.toggled.connect(self.__penWidthActionGroup.setEnabled)

        self.__freehandAction.setChecked(True)
        penWidthAction.setChecked(True)

        self.__toolBar.addSeparator()
        self.__changeBackgroundImageAction = self.__toolBar.addAction(
            QIcon.fromTheme('insert-image'), "Change background image")
        self.__changeBackgroundImageAction.triggered.connect(
            self.__changeBackgroundImage)

        self.__takeWebcamImageButton = self.__toolBar.addAction(
                QIcon.fromTheme('camera-photo'),
                "Use an image from the webcam as background image")
        self.__takeWebcamImageButton.triggered.connect(self.__takeWebcamImage)

        self.__resetBackgroundButton = self.__toolBar.addAction(
                QIcon.fromTheme('edit-clear'),
                "Reset board background")
        self.__resetBackgroundButton.triggered.connect(self.__resetBackground)

    def __takeWebcamImage(self):
        dlg = WebcamImageCapture(self)

        if dlg.exec_() == QDialog.Accepted:
            self.__canvasBrowser.currentCanvas().setBackgroundImage(dlg.capturedImage)

        # HACK HACK HACK HACK, closeEvent() is not called when the window closes
        # normally, so the camera *can* remain active (ie. busy) and unusable
        if dlg.camera is not None:
            dlg.camera.stop()

    def __resetBackground(self):
        self.__canvasBrowser.currentCanvas().resetBackground()

    def __changeBackgroundImage(self):
        canvas = self.__canvasBrowser.currentCanvas()
        result = QFileDialog.getOpenFileName(self,
                                             "Choose background image file",
                                             "", "Images (*.png *.jpg)")[0]
        if not result:
            return

        canvas.setBackgroundImage(result)

    def __changeCanvas(self):
        canvas = self.__canvasBrowser.currentCanvas()
        if canvas is not None:
            canvas.setArtist(self.__artist)
        self.centralWidget().setScene(canvas)

def main():
    if len(sys.argv) - 1 != 0:
        print("error: invalid number of arguments (%d), expected 0"
              % (len(sys.argv) - 1), file=sys.stderr)
        return 1

    app = QApplication(sys.argv)

    busName  = dbus.service.BusName('org.puavo.blackboard',
                                    bus=dbus.SessionBus())

    win = MainWindow(busName)
    win.setWindowTitle("Blackboard")

    win.show()
    win.showMaximized()

    return app.exec_()

if __name__ == "__main__":
    sys.exit(main())
