# 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.utils import utils

from common.FileSelectionWidget import *
from common.StatusLabelWidget import *
from common.StatusSignalingWidget import *

from LidarDatabaseWidget import *
from LidarTableWidget import *
from OutputViewerWidget import *
from PlotWidgets import *
from PolarOutput import *
from PolarParams import *
from PolarRetrievalProcess import *
from RetrievalProgressDialog import *

try:
    from PolarPerturbanceProcess import *
except ImportError:
    polarPerturbanceProcessPluginLoaded = False
else:
    polarPerturbanceProcessPluginLoaded = True

if polarPerturbanceProcessPluginLoaded:
    from PolarPerturbanceParams import *

__all__ = ['PolarRetrieverWidget']

# *****************************************************************************
class PolarRetrieverWidget(QWidget):
    """Main GUI window of the aerosol backscatter and depolarization retrieval
    algorithm utilizing polarized lidar signals."""

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent = None):

        QWidget.__init__(self, parent)
        # Free the resources if other windows would still exist upon closure.
        self.setAttribute(Qt.WA_DeleteOnClose)

        # ---- Members ----
        # This will hold the widget opened by 'View output' button click.
        self.outputViewerWidget = None

        self.algorithmParams = PolarParams.loadFromFile()
        if polarPerturbanceProcessPluginLoaded:
            self.perturbanceParams = PolarPerturbanceParams.loadFromFile()

        # ---- Layout: main widgets ----
        layout = QVBoxLayout()

        self.lidarDatabaseWidget = LidarDatabaseWidget()
        self.lidarDatabaseWidget.dataListChanged.connect(
            self.onLidarDataListChanged)

        self.lidarTableWidget = LidarTableWidget()
        self.lidarTableWidget.addAllowedChannelSequence([
            LidarChannelInfo(532.0, (0, 3)),
            LidarChannelInfo(532.0, 2)])
        self.lidarTableWidget.addRequiredAttribute('lidarRefValue')

        lidarLayout = QVBoxLayout()
        lidarLayout.addWidget(self.lidarDatabaseWidget)
        # Set stretch factor to 1 to prevent vertical shrinkage.
        lidarLayout.addWidget(self.lidarTableWidget, 1)
        lidarLayout.setContentsMargins(0, 0, 0, 0)

        layout.addLayout(lidarLayout)

        self.retrievalSelectionWidget = PolarRetrievalSelectionWidget()
        layout.addWidget(self.retrievalSelectionWidget)

        # ---- Layout: status bar and button box ----
        self.statusLabel = StatusLabelWidget()
        layout.addWidget(self.statusLabel)

        self.paramsButton = QPushButton('&Parameters')
        self.paramsButton.setIcon(gui.loadIcon('parameters'))
        self.paramsButton.clicked.connect(self.onParamsButtonClicked)

        if polarPerturbanceProcessPluginLoaded:
            self.perturbanceButton = QPushButton('Per&turbance options')
            self.perturbanceButton.clicked.connect(
                self.onPerturbanceButtonClicked)

        self.retrieveButton = QPushButton('&Retrieve')
        self.retrieveButton.clicked.connect(self.onRetrieveButtonClicked)

        self.viewOutputButton = QPushButton('&View output')
        self.viewOutputButton.clicked.connect(self.onViewOutputButtonClicked)

        buttonBoxLayout = QHBoxLayout()
        buttonBoxLayout.addWidget(self.paramsButton)
        if polarPerturbanceProcessPluginLoaded:
            buttonBoxLayout.addWidget(self.perturbanceButton)
        buttonBoxLayout.addStretch()
        buttonBoxLayout.addWidget(self.retrieveButton)
        buttonBoxLayout.addWidget(self.viewOutputButton)
        layout.addLayout(buttonBoxLayout)

        self.setLayout(layout)

        self.setWindowTitle('Depolarization retriever')
        self.setWindowIcon(gui.loadIcon('institute-of-physics'))

        # ---- Initialization ----
        for widget in (self.lidarDatabaseWidget, self.retrievalSelectionWidget,
            self.lidarTableWidget):

            widget.statusMessageChanged.connect(self.onStatusMessageChanged)
            widget.repaintRequested.connect(
                self.statusLabel.repaintParentWindow)

        self.loadSettings()

        self.lidarTableWidget.getTableWidget().setFocus()

    # ---- Private overridden methods -----------------------------------------
    def closeEvent(self, event):
        self.saveSettings()
        event.accept()

    # ---- Private methods ----------------------------------------------------
    def saveSettings(self):
        settings = QSettings('settings/interface/PolarRetrieverWidget.ini',
            QSettings.IniFormat)

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

        # Save input and output file paths.
        self.lidarDatabaseWidget.saveSettings(settings)
        self.lidarTableWidget.saveSettings(settings)
        self.retrievalSelectionWidget.saveSettings(settings)

    def loadSettings(self):
        settings = QSettings('settings/interface/PolarRetrieverWidget.ini',
            QSettings.IniFormat)

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

        # Load input and output file paths.
        self.lidarDatabaseWidget.loadSettings(settings)
        self.lidarTableWidget.loadSettings(settings)
        self.retrievalSelectionWidget.loadSettings(settings)

    @staticmethod
    def getLidarGridErrorMessage(lidarInputs):
        """Check if boundaries and reference point heights of the given
        'PreparedLidarInput' instances, otherwise suitable for a retrieval,
        coincide with each other.

        Return 'None' if the test succeeds, and an appropriate error message
        otherwise."""

        assert len(lidarInputs) == 2

        # Grid step of the lidar inputs does not have to be checked here,
        # because it should already have been checked in
        # 'LidarTableWidget.onSelectionChanged'.

        if lidarInputs[0].firstInputIndex != lidarInputs[1].firstInputIndex:
            return ('Lower boundaries of the selected lidar measurements are '
                'not the same')

        elif lidarInputs[0].lastInputIndex != lidarInputs[1].lastInputIndex:
            return ('Upper boundaries of the selected lidar measurements are '
                'not the same')

        elif lidarInputs[0].refPointIndex != lidarInputs[1].refPointIndex:
            return ('Reference point heights of the selected lidar '
                'measurements are not the same')

        else:
            return None

    # ---- Private slots ------------------------------------------------------
    def onLidarDataListChanged(self):
        """Populate lidar table with contents of the newly loaded lidar
        database."""

        lidarDataList = self.lidarDatabaseWidget.getDataList()

        self.lidarTableWidget.connectDataList(lidarDataList)

        # Do nothing more if the database fails to load or is empty.
        if len(lidarDataList) == 0:
            return

    def onStatusMessageChanged(self):
        """Update Retrieve button state and show the most relevant error
        message in 'self.statusLabel'."""

        self.viewOutputButton.setEnabled(
            self.retrievalSelectionWidget.getStatusMessage() is None)

        # List of widgets that may generate errors. Widget order defines error
        # message priority.
        widgetList = (self.lidarDatabaseWidget, self.retrievalSelectionWidget,
            self.lidarTableWidget)

        statusMessages = [widget.getStatusMessage() for widget in widgetList]

        if any(message is not None for message in statusMessages):
            self.statusLabel.setMostSevereStatusMessage(statusMessages)
            self.retrieveButton.setEnabled(False)
            return

        lidarInputs = self.lidarTableWidget.getData()

        assert len(lidarInputs) == 2
        assert (lidarInputs[0].polarization in (0, 3))
        assert lidarInputs[1].polarization == 2

        # Make sure that lidar grids of the selected measurements are strictly
        # coincident.
        lidarGridErrorMessage = self.getLidarGridErrorMessage(lidarInputs)

        if lidarGridErrorMessage is not None:
            self.statusLabel.setError(lidarGridErrorMessage)
            self.retrieveButton.setEnabled(False)
            return

        # Display information about selected lidar channels to the user.
        baseChannelStr = ('unpolarized' if  lidarInputs[0].polarization == 0
            else 'parallel-polarized')

        self.statusLabel.setCompleted('The data are ready for the retrieval '
            '(with %s base lidar channel)' %
            txt.quote(baseChannelStr, addQuotes = False))

        self.retrieveButton.setEnabled(True)

    def onParamsButtonClicked(self):
        paramsDialog = PolarParamsDialog(self, self.algorithmParams)
        paramsDialog.exec_()

    if polarPerturbanceProcessPluginLoaded:
        def onPerturbanceButtonClicked(self):

            paramsDialog = PolarPerturbanceParamsDialog(
                self, self.perturbanceParams)
            paramsDialog.exec_()

    def onRetrieveButtonClicked(self):
        """Launch the retrieval modal dialog."""

        # If there is an error, retrieval button should be disabled.
        assert self.lidarTableWidget.getStatusMessage() is None

        lidarInputs = self.lidarTableWidget.getData()

        retrievalProcess = PolarRetrievalProcess(lidarInputs,
            self.algorithmParams)
        if polarPerturbanceProcessPluginLoaded:
            retrievalProcess = PolarPerturbanceProcess(lidarInputs,
                self.algorithmParams, self.perturbanceParams)

        progressDialog = PolarRetrievalDialog(self, retrievalProcess,
            self.retrievalSelectionWidget.getFilePath())

        progressDialog.exec_()

    def onViewOutputButtonClicked(self):
        """Open the output database in the output viewer window, select the
        last database record and activate the window.

        If there is no output viewer window yet, or if the window has been
        closed, create it."""

        if self.outputViewerWidget is not None:
            # Check if the previously created window still exists.
            try:
                self.outputViewerWidget.objectName()
            except RuntimeError:
                self.outputViewerWidget = None

        if self.outputViewerWidget is None:
            # Create a new widget with no database selected.
            self.outputViewerWidget = OutputViewerWidget(
                skipDatabaseLoading = True)

        # Open the output database in the widget.
        self.outputViewerWidget.openLastDatabaseRecord(
            self.retrievalSelectionWidget.getFilePath())
        # This will have no effect if the widget had already existed.
        self.outputViewerWidget.show()

        self.outputViewerWidget.activateWindow()

