# 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
import os.path
import re
import shutil

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 *

__all__ = ['AerlidToolDialog']

# *****************************************************************************
class AerlidToolDialog(QDialog):
    """Dialog used to process Dubovik AERONET data files using AERLID tool.

    Processing starts when the dialog's 'exec_' method is called. During the
    run of the program, its output is printed out. If processing completes
    successfully, the dialog would close automatically, and 'getErrorMessage'
    method would return 'None'. Otherwise, the user would have to close the
    dialog manually, and 'getErrorMessage' would return an appropriate error
    message."""

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent, packageDir, outputWavelengths,
        dubovikFilePath, aerlidFilePath):
        """Parameters:
          - 'packageDir': path to the root directory of the AERLID package. The
            program itself has to reside in the 'run' subdirectory.
          - 'outputWavelengths': a list of wavelengths, in nanometers, to
            generate data for (currently, there should be exactly 3 elements in
            the list).
          - 'dubovikFilePath': path to the 'Combined Dubovik Retrievals'
            AERONET data file to be used as input (these files may be
            downloaded from 'http://aeronet.gsfc.nasa.gov').
          - 'aerlidFilePath': path to the file to store AERLID's output in."""

        QDialog.__init__(self, parent)

        # ---- Members ----
        # Make sure that 'outputWavelengths' holds values in nanometers.
        assert any(200.0 <= wl <= 2000.0 for wl in outputWavelengths)
        # AERLID tool currently requires exactly 3 output wavelengths.
        assert len(outputWavelengths) == 3

        self.packageDir = packageDir
        self.outputWavelengths = outputWavelengths
        self.dubovikFilePath = dubovikFilePath
        self.aerlidFilePath = aerlidFilePath

        # Process to run the AERLID program in.
        self.aerlidProcess = QProcess()
        # Display both 'stdout' and 'stderr' data in the same output window.
        self.aerlidProcess.setProcessChannelMode(QProcess.MergedChannels)

        # AERLID is a Fortran program, and Fortran programs will apply
        # buffering to their outputs unless output is directly connected to a
        # console ('tty') device. In our case, output device is a pipe, thus
        # buffering normally will occur. This is not a desirable behaviour,
        # because buffering prevents the output from being displayed in real
        # time. Fortunately, AERLID is compiled with gfortran, and gfortran
        # allows to disable the buffering by means of an environment variable.
        processEnvironment = QProcessEnvironment()
        processEnvironment.insert('GFORTRAN_UNBUFFERED_PRECONNECTED', 'y')
        self.aerlidProcess.setProcessEnvironment(processEnvironment)

        self.aerlidProcess.readyRead.connect(self.onOutputAvailable)
        self.aerlidProcess.error.connect(self.onProcessError)
        self.aerlidProcess.finished.connect(self.onProcessFinished)

        self.errorMessage = None

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

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

        self.outputWindow = ConsoleWidget()
        layout.addWidget(self.outputWindow)

        self.progressBar = QProgressBar()
        layout.addWidget(self.progressBar)

        self.closeButton = QPushButton('Close')
        # This button shouldn't get activated upon an Enter key press.
        self.closeButton.setAutoDefault(False)
        # This dialog shouldn't normally be closed by the user. Thus, 'Close'
        # button is normally not required altogether, and should only be shown
        # in case of an error.
        self.closeButton.setVisible(False)
        self.closeButton.clicked.connect(self.reject)

        buttonBoxLayout = QHBoxLayout()
        buttonBoxLayout.addStretch()
        buttonBoxLayout.addWidget(self.closeButton)
        layout.addLayout(buttonBoxLayout)

        self.setLayout(layout)

        self.setWindowTitle('Processing AERONET data...')
        self.setWindowIcon(gui.loadIcon('institute-of-physics'))

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

        # ---- Initialization -----
        self.rejected.connect(self.onRejected)

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

    def getErrorMessage(self):
        """Return a rich text message describing the failed processing of the
        data or 'None' if the processing has completed successfully."""

        return self.errorMessage

    # ---- Private methods ----------------------------------------------------
    def setErrorMessage(self, message):

        self.errorMessage = message
        self.statusLabel.setError(message)
        self.closeButton.setVisible(True)
        self.closeButton.setFocus()

        # Showing the 'Close' button and updating the information label would
        # normally modify the size of the output window. Make sure that the
        # most relevant messages are still visible there.
        self.outputWindow.ensureCursorVisible()

        self.setWindowTitle(self.windowTitle() + ' (Failed)')

    def getInputFilePath(self):
        return os.path.join(self.packageDir, 'input', 'input.txt')

    def getOutputFilePath(self):
        return os.path.join(self.packageDir, 'output', 'output.txt')

    def getParamsFilePath(self):
        return os.path.join(self.packageDir, 'run', 'aerlid_input.txt')

    def getProgramFilePath(self):
        return os.path.join(self.packageDir, 'run', 'aerlid.exe')

    def prepareInputFile(self):
        """Create intermediate input data file for the AERLID program and store
        it at 'self.getInputFilePath()'.

        Return the number of measurements stored in the input file, or raise
        'txt.Error' if the file couldn't be created."""

        inputFilePath = self.getInputFilePath()

        try:
            dubovikFile = open(self.dubovikFilePath)
        except IOError:
            self.setErrorMessage(txt.Error(
                'AERONET data file %s may not be opened for reading' %
                txt.quotePath(self.dubovikFilePath)))

        try:
            inputFile = open(inputFilePath, 'w')
        except IOError:
            dubovikFile.close()

            raise txt.Error(
                'Failed to open AERLID input data file (%s) for writing' %
                txt.quotePath(inputFilePath))

        recordCount = 0

        # Create a copy of the file at 'dubovikFilePath', with backslashes
        # removed and invalid numeric values replaced with '-999'.
        try:
            for line in dubovikFile:
                line = line.replace('N/A', '-999')
                line = line.replace('/', '-')

                inputFile.write(line)
                # Don't take file header into account at first, just count
                # lines.
                recordCount += 1

        except IOError:
            raise txt.Error('Failed to write AERLID input data file (%s)' %
                txt.quotePath(inputFilePath))
        finally:
            dubovikFile.close()
            inputFile.close()

        # Remove file header from 'recordCount'. Normally, the header will
        # contain exactly 4 lines.
        recordCount -= 4

        if recordCount < 0:
            raise txt.Error('%s is not a valid AERONET data file' %
                txt.quotePath(self.dubovikFilePath))

        return recordCount

    def prepareParamsFile(self, recordCount):
        """Create parameters data file for the AERLID program and store it at
        'self.getParamsFilePath()'."""

        paramsFilePath = self.getParamsFilePath()

        try:
            paramsFile = open(paramsFilePath, 'w')
        except IOError:
            raise txt.Error(
                'Failed to open AERLID parameters data file (%s) for writing' %
                txt.quotePath(paramsFilePath))

        try:
            # Format the file in a nicely aligned way. Left part contains data
            # and is 25 chars wide; right part is for comments.
            paramsFile.write('LOGICAL FLAGS ---\n')
            # Boolean flags.
            paramsFile.write('%-25s' % 'f t f f' +
                'laprn, llput, ltest, l_f33_f44\n')
            paramsFile.write('-----------------\n')
            # Integer that seems to specify the size of arrays used in the
            # AERLID program. Use the exact value here.
            paramsFile.write('%-25d' % recordCount +
                'nmeas (number of lines for reading of Aeronet data)\n')
            # These are basic wavelengths that should be present in every
            # AERONET data file.
            paramsFile.write('0.440 0.675 0.870 1.020  WAVEA\n')

            # Output wavelengths in micrometers.
            wavelengthStr = ''
            for wl in self.outputWavelengths:
                wavelengthStr += str(wl / 1000.0) + ' '

            paramsFile.write('%-25s' % wavelengthStr + 'WAVEL\n')

            inputFileName = os.path.basename(self.getInputFilePath())
            outputFileName = os.path.basename(self.getOutputFilePath())

            paramsFile.write('FILENAMES -------\n')
            paramsFile.write('%-25s' % inputFileName + 'input_filename\n')
            paramsFile.write('%-25s' % outputFileName + 'out_filename\n')

        except IOError:
            raise txt.Error(
                'Failed to write AERLID parameters data file (%s)' %
                txt.quotePath(paramsFilePath))
        finally:
            paramsFile.close()

    def copyOutputFile(self):
        """Copy output file generated by AERLID program to its final location
        specified by 'self.aerlidFilePath'."""

        fileDir = os.path.dirname(self.aerlidFilePath)

        # Create destination directory, if it does not exist.
        try:
            # Using 'isdir' instead of 'exists' will make the code fail if the
            # required directory does not exist and may not be created due to a
            # naming conflict with an existing ordinary file.
            if not os.path.isdir(fileDir):
                os.makedirs(fileDir)

        except OSError:
            raise txt.Error(
                u'Couldn\u2019t create directory for the AERLID output file '
                '(%s)' % txt.quotePath(self.aerlidFilePath))

        try:
            shutil.copyfile(self.getOutputFilePath(), self.aerlidFilePath)

        except IOError:
            raise txt.Error(
                u'Couldn\u2019t copy AERLID output file to its final location '
                '(%s)' % txt.quotePath(self.aerlidFilePath))

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

        # Prepare AERLID input files.
        try:
            recordCount = self.prepareInputFile()
            self.prepareParamsFile(recordCount)

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

        self.statusLabel.setProgress(
            'Running AERLID tool for AERONET file %s...' %
            txt.quotePath(self.dubovikFilePath))

        # Initialize the progress bar.
        self.progressBar.setRange(0, recordCount)
        self.progressBar.setValue(0)

        # Start the external process.
        self.aerlidProcess.setWorkingDirectory(self.packageDir)
        self.aerlidProcess.start(self.getProgramFilePath())

    def onOutputAvailable(self):
        """Print AERLID program's output to the output window."""

        while self.aerlidProcess.canReadLine():
            # Use 'QString' to convert 'QByteArray' into a Unicode string.
            outputLine = QString(self.aerlidProcess.readLine())

            self.outputWindow.appendOutput(outputLine)

            # Check if the string starts with 'imeas=' followed by an integer.
            # it does, update the progress bar.
            matchObj = re.match(r'imeas=\s+(\d+)\s+', outputLine)

            if matchObj is not None:
                self.progressBar.setValue(int(matchObj.group(1)))

    def onProcessError(self):
        self.setErrorMessage('Failed to run AERLID tool (%s)' %
            txt.quotePath(self.getProgramFilePath()))

    def onProcessFinished(self):

        # If the process had actually failed to start, just leave the message
        # about that intact.
        if self.errorMessage is not None:
            return

        # Check if AERLID program has completed successfully.
        returnCode = self.aerlidProcess.exitCode()

        if returnCode != 0:
            self.setErrorMessage(
                'Failed to run AERLID tool (return code: %d)' % returnCode)
            return

        # Copy the output file to its final location.
        try:
            self.copyOutputFile()

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

        # Close the dialog box window.
        self.accept()

    def onRejected(self):
        """Terminate the process, if it's still running."""

        if self.errorMessage is None:
            # This may only happen if the user presses the dialog's window
            # close button while the process is running.
            self.setErrorMessage('Failed to run AERLID tool (user abort)')

        # If the process had already finished, this would be a no-op.
        self.aerlidProcess.kill()
