# 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
import re
import urllib
import zipfile

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

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

from common.HttpRequestDialog import *

__all__ = ['downloadAeronetSiteListFile', 'downloadAeronetDubovikDataFile']

# *****************************************************************************
def downloadAeronetSiteListFile(parentWidget, filePath):
    """Download AERONET site list file and store it at the given location on
    disk.

    Return 'True' if the operation succeeds and 'False' otherwise.

    'parentWidget' has to be the widget to own the download progress dialog."""

    requestDialog = HttpRequestDialog(parentWidget,
        'Updating AERONET site list...',
        'Downloading AERONET site list file',
        'http://aeronet.gsfc.nasa.gov/aeronet_locations.txt')

    requestDialog.setDestinationFilePath(filePath)

    if requestDialog.exec_() == QDialog.Rejected:
        return False
    else:
        return True

# *****************************************************************************
def downloadAeronetDubovikDataFile(parentWidget, siteName, year, dataLevel,
    filePath):
    """Download specified AERONET inversion product data file and store it at
    the given location on disk.

    Return 'True' if the operation succeeds and 'False' otherwise.

    'parentWidget' has to be the widget to own download progress and AERONET
    download confirmation dialogs."""

    # ---- Product query ----
    queryData = {
        'site' : siteName,
        # First day to download data for (January, 1; year is 1900-based).
        'day' : 1,
        'month' : 1,
        'year' : year - 1900,
        # Last day to download data for (December, 31; year is 1900-based).
        'day2' : 31,
        'month2' : 12,
        'year2' : year - 1900,
        # This flag is used to request combined Dubovik data files.
        'DUBOV' : 1,
        'TYPE' : '151' if dataLevel == '1.5' else '21',
        # This parameter has a predefined constant value in the HTML form.
        'MODE' : '40',
        # This value is used to query data for "All Points".
        'AVG' : '10',
        'Submit' : 'Download'
    }

    queryDialog = HttpRequestDialog(parentWidget,
        'Downloading AERONET data file...',
        'Querying AERONET inversion product for %s, %s, level %s' %
        (txt.quote(siteName), txt.quoteNumber(year), txt.quote(dataLevel)),
        'http://aeronet.gsfc.nasa.gov/cgi-bin/print_warning_opera_v2_inv',
        urllib.urlencode(queryData))

    if queryDialog.exec_() == QDialog.Rejected:
        return False

    queryResponseText = queryDialog.getDownloadedData()

    # ---- Download confirmation ----
    confirmationDialog = AeronetConfirmationDialog(parentWidget,
        queryResponseText)

    if confirmationDialog.exec_() == QDialog.Rejected:
        return False

    # ---- Zip file download ----
    zipFilePath = os.path.join(os.path.dirname(filePath),
        confirmationDialog.getProductFileName())

    downloadDialog = HttpRequestDialog(parentWidget,
        'Downloading AERONET data file...',
        'Downloading AERONET inversion product for %s, %s, level %s' %
        (txt.quote(siteName), txt.quoteNumber(year), txt.quote(dataLevel)),
        confirmationDialog.getProductUrl())

    downloadDialog.setDestinationFilePath(zipFilePath)

    if downloadDialog.exec_() == QDialog.Rejected:
        return False

    # ---- Zip file extraction ----
    (productFileDir, productFileName) = os.path.split(filePath)

    # Extract the product file from the downloaded zip archive.
    while True:
        try:
            zipFile = zipfile.ZipFile(zipFilePath)

            try:
                # Raise an assertion exception if contents of the Zip archive
                # don't meet out expectations. If any compatibility problems
                # arise, they should be regarded as this program's errors.
                assert zipFile.namelist() == [productFileName]
                zipFile.extract(productFileName, productFileDir)
                break

            finally:
                zipFile.close()

        except (IOError, OSError, zipfile.BadZipfile):

            # Display a message box to the user, explaining the problem.
            answer = QMessageBox.critical(parentWidget,
                'Failed to save data to disk',
                'Failed to extract data from the downloaded zip archive (%s)' %
                    txt.quotePath(zipFilePath),
                QMessageBox.Retry | QMessageBox.Abort, QMessageBox.Retry)

            if answer == QMessageBox.Abort:
                return False

    # Delete the downloaded zip file.
    while True:
        try:
            os.remove(zipFilePath)
            break

        except OSError:

            # Display a message box to the user, explaining the problem.
            answer = QMessageBox.critical(parentWidget,
                'Failed to clean up temporary data',
                'Failed to delete the downloaded zip archive (%s)' %
                    txt.quotePath(zipFilePath),
                QMessageBox.Retry | QMessageBox.Ignore, QMessageBox.Retry)

            if answer == QMessageBox.Ignore:
                break

    return True

