#!/usr/bin/env python

__version__ = 0.2

import sys
import logging

from PyQt4.QtCore import (QPoint, QRectF, QPointF, Qt, SIGNAL, QTimer, QObject)
from PyQt4.QtGui import (QApplication, QMainWindow, QGraphicsView,
    QGraphicsScene, QImage, QWidget, QHBoxLayout, QPen,
    QVBoxLayout, QPushButton, QGraphicsEllipseItem, QGraphicsItem,
    QPainter, QKeySequence, QAction, QIcon, QFileDialog, QProgressBar,
    QBrush, QFrame, QLabel)
import numpy as np

import default_config as config
from leedbase import *

logging.basicConfig(filename = config.loggingFilename, level=config.loggingLevel)

class QGraphicsSpotView(QGraphicsEllipseItem):
    """ Provides an QGraphicsItem to display a Spot on a QGraphicsScene.
    
        Circle class providing a circle that can be moved by mouse and keys.
        
    """

    def __init__(self, point, radius, parent=None):
        offset = QPointF(radius, radius)
        super(QGraphicsSpotView, self).__init__(QRectF(-offset, offset),  parent)
        self.setPen(QPen(Qt.blue))
        self.setPos(point)
        self.setFlags(QGraphicsItem.ItemIsSelectable|
                      QGraphicsItem.ItemIsMovable|
                      QGraphicsItem.ItemIsFocusable)

    def keyPressEvent(self, event):
        """ Handles keyPressEvents.

            The circle can be moved using the arrow keys. Applying Shift
            at the same time allows fine adjustments.

            The circles radius can be changed using the plus and minus keys.
        """

        if event.key() == Qt.Key_Plus:
            self.changeSize(config.QGraphicsSpotView_spotSizeChange)
        elif event.key() == Qt.Key_Minus:
            self.changeSize(-config.QGraphicsSpotView_spotSizeChange)
        elif event.key() == Qt.Key_Right:
            if event.modifiers() & Qt.ShiftModifier:
                self.moveRight(config.QGraphicsSpotView_smallMove)
            else:
                self.moveRight(config.QGraphicsSpotView_bigMove)
        elif event.key() == Qt.Key_Left:
            if event.modifiers() & Qt.ShiftModifier:
                self.moveLeft(config.QGraphicsSpotView_smallMove)
            else:
                self.moveLeft(config.QGraphicsSpotView_bigMove)
        elif event.key() == Qt.Key_Up:
            if event.modifiers() & Qt.ShiftModifier:
                self.moveUp(config.QGraphicsSpotView_smallMove)
            else:
                self.moveUp(config.QGraphicsSpotView_bigMove)
        elif event.key() == Qt.Key_Down:
            if event.modifiers() & Qt.ShiftModifier:
                self.moveDown(config.QGraphicsSpotView_smallMove)
            else:
                self.moveDown(config.QGraphicsSpotView_bigMove)

    def onPositionChange(self, point):
        """ Handles incoming position change request."""
        self.setPos(point)

    def radius(self):
        return self.rect().width() / 2.0

    def onRadiusChange(self, radius):
        """ Handles incoming radius change request."""
        self.changeSize(radius - self.radius())

    def moveRight(self, distance):
        """ Moves the circle distance to the right."""
        self.setPos(self.pos() + QPointF(distance, 0.0))

    def moveLeft(self, distance):
        """ Moves the circle distance to the left."""
        self.setPos(self.pos() + QPointF(-distance, 0.0))

    def moveUp(self, distance):
        """ Moves the circle distance up."""
        self.setPos(self.pos() + QPointF(0.0, -distance))

    def moveDown(self, distance):
        """ Moves the circle distance down."""
        self.setPos(self.pos() + QPointF(0.0, distance))

    def changeSize(self, inc):
        """ Change radius by inc.
        
            inc > 0: increase
            inc < 0: decrease
        """

        inc /= 2**0.5 
        self.setRect(self.rect().adjusted(-inc, -inc, +inc, +inc))

class QSpotModel(QObject):
    """
    Wraps a SpotModel to offer signals.

    Provides the following signals:
    - intensityChanged
    - positionChanged
    - radiusChanged
    """

    def __init__(self, parent = None):
        super(QSpotModel, self).__init__(parent)
        self.m = SpotModel()
    
    def update(self, x, y, intensity, energy, radius):
        self.m.update(x, y, intensity, energy, radius)
        QObject.emit(self, SIGNAL("positionChanged"), QPointF(x, y))
        QObject.emit(self, SIGNAL("radiusChanged"), radius)
        QObject.emit(self, SIGNAL("intensityChanged"), intensity)

