import time
import logging
import re
import copy

from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets

from sas.qtgui.Plotting.PlotterData import Data1D
from sas.qtgui.Plotting.Plotter import PlotterWidget
from sas.qtgui.Plotting.PlotterData import Data2D
from sas.qtgui.Plotting.Plotter2D import Plotter2DWidget
import sas.qtgui.Utilities.GuiUtils as GuiUtils

from .UI.DataOperationUtilityUI import Ui_DataOperationUtility

BG_WHITE = "background-color: rgb(255, 255, 255); color: rgb(0, 0, 0);"
BG_RED = "background-color: rgb(244, 170, 164);"


class DataOperationUtilityPanel(QtWidgets.QDialog, Ui_DataOperationUtility):
    def __init__(self, parent=None):
        super(DataOperationUtilityPanel, self).__init__()
        self.setupUi(self)
        self.manager = parent
        self.communicator = self.manager.communicator()
        # disable the context help icon
        self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)

        # To store input datafiles
        self.filenames = None
        self.list_data_items = []
        self.data1 = None
        self.data2 = None
        # To store the result
        self.output = None

        # To update content of comboboxes with files loaded in DataExplorer
        self.communicator.sendDataToPanelSignal.connect(self.updateCombobox)

        # change index of comboboxes
        self.cbData1.currentIndexChanged.connect(self.onSelectData1)
        self.cbData2.currentIndexChanged.connect(self.onSelectData2)
        self.cbOperator.currentIndexChanged.connect(self.onSelectOperator)

        # edit Coefficient text edit
        self.txtNumber.textChanged.connect(self.onInputCoefficient)
        self.txtOutputData.textChanged.connect(self.onCheckOutputName)

        # push buttons
        self.cmdClose.clicked.connect(self.onClose)
        self.cmdHelp.clicked.connect(self.onHelp)
        self.cmdCompute.clicked.connect(self.onCompute)
        self.cmdReset.clicked.connect(self.onReset)

        self.cmdCompute.setEnabled(False)

        # validator for coefficient
        self.txtNumber.setValidator(GuiUtils.DoubleValidator())

        self.layoutOutput = QtWidgets.QHBoxLayout()
        self.layoutData1 = QtWidgets.QHBoxLayout()
        self.layoutData2 = QtWidgets.QHBoxLayout()

        # Create default layout for initial graphs (when they are still empty)
        self.newPlot(self.graphOutput, self.layoutOutput)
        self.newPlot(self.graphData1, self.layoutData1)
        self.newPlot(self.graphData2, self.layoutData2)

        # Flag to enable Compute pushbutton
        self.data2OK = False
        self.data1OK = False

    def updateCombobox(self, filenames):
        """ Function to fill comboboxes with names of datafiles loaded in
         DataExplorer. For Data2, there is the additional option of choosing
         a number to apply to data1 """
        self.filenames = filenames

        if list(filenames.keys()):
            # clear contents of comboboxes
            self.cbData1.clear()
            self.cbData1.addItems(['Select Data'])
            self.cbData2.clear()
            self.cbData2.addItems(['Select Data', 'Number'])

            list_datafiles = []

            for key_id in list(filenames.keys()):
                if filenames[key_id].title:
                    # filenames with titles
                    new_title = filenames[key_id].title
                    list_datafiles.append(new_title)
                    self.list_data_items.append(new_title)

                else:
                    # filenames without titles by removing time.time()
                    new_title = re.sub('\d{10}\.\d{2}', '', str(key_id))
                    self.list_data_items.append(new_title)
                    list_datafiles.append(new_title)

            # update contents of comboboxes
            self.cbData1.addItems(list_datafiles)
            self.cbData2.addItems(list_datafiles)

    def onHelp(self):
        """
        Bring up the Data Operation Utility Documentation whenever
        the HELP button is clicked.
        Calls Documentation Window with the path of the location within the
        documentation tree (after /doc/ ....".
        """
        location = "/user/qtgui/Calculators/data_operator_help.html"
        self.manager.showHelp(location)

    def onClose(self):
        """ Close dialog """
        self.onReset()

        self.cbData1.clear()
        self.cbData1.addItems(['No Data Available'])
        self.cbData2.clear()
        self.cbData2.addItems(['No Data Available'])
        self.close()


    def onCompute(self):
        """ perform calculation """
        # set operator to be applied
        operator = self.cbOperator.currentText()
        # calculate and send data to DataExplorer
        output = None
        try:
            data1 = self.data1
            data2 = self.data2
            output = eval("data1 %s data2" % operator)
        except Exception as ex:
            logging.error(ex)
            return

        self.output = output

        # if outputname was unused, write output result to it
        # and display plot
        if self.onCheckOutputName():
            # add outputname to self.filenames
            self.list_data_items.append(str(self.txtOutputData.text()))
            # send result to DataExplorer
            self.onPrepareOutputData()
            # plot result
            self.updatePlot(self.graphOutput, self.layoutOutput, self.output)

        # Add the new plot to the comboboxes
        self.cbData1.addItem(self.output.name)
        self.cbData2.addItem(self.output.name)
        self.filenames[self.output.name] = self.output

    def onPrepareOutputData(self):
        """ Prepare datasets to be added to DataExplorer and DataManager """
        name = self.txtOutputData.text()
        self.output.name = name
        new_item = GuiUtils.createModelItemWithPlot(
            self.output,
            name=name)

        new_datalist_item = {name + str(time.time()):
                                 self.output}
        self.communicator. \
            updateModelFromDataOperationPanelSignal.emit(new_item, new_datalist_item)

    def onSelectOperator(self):
        """ Change GUI when operator changed """
        self.lblOperatorApplied.setText(self.cbOperator.currentText())
        self.newPlot(self.graphOutput, self.layoutOutput)

    def onReset(self):
        """
        Reset Panel to its initial state (default values) keeping
        the names of loaded data
        """
        self.txtNumber.setText('1.0')
        self.txtOutputData.setText('MyNewDataName')

        self.txtNumber.setEnabled(False)
        self.cmdCompute.setEnabled(False)

        self.cbData1.setCurrentIndex(0)
        self.cbData2.setCurrentIndex(0)
        self.cbOperator.setCurrentIndex(0)

        self.data1OK = False
        self.data2OK = False

        # Empty graphs
        self.newPlot(self.graphOutput, self.layoutOutput)
        self.newPlot(self.graphData1, self.layoutData1)
        self.newPlot(self.graphData2, self.layoutData2)

    def onSelectData1(self):
        """ Plot for selection of Data1 """
        choice_data1 = str(self.cbData1.currentText())

        wrong_choices = ['No Data Available', 'Select Data', '']

        if choice_data1 in wrong_choices:
            # check validity of choice: input = filename
            self.newPlot(self.graphData1, self.layoutData1)
            self.data1 = None
            self.data1OK = False
            self.cmdCompute.setEnabled(False) # self.onCheckChosenData())
            return

        else:
            self.data1OK = True
            # get Data1
            key_id1 = self._findId(choice_data1)
            self.data1 = self._extractData(key_id1)
            # plot Data1
            self.updatePlot(self.graphData1, self.layoutData1, self.data1)
            # plot default for output graph
            self.newPlot(self.graphOutput, self.layoutOutput)
            # Enable Compute button only if Data2 is defined and data compatible
            self.cmdCompute.setEnabled(self.onCheckChosenData())

    def onSelectData2(self):
        """ Plot for selection of Data2 """
        choice_data2 = str(self.cbData2.currentText())
        wrong_choices = ['No Data Available', 'Select Data', '']

        if choice_data2 in wrong_choices:
            self.newPlot(self.graphData2, self.layoutData2)
            self.txtNumber.setEnabled(False)
            self.data2OK = False
            self.onCheckChosenData()
            self.cmdCompute.setEnabled(False)
            return

        elif choice_data2 == 'Number':
            self.data2OK = True
            self.txtNumber.setEnabled(True)
            self.data2 = float(self.txtNumber.text())

            # Enable Compute button only if Data1 defined and compatible data
            self.cmdCompute.setEnabled(self.onCheckChosenData())
            # Display value of coefficient in graphData2
            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
            # plot default for output graph
            self.newPlot(self.graphOutput, self.layoutOutput)
            self.onCheckChosenData()

        else:
            self.txtNumber.setEnabled(False)
            self.data2OK = True
            key_id2 = self._findId(choice_data2)
            self.data2 = self._extractData(key_id2)
            self.cmdCompute.setEnabled(self.onCheckChosenData())

            # plot Data2
            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
            # plot default for output graph
            self.newPlot(self.graphOutput, self.layoutOutput)

    def onInputCoefficient(self):
        """ Check input of number when a coefficient is required
        for operation """
        if self.txtNumber.isModified():
            input_to_check = str(self.txtNumber.text())

            if input_to_check is None or input_to_check is '':
                msg = 'DataOperation: Number requires a float number'
                logging.warning(msg)
                self.txtNumber.setStyleSheet(BG_RED)

            elif float(self.txtNumber.text()) == 0.:
                # should be check that 0 is not chosen
                msg = 'DataOperation: Number requires a non zero number'
                logging.warning(msg)
                self.txtNumber.setStyleSheet(BG_RED)

            else:
                self.txtNumber.setStyleSheet(BG_WHITE)
                self.data2 = float(self.txtNumber.text())
                self.updatePlot(self.graphData2, self.layoutData2, self.data2)

    def onCheckChosenData(self):
        """ check that data1 and data2 are compatible """

        if not all([self.data1OK, self.data2OK]):
            return False
        else:
            if self.cbData2.currentText() == 'Number':
                self.cbData1.setStyleSheet(BG_WHITE)
                self.cbData2.setStyleSheet(BG_WHITE)
                return True

            elif self.data1.__class__.__name__ != self.data2.__class__.__name__:
                self.cbData1.setStyleSheet(BG_RED)
                self.cbData2.setStyleSheet(BG_RED)
                print(self.data1.__class__.__name__ != self.data2.__class__.__name__)
                logging.warning('Cannot compute data of different dimensions')
                return False

            elif self.data1.__class__.__name__ == 'Data1D'\
                    and (len(self.data2.x) != len(self.data1.x) or
                             not all(i == j for i, j in zip(self.data1.x, self.data2.x))):
                logging.warning('Cannot compute 1D data of different lengths')
                self.cbData1.setStyleSheet(BG_RED)
                self.cbData2.setStyleSheet(BG_RED)
                return False

            elif self.data1.__class__.__name__ == 'Data2D' \
                    and (len(self.data2.qx_data) != len(self.data1.qx_data) \
                    or len(self.data2.qy_data) != len(self.data1.qy_data)
                    or not all(i == j for i, j in
                                     zip(self.data1.qx_data, self.data2.qx_data))
                    or not all(i == j for i, j in
                                zip(self.data1.qy_data, self.data2.qy_data))
                         ):
                self.cbData1.setStyleSheet(BG_RED)
                self.cbData2.setStyleSheet(BG_RED)
                logging.warning('Cannot compute 2D data of different lengths')
                return False

            else:
                self.cbData1.setStyleSheet(BG_WHITE)
                self.cbData2.setStyleSheet(BG_WHITE)
                return True

    def onCheckOutputName(self):
        """ Check that name of output does not already exist """
        name_to_check = str(self.txtOutputData.text())
        self.txtOutputData.setStyleSheet(BG_WHITE)

        if name_to_check is None or name_to_check == '':
            self.txtOutputData.setStyleSheet(BG_RED)
            logging.warning('No output name')
            return False

        elif name_to_check in self.list_data_items:
            self.txtOutputData.setStyleSheet(BG_RED)
            logging.warning('The Output data name already exists')
            return False

        else:
            self.txtOutputData.setStyleSheet(BG_WHITE)
            return True

    # ########
    # Modification of inputs
    # ########
    def _findId(self, name):
        """ find id of name in list of filenames """
        isinstance(name, str)

        for key_id in list(self.filenames.keys()):
            # data with title
            if self.filenames[key_id].title:
                input = self.filenames[key_id].title
            # data without title
            else:
                input = str(key_id)
            if name in input:
                return key_id

    def _extractData(self, key_id):
        """ Extract data from file with id contained in list of filenames """
        data_complete = self.filenames[key_id]
        dimension = data_complete.__class__.__name__

        if dimension in ('Data1D', 'Data2D'):
            return copy.deepcopy(data_complete)

        else:
            logging.warning('Error with data format')
            return

    # ########
    # PLOTS
    # ########
    def newPlot(self, graph, layout):
        """ Create template for graphs with default '?' layout"""
        assert isinstance(graph, QtWidgets.QGraphicsView)
        assert isinstance(layout, QtWidgets.QHBoxLayout)

        # clear layout
        if layout.count() > 0:
            item = layout.takeAt(0)
            layout.removeItem(item)

        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.prepareSubgraphWithData("?"))

        graph.setLayout(layout)

    def updatePlot(self, graph, layout, data):
        """ plot data in graph after clearing its layout """

        assert isinstance(graph, QtWidgets.QGraphicsView)
        assert isinstance(layout, QtWidgets.QHBoxLayout)

        # clear layout
        if layout.count() > 0:
            item = layout.takeAt(0)
            layout.removeItem(item)

        layout.setContentsMargins(0, 0, 0, 0)

        if isinstance(data, Data2D):
            # plot 2D data
            plotter2D = Plotter2DWidget(self, quickplot=True)
            plotter2D.scale = 'linear'

            plotter2D.ax.tick_params(axis='x', labelsize=8)
            plotter2D.ax.tick_params(axis='y', labelsize=8)

            # Draw zero axis lines.
            plotter2D.ax.axhline(linewidth=1)
            plotter2D.ax.axvline(linewidth=1)

            graph.setLayout(layout)
            layout.addWidget(plotter2D)
            # remove x- and ylabels
            plotter2D.y_label = ''
            plotter2D.x_label = ''
            plotter2D.plot(data=data, show_colorbar=False)
            plotter2D.show()

        elif isinstance(data, Data1D):
            # plot 1D data
            plotter = PlotterWidget(self, quickplot=True)
            data.scale = 'linear'
            plotter.showLegend = False
            graph.setLayout(layout)
            layout.addWidget(plotter)

            plotter.ax.tick_params(axis='x', labelsize=8)
            plotter.ax.tick_params(axis='y', labelsize=8)

            plotter.plot(data=data, hide_error=True, marker='.')

            plotter.show()

        elif float(data) and self.cbData2.currentText() == 'Number':
            # display value of coefficient (to be applied to Data1)
            # in graphData2
            layout.addWidget(self.prepareSubgraphWithData(data))

            graph.setLayout(layout)

    def prepareSubgraphWithData(self, data):
        """ Create graphics view containing scene with string """
        scene = QtWidgets.QGraphicsScene()
        scene.addText(str(data))

        subgraph = QtWidgets.QGraphicsView()
        subgraph.setScene(scene)

        return subgraph
