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

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

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

from common.StatusLabelWidget import *

__all__ = ['HttpRequestDialog']

# *****************************************************************************
class HttpRequestDialog(QDialog):
    """Dialog that makes it possible to download a file from the Internet using
    either GET or POST HTTP request.

    If 'setDestinationFilePath' is called prior to 'exec_', a temporary file is
    created during the download with name obtained by adding '.tmp' to the name
    of the destination file. If the download completes successfully, temporary
    file is renamed to the destination one, possibly overwriting the target.
    If the download fails or is aborted by the user, temporary file is deleted.

    If 'setDestinationFilePath' is not called, then no files are created during
    the download, and 'getDownloadData' should be used to obtain contents of
    the downloaded file instead."""

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent, windowTitle, progressMessage, url,
        requestData = None):
        """Use 'requestData' different from 'None' to send a POST request, and
        the default value to send a GET request."""

        QDialog.__init__(self, parent)

        # ---- Members ----
        self.url = QUrl(url)

        self.requestData = requestData

        # Path to the final location of the file to be downloaded, if any.
        self.destinationFilePath = None
        # Path to the temporary file, or 'None' if downloaded data are to be
        # stored in an internal variable instead of a file on disk.
        self.tmpFilePath = None
        # Opened temporary file, or 'None' if it is not opened or non-existent.
        self.tmpFile = None
        # String holding the received data bytes, or 'None' if a temporary file
        # is used to hold the data instead of an internal variable.
        self.downloadedData = ''

        self.networkAccessManager = QNetworkAccessManager(self)
        self.networkReply = None

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

        self.statusLabel = StatusLabelWidget()
        self.statusLabel.disableWordWrapping()
        self.statusLabel.setProgress(progressMessage)
        layout.addWidget(self.statusLabel)

        # Show the URL, but hide passwords and query fields, if any.
        urlString = unicode(self.url.toString(
            QUrl.RemoveUserInfo | QUrl.RemoveQuery))

        self.urlLabel = QLabel('(%s)...' % urlString)
        self.urlLabel.setWordWrap(False)
        layout.addWidget(self.urlLabel)

        # This will display the number of bytes downloaded.
        self.progressLabel = QLabel()
        self.progressLabel.setWordWrap(False)
        layout.addWidget(self.progressLabel)

        self.progressBar = QProgressBar()
        # Don't show the progress bar (as well as the number of bytes
        # downloaded) until the download process actually starts.
        self.progressBar.hide()
        # Preserve space for the progress bar in the window layout.
        layout.addItem(gui.UnhidableWidgetItem(self.progressBar))

        layout.addStretch()

        self.cancelButton = QPushButton('Cancel')
        # This button shouldn't get activated upon an Enter key press.
        self.cancelButton.setAutoDefault(False)
        self.cancelButton.clicked.connect(self.reject)

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

        self.setLayout(layout)

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

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

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

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

    def setDestinationFilePath(self, destinationFilePath):
        """Call this prior to 'exec_' to instruct the dialog to store
        downloaded data in the given file on disk instead of a local
        variable."""

        assert self.tmpFile is None  # Just to feel safe.

        self.destinationFilePath = destinationFilePath
        self.tmpFilePath = destinationFilePath + '.tmp'

        self.downloadedData = None

    def getDownloadedData(self):
        """Return an 'str' object holding the contents of the downloaded data,
        or 'None' if the data have been stored in a file on disk instead of a
        local variable."""

        return self.downloadedData

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

        # Qt says that 'QNetworkReply' objects should be deleted by the caller.
        if self.networkReply is not None:
            self.networkReply.deleteLater()
            self.networkReply = None

        event.accept()

    # ---- Private methods ----------------------------------------------------
    def restartDownload(self):
        """Abort the currently running download process and initiate a new one
        with the same request paremeters."""

        if self.tmpFile is not None:
            self.tmpFile.close()
            # Don't delete the temporary file as it'll be reopened immediately.
            self.tmpFile = None

        if self.downloadedData is not None:
            self.downloadedData = ''

        # Don't fire 'error' and 'finished' signals for a cancelled download.
        with gui.SignalBlocker(self.networkReply):
            self.networkReply.abort()

        # Qt says that 'QNetworkReply' objects should be deleted by the caller.
        self.networkReply.deleteLater()
        self.networkReply = None

        # Restore original appearance of the dialog's window.
        self.progressLabel.setText('')
        self.progressBar.hide()

        QTimer.singleShot(0, self.onInitComplete)

    def openTmpFile(self):
        """Create an empty file at 'self.tmpFilePath' and open it.

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

        assert self.tmpFilePath is not None and self.tmpFile is None

        tmpFileDir = os.path.dirname(self.tmpFilePath)

        # Create destination directory, if it does not exist.
        while True:
            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(tmpFileDir):
                    os.makedirs(tmpFileDir)
                break

            except OSError:

                # Display a message box to the user, explaining the problem.
                answer = QMessageBox.critical(self.parentWidget(),
                    'Failed to save data to disk',
                    'Failed to create directory for the destination file '
                        '(%s)' % txt.quotePath(tmpFileDir),
                    QMessageBox.Retry | QMessageBox.Abort, QMessageBox.Retry)

                if answer == QMessageBox.Abort:
                    return False

        while True:
            try:
                self.tmpFile = open(self.tmpFilePath, 'wb')
                break

            except IOError:

                # Display a message box to the user, explaining the problem.
                answer = QMessageBox.critical(self.parentWidget(),
                    'Failed to save data to disk',
                    'Failed to open the temporary data file (%s) for writing' %
                        txt.quotePath(self.tmpFilePath),
                    QMessageBox.Retry | QMessageBox.Abort, QMessageBox.Retry)

                if answer == QMessageBox.Abort:
                    return False

        return True

    def writeTmpFile(self, data):
        """Write the given data to 'self.tmpFile'.

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

        assert self.tmpFilePath is not None and self.tmpFile is not None

        while True:
            try:
                self.tmpFile.write(data)
                break

            except IOError:

                # Display a message box to the user, explaining the problem.
                answer = QMessageBox.critical(self.parentWidget(),
                    'Failed to save data to disk',
                    'Failed to write the temporary data file (%s)' %
                        txt.quotePath(self.tmpFilePath),
                    QMessageBox.Retry | QMessageBox.Abort, QMessageBox.Retry)

                if answer == QMessageBox.Abort:
                    return False

        return True

    def moveTmpFile(self):
        """Move the file at 'self.tmpFilePath' to its final location.

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

        assert self.tmpFilePath is not None and self.tmpFile is None

        while True:
            try:
                # Use 'copyfile' instead of 'move' to make sure that this code
                # will fail if destination path turns out to be a directory
                # (in that case, 'move' will silently move the file there).
                shutil.copyfile(self.tmpFilePath, self.destinationFilePath)
                break

            # 'copyfile' can probably raise both 'IOError' and 'OSError'.
            except (IOError, OSError):

                # Display a message box to the user, explaining the problem.
                answer = QMessageBox.critical(self.parentWidget(),
                    'Failed to save data to disk',
                    'Failed to move the temporary data file to its final '
                        'location (%s)'
                        % txt.quotePath(self.destinationFilePath),
                    QMessageBox.Retry | QMessageBox.Abort, QMessageBox.Retry)

                if answer == QMessageBox.Abort:
                    return False

        # Use a separete function call to delete the temporary file.
        self.deleteTmpFile()
        return True

    def deleteTmpFile(self):
        """Delete the file at 'self.tmpFilePath'."""

        assert self.tmpFilePath is not None and self.tmpFile is None

        while True:
            try:
                os.remove(self.tmpFilePath)
                break

            except OSError:

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

                if answer == QMessageBox.Ignore:
                    break

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

        request = QNetworkRequest(self.url)

        # Send either HTTP GET or POST request depending on 'self.requestData'.
        if self.requestData is None:
            self.networkReply = self.networkAccessManager.get(request)

        else:
            request.setHeader(QNetworkRequest.ContentTypeHeader,
                'application/x-www-form-urlencoded')

            self.networkReply = self.networkAccessManager.post(request,
                QByteArray(self.requestData))

        self.networkReply.downloadProgress.connect(self.onDownloadProgress)
        self.networkReply.error.connect(self.onDownloadError)
        self.networkReply.finished.connect(self.onDownloadFinished)

    def onDownloadProgress(self, bytesReceived, bytesTotal):

        # 'downloadProgress' signal is emitted with both byte counts set to 0
        # immediately after the request was sent. Don't show anything to the
        # user and don't try to create the temporary file, if any, until the
        # server actually responds.
        if bytesReceived == bytesTotal == 0:
            return

        self.progressBar.show()

        if bytesTotal != -1:
            self.progressLabel.setText('%d bytes of %d' %
                (bytesReceived, bytesTotal))
            self.progressBar.setRange(0, bytesTotal)
            self.progressBar.setValue(bytesReceived)

        else:
            self.progressLabel.setText('%d bytes' % bytesReceived)
            self.progressBar.setRange(0, 0)
            self.progressBar.setValue(0)

        # Create and open the temporary data file, if any.
        if self.tmpFilePath is not None and self.tmpFile is None:

            if not self.openTmpFile():
                self.reject()
                return

        data = str(self.networkReply.readAll())

        # Write the data either to the local variable or to the temporary file.
        if self.downloadedData is not None:
            self.downloadedData += data

        else:
            assert self.tmpFile is not None
            if not self.writeTmpFile(data):
                self.reject()
                return

    def onDownloadError(self, networkErrorCode):

        answer = QMessageBox.critical(self,
            'Internet connection error',
            self.networkReply.errorString(),
            QMessageBox.Retry | QMessageBox.Abort, QMessageBox.Retry)

        if answer == QMessageBox.Retry:
            self.restartDownload()
        else:
            self.reject()

    def onDownloadFinished(self):

        # Move the temporary file, if any, to its final location.
        if self.tmpFile is not None:
            self.tmpFile.close()
            self.tmpFile = None

            if not self.moveTmpFile():
                self.reject()
                return

        self.accept()

    def onRejected(self):

        # Delete the temporary file, if any, and abort the download.
        if self.tmpFile is not None:
            self.tmpFile.close()
            self.tmpFile = None
            self.deleteTmpFile()

        # Don't fire 'error' and 'finished' signals for a cancelled download.
        with gui.SignalBlocker(self.networkReply):
            self.networkReply.abort()