class GraphicsScene(QGraphicsScene):
    """ Custom GraphicScene having all the main content."""

    def __init__(self, parent=None):
        super(GraphicsScene, self).__init__(parent)
    
    def mousePressEvent(self, event):
        """ Processes mouse events through either            
              - propagating the event
            or 
              - instantiating a new Circle (on left-click)
        """
       
        if self.itemAt(event.scenePos()):
            super(GraphicsScene, self).mousePressEvent(event)
        elif event.button() == Qt.LeftButton:
            item = QGraphicsSpotView(event.scenePos(),
                         config.GraphicsScene_defaultRadius)
            self.clearSelection()
            self.addItem(item)
            item.setSelected(True)
            self.setFocusItem(item)

    def keyPressEvent(self, event):
        """ Processes key events through either            
              - deleting the focus item
            or   
              - propagating the event

        """

        item = self.focusItem()
        if item:
            if event.key() == Qt.Key_Delete:
                self.removeItem(item)
                del item
            else:
                super(GraphicsScene, self).keyPressEvent(event)

    def drawBackground(self, painter, rect):
        """ Draws image in background if it exists. """
        if hasattr(self, "image"):
            painter.drawImage(QPoint(0, 0), self.image)

    def setBackground(self, image):
        """ Sets the background image. """
        self.image = image
        self.update()
    
    def removeAll(self):
        """ Remove all items from the scene (leaves background unchanged). """
        for item in self.items():
            self.removeItem(item)

class GraphicsView(QGraphicsView):
    """ Custom GraphicsView to display the scene. """
    def __init__(self, parent=None):
        super(GraphicsView, self).__init__(parent)
        self.setRenderHints(QPainter.Antialiasing)
    
#    def wheelEvent(self, event):
#        factor = 1.41 ** (-event.delta() / 240.0)
#        self.scale(factor, factor)
    
    def resizeEvent(self, event):
        self.fitInView(self.sceneRect(), Qt.KeepAspectRatio)
    
    def drawBackground(self, painter, rect):
        painter.fillRect(rect, QBrush(Qt.lightGray))
        self.scene().drawBackground(painter, rect)

class FileDialog(QFileDialog):
    def __init__(self, **kwargs):
        super(FileDialog, self).__init__(**kwargs)
        self.setFileMode(QFileDialog.ExistingFiles)

