"""
Series of data are loaded from a .csv file, and their names are
displayed in a checkable list view. The user can select the series
it wants from the list and plot them on a matplotlib canvas.
Use the sample .csv file that comes with the script for an example
of data series.

[2016-11-06] Convert to QtQuick 2.0 - Qt.labs.controls 1.0
[2016-11-05] Convert to QtQuick 2.0 - QtQuick Controls 1.0
[2016-11-01] Update to PyQt5.6 and python 3.5

Frederic Collonval (fcollonval@gmail.com)

Inspired from the work of Eli Bendersky (eliben@gmail.com):
https://github.com/eliben/code-for-blog/tree/master/2009/pyqt_dataplot_demo

License: MIT License
Last modified: 2016-11-06
"""
import sys, os, csv
from PyQt5.QtCore import QAbstractListModel, QModelIndex, QObject, QSize, Qt, QUrl, QVariant, pyqtProperty, pyqtSlot, pyqtSignal
from PyQt5.QtGui import QGuiApplication, QColor, QImage, QPixmap
# from PyQt5.QtWidgets import QApplication
from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType
from PyQt5.QtQuick import QQuickImageProvider

import matplotlib
matplotlib.use('Agg')
# from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
# from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
# from matplotlib.figure import Figure
import matplotlib.pyplot as plt

import numpy as np

sys.path.append('../backend')
from backend_qtquick5 import FigureCanvasQTAggToolbar, MatplotlibIconProvider

class DataSerie(object):

    def __init__(self, name, data, selected=False):
        self._name = name
        self._data = data
        self._selected = selected
    
    def name(self):
        return self._name
    
    def selected(self):
        return self._selected
        
    def data(self):
        return self._data

class DataSeriesModel(QAbstractListModel):

    # Define role enum
    SelectedRole = Qt.UserRole
    NameRole = Qt.UserRole + 1
    DataRole = Qt.UserRole + 2

    _roles = {
        SelectedRole : b"selected",
        NameRole : b"name",
        DataRole : b"data"
    }
    
    lengthDataChanged = pyqtSignal()
    
    def __init__(self, parent=None):
        QAbstractListModel.__init__(self, parent)
        
        self._data_series = list()
        self._length_data = 0
    
    @pyqtProperty(int, notify=lengthDataChanged)
    def lengthData(self):
        return self._length_data
    
    @lengthData.setter
    def lengthData(self, length):
        if self._length_data != length:
            self._length_data = length
            self.lengthDataChanged.emit()

    def roleNames(self):
        return self._roles
    
    def load_from_file(self, filename=None):
        self._data_series.clear()
        self._length_data = 0

        if filename:
            with open(filename, 'r') as f:
                for line in csv.reader(f):
                    series = DataSerie(line[0], 
                                       [i for i in map(int, line[1:])])
                    self.add_data(series)
                    
    def add_data(self, data_series):
        self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
        self._data_series.append(data_series)
        self.lengthData = max(self.lengthData, len(data_series.data()))
        self.endInsertRows()
    
    def rowCount(self, parent=QModelIndex()):
        return len(self._data_series)
        
    def data(self, index, role=Qt.DisplayRole):
        if(index.row() < 0 or index.row() >= len(self._data_series)):
            return QVariant()
        
        series = self._data_series[index.row()]
        
        if role == self.SelectedRole:
            return series.selected()
        elif role == self.NameRole:
            return series.name()
        elif role == self.DataRole:
            return series.data()
        
        return QVariant()
    
    def setData(self, index, value, role=Qt.EditRole):
        if(index.row() < 0 or index.row() >= len(self._data_series)):
            return False
        
        series = self._data_series[index.row()]
        
        if role == self.SelectedRole:
            series._selected = not value
            self.dataChanged.emit(index, index, [role,])
            return True
                
        return False
                    
class Form(QObject):

    xFromChanged = pyqtSignal()
    xToChanged = pyqtSignal()
    legendChanged = pyqtSignal()
    statusTextChanged = pyqtSignal()
    stateChanged = pyqtSignal()

    def __init__(self, parent=None, data=None):
        QObject.__init__(self, parent)
        
        self._status_text = "Please load a data file"
        
        self._filename = ""
        self._x_from = 0
        self._x_to = 1
        self._legend = False
        
        # default dpi=80, so size = (480, 320)
        self._figure = None
        self.axes = None
        
        self._data = data

    @property
    def figure(self):
        return self._figure
    
    @figure.setter
    def figure(self, fig):
        self._figure = fig
        self._figure.set_facecolor('white')
        self.axes = self.figure.add_subplot(111)    
        
        # Signal connection
        self.xFromChanged.connect(self._figure.canvas.draw_idle)
        self.xToChanged.connect(self._figure.canvas.draw_idle)
        self.legendChanged.connect(self._figure.canvas.draw_idle)
        self.stateChanged.connect(self._figure.canvas.draw_idle)
        
    @pyqtProperty('QString', notify=statusTextChanged)
    def statusText(self):
        return self._status_text
    
    @statusText.setter
    def statusText(self, text):
        if self._status_text != text:
            self._status_text = text
            self.statusTextChanged.emit()

    @pyqtProperty('QString')
    def filename(self):
        return self._filename
    
    @filename.setter
    def filename(self, filename):
        if filename:
            filename = QUrl(filename).toLocalFile()
            if filename != self._filename:
                self._filename = filename
                self._data.load_from_file(filename)
                self.statusText = "Loaded " + filename
                self.xTo = self._data.lengthData
                self.update_figure()

    @pyqtProperty(int, notify=xFromChanged)
    def xFrom(self):
        return self._x_from
    
    @xFrom.setter
    def xFrom(self, x_from):
        if self.figure is None:
            return
            
        x_from = int(x_from)
        if self._x_from != x_from:
            self._x_from = x_from
            self.axes.set_xlim(left=self._x_from)
            self.xFromChanged.emit()
        
    @pyqtProperty(int, notify=xToChanged)
    def xTo(self):
        return self._x_to
    
    @xTo.setter
    def xTo(self, x_to):
        if self.figure is None:
            return
            
        x_to = int(x_to)
        if self._x_to != x_to:
            self._x_to = x_to
            self.axes.set_xlim(right=self._x_to)
            self.xToChanged.emit()
        
    @pyqtProperty(bool, notify=legendChanged)
    def legend(self):
        return self._legend
    
    @legend.setter
    def legend(self, legend):
        if self.figure is None:
            return
            
        if self._legend != legend:
            self._legend = legend
            if self._legend:
                self.axes.legend()
            else:
                leg = self.axes.get_legend()
                if leg is not None:
                    leg.remove()
            self.legendChanged.emit()
    
    @pyqtProperty('QString', constant=True)
    def about(self):
        msg = __doc__
        return msg.strip()
        
    
    @pyqtSlot()
    def update_figure(self):
        if self.figure is None:
            return
    
        self.axes.clear()
        self.axes.grid(True)
        
        has_series = False

        for row in range(self._data.rowCount()):
            model_index = self._data.index(row, 0)
            checked = self._data.data(model_index, DataSeriesModel.SelectedRole)
            
            if checked:
                has_series = True
                name = self._data.data(model_index, DataSeriesModel.NameRole)                
                values = self._data.data(model_index, DataSeriesModel.DataRole)
                
                self.axes.plot(range(len(values)), values, 'o-', label=name)

        self.axes.set_xlim((self.xFrom, self.xTo))
        if has_series and self.legend:
            self.axes.legend()
        
        self.stateChanged.emit()
        
        
def main():
    argv = sys.argv
    
    # Trick to set the style / not found how to do it in pythonic way
    argv.extend(["-style", "universal"])
    app = QGuiApplication(argv)

    qmlRegisterType(FigureCanvasQTAggToolbar, "Backend", 1, 0, "FigureToolbar")    
    imgProvider = MatplotlibIconProvider()
    
    # !! You must specified the QApplication as parent of QQmlApplicationEngine
    # otherwise a segmentation fault is raised when exiting the app
    engine = QQmlApplicationEngine(parent=app)
    engine.addImageProvider("mplIcons", imgProvider)
    
    context = engine.rootContext()
    data_model = DataSeriesModel()
    context.setContextProperty("dataModel", data_model)
    mainApp = Form(data=data_model)
    context.setContextProperty("draw_mpl", mainApp)
    
    engine.load(QUrl('main.qml'))
    
    win = engine.rootObjects()[0]
    mainApp.figure = win.findChild(QObject, "figure").getFigure()
    
    rc = app.exec_()
    # There is some trouble arising when deleting all the objects here
    # but I have not figure out how to solve the error message.
    # It looks like 'app' is destroyed before some QObject
    sys.exit(rc)


if __name__ == "__main__":
    main()