# Copyright (c) 2011-2014, B.I.Stepanov Institute of Physics, National Academy
# of Sciences of Belarus.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from PyQt4.QtCore import *
from PyQt4.QtGui import *

from common.utils import gui
from common.utils import txt

from common.ConsoleWidget import *
from common.StatusLabelWidget import *

from MatlabProcess import *

__all__ = ['RetrievalProgressDialog']

# *****************************************************************************
class RetrievalProgressDialog(QDialog):
    """Graphical user interface to a 'MatlabProcess'. Displays retrieval
    progress and retrieval results to the user; when the retrieval is
    successfully completed, handles saving of the output data to a database and
    optionally to an Excel file.

    Attributes:
      - 'retrievalProcess': a properly initialized 'MatlabProcess' instance.
      - 'outputFilePath': path to the database file to append the retrieval
        results to."""

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent, retrievalProcess, outputFilePath):

        QDialog.__init__(self, parent)

        # ---- Members ----
        self.retrievalProcess = retrievalProcess
        self.retrievalProcess.outputAvailable.connect(self.onOutputAvailable)
        self.retrievalProcess.retrievalFinished.connect(
            self.onRetrievalFinished)

        self.outputFilePath = outputFilePath

        # A list of the data instance holding the retrieval results and
        # optimization process snapshots or 'None' if the retrieval has not
        # finished successfully.
        self.retrievalOutputs = None

        # 'True' if the retrieval results have been successfully appended to
        # the database (whereas Excel file saving might probably have failed).
        self.dbFileSaved = False

        # ---- Layout ---------------------------------------------------------
        layout = QVBoxLayout()

        self.statusLabel = StatusLabelWidget()
        layout.addWidget(self.statusLabel)

        # Maximum width of the Matlab output is normally 81 characters.
        self.outputWindow = ConsoleWidget(85, 25)

        self.plotWidget = self.createPlotWidget()

        # Below the plot widget is a slider that allows to select an iteration
        # to display the plots for.
        self.iterationSlider = QSlider()
        self.iterationSlider.setOrientation(Qt.Horizontal)
        self.iterationSlider.setTickPosition(QSlider.TicksBelow)
        self.iterationSlider.setTickInterval(1)
        # This will be updated upon completion of the optimization process.
        self.iterationSlider.setRange(0, 1)
        self.iterationSlider.setSingleStep(1)
        # This is required in order for the mouse wheel to always scroll one
        # iteration at a time.
        self.iterationSlider.setPageStep(1)
        self.iterationSlider.valueChanged.connect(self.onSliderValueChanged)

        # Stack two labels upon each other: an actual iteration indicator and
        # an invisible label holding a value which is presumably the longest
        # possible one for the indicator string. Thus, right border of the
        # slider won't change regardless of the iteration selected.
        self.iterationLabel = QLabel('')
        dummyIterationLabel = QLabel('(000 of 000)')

        iterationLabelLayout = QStackedLayout()
        iterationLabelLayout.addWidget(self.iterationLabel)
        iterationLabelLayout.addWidget(dummyIterationLabel)
        iterationLabelLayout.setCurrentWidget(self.iterationLabel)

        plotLayout = QGridLayout()
        plotLayout.addWidget(self.plotWidget, 0, 0, 1, 3)
        plotLayout.addWidget(QLabel(
            'Iteration of the optimization process:'), 1, 0)
        plotLayout.addWidget(self.iterationSlider, 1, 1)
        plotLayout.addLayout(iterationLabelLayout, 1, 2)
        # Limit the size of the 'iterationLabelLayout'.
        plotLayout.setRowStretch(0, 1)
        plotLayout.setColumnStretch(1, 1)

        self.outputPlot = QWidget()
        self.outputPlot.setLayout(plotLayout)

        self.tabWidget = QTabWidget()
        self.tabWidget.addTab(self.outputWindow, 'Matlab output')
        self.tabWidget.addTab(self.outputPlot, 'Solution plots')
        self.tabWidget.setTabEnabled(
            self.tabWidget.indexOf(self.outputPlot), False)
        layout.addWidget(self.tabWidget)

        self.excelOutputCheckBox = QCheckBox('Save data in an &Excel file '
            'along with the Access database')
        layout.addWidget(self.excelOutputCheckBox)

        self.saveButton = QPushButton('&Save')
        self.saveButton.setAutoDefault(False)
        self.saveButton.setDefault(True)
        self.saveButton.clicked.connect(self.onSaveButtonClicked)

        self.cancelButton = QPushButton('Cancel')
        # Disable auto default buttons so that an Enter key press could not
        # cancel the retrieval during its progress.
        self.cancelButton.setAutoDefault(False)
        self.cancelButton.clicked.connect(self.onCancelButtonClicked)

        buttonBoxLayout = QHBoxLayout()
        buttonBoxLayout.addStretch()
        buttonBoxLayout.addWidget(self.saveButton)
        buttonBoxLayout.addWidget(self.cancelButton)
        layout.addLayout(buttonBoxLayout)

        self.setLayout(layout)

        # The retrieval should start immediately after the construction.
        self.setWindowTitle('Retrieving...')
        self.setWindowIcon(gui.loadIcon('institute-of-physics'))

        self.setWindowFlags(
            self.windowFlags() & ~Qt.WindowContextHelpButtonHint)

        # ---- Initialization -------------------------------------------------
        # Close the dialog upon completion.
        self.accepted.connect(self.close)
        self.rejected.connect(self.close)

        # Disable the save button until the retrieval finishes.
        self.saveButton.setEnabled(False)

        # The retrieval should start immediately after the construction.
        self.statusLabel.setProgress('Running the retrieval algorithm...')

        self.loadSettings()

        # Space key will trigger the checkbox, Esc will cancel the retrieval,
        # and Enter will apply it, when finished.
        self.excelOutputCheckBox.setFocus()

        # Don't start the external process until 'exec_' method is called.
        QTimer.singleShot(0, self.onInitComplete)

    # ---- Protected overridable methods --------------------------------------
    def createPlotWidget(self):
        """Create a widget suitable to visually represent data instances
        holding results of a retrieval (i.e. those returned by
        'MatlabProcess.getRetrievalOutput')."""
        return QWidget()

    def getSettingsFilePath(self):
        """Return path to an INI file to store the dialog's interface settings
        in."""
        return None

    def plotData(self, dataInstance):
        """Display the given retrieval output data instance in the plot widget
        created by 'createPlotWidget'."""

        self.plotWidget.plotData(dataInstance)

    # ---- Private overridden methods -----------------------------------------
    def closeEvent(self, event):
        """Terminate the retrieval process, if any, and save the dialog
        settings."""

        self.retrievalProcess.cancelRetrieval()
        self.saveSettings()
        event.accept()

    # ---- Private methods ----------------------------------------------------
    def saveSettings(self):
        settings = QSettings(self.getSettingsFilePath(), QSettings.IniFormat)

        # Save window size, position and maximization state.
        gui.saveWindowSettings(self, settings)

        # Save the Excel file saving flag.
        settings.setValue('saveExcelOutput',
            self.excelOutputCheckBox.checkState() == Qt.Checked)

    def loadSettings(self):
        settings = QSettings(self.getSettingsFilePath(), QSettings.IniFormat)

        # Load window size, position and maximization state.
        gui.loadWindowSettings(self, settings)

        # Load the Excel file saving flag. If the settings file does not exist,
        # check box will remain unchecked.
        if settings.contains('saveExcelOutput'):
            if settings.value('saveExcelOutput').toBool():
                self.excelOutputCheckBox.setCheckState(Qt.Checked)
            else:
                self.excelOutputCheckBox.setCheckState(Qt.Unchecked)

    # ---- Private slots ------------------------------------------------------
    def onInitComplete(self):
        """Prepare input data and start the retrieval process."""

        self.retrievalProcess.startRetrieval()

    def onOutputAvailable(self, outputLine):
        """Print the Matlab process output to the output window."""

        # Convert 'QString' to the Python data type, just to feel safe.
        # Note: it seems that PyQt signals and slots mechanism will always
        # convert 'unicode's and 'str's, even if the data type specified in
        # 'pyqtSignal' is a Python one.
        self.outputWindow.appendOutput(unicode(outputLine))

    def onRetrievalFinished(self):
        """Interpret the output of the Matlab process and switch the dialog to
        either retrieval done or retrieval failed mode."""

        errorMessage = self.retrievalProcess.getErrorMessage()

        if errorMessage is not None:
            # The retrieval has failed for some reason.
            self.setWindowTitle('Retrieving... (Failed)')
            self.statusLabel.setError(errorMessage)

            # The checkbox is of no use now.
            self.excelOutputCheckBox.setEnabled(False)
            # The only thing to do now is to accept the situation as it is.
            self.cancelButton.setFocus(True)

        else:
            self.retrievalOutputs = (
                self.retrievalProcess.getAllRetrievalOutputs())
            assert self.retrievalOutputs is not None

            # The retrieval has completed successfuly.
            self.setWindowTitle('Retrieving... (Done)')
            self.statusLabel.setCompleted(
                'The retrieval has completed successfully')

            # Hint that the retrieval results are available but not saved yet.
            self.saveButton.setEnabled(True)

            # Show the plot representing the data to be saved.
            self.tabWidget.setTabEnabled(
                self.tabWidget.indexOf(self.outputPlot), True)
            self.tabWidget.setCurrentWidget(self.outputPlot)

            self.iterationSlider.setMaximum(len(self.retrievalOutputs) - 1)
            self.iterationSlider.setValue(len(self.retrievalOutputs) - 1)

            # It seems that 'setValue' does not invoke the signal if the value
            # is zero (i.e. if initial approximation har satisfied the
            # optimization criteria).
            if len(self.retrievalOutputs) == 1:
                self.onSliderValueChanged(0)

    def onSliderValueChanged(self, value):

        self.iterationLabel.setText('(%d of %d)' % (
            value, self.iterationSlider.maximum()))

        self.plotData(self.retrievalOutputs[value])

    def onSaveButtonClicked(self):

        # Save button should be unavailable until the retrieval competion.
        assert self.retrievalOutputs is not None

        try:
            # Append the retrieved data to the database.
            if not self.dbFileSaved:
                self.statusLabel.setProgress(
                    'Saving data to the output database...', True)

                self.retrievalOutputs[-1].writeData(self.outputFilePath)
                self.dbFileSaved = True

            # Append the data to the Excel file, if required.
            if self.excelOutputCheckBox.checkState() == Qt.Checked:
                self.excelOutputCheckBox.setEnabled(False)
                self.statusLabel.setProgress(
                    'Saving data to the Excel file...', True)

                excelFilePath = (
                    self.retrievalOutputs[-1].getExcelExportFilePath(
                    self.outputFilePath))
                self.retrievalOutputs[-1].exportDataToExcel(excelFilePath)

        # If anything goes wrong, give the user a chance to fix the problem.
        # The error message in the status label should make it clear which
        # stage of the saving process is 'Retry' button related to.
        except txt.Error as e:
            self.statusLabel.setError(e.text)
            self.saveButton.setText('&Retry')
            return

        # If everything goes right, close the dialog.
        self.accept()

    def onCancelButtonClicked(self):
        """Close the dialog without saving the data, if any."""
        self.reject()
