import string
from functools import partial

import numexpr as ne
import pandas as pd
from graphysio import dialogs, transformations
from graphysio.algorithms import filters
from graphysio.plotwidgets import (
    LoopWidget,
    PlotWidget,
    POISelectorWidget,
    SpectrogramWidget,
)
from graphysio.structures import CycleId, Parameter, PlotData
from graphysio.writedata import exporter
from pyqtgraph.Qt import QtCore, QtGui


class TSWidget(PlotWidget):
    def __init__(self, plotdata, parent=None, properties=None):
        if properties is None:
            properties = {}
        super().__init__(plotdata.name, parent=parent, properties=properties)
        self.exporter = exporter.TsExporter(self, plotdata.name)
        self.appendData(plotdata)

    def filterCurve(self, oldcurve, filtername, asnew=False):
        newseries, newsamplerate = filters.filter(
            oldcurve, filtername, dialogs.askUserValue
        )
        if asnew:
            newname = self.validateNewCurveName(newseries.name)
            if newname != newseries.name:
                newseries = newseries.rename(newname)
            newcurve = self.addSeriesAsCurve(series=newseries)
            if newsamplerate and newcurve:
                newcurve.set_samplerate(newsamplerate)
        else:
            newseries = newseries.rename(oldcurve.series.name)
            oldcurve.replace_data(newseries)
            if newsamplerate:
                oldcurve.set_samplerate(newsamplerate)

    def filterFeet(self, curve, filtername, asnew=False):
        feetdict = curve.feetitem.indices
        oldstarts = feetdict['start']
        oldstops = feetdict['stop']
        starts, stops = filters.filterFeet(
            oldstarts, oldstops, filtername, dialogs.askUserValue
        )
        feetdict['start'] = starts
        feetdict['stop'] = stops
        curve.feetitem.render()

    def keyPressEvent(self, event):
        if event.key() == QtCore.Qt.Key_Delete:
            for curve in self.curves.values():
                curve.feetitem.removeSelection()

    # Menu Curves
    def showCurveList(self):
        dlgCurveSelection = dialogs.DlgCurveSelection(
            parent=self.parent,
            visible=self.curves.values(),
            hidden=self.hiddencurves,
        )

        def cb(result):
            currently_visible = set(self.curves.values())
            currently_hidden = self.hiddencurves
            allcurves = currently_visible | currently_hidden
            newly_visible, curveproperties = result
            newly_hidden = allcurves - newly_visible

            for c in curveproperties:
                self.applyCurveProperties(c, curveproperties[c])

            for c in newly_visible:
                self.addCurve(c)

            for c in newly_hidden:
                self.removeCurve(c)

            self.hiddencurves = newly_hidden
            self.rebuildLegend()

        dlgCurveSelection.dlgdata.connect(cb)
        dlgCurveSelection.exec_()

    def launchCycleDetection(self):
        dlgCycles = dialogs.DlgCycleDetection(parent=self.parent)

        def cb(choices):
            for curvename, choice in choices.items():
                curve = self.curves[curvename]
                curve.addFeet(CycleId(choice))

        dlgCycles.dlgdata.connect(cb)
        dlgCycles.exec_()

    def launchFilter(self, filterfeet):
        dlgFilter = dialogs.DlgFilter(parent=self.parent, filterfeet=filterfeet)

        def cb(result):
            createnew, curvechoices = result
            filterfunc = self.filterFeet if filterfeet else self.filterCurve
            for curvename, choice in curvechoices.items():
                if choice == 'None':
                    continue
                curve = self.curves[curvename]
                filterfunc(curve, choice, asnew=createnew)

        dlgFilter.dlgdata.connect(cb)
        dlgFilter.exec_()

    def setDateTime(self):
        if len(self.curves) < 1:
            return
        sortedcurves = sorted(
            self.curves.values(), key=lambda curve: curve.series.index[0]
        )
        fstcurve = sortedcurves[0]
        curtimestamp = fstcurve.series.index[0]
        dlg = dialogs.DlgSetDateTime(prevdatetime=curtimestamp)
        dlg.exec_()
        newtimestamp = dlg.dlgdata
        if not newtimestamp:
            return
        offset = newtimestamp - curtimestamp
        for curve in self.curves.values():
            curve.series.index += offset
            curve.render()

    def launchTransformation(self):
        param = Parameter(
            "Choose Transformation",
            list(transformations.Transformations.keys()),
        )
        qresult = dialogs.askUserValue(param)
        if qresult is None:
            return
        trans = transformations.Transformations[qresult]
        for curve in trans(self):
            self.addCurve(curve)

    # Menu Plot
    def launchPOIWidget(self):
        curvenames = list(self.curves.keys())
        q = Parameter('Select Curve', curvenames)
        curvename = dialogs.askUserValue(q)
        if not curvename:
            return
        curve = self.curves[curvename]
        poiselector = POISelectorWidget(
            curve.series, parent=self.parent, properties=self.properties
        )
        self.parent.addTab(poiselector, curve.name())

    def launchSpectrogram(self):
        curvenames = list(self.curves.keys())
        q = Parameter('Select Curve', curvenames)
        curvename = dialogs.askUserValue(q)
        q = Parameter('Enter time window', 'time')
        window = dialogs.askUserValue(q)
        if not curvename:
            return
        curve = self.curves[curvename]
        spectro = SpectrogramWidget(
            curve.series, curve.samplerate, window, parent=self.parent
        )
        self.parent.addTab(spectro, curve.name())

    def launchCurveAlgebra(self):
        curvecorr = {n: c for n, c in zip(string.ascii_lowercase, self.curves.keys())}
        dlgCurveAlgebra = dialogs.DlgCurveAlgebra(self, curvecorr)

        def cb(formula):
            symbols = list(ne.NumExpr(formula).input_names)
            curvenames = [curvecorr[x] for x in symbols]
            sers = [self.curves[c].series for c in curvenames]
            if not len(sers):
                return

            argsdf = pd.concat(sers, axis=1, keys=symbols)
            argsdf = argsdf.interpolate()
            args = argsdf.to_dict(orient='series')

            newvals = ne.evaluate(formula, local_dict=args)
            newname = self.validateNewCurveName(formula, True)
            newseries = pd.Series(
                newvals, index=argsdf.index, name=newname, dtype='float64'
            )
            self.addSeriesAsCurve(newseries)

        dlgCurveAlgebra.dlgdata.connect(cb)
        dlgCurveAlgebra.exec_()

    # Menu Selection
    def launchNewPlotFromSelection(self):
        xmin, xmax = self.vbrange
        sers = []
        for c in self.curves.values():
            series = c.series.loc[xmin:xmax]
            sers.append(series)
        df = pd.concat(sers, axis=1, keys=[s.name for s in sers])
        newname = f'{self.name}-sub'
        plotdata = PlotData(data=df, name=newname)
        self.parent.createNewPlotWithData(plotdata)

    def launchAppendToPlotFromSelection(self):
        tabWidget = self.parent.tabWidget
        ntabs = tabWidget.count()
        tabdict = {tabWidget.tabText(idx): idx for idx in range(ntabs)}
        desttabname, ok = QtGui.QInputDialog.getItem(
            self,
            'Select destination',
            'Destination plot',
            list(tabdict.keys()),
            editable=False,
        )
        if not ok:
            return
        xmin, xmax = self.vbrange
        sers = []
        for c in self.curves.values():
            series = c.series.loc[xmin:xmax]
            sers.append(series)
        df = pd.concat(sers, axis=1, keys=[s.name for s in sers])
        newname = f'{self.name}-sub'
        plotdata = PlotData(data=df, name=newname)
        self.parent.appendToPlotWithData(plotdata, destidx=tabdict[desttabname])

    def launchLoop(self):
        dlgSetupPU = dialogs.DlgSetupPULoop(self, parent=self.parent)

        def cb(result):
            uname, pname = result
            u = self.curves[uname]
            p = self.curves[pname]
            loopwidget = LoopWidget(u, p, self.vbrange, parent=self.parent)
            newname = f'{self.name}-{p.name()}-loops'
            self.parent.addTab(loopwidget, newname)

        dlgSetupPU.dlgdata.connect(cb)
        dlgSetupPU.exec_()

    @property
    def menu(self):
        mcurves = {
            'Visible Curves': self.showCurveList,
            'Cycle Detection': self.launchCycleDetection,
            'Filter Curve': partial(self.launchFilter, filterfeet=False),
            'Filter Feet': partial(self.launchFilter, filterfeet=True),
            'Transformation': self.launchTransformation,
            'Set Date / Time': self.setDateTime,
        }
        mselect = {
            'As new plot': self.launchNewPlotFromSelection,
            'Append to other plot': self.launchAppendToPlotFromSelection,
            'Generate PU-Loops': self.launchLoop,
        }
        mplot = {
            'POI Selector': self.launchPOIWidget,
            'Spectrogram': self.launchSpectrogram,
            'Curve Algebra': self.launchCurveAlgebra,
        }
        mexport = {
            'Curves': self.exporter.curves,
            'Time info': self.exporter.periods,
            'Cycle info': self.exporter.cyclepoints,
            'Cycles to directory': self.exporter.cycles,
        }
        return {
            'Curves': mcurves,
            'Plot': mplot,
            'Seletion': mselect,
            'Export': mexport,
        }