# *****************************************************************************
class PolarRetrievalSelectionWidget(StatusSignalingWidget):
    """Widget responsible for selection and initialization of the output
    aerosol database in the the aerosol backscatter and depolarization
    retrieval algorithm utilizing polarized lidar signals."""

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent = None):
        StatusSignalingWidget.__init__(self, parent)

        layout = QVBoxLayout()

        self.fileSelector = FileSelectionWidget(
            'Depolarization retrieval &output database:',
            'Microsoft Access databases (*.mdb)',
            'Select depolarization retrieval output database',
            'Open an existing depolarization retrieval output database file')
        self.fileSelector.showCreateButton(
            'Select name for a new depolarization retrieval output database',
            'Create a new empty depolarization retrieval output database file')
        self.fileSelector.fileOpened.connect(self.onFileOpened)
        self.fileSelector.fileCreated.connect(self.onFileCreated)
        layout.addWidget(self.fileSelector)

        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def getFilePath(self):
        return self.fileSelector.getFilePath()

    def saveSettings(self, settings):
        with utils.SettingsGrouper(settings, 'PolarRetrievalSelectionWidget'):
            self.fileSelector.saveSettings(settings, 'filePath')

    def loadSettings(self, settings):
        with utils.SettingsGrouper(settings, 'PolarRetrievalSelectionWidget'):
            self.fileSelector.loadSettings(settings, 'filePath')

    # ---- Private slots ------------------------------------------------------
    def onFileOpened(self):
        """Check format of an existing database file selected by the user."""

        filePath = self.fileSelector.getFilePath()

        if filePath is None:
            self.setErrorMessage(
                'Depolarization retrieval output database is not specified')
            return

        try:
            self.setProgressMessage(
                'Checking depolarization retrieval output database...', True)
            PolarOutput.checkDataFormat(filePath)

        except txt.Error as e:
            self.setErrorMessage(e.text)
            return

        self.clearStatusMessage()

    def onFileCreated(self):
        """Create a new database file at the file path selected by the user."""

        filePath = self.fileSelector.getFilePath()

        try:
            self.setProgressMessage(
                'Creating depolarization retrieval output database...', True)
            PolarOutput.createDatabase(filePath)

        except txt.Error as e:
            self.setErrorMessage(e.text)
            self.fileSelector.invalidateFilePath()
            return

        # Check format of the newly created database file.
        self.onFileOpened()

# *****************************************************************************
class PolarRetrievalDialog(RetrievalProgressDialog):
    """Graphical user interface for the optimization process."""

    # ---- Private overridden methods -----------------------------------------
    def createPlotWidget(self):
        return PolarPlotWidget()

    def getSettingsFilePath(self):
        return 'settings/interface/PolarRetrievalDialog.ini'