# *****************************************************************************
class AeronetConfirmationDialog(QDialog):
    """Dialog that displays contents of an AERONET download confirmation page
    and allows the user to either agree with proposed conditions or cancel the
    download."""

    # Private attributes (parsed from the page contents):
    #   - 'productUrl': url that may be used to obtain the Zip archive holding
    #     the requested product file, or 'None' if none of the products match
    #     the request.
    #   - 'productFileName': name of the Zip file, or 'None' if none of the
    #     products match the request.
    #   - 'windowTitle': contents of the '<title>' HTML tag.
    #   - 'htmlText': contents of the confirmation page, prepared to be
    #     displayed in the dialog's window.

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent, confirmationPageText):
        QDialog.__init__(self, parent)

        # ---- Members ----
        self.parseConfirmationPageText(confirmationPageText)

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

        self.outputWindow = HtmlDisplayWidget()
        self.outputWindow.setText(self.htmlText)
        layout.addWidget(self.outputWindow)

        if self.productUrl is not None:

            # If the requested product is found, show "Accept" and
            # "Do Not Accept" buttons (just like the Web page does).
            acceptButton = QPushButton('&Accept')
            acceptButton.setAutoDefault(False)
            acceptButton.setDefault(True)
            acceptButton.clicked.connect(self.accept)

            doNotAcceptButton = QPushButton('Do &Not Accept')
            doNotAcceptButton.setAutoDefault(False)
            doNotAcceptButton.clicked.connect(self.reject)

            buttons = [acceptButton, doNotAcceptButton]

        else:
            # If no product is not found, display a single "Close" button.
            closeButton = QPushButton('Close')
            closeButton.setAutoDefault(False)
            closeButton.clicked.connect(self.reject)

            buttons = [closeButton]

        buttonBoxLayout = QHBoxLayout()
        buttonBoxLayout.addStretch()

        for button in buttons:
            buttonBoxLayout.addWidget(button)

        buttonBoxLayout.addStretch()
        layout.addLayout(buttonBoxLayout)

        self.setLayout(layout)

        self.setWindowTitle(self.windowTitle)
        self.setWindowIcon(gui.loadIcon('world'))

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

    def getProductUrl(self):
        return self.productUrl

    def getProductFileName(self):
        return self.productFileName

    # ---- Private methods ----------------------------------------------------
    def parseConfirmationPageText(self, pageText):

        # If the page's contents won't meet our expectations, an exception will
        # be raised, as it's actually the responsibility of this code to
        # maintain compatibility with AERONET Web interface, and thus any
        # compatibility problems should be regarded as this program's errors.

        # Search for the page's title. This will be used as the title of the
        # dialog box.
        titleMatch = re.search(r'<title>([^<]*)</title>', pageText,
            re.IGNORECASE)
        assert titleMatch is not None
        self.windowTitle = titleMatch.group(1)

        # Strip HTML header from the text to be displayed in the dialog.
        bodyMatch = re.search(r'<body[^>]*>', pageText, re.IGNORECASE)
        assert bodyMatch is not None

        htmlTextStart = bodyMatch.end()

        # This will contain additional text to be inserted before contents of
        # the HTML's '<body>'.
        htmlPrefixText = ''

        # This text is currently placed under '<head>' tag instead of the
        # '<body>' one in AERONET confirmation pages. Use 'htmlPrefixText' to
        # manually insert it before the '<body>'s contents.
        headerText = '<center><h1>AERONET Download Site</h1></center>'
        headerMatch = re.search(headerText, pageText, re.IGNORECASE)

        if headerMatch is not None and headerMatch.start() < bodyMatch.end():
            htmlPrefixText += headerText + '\n'

        # Look for the code implementing the "Accept" button, and parse product
        # file name out of it. If this button exists, then product data file
        # request would have been satisfied.
        zipFileMatch = re.search(r'<a href="/zip_files_inv/([^"]*)"[^>]*>'
            r'<img src="/accept.gif"[^>]*></a>', pageText, re.IGNORECASE)

        if zipFileMatch is not None:

            self.productFileName = zipFileMatch.group(1)
            self.productUrl = ('http://aeronet.gsfc.nasa.gov/zip_files_inv/' +
                self.productFileName)

            # Strip the page's footer (including the buttons) from the text to
            # be displayed in the dialog.
            acceptMessageText = 'If you accept the above conditions,'
            acceptMessageMatch = re.search('<p>' + acceptMessageText, pageText,
                re.IGNORECASE)

            assert acceptMessageMatch is not None

            htmlTextEnd = acceptMessageMatch.start()

        else:
            self.productUrl = None
            self.productFileName = None

            # If none of the products match the request, the following message
            # would currently replace the buttons at the bottom of the
            # confirmation page. For clarity, place this message at the very
            # beginning of the text to be displayed in the dialog.
            errorMessageText = 'No data is available during the given period.'
            errorMessageMatch = re.search('<p>' + errorMessageText, pageText,
                re.IGNORECASE)

            assert errorMessageMatch is not None

            htmlTextEnd = errorMessageMatch.start()

            htmlPrefixText += errorMessageText + '<p>\n'

        self.htmlText = htmlPrefixText + pageText[htmlTextStart : htmlTextEnd]

# *****************************************************************************
class HtmlDisplayWidget(QTextEdit):
    """A widget suitable for displaying of static HTML pages."""

    def __init__(self, width = 80, height = 30, parent = None):
        """'width' and 'height' should specify preferred dimensions of the
        output window, in characters (using the default font)."""

        QTextEdit.__init__(self, parent)

        self.setReadOnly(True)

        self.width = width
        self.height = height

    # ---- Private overridden methods -----------------------------------------
    def sizeHint(self):
        # This will modify default size of the widget, so that it could hold a
        # reasonable amount of text out of the box.

        metrics = QFontMetrics(self.font())

        frameWidth = self.frameWidth()
        # Document margin is float, but it has an integer value by default, and
        # that value seems to be an appropriate one.
        margin = int(self.document().documentMargin())

        # Use the same code as in 'ConsoleWidget's 'sizeHint', but use 'x'
        # character instead of ' ' to calculate required dimensions of the
        # window, as the former better suits metrics of variable-width fonts.

        width = metrics.width('x' * self.width) + frameWidth * 2 + margin * 2
        height = metrics.height() * self.height + frameWidth * 2 + margin

        width += self.verticalScrollBar().sizeHint().width()

        return QSize(width, height)