class MainWindow(QMainWindow):
    """ easyLeed's main window. """
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle("easyLeed %s" % __version__)

        self.scene = GraphicsScene(self)
        self.view = GraphicsView()
        self.view.setScene(self.scene)
        self.view.setMinimumSize(640, 480)
        self.setCentralWidget(self.view)

        processRunAction = self.createAction("&Run", self.run,
                QKeySequence("Ctrl+r"), None,
                "Run the analysis of the images.")
        processRestartAction = self.createAction("&Restart", self.restart,
                QKeySequence("Ctrl+z"), None,
                "Reset chosen points and jump to first image.")
        processNextAction = self.createAction("&Next Image", self.next_,
                QKeySequence("Ctrl+n"), None,
                "Open next image.")
        processPreviousAction = self.createAction("&Previous Image", self.previous,
                QKeySequence("Ctrl+p"), None,
                "Open previous image.")
        self.processActions = [processNextAction, processPreviousAction, None, processRunAction, processRestartAction]
        fileOpenAction = self.createAction("&Open...", self.fileOpen,
                QKeySequence.Open, None,
                "Open a directory containing the image files.")
        self.fileSaveAction = self.createAction("&Save intensities...", self.saveIntensity,
                QKeySequence.Save, None,
                "Save the calculated intensities to a text file.")
        self.fileActions = [fileOpenAction, self.fileSaveAction]
        fileMenu = self.menuBar().addMenu("&File")
        self.fileSaveAction.setEnabled(False)
        self.addActions(fileMenu, self.fileActions)
        processMenu = self.menuBar().addMenu("&Process")
        self.addActions(processMenu, self.processActions)
        self.enableProcessActions(False)

        self.statusBar().showMessage("Ready", 5000)
        self.energyLabel = QLabel()
        self.energyLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        self.statusBar().addPermanentWidget(self.energyLabel)
  
    def addActions(self, target, actions):
        for action in actions:
            if action is None:
                target.addSeparator()
            else:
                target.addAction(action)
    
    def enableProcessActions(self, enable):
        for action in self.processActions:
            if action:
                action.setEnabled(enable)

    def next_(self):
        try:
            image = self.loader.next()
        except StopIteration:
            self.statusBar().showMessage("Reached last picture", 5000)
        else:
            self.setImage(image)

    def previous(self):
        try:
            image = self.loader.previous()
        except StopIteration:
            self.statusBar().showMessage("Reached first picture", 5000)
        else:
            self.setImage(image)

    def restart(self):
        self.scene.removeAll()
        self.loader.restart()
        self.setImage(self.loader.next())

    def setImage(self, image):
        npimage, energy = image
        qimage = npimage2qimage(npimage)
        self.view.setSceneRect(QRectF(qimage.rect()))
        self.scene.setBackground(qimage)
        self.current_energy = energy
        self.energyLabel.setText("Energy %s eV" % self.current_energy)

    def createAction(self, text, slot=None, shortcut=None, icon=None,
                     tip=None, checkable=False, signal="triggered()"):
        action = QAction(text, self)
        if icon is not None:
            action.setIcon(QIcon(":/{0}.png".format(icon)))
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot is not None:
            self.connect(action, SIGNAL(signal), slot)
        if checkable:
            action.setCheckable(True)
        return action

    def saveIntensity(self):
        filename = str(QFileDialog.getSaveFileName(self, "Save intensities to a file"))
        if filename:
            self.worker.save(filename)

    def fileOpen(self):
        """ Prompts the user to select input image files."""
        self.scene.removeAll()
        dialog = FileDialog(parent = self,
                caption = "Choose image files", filter= ";;".join(IMAGE_FORMATS))
        if dialog.exec_():
            files = dialog.selectedFiles();
            filetype = IMAGE_FORMATS[str(dialog.selectedNameFilter())]
            files = [str(file_) for file_ in files]
            self.loader = filetype.loader(files, regex=config.IO_energyRegex)
            self.setImage(self.loader.next())
            self.enableProcessActions(True)
            
    def stopProcessing(self):
        self.stopped = True

    def run(self):
        import time
        time_before = time.time()
        
        self.stopped = False
        progress = QProgressBar()
        stop = QPushButton("Stop", self)
        self.connect(stop, SIGNAL("clicked()"), self.stopProcessing)
        progress.setMinimum(int(self.loader.current_energy()))
        progress.setMaximum(int(self.loader.energies[-1]))
        statusLayout = QHBoxLayout()
        statusLayout.addWidget(progress)
        statusLayout.addWidget(stop)
        statusWidget = QWidget(self)
        statusWidget.setLayout(statusLayout)
        self.statusBar().addWidget(statusWidget)
        self.view.setInteractive(False)
        self.scene.clearSelection()
        self.worker = Worker(self.scene.items(), self.current_energy, parent=self)
        self.fileSaveAction.setEnabled(True)
        for image in self.loader:
            if self.stopped:
                break
            progress.setValue(int(image[1]))
            QApplication.processEvents()
            self.setImage(image)
            self.worker.process(image)
            QApplication.processEvents()
        self.view.setInteractive(True)
        self.statusBar().removeWidget(statusWidget)

        print time.time() - time_before

    def disableInput(self):
        for item in self.scene.items():
            item.setFlag(QGraphicsItem.ItemIsSelectable, False)
            item.setFlag(QGraphicsItem.ItemIsFocusable, False)
            item.setFlag(QGraphicsItem.ItemIsMovable, False)

class Worker(QObject):
    """ Worker that manages the spots."""

    def __init__(self, spots, energy, parent=None):
        super(Worker, self).__init__(parent)
        self.spots_map = {}
        for spot in spots:
            pos = spot.scenePos()
            tracker = Tracker(pos.x(), pos.y(), spot.radius(), energy,
                        input_precision = config.Tracking_inputPrecision,
                        window_scaling = config.Tracking_windowScalingOn,
                        min_window_size = config.Tracking_minWindowSize)
            self.spots_map[spot] = (QSpotModel(self), tracker)

        for view, tup in self.spots_map.iteritems():
            self.connect(tup[0], SIGNAL("positionChanged"), view.onPositionChange)
            self.connect(tup[0], SIGNAL("radiusChanged"), view.onRadiusChange)

    def process(self, image):
        for model, tracker in self.spots_map.itervalues():
            tracker_result = tracker.feed_image(image)
            model.update(*tracker_result)

    def save(self, filename):
        intensities = [model.m.intensity for model, tracker \
                                in self.spots_map.itervalues()]
        energy = [model.m.energy for model, tracker in self.spots_map.itervalues()]
        zipped = zip(energy[0], *intensities)
        np.savetxt(filename, zipped)

def main():
    app = QApplication(sys.argv)
    form = MainWindow()
    form.show()
    app.exec_()

if __name__ == "__main__":
    main()