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

import os.path

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtSvg import *
from PyQt4.Qwt5 import *

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

from common.DataTableWidget import *
from common.FileSelectionWidget import *
from common.StatusLabelWidget import *

from LiricOutput import *
from PlotWidgets import *

try:
    from RamanOutput import *
except ImportError:
    ramanOutputPluginLoaded = False
else:
    ramanOutputPluginLoaded = True

try:
    from PolarOutput import *
except ImportError:
    polarOutputPluginLoaded = False
else:
    polarOutputPluginLoaded = True

__all__ = ['OutputViewerWidget']

# *****************************************************************************
class OutputViewerWidget(QWidget):
    """Window used to display the contents of an output database of the aerosol
    profile retrieval algorithm."""

    # ---- Private class attributes -------------------------------------------
    tableColumnLists = {}
    plotWidgetClasses = {}

    icon355  = gui.loadIcon('button-purple')
    icon532  = gui.loadIcon('button-green')
    icon1064 = gui.loadIcon('button-red')

    tableColumnLists[LiricOutput] = [
        TableWidgetColumn('startDate', 'Date', 'date',
            'Start date for the lidar measurement group'),
        TableWidgetColumn('startTime', 'TStart', 'time',
            'Start time for the lidar measurement group'),
        TableWidgetColumn('stopTime', 'TStop', 'time',
            'Stop time for the lidar measurement group'),
        TableWidgetColumn('photometerTime', 'TPhoto', 'time',
            'Time of the matching photometer measurement'),

        TableWidgetColumn('retrievalDate', 'DRetr', 'date',
            'Retrieval date'),
        TableWidgetColumn('retrievalTime', 'TRetr', 'time',
            'Retrieval time'),

        TableWidgetColumn('gridHeightStep', 'HStep', '%.1f',
            'Height step of the data grid (m)'),

        TableWidgetColumn('calculatedVFine', 'V-F', '%.3f',
            u'Retrieved concentration of fine aerosol mode (\u03bcm)'),
        TableWidgetColumn('calculatedVCoarse', 'V-C', '%.3f',
            u'Retrieved concentration of coarse aerosol mode (\u03bcm)'),
        TableWidgetColumn('calculatedVSpherical', 'V-Cs', '%.3f',
            u'Retrieved concentration of coarse spherical aerosol mode '
            u'(\u03bcm)'),
        TableWidgetColumn('calculatedVSpheroid', 'V-Cns', '%.3f',
            u'Retrieved concentration of coarse non-spherical aerosol mode '
            u'(\u03bcm)'),

        TableWidgetColumn('calculatedSphericity', '%sph', '%.1f',
            'Retrieved fraction of spherical aerosol particles (%)'),

        TableWidgetColumn('residual355', 'R-355', '%.1e',
            'Lidar equation residual at 355 nm', icon355),
        TableWidgetColumn('residual532', 'R-532', '%.1e',
            'Lidar equation residual at 532 nm', icon532),
        TableWidgetColumn('residual1064', 'R-1064', '%.1e',
            'Lidar equation residual at 1064 nm', icon1064),
        TableWidgetColumn('residual532C', u'R-532,\u22a5', '%.1e',
            'Lidar equation residual at 532 nm for cross-polarized '
            'channel', icon532),

        TableWidgetColumn('residualFine', 'R-F', '%.1e',
            'Concentration equation residual for fine mode'),
        TableWidgetColumn('residualCoarse', 'R-C', '%.1e',
            'Concentration equation residual for coarse mode'),
        TableWidgetColumn('residualSpherical', 'R-Cs', '%.1e',
            'Concentration equation residual for coarse spherical mode'),
        TableWidgetColumn('residualSpheroid', 'R-Cns', '%.1e',
            'Concentration equation residual for coarse non-spherical '
            'mode'),

        TableWidgetColumn('resSmoothFine', 'Rsm-F', '%.1e',
            'Smoothness equation residual for fine mode'),
        TableWidgetColumn('resSmoothCoarse', 'Rsm-C', '%.1e',
            'Smoothness equation residual for coarse mode'),
        TableWidgetColumn('resSmoothSpherical', 'Rsm-Cs', '%.1e',
            'Smoothness equation residual for coarse spherical mode'),
        TableWidgetColumn('resSmoothSpheroid', 'Rsm-Cns', '%.1e',
            'Smoothness equation residual for coarse non-spherical mode')
    ]

    plotWidgetClasses[LiricOutput] = LiricPlotWidget

    if ramanOutputPluginLoaded:
        iconSource  = gui.loadIcon('button-green')
        iconRaman  = gui.loadIcon('button-yellow')

        tableColumnLists[RamanOutput] = [
            TableWidgetColumn('startDate', 'Date', 'date',
                'Start date for the lidar measurement group'),
            TableWidgetColumn('startTime', 'TStart', 'time',
                'Start time for the lidar measurement group'),
            TableWidgetColumn('stopTime', 'TStop', 'time',
                'Stop time for the lidar measurement group'),

            TableWidgetColumn('retrievalDate', 'DRetr', 'date',
                'Retrieval date'),
            TableWidgetColumn('retrievalTime', 'TRetr', 'time',
                'Retrieval time'),

            TableWidgetColumn('gridHeightStep', 'HStep', '%.1f',
                'Height step of the data grid (m)'),

            TableWidgetColumn('wavelengthSource', 'Source', '%.1f',
                'Wavelength of the source lidar channel (nm)'),
            TableWidgetColumn('wavelengthRaman', 'Raman', '%.1f',
                'Wavelength of the receiving lidar channel (nm)'),

            TableWidgetColumn('residualSource', 'R-source', '%.1e',
                'Lidar equation residual at the source lidar channel',
                iconSource),
            TableWidgetColumn('residualRaman', 'R-Raman', '%.1e',
                'Lidar equation residual at the Raman lidar channel',
                iconRaman),

            TableWidgetColumn('resSmoothBackscatter', 'Rsm-Bs', '%.1e',
                'Smoothness equation residual for backscatter ratio profile'),
            TableWidgetColumn('resSmoothLidar', 'Rsm-L', '%.1e',
                'Smoothness equation residual for lidar ratio profile'),
            TableWidgetColumn('resDeviateLidar', 'Rdev-L', '%.1e',
                'Middle value deviation equation residual for lidar ratio '
                'profile'),
        ]

        plotWidgetClasses[RamanOutput] = RamanPlotWidget

    if polarOutputPluginLoaded:

        tableColumnLists[PolarOutput] = [
            TableWidgetColumn('startDate', 'Date', 'date',
                'Start date for the lidar measurement group'),
            TableWidgetColumn('startTime', 'TStart', 'time',
                'Start time for the lidar measurement group'),
            TableWidgetColumn('stopTime', 'TStop', 'time',
                'Stop time for the lidar measurement group'),

            TableWidgetColumn('retrievalDate', 'DRetr', 'date',
                'Retrieval date'),
            TableWidgetColumn('retrievalTime', 'TRetr', 'time',
                'Retrieval time'),

            TableWidgetColumn('gridHeightStep', 'HStep', '%.1f',
                'Height step of the data grid (m)'),

            TableWidgetColumn('polarizationBase', 'Polar', '%d',
                'Polarization ID of the base lidar channel'),

            TableWidgetColumn('aerBackscatterRatioRef', 'R', '%.1e',
                'Retrieved value of aerosol backscatter ratio at the '
                'reference point'),
            TableWidgetColumn('lidarCorrection', 'Q', '%.1e',
                'Retrieved value of the cross-polarized lidar correcion '
                'coefficient'),

            TableWidgetColumn('residualBase', 'R-Base', '%.1e',
                'Lidar equation residual for the base lidar channel'),
            TableWidgetColumn('residualCross', 'R-Cross', '%.1e',
                'Lidar equation residual for the cross-polarized lidar '
                'channel'),

            TableWidgetColumn('resSmoothParallel', 'Rsm-Parallel', '%.1e',
                'Smoothness equation residual for parallel backscatter ratio '
                'profile'),
            TableWidgetColumn('resSmoothCross', 'Rsm-Cross', '%.1e',
                'Smoothness equation residual for cross-polarized backscatter '
                'ratio profile'),
        ]

        plotWidgetClasses[PolarOutput] = PolarPlotWidget

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent = None, skipDatabaseLoading = False):
        """If 'skipDatabaseLoading' is 'True', no database file is loaded upon
        construction; otherwise, the database file whose name is stored in the
        interface settings is loaded (without selecting any of the records)."""

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

        # ---- Members ----
        # This will be a list of 'self.dataType' instances.
        self.data = []
        self.dataType = None

        # ---- Layout: main widgets ----
        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.showReloadButton(
            'Reload the currently selected aerosol profile output database')
        self.fileSelector.fileOpened.connect(self.onFileOpened)
        layout.addWidget(self.fileSelector)

        self.table = DataTableWidget()
        self.table.itemSelectionChanged.connect(self.onSelectionChanged)

        self.plotWidgets = {}
        self.plotWidgetStack = QStackedWidget()

        for dataType in self.plotWidgetClasses:
            self.plotWidgets[dataType] = self.plotWidgetClasses[dataType]()
            self.plotWidgetStack.addWidget(self.plotWidgets[dataType])

        self.splitter = QSplitter(Qt.Vertical)
        self.splitter.setChildrenCollapsible(False)
        self.splitter.addWidget(self.table)
        self.splitter.addWidget(self.plotWidgetStack)
        layout.addWidget(self.splitter)

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

        self.savePlotsButton = QPushButton('&Save plots as')
        self.savePlotsButton.setIcon(gui.loadIcon('print-frame'))
        self.savePlotsButton.clicked.connect(self.onSavePlotsButtonClicked)

        self.exportButton = QPushButton(
            '&Export the database to the Excel file')
        self.exportButton.setIcon(gui.loadIcon('microsoft-office-excel'))
        self.exportButton.clicked.connect(self.onExportButtonClicked)

        buttonBoxLayout = QHBoxLayout()
        buttonBoxLayout.addWidget(self.savePlotsButton)
        buttonBoxLayout.addWidget(self.exportButton)
        buttonBoxLayout.addStretch()
        layout.addLayout(buttonBoxLayout)

        self.setLayout(layout)

        self.setWindowTitle('Output viewer')
        self.setWindowIcon(gui.loadIcon('institute-of-physics'))

        # ---- Initialization -------------------------------------------------
        self.table.setColumns(self.tableColumnLists[LiricOutput])

        self.plotWidgetStack.setCurrentWidget(self.plotWidgets[LiricOutput])

        self.loadSettings(skipDatabaseLoading)

        self.table.setFocus()

    def openDatabase(self, dbFilePath):
        """Load or reload the given database file."""

        self.fileSelector.openFile(dbFilePath)

    def openLastDatabaseRecord(self, dbFilePath):
        """Load or reload the given database file and select its last record on
        success."""

        self.fileSelector.openFile(dbFilePath)
        if self.table.rowCount() > 0:
            self.table.setCurrentCell(self.table.rowCount() - 1, 0)

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

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

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

        self.fileSelector.saveSettings(settings, 'filePath')
        settings.setValue('splitterState', self.splitter.saveState())

        # Save settings for the plot saving command.
        settings.setValue('savePlotsDir', txt.portablePath(self.savePlotsDir))
        settings.setValue('savePlotsFileFormat', self.savePlotsFileFormat)

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

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

        if not skipDatabaseLoading:
            self.fileSelector.loadSettings(settings, 'filePath')

        if settings.contains('splitterState'):
            self.splitter.restoreState(
                settings.value('splitterState').toByteArray())
        else:
            self.splitter.setSizes([1000, 1000])

        # Current directory for the plot saving command.
        self.savePlotsDir = txt.absolutePath(
            settings.value('savePlotsDir', '.').toString())
        # Current file format id for the plot saving dialog.
        self.savePlotsFileFormat = unicode(
            settings.value('savePlotsFileFormat', 'png').toString())

    def clear(self):
        """Silently remove all the data and clear table and plot widgets."""

        del self.data[:]
        # Suspend the selection changed signal while the table cleans up.
        with gui.SignalBlocker(self.table):
            self.table.clearTable()

        if self.dataType is not None:
            self.plotWidgets[self.dataType].clear()
            self.dataType = None

        # Disable the plot saving and export buttons as there are no data.
        self.savePlotsButton.setEnabled(False)
        self.exportButton.setEnabled(False)

    def updateStatusLabel(self):
        selectedRow = self.table.getSelectedRow()

        if selectedRow is None:
            self.statusLabel.setInfo(
                'Aerosol profile output record is not selected')
        else:
            self.statusLabel.clear()

        return selectedRow

    # ---- Private slots ------------------------------------------------------
    def onFileOpened(self):

        # Remove the old data, if any.
        self.clear()

        filePath = self.fileSelector.getFilePath()

        if filePath is None:
            self.statusLabel.setError(
                'Aerosol profile output database is not selected')
            return

        self.statusLabel.setProgress(
            'Loading aerosol profile output database...', True)

        firstErrorMessage = None

        for dataType in self.tableColumnLists:
            try:
                self.data = dataType.readDataList(filePath)

            except txt.Error as e:
                if firstErrorMessage is None:
                    firstErrorMessage = e.text

            else:
                self.table.setColumns(self.tableColumnLists[dataType])
                self.plotWidgetStack.setCurrentWidget(
                    self.plotWidgets[dataType])
                self.dataType = dataType
                break

        if self.dataType is None:
            self.statusLabel.setError(firstErrorMessage)
            return

        if len(self.data) == 0:
            self.statusLabel.setInfo(
                'Aerosol profile output database contains no records')
            return

        # From this time on, no error checking is performed.
        self.table.populateTable(self.data)

        # Select the most recent database record.
        self.table.setCurrentCell(len(self.data) - 1, 0)

        # Remove the previous error message and update the plot saving button.
        self.onSelectionChanged()
        # Enable the export button as the data have loaded without errors.
        self.exportButton.setEnabled(True)

        # Table is the only actively used widget. Return control to it.
        self.table.setFocus()

    def onSelectionChanged(self):

        selectedRow = self.updateStatusLabel()

        if selectedRow is None:
            self.savePlotsButton.setEnabled(False)
        else:
            errorMessage = self.data[selectedRow].getErrorMessage()

            if errorMessage is not None:
                self.statusLabel.setWarning(errorMessage)
                self.plotWidgetStack.currentWidget().clear()
                self.savePlotsButton.setEnabled(False)
            else:
                self.plotWidgetStack.currentWidget().plotData(
                    self.data[selectedRow])
                self.savePlotsButton.setEnabled(True)

    def onExportButtonClicked(self):

        dbFilePath = self.fileSelector.getFilePath()
        excelFilePath = self.dataType.getExcelExportFilePath(dbFilePath)

        if os.path.exists(excelFilePath):
            answer = QMessageBox.question(self, 'File already exists',
                'The excel file (%s) already exists. Would you like to '
                'overwrite it?' % txt.quotePath(excelFilePath),
                QMessageBox.Yes | QMessageBox.No)

            if answer == QMessageBox.No:
                return

        # Excel export may take a long time. Display a message in the info
        # label so that the user could realize that the action is in progress.
        self.statusLabel.setProgress(
            'Exporting the database to the Excel file...', True)

        try:
            self.dataType.exportDataListToExcel(excelFilePath, self.data),

        except txt.Error as e:
            QMessageBox.critical(self, 'Failed to save the file', e.text)

            # Remove the progress message.
            self.updateStatusLabel()

        else:
            # Display a message to let the user know that the action has
            # completed. The message will clear when a different database
            # record is selected or a new database file is opened.
            self.statusLabel.setCompleted(
                'The Excel file (%s) has been saved successfully' %
                txt.quotePath(excelFilePath))

        # Table is the only actively used widget. Return control to it.
        self.table.setFocus()

    def onSavePlotsButtonClicked(self):

        if not os.path.isdir(self.savePlotsDir):
            self.savePlotsDir = '.'

        # Supported file formats in the order they will appear in the save
        # dialog.
        fileFormatIds = ['png', 'emf']

        # Descriptive strings to be displayed to the user.
        fileFormatStrings = {
            'png': 'Portable network graphics (*.png)',
            'emf': 'Windows enhanced metafile (*.emf)'
        }

        # Earlier PyQt versions have a bug that renders EMF support impossible.
        if PYQT_VERSION < 0x40600:
            del fileFormatIds['emf']

        if self.savePlotsFileFormat not in fileFormatIds:
            self.savePlotsFileFormat = 'png'

        imageFilePath = QFileDialog.getSaveFileName(self, 'Save plots as',
            self.savePlotsDir,
            ';;'.join(fileFormatStrings[id] for id in fileFormatIds),
            fileFormatStrings[self.savePlotsFileFormat])
        if imageFilePath.isNull():
            return

        # Convert 'QString' to the Python data type, and replace directory
        # separators with the proper ones, if required.
        imageFilePath = txt.absolutePath(imageFilePath)

        self.savePlotsDir = os.path.dirname(imageFilePath)

        imageFileExt = os.path.splitext(imageFilePath)[1].lstrip('.').lower()
        self.savePlotsFileFormat = imageFileExt

        # Image file saving may take a long time in case of EMF export. Display
        # a message to let the user know that the action is in progress.
        self.statusLabel.setProgress('Saving the image file...', True)

        # Create a special instance of the plot widget for image saving.
        imagePlotWidget = self.plotWidgetClasses[self.dataType](
            displayMode = 'emf' if imageFileExt == 'emf' else 'image')
        imagePlotWidget.plotData(self.data[self.table.getSelectedRow()])
        imagePlotWidget.saveAsImage(imageFilePath)

        # Inform the user that the action has completed. The message will clear
        # when a different database record is selected or a new database file
        # is opened.
        self.statusLabel.setCompleted(
            'Image file (%s) has been saved' % txt.quotePath(imageFilePath))

        # Table is the only actively used widget. Return control to it.
        self.table.setFocus()
