# 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 LiricOutput import *
from LiricRetrievalProcess import *
from ManualParams import *
from OutputViewerWidget import *
from PhotometerDatabaseWidget import *
from PhotometerTableWidget import *
from PlotWidgets import *
from RetrievalProgressDialog import *

try:
    from LiricPerturbanceProcess import *
except ImportError:
    liricPerturbanceProcessPluginLoaded = False
else:
    liricPerturbanceProcessPluginLoaded = True

if liricPerturbanceProcessPluginLoaded:
    from LiricPerturbanceParams import *

__all__ = ['ManualRetrieverWidget']

# *****************************************************************************
class ManualRetrieverWidget(QWidget):
    """Main window of the manual version of aerosol profile retrieval algorithm
    GUI."""

    # ---- 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 = ManualParams.loadFromFile()
        if liricPerturbanceProcessPluginLoaded:
            self.perturbanceParams = LiricPerturbanceParams.loadFromFile()

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

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

        self.lidarTableWidget = LidarTableWidget()
        self.lidarTableWidget.addAllowedChannelSequence([
            LidarChannelInfo(355.0, (0, 3)),
            LidarChannelInfo(532.0, (0, 3)),
            LidarChannelInfo(1064.0, (0, 3))])
        self.lidarTableWidget.addAllowedChannelSequence([
            LidarChannelInfo(355.0, (0, 3)),
            LidarChannelInfo(532.0, (0, 3)),
            LidarChannelInfo(1064.0, (0, 3)),
            LidarChannelInfo(532.0, 2)])
        self.lidarTableWidget.addAllowedChannelSequence([
            LidarChannelInfo(355.0, (0, 3)),
            LidarChannelInfo(532.0, (0, 3)),
            LidarChannelInfo(1064.0, (0, 3)),
            LidarChannelInfo(355.0, 2)])

        self.lidarTableWidget.selectionChanged.connect(
            self.onLidarSelectionChanged)

        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)

        lidarWidget = QWidget()
        lidarWidget.setLayout(lidarLayout)

        self.photometerDatabaseWidget = PhotometerDatabaseWidget()
        self.photometerDatabaseWidget.dataListChanged.connect(
            self.onPhotometerDataListChanged)

        self.photometerTableWidget = PhotometerTableWidget()

        photometerLayout = QVBoxLayout()
        photometerLayout.addWidget(self.photometerDatabaseWidget)
        photometerLayout.addWidget(self.photometerTableWidget)
        photometerLayout.setContentsMargins(0, 0, 0, 0)

        photometerWidget = QWidget()
        photometerWidget.setLayout(photometerLayout)

        self.splitter = QSplitter(Qt.Vertical)
        self.splitter.setChildrenCollapsible(False)
        self.splitter.addWidget(lidarWidget)
        self.splitter.addWidget(photometerWidget)
        layout.addWidget(self.splitter)

        self.retrievalSelectionWidget = RetrievalSelectionWidget()
        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 liricPerturbanceProcessPluginLoaded:
            self.perturbanceButton = QPushButton('Per&turbance options')
            self.perturbanceButton.clicked.connect(
                self.onPerturbanceButtonClicked)

        # self.helpButton = QPushButton('')
        # self.helpButton.setIcon(gui.loadIcon('question'))
        # self.helpButton.clicked.connect(self.onHelpButtonClicked)

        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 liricPerturbanceProcessPluginLoaded:
            buttonBoxLayout.addWidget(self.perturbanceButton)
        buttonBoxLayout.addStretch()
        buttonBoxLayout.addWidget(self.retrieveButton)
        buttonBoxLayout.addWidget(self.viewOutputButton)
        # buttonBoxLayout.addWidget(self.helpButton)
        layout.addLayout(buttonBoxLayout)

        self.setLayout(layout)

        self.setWindowTitle('Manual aerosol profile retriever')
        self.setWindowIcon(gui.loadIcon('institute-of-physics'))

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

            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/ManualRetrieverWidget.ini',
            QSettings.IniFormat)

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

        # Save splitter state.
        settings.setValue('splitterState', self.splitter.saveState())

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

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

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

        # Load splitter state.
        if settings.contains('splitterState'):
            self.splitter.restoreState(
                settings.value('splitterState').toByteArray())

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

    # ---- Private slots ------------------------------------------------------
    def onLidarDataListChanged(self):
        """Populate lidar table with contents of the newly loaded lidar
        database. After that, find the most relevant photometer data subset for
        the loaded lidar database and load it if the user agrees to do that."""

        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

        # Find the most frequent latitude, longitude and measurement year
        # combination for the loaded lidar database.
        latitude, longitude, year = getLidarLatLongAndYear(lidarDataList)

        # Find the nearest AERONET station for the loaded database. This will
        # return 'None' if AERONET site list file has not been loaded yet.
        #
        # Since AERONET site list is only loaded in 'PhotometerDatabaseWidget's
        # 'loadSettings', and the latter is called only when initialization
        # of the lidar database is completed, the following will return 'None'
        # during the application initialization process. Thus, photometer data
        # lookup won't occur on application startup, and photometer data
        # parameters stored in the settings file will be used instead.
        siteName = self.photometerDatabaseWidget.getNearestSiteName(
            latitude, longitude)

        if siteName is None:
            return

        # Don't display a message box if photometer data needn't to be altered.
        if (siteName == self.photometerDatabaseWidget.getSiteName() and
            year == self.photometerDatabaseWidget.getYear()):
            return

        dataLevel = self.photometerDatabaseWidget.getDataLevel()

        msgBox = QMessageBox(self)
        msgBox.setWindowTitle('Selecting photometer data')
        msgBox.setText(
            'The most relevant Aeronet site for this lidar database is %s '
            '(year %s).' % (txt.quote(siteName), txt.quoteNumber(year)))
        msgBox.setInformativeText(
            'Would you like to load photometer data file for %s, year %s, '
            'data level %s?' %
            (txt.quote(siteName), txt.quoteNumber(year), txt.quote(dataLevel)))
        msgBox.setIcon(QMessageBox.Question)
        msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)

        # Load the most relevant photometer data subset, if the user agreed.
        if msgBox.exec_() == QMessageBox.Yes:
            self.photometerDatabaseWidget.setSiteNameAndYear(siteName, year)

    def onLidarSelectionChanged(self):
        """Update synchronization icons in the photometer table widget
        according to the selected lidar data and update scrolling of the table
        so that the best-matching photometer measurement is displayed at its
        center."""

        selectedLidarData = self.lidarTableWidget.getSelectedData()

        if len(selectedLidarData) == 0:
            startDateTime = None
            stopDateTime = None

        else:
            startDateTime = min(lidarInput.getStartDateTime()
                for lidarInput in selectedLidarData)
            stopDateTime = max(lidarInput.getStopDateTime()
                for lidarInput in selectedLidarData)

        self.photometerTableWidget.updateSynchronizationHints(
            startDateTime, stopDateTime)

    def onPhotometerDataListChanged(self):
        """Populate photometer table with the newly loaded photometer data."""

        # The data will be modified directly by the photometer table widget,
        # if required.
        self.photometerTableWidget.connectDataList(
            self.photometerDatabaseWidget.getDataList())

        # Update photometer synchronization icons and scroll to the
        # best-matching photometer measurement.
        self.onLidarSelectionChanged()

    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.photometerDatabaseWidget,
            self.retrievalSelectionWidget,
            self.lidarTableWidget, self.photometerTableWidget)

        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()

        gridDisplacement = round(self.algorithmParams.photometerDisplacement /
            lidarInputs[0].gridHeightStep)

        if gridDisplacement > min(
            input.firstInputIndex for input in lidarInputs):

            self.statusLabel.setError('Photometer displacement is larger than '
                'lower boundary of the lidar measurements')
            self.retrieveButton.setEnabled(False)
            return

        # Display the list of aerosol modes that may be retrieved with selected
        # data.
        if len(lidarInputs) == 3:
            aerosolModes = '2 aerosol modes: %s, %s' % (
                txt.quote('Fine'), txt.quote('Coarse'))
        else:
            assert len(lidarInputs) == 4
            aerosolModes = '3 aerosol modes: %s, %s, %s' % (
                txt.quote('Fine'), txt.quote('Coarse Spherical'),
                txt.quote('Coarse Spheroid'))

        self.statusLabel.setCompleted(
            'The data are ready for the retrieval (%s)' % aerosolModes)

        self.retrieveButton.setEnabled(True)

    def onParamsButtonClicked(self):

        paramsDialog = ManualParamsDialog(self, self.algorithmParams)
        paramsDialog.exec_()

        # A change in parameters may lead to modification of the widget's error
        # status. Call 'onStatusMessageChanged' to perform the error checking.
        self.onStatusMessageChanged()

    if liricPerturbanceProcessPluginLoaded:
        def onPerturbanceButtonClicked(self):

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

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

        # Make sure that manually entered AOT correction values won't get lost
        # if something goes badly wrong during the retrieval.
        self.photometerDatabaseWidget.saveAotCorrFile()

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

        lidarInputs = self.lidarTableWidget.getData()
        photometerInput = self.photometerTableWidget.getData()

        retrievalProcess = LiricRetrievalProcess(lidarInputs, photometerInput,
            self.algorithmParams)
        if liricPerturbanceProcessPluginLoaded:
            retrievalProcess = LiricPerturbanceProcess(lidarInputs,
                photometerInput, self.algorithmParams, self.perturbanceParams)

        progressDialog = LiricRetrievalDialog(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()

# **** Private functions ******************************************************
def getLidarLatLongAndYear(lidarDataList):
    """Return a 3-tuple of the most relevant latitude, longitude, and
    measurement year for the given non-empty list of 'PreparedLidarInput'
    instances.

    Normally, geodetic coordinates of a lidar station and measurement year will
    be constant within a single lidar database file, and these constant values
    will be the returned ones. Otherwise, the most frequent combination of
    latitude, longitude and year will be returned."""

    # '(latitude, longitude, year)' combinations present in the database.
    latLongYears = []
    latLongYearFrequencies = []

    for input in lidarDataList:

        latLongYear = (input.latitude, input.longitude, input.startDate.year)

        if latLongYear not in latLongYears:
            latLongYears.append(latLongYear)
            latLongYearFrequencies.append(1)

        else:
            i = latLongYears.index(latLongYear)
            latLongYearFrequencies[i] += 1

    # This will raise an exception if 'lidarDataList' is empty.
    bestLatLongYear = latLongYears[0]
    maxFrequency = latLongYearFrequencies[0]

    # Look for the most frequent combination of latitude, longitude and year.
    for i in range(1, len(latLongYears)):
        if latLongYearFrequencies[i] > maxFrequency:
            bestLatLongYear = latLongYears[i]
            maxFrequency = latLongYearFrequencies[i]

    return bestLatLongYear

# *****************************************************************************
class RetrievalSelectionWidget(StatusSignalingWidget):
    """Widget responsible for selection and initialization of the output
    aerosol profile database in the manual version of aerosol profile retrieval
    algorithm."""

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

        layout = QVBoxLayout()

        self.fileSelector = FileSelectionWidget(
            'Aerosol profile &output database:',
            'Microsoft Access databases (*.mdb)',
            'Select aerosol profile output database',
            'Open an existing aerosol profile output database file')
        self.fileSelector.showCreateButton(
            'Select name for a new aerosol profile output database',
            'Create a new empty aerosol profile 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, 'RetrievalSelectionWidget'):
            self.fileSelector.saveSettings(settings, 'filePath')

    def loadSettings(self, settings):
        with utils.SettingsGrouper(settings, 'RetrievalSelectionWidget'):
            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(
                'Aerosol profile output database is not specified')
            return

        try:
            self.setProgressMessage(
                'Checking aerosol profile output database...', True)
            LiricOutput.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 aerosol profile output database...', True)
            LiricOutput.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 LiricRetrievalDialog(RetrievalProgressDialog):
    """Graphical user interface for the optimization process."""

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

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