# DataFile.py
# by Robin Prillwitz
# 11.2.2020
#

import os
import sys
from pathlib import Path
from PyQt5 import QtGui, QtCore, QtWidgets, uic
import pyqtgraph as pg

import pandas as pd
import numpy as np
from scipy import integrate
from scipy.interpolate import interp1d
from scipy.ndimage.filters import gaussian_filter1d

import Config
import ListItem


# Handles one data file and its processing
# inherits from QWidget to emit signals
class DataFile(ListItem.ListItem):

    def __init__(self, filename, color, config=None, parent=None):
        super().__init__(color, config=config, parent=parent)
        self.filename = filename

        self.__initSettings()
        self.__showListItem()

        self.__readData()

        if self.config["xColumn"] == -1 or self.config["yColumn"] == -1:
            self.__selectData()

        self.recalculate()
        self.updatePlot()

        self.__toggleSettings()

    def __copy__(self):
        return DataFile(self.filename, self.config["color"], config=self.config, parent=self.parent)

    # displays the list item in the file list
    def __showListItem(self):
        boldFont = QtGui.QFont()
        boldFont.setBold(True)

        self.box = QtWidgets.QFrame()
        self.Hlayout = QtWidgets.QHBoxLayout()
        self.Vlayout = QtWidgets.QVBoxLayout()

        self.enable = QtWidgets.QCheckBox()
        self.enable.setChecked(self.config["enabled"])
        self.enable.stateChanged.connect(self.setEnabled)
        self.enable.setObjectName("enable_checkbox")
        self.enable.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.enable.setStyleSheet(
            "background-color: hsv({:d},{:d}%,{:d}%); color: black;".format(
                self.config["color"][0], self.config["color"][1], self.config["color"][2]))

        self.Hlayout.addWidget(self.enable)

        self.label = QtWidgets.QLabel(os.path.basename(self.filename))
        self.label.setAlignment(QtCore.Qt.AlignCenter)
        self.label.setFont(boldFont)
        self.Hlayout.addWidget(self.label)
        self.Hlayout.addStretch(1)

        self.settingsBtn = QtWidgets.QPushButton(QtGui.QIcon(Config.getResource("assets/left.png")), "")
        self.settingsBtn.clicked.connect(self.__toggleSettings)
        self.settingsBtn.setFlat(True)
        self.settingsBtn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.settingsBtn.setIconSize(QtCore.QSize(16, 16))
        self.settingsBtn.setFixedSize(30, 30)
        self.Hlayout.addWidget(self.settingsBtn)

        self.Hlayout.addStrut(40)
        self.Hlayout.setAlignment(QtCore.Qt.AlignCenter)
        self.Hlayout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
        self.Hlayout.setContentsMargins(10,0,10,0)
        self.Vlayout.setContentsMargins(0,0,0,0)

        self.box.setLayout(self.Hlayout)
        self.Vlayout.addWidget(self.box)
        self.Vlayout.addWidget(self.settings)

        self.frame.setLayout(self.Vlayout)

    def __initSettings(self):
        self.settings = QtWidgets.QFrame()
        self.settings.setObjectName("settings")
        self.settings.setContentsMargins(0, 0, 20, 0)
        self.settings.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)

        self.GLayout = QtWidgets.QGridLayout()

        # Offsets
        self.x_offset = ListItem.SuperSpinner(None)
        self.x_offset.setRange(-9999, 9999)
        self.x_offset.setValue(self.config["xOffset"])
        # self.x_offset.setFixedWidth(75)
        self.x_offset.valueChanged.connect(lambda x, who="xOffset": self.applyChange(x, who))
        self.x_offset_label = QtWidgets.QLabel("x-Offset:")
        self.x_offset_label.setBuddy(self.x_offset)
        self.x_offset_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)

        self.y_offset = ListItem.SuperSpinner(None)
        self.y_offset.setRange(-9999, 9999)
        self.y_offset.setValue(self.config["yOffset"])
        # self.y_offset.setFixedWidth(75)
        self.y_offset.valueChanged.connect(lambda y, who="yOffset": self.applyChange(y, who))
        self.y_offset_label = QtWidgets.QLabel("y-Offset:")
        self.y_offset_label.setBuddy(self.y_offset)
        self.y_offset_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)

        # Color Picker
        self.colorPickerBtn = QtWidgets.QPushButton("  ")
        self.colorPickerBtn.setObjectName("color_select_button")
        self.colorPickerBtn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.colorPickerBtn.setStyleSheet("background-color: hsv({:d},{:d}%,{:d}%); color: black;".format(
            self.config["color"][0], self.config["color"][1], self.config["color"][2]))
        self.colorPickerBtn.clicked.connect(lambda x, who="color": self.applyChange(x, who))
        self.colorPickerLabel = QtWidgets.QLabel("Farbe:")
        self.colorPickerLabel.setBuddy(self.colorPickerBtn)
        self.colorPickerLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)

        # Width Spinner
        self.widthSpinner = QtWidgets.QSpinBox()
        self.widthSpinner.setRange(1, 20)
        self.widthSpinner.setValue(self.config["width"])
        self.widthSpinner.valueChanged.connect(lambda x, who="width": self.applyChange(x, who))
        self.widthLabel = QtWidgets.QLabel("Breite:")
        self.widthLabel.setBuddy(self.widthSpinner)
        self.widthLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)

        # Interpolation
        self.interpolationBox = QtWidgets.QComboBox()
        self.interpolationBox.addItems(["keine", "linear", "slinear", "quadratic", "cubic", "nearest", "zero", "previous", "next"])
        self.interpolationBox.setCurrentText(self.config["interpolation"])
        self.interpolationBox.currentTextChanged.connect(lambda x, who="interpolation": self.applyChange(x, who))
        self.interpolationLabel = QtWidgets.QLabel("Interpolation:")
        self.interpolationLabel.setBuddy(self.interpolationBox)
        self.interpolationLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)

        # Integration
        self.integrationBox = QtWidgets.QSpinBox()
        self.integrationBox.setRange(-10, 10)
        self.integrationBox.setValue(self.config["integrate"])
        self.integrationBox.valueChanged.connect(lambda x, who="integrate": self.applyChange(x, who))
        self.integrationLabel = QtWidgets.QLabel("Integration:")
        self.integrationLabel.setBuddy(self.integrationBox)
        self.integrationLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)

        # Filter
        self.filterBox = QtWidgets.QDoubleSpinBox()
        self.filterBox.setValue(self.config["filter"])
        self.filterBox.valueChanged.connect(lambda x, who="filter": self.applyChange(x, who))
        self.filterLabel = QtWidgets.QLabel("Filter:")
        self.filterLabel.setBuddy(self.integrationBox)
        self.filterLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)

        self.reassignBtn = QtWidgets.QPushButton("Umbesetzen")
        self.reassignBtn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.reassignBtn.clicked.connect(lambda _: [
            self.__readData(),
            self.__selectData(),
            self.update()
        ])

        self.GLayout.addWidget(self.x_offset_label,              0, 0)
        self.GLayout.addWidget(self.x_offset,                    0, 1)
        self.GLayout.addWidget(self.y_offset_label,              1, 0)
        self.GLayout.addWidget(self.y_offset,                    1, 1)

        self.GLayout.addWidget(self.colorPickerLabel,            2, 0)
        self.GLayout.addWidget(self.colorPickerBtn,              2, 1)

        self.GLayout.addWidget(self.widthLabel,                  3, 0)
        self.GLayout.addWidget(self.widthSpinner,                3, 1)

        self.GLayout.addWidget(self.interpolationLabel,          4, 0)
        self.GLayout.addWidget(self.interpolationBox,            4, 1)

        self.GLayout.addWidget(self.filterLabel,                 5, 0)
        self.GLayout.addWidget(self.filterBox,                   5, 1)

        self.GLayout.addWidget(self.integrationLabel,            6, 0)
        self.GLayout.addWidget(self.integrationBox,              6, 1)

        self.GLayout.addWidget(self.reassignBtn,                 7, 1)

        self.settings.setLayout(self.GLayout)

    def __reparseData(self, dialog):
        sep = dialog.findChild(QtWidgets.QLineEdit, "sepLineEdit").text()
        dec = dialog.findChild(QtWidgets.QLineEdit, "decimalLineEdit").text()
        if not sep or not dec:
            return

        self.config["seperator"] = sep
        self.config["decimal"] = dec

        dialog.close()
        del dialog

        self.__readData(sep=self.config["seperator"], decimal=self.config["decimal"])
        self.__selectData()

    def __selectData(self):
        dialog = uic.loadUi(Config.getResource("./ui/file_open_dialog.ui"))
        dialog.setWindowTitle(os.path.basename(self.filename))

        # dialog cannot be cancelled on first assignment
        if self.config["xColumn"] == -1 or self.config["yColumn"] == -1:
            buttonBox = dialog.findChild(QtWidgets.QDialogButtonBox, "buttonBox")
            for button in buttonBox.buttons():
                if buttonBox.buttonRole(button) == QtWidgets.QDialogButtonBox.RejectRole:
                    button.setCursor(QtGui.QCursor(QtCore.Qt.ForbiddenCursor))
                    button.setDisabled(True)

        itemList = self.data.columns.values.tolist()

        xComboBox = dialog.findChild(QtWidgets.QComboBox, "xComboBox")
        yComboBox = dialog.findChild(QtWidgets.QComboBox, "yComboBox")
        xComboBox.addItems(itemList)
        yComboBox.addItems(itemList)

        if self.config["xColumn"] != -1:
            xComboBox.setCurrentText(self.config["xColumn"])
        else:
            xComboBox.setCurrentIndex(0)

        if self.config["yColumn"] != -1:
            yComboBox.setCurrentText(self.config["yColumn"])
        else:
            yComboBox.setCurrentIndex(1 if len(itemList) > 0 else 0)

        dialog.findChild(QtWidgets.QLineEdit, "sepLineEdit").setText(str(self.config["seperator"]))
        dialog.findChild(QtWidgets.QLineEdit, "decimalLineEdit").setText(str(self.config["decimal"]))

        reparseBtn = dialog.findChild(QtWidgets.QPushButton, "reparseBtn")
        reparseBtn.clicked.connect(lambda: self.__reparseData(dialog))

        ret = dialog.exec_()
        dialog.close()

        if ret:
            l = self.data[xComboBox.currentText()].copy()
            if np.all(l.to_numpy() == np.sort(l)):
                self.config["xColumn"] = xComboBox.currentText()
                self.config["yColumn"] = yComboBox.currentText()
            else:
                self.showError("Unsortierte x-Achse",
                    """<p>Die angegebene x-Achse ist nicht chronologisch sortiert!
                    Die korrekte Darstellung kann nicht garantiert werden.</p>""")
        else:
            if self.config["xColumn"] == -1 or self.config["yColumn"] == -1:
                self.modData =  self.interpData = pd.DataFrame([0, 0.001], [0, 0])

    def __toggleSettings(self):
        if self.settings.isHidden():
            self.settings.show()
            self.settingsBtn.setIcon(QtGui.QIcon(Config.getResource("assets/down.png")))
            self.item.setSizeHint(self.Vlayout.sizeHint())
        else:
            self.settings.hide()
            self.settingsBtn.setIcon(QtGui.QIcon(Config.getResource("assets/left.png")))
            sizeHint = self.Hlayout.sizeHint()
            sizeHint.setHeight(40+7)    # strut height + padding (don't ask)
            self.item.setSizeHint(sizeHint)

        self.sigUpdateUI.emit()

    def __readData(self, sep=Config.SEPERATOR, decimal=Config.DECIMAL):
        if not os.path.isfile(self.filename):
            self.data = pd.DataFrame([0, 0.001], [0, 0])
            return

        self.data = pd.read_csv(self.filename, sep=sep, decimal=decimal, header=0, skipinitialspace=True)

    # applies all calculations and interpolation
    def recalculate(self):
        dlg = pg.ProgressDialog("Berechnung", cancelText=None, busyCursor=False, disable=False, wait=250)
        dlg.setValue(0)

        if self.config["xColumn"] == -1 or self.config["yColumn"] == -1:
            return

        dlg += 10

        self.calculateCommon(dlg)

        x = self.modData["x"].values
        y = self.modData["y"].values

        dlg += 10

        # interpolate data
        if self.config["interpolation"] != "keine" and self.config["interpolation"] != "linear":

            # generate common x samples
            xnew = np.linspace(
                x.min(), # from
                x.max(), # to
                int(min([max([
                    np.ceil(((x.max() - x.min()) / Config.DIVISION) * Config.PPD),
                    Config.PPD,
                    len(x)
                ]), Config.MAX]))
            )

            dlg += 10
            spl = interp1d(x, y, kind=self.config["interpolation"], copy=False,
                    assume_sorted=True, bounds_error=False, fill_value=0)
            dlg += 10
            y = spl(xnew)
            x = xnew
            dlg += 10

        dlg += 10

        self.dataUpdated = True

        # save data
        self.interpData = {'x': x, 'y': y}

        dlg.setValue(100)

        if self.sigCalc:
            self.sigCalc.emit()

    # reflects updated values in the UI
    def updateUI(self):
        self.x_offset.setValue(self.config["xOffset"])
        self.y_offset.setValue(self.config["yOffset"])

    def getSelected(self):
        return None

    def deleteSelected(self, plot, selected):
        if self.item == selected:
            plot.plt.removeItem(self.plot)
            plot.plt.removeItem(self.cursor)
            del self
            return False # may be unreachable
        else:
            return True # = item stays alive

    def setZIndex(self, zIndex):
        self.config["zIndex"] = zIndex
        self.updatePlot()
        return zIndex - 1

    def toDict(self):
        return {
            "type": "datafile",
            "containing": {
                "filename": self.filename,
                "data": self.data.to_dict(),
                "config": self.config
            }
        }