import copy
import logging
import re
import time

from PySide6 import QtWidgets

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

from .UI.DataOperationUtilityUI import Ui_DataOperationUtility

logger = logging.getLogger(__name__)


class DataOperationUtilityPanel(QtWidgets.QDialog, Ui_DataOperationUtility):
    def __init__(self, parent=None):
        super(DataOperationUtilityPanel, self).__init__()
        self.setupUi(self)
        self.manager = parent
        self.communicator = GuiUtils.communicator

        # 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.cmdSaveData.clicked.connect(self.onSaveData)

        self.cmdCompute.setEnabled(False)
        self.cmdSaveData.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].name:
                    # filenames with titles
                    new_title = filenames[key_id].name
                    list_datafiles.append(new_title)
                    self.list_data_items.append(new_title)

                else:
                    # filenames without titles by removing time.time()
                    new_title = re.sub(r'\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:
            logger.error(ex)
            return

        self.output = output

        # if outputname was unused, write output result to it
        # and display plot
        if self.onCheckOutputName():
            # plot result
            self.updatePlot(self.graphOutput, self.layoutOutput, self.output)

    def onSaveData(self):
        """ Save current output in data file and add it to the DataExplorer """
        # If output is empty, then compute it first
        if self.output is None:
            self.onCompute()
        # if outputname was unused, proceed to save files
        if self.onCheckOutputName():
            # add outputname to self.filenames
            self.list_data_items.append(str(self.txtOutputData.text()))
            # send result to DataExplorer
            self.onPrepareOutputData()
            # Add the new plot to the comboboxes
            self.cbData1.addItem(self.output.name)
            self.cbData2.addItem(self.output.name)
            if self.filenames is None:
                self.filenames = {}
            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
        self.output.id = name + str(time.time())
        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)

        # Initialize output
        self.output = None

    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.cmdSaveData.setEnabled(False)

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

        self.data1OK = False
        self.data2OK = False

        # Initialize output
        self.output = None

        # 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', '']
        # Initialize output
        self.output = None

        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())
            self.cmdSaveData.setEnabled(False)
            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 and SaveData buttons only if Data2 is defined and data compatible
            self.cmdCompute.setEnabled(self.onCheckChosenData())
            self.cmdSaveData.setEnabled(self.onCheckChosenData())

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

        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)
            self.cmdSaveData.setEnabled(False)
            return

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

            # Enable Compute and SaveData buttons only if Data1 defined and compatible data
            self.cmdCompute.setEnabled(self.onCheckChosenData())
            self.cmdSaveData.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())
            self.cmdSaveData.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 == '':
                msg = 'DataOperation: Number requires a float number'
                logger.warning(msg)
                self.txtNumber.setStyleSheet(BG_ERROR)

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

            else:
                self.txtNumber.setStyleSheet(BG_DEFAULT)
                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_DEFAULT)
                self.cbData2.setStyleSheet(BG_DEFAULT)
                return True

            elif self.data1.__class__.__name__ != self.data2.__class__.__name__:
                self.cbData1.setStyleSheet(BG_ERROR)
                self.cbData2.setStyleSheet(BG_ERROR)
                print(self.data1.__class__.__name__ != self.data2.__class__.__name__)
                logger.error('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))):
                logger.error('Cannot compute 1D data of different lengths')
                self.cbData1.setStyleSheet(BG_ERROR)
                self.cbData2.setStyleSheet(BG_ERROR)
                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_ERROR)
                self.cbData2.setStyleSheet(BG_ERROR)
                logger.error('Cannot compute 2D data of different lengths')
                return False

            else:
                self.cbData1.setStyleSheet(BG_DEFAULT)
                self.cbData2.setStyleSheet(BG_DEFAULT)
                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_DEFAULT)

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

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

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

    # ########
    # Modification of inputs
    # ########
    def _findId(self, name: str) -> str | None:
        """Verify the displayed name matches a name in the data explorer"""
        name_list = [str(self.filenames[key_id].name) for key_id in self.filenames.keys()]
        key_list = [str(key_id) for key_id in self.filenames.keys()]
        # The name must completely match the key or name to ensure 'non-alphabetic-data_name'
        # does not match 'M1[non-alphabetic-data_name]'
        # See https://github.com/SasView/sasview/issues/3796 for reference.
        if name in name_list or name in key_list:
            return str(name)
        else:
            return None

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

    # ########
    # 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)
            widget = item.widget()
            if widget is not None:
                widget.deleteLater()

        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)
            widget = item.widget()
            if widget is not None:
                widget.deleteLater()

        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
