/*! @file

    @id $Id$
*/
//       1         2         3         4         5         6         7         8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890

#ifndef __DOWNLOAD_MANAGER_HXX
#define __DOWNLOAD_MANAGER_HXX

#include <qbrowserlib/log.hxx>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QSslError>
#include <QtNetwork/QSslConfiguration>
#include <map>

#include <cassert>

//! @addtogroup qbrowserlib
//! @{
namespace qbrowserlib {
  
  class DownloadManager: public QObject {
      
      Q_OBJECT;
      
    public:

      //! Append a Network Manager
      DownloadManager& operator+=(QNetworkAccessManager* n) {
        TRC;
        assert(connect(n, SIGNAL(created(QNetworkReply*)),
                       SLOT(add(QNetworkReply*))));
        return *this;
      }
      
      //! Append a Network Reply
      DownloadManager& operator+=(QNetworkReply* reply) {
        TRC;
        add(reply);
        return *this;
      }
      
      static QString networkError(QNetworkReply::NetworkError err) {
        TRC_FN; LOG_FN<<err;
        switch (err) {
          case QNetworkReply::NoError:
            return tr("Network connection successful, remote host can be"
                      " reached.");
          case QNetworkReply::ConnectionRefusedError:
            return tr("The remote server refused the connection (the server is"
                      " not accepting requests).");
          case QNetworkReply::RemoteHostClosedError:
            return tr("The remote server closed the connection prematurely,"
                      " before the entire reply was received and processed.");
          case QNetworkReply::HostNotFoundError:
            return tr("The remote host name was not found (invalid hostname).");
          case QNetworkReply::TimeoutError:
            return tr("The connection to the remote server timed out.");
          case QNetworkReply::OperationCanceledError:
            return tr("The operation was canceled via calls to abort() or"
                      " close() before it was finished.");
          case QNetworkReply::SslHandshakeFailedError:
            return tr("The SSL/TLS handshake failed and the encrypted channel"
                      " could not be established. See SSL-Errors above.");
          case QNetworkReply::ProxyConnectionRefusedError:
            return tr("The connection to the proxy server was refused (the"
                      " proxy server is not accepting requests).");
          case QNetworkReply::ProxyConnectionClosedError:
            return tr("The proxy server closed the connection prematurely,"
                      " before the entire reply was received and processed.");
          case QNetworkReply::ProxyNotFoundError:
            return tr("The proxy host name was not found (invalid proxy"
                      " hostname).");
          case QNetworkReply::ProxyTimeoutError:
            return tr("The connection to the proxy timed out or the proxy did"
                      " not reply in time to the request sent.");
          case QNetworkReply::ProxyAuthenticationRequiredError:
            return tr("The proxy requires authentication in order to honour the"
                      " request but did not accept any credentials offered"
                      " (if any).");
          case QNetworkReply::ContentAccessDenied:
            return tr("The access to the remote content was denied (similar to"
                      " HTTP error 401).");
          case QNetworkReply::ContentOperationNotPermittedError:
            return tr("The operation requested on the remote content is not"
                      " permitted.");
          case QNetworkReply::ContentNotFoundError:
            return tr("The remote content was not found at the server (similar"
                      " to HTTP error 404).");
          case QNetworkReply::AuthenticationRequiredError:
            return tr("The remote server requires authentication to serve the"
                      " content but the credentials provided were not accepted"
                      " (if any).");
          case QNetworkReply::ProtocolUnknownError:
            return tr("The Network Access API cannot honor the request because"
                      " the protocol is not known.");
          case QNetworkReply::ProtocolInvalidOperationError:
            return tr("The requested operation is invalid for this protocol.");
          case QNetworkReply::UnknownNetworkError:
            return tr("An unknown network-related error was detected.");
          case QNetworkReply::UnknownProxyError:
            return tr("An unknown proxy-related error was detected.");
          case QNetworkReply::UnknownContentError:
            return tr("An unknonwn error related to the remote content was"
                      " detected.");
          case QNetworkReply::ProtocolFailure:
            return tr("A breakdown in protocol was detected (parsing error,"
                      " invalid or unexpected responses, etc.).");
          default:
            return tr("Unknown network error (code: %1).").arg(err);
        }
      }
      
    Q_SIGNALS:
      
      void progress(qint64 done, qint64 total);
      void started();
      void finished();
      void error(QString);
      void metaDataChanged(QNetworkReply*);
                                          
    public Q_SLOTS:
      
      void add(QNetworkReply* reply) {
        TRC; LOG<<_downloads.size()<<reply->url().toString();
        LOG<<reply;
        _downloads[reply].progress = Progress(0, 0);
        assert(connect(reply, SIGNAL(downloadProgress(qint64, qint64)),
                       SLOT(downloadProgress(qint64, qint64))));
        assert(connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
                       SLOT(error(QNetworkReply::NetworkError))));
        assert(connect(reply, SIGNAL(destroyed(QObject*)),
                       SLOT(slotDestroyed(QObject*))));
        assert(connect(reply, SIGNAL(finished()),
                       SLOT(slotFinished())));
        assert(connect(reply, SIGNAL(metaDataChanged()),
                       SLOT(slotMetaDataChanged())));
        assert(connect(reply, SIGNAL(sslErrors(const QList<QSslError>&)),
                       SLOT(sslErrors(const QList<QSslError>&))));
        assert(connect(reply, SIGNAL(uploadProgress(qint64, qint64)),
                       SLOT(uploadProgress(qint64, qint64))));
        if (_downloads.size()==1) started();
      }
      
      void abort() {
        while (_downloads.size()) _downloads.begin()->first->abort();
      }
      
    private:
      
      void calcProgress() {
        //TRC;
        qint64 done(0);
        qint64 total(0);
        for (Downloads::iterator it(_downloads.begin());
             it!=_downloads.end(); ++it) {
          done += it->second.progress.first;
          total += it->second.progress.second;
        }
        progress(done, total);
      }
    
    private Q_SLOTS:
      
      void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
        //TRC; LOG<<bytesReceived<<bytesTotal;
        _downloads[qobject_cast<QNetworkReply*>(sender())].progress.first
          = bytesReceived;
        _downloads[qobject_cast<QNetworkReply*>(sender())].progress.second
          = bytesTotal;
        calcProgress();
      }
      void error(QNetworkReply::NetworkError code) {
        TRC; LOG<<"Status:"<<networkError(code);
        QNetworkReply* reply(qobject_cast<QNetworkReply*>(sender()));
        _downloads[reply].error +=
          tr("<h1>Network Error</h1>"
             "<dl><dt>URL:</dt><dd>%1</dd>"
             "<dt>Error Code:</dt><dd>%3</dd>"
             "<dt>Error Details:</dt><dd>%2</dd></dl>")
          .arg(reply->url().toString())
          .arg(networkError(code))
          .arg(code);
      }
      void slotDestroyed(QObject* obj) {
        TRC; LOG<<_downloads.size();
        _downloads.erase((QNetworkReply*)obj);
      }
      void slotFinished() {
        TRC; LOG<<_downloads.size();
        QNetworkReply* reply(qobject_cast<QNetworkReply*>(sender()));
        if (_downloads[reply].error.size())
          error(_downloads[reply].error);
        _downloads.erase(reply);
        if (_downloads.size()==0) finished();
      }
      void slotMetaDataChanged() {
        TRC;
        QNetworkReply* reply(qobject_cast<QNetworkReply*>(sender()));
        if (!reply) return;
        LOG<<"Location:"<<reply->header(QNetworkRequest::LocationHeader)
          .toString();
        LOG<<"Content-Type:"<<reply->header(QNetworkRequest::ContentTypeHeader)
          .toString();
        LOG<<"Content-Disposition:"<<reply->rawHeader("Content-Disposition");
        LOG<<"Status:"<<networkError(reply->error());
        LOG<<"URL:"<<reply->url().toString();
        LOG<<"File:"<<reply->url().toLocalFile();
        LOG<<"Path:"<<reply->url().path();
        metaDataChanged(reply);
      }
      void sslErrors(const QList<QSslError> & errors) {
        TRC;
        QNetworkReply* reply(qobject_cast<QNetworkReply*>(sender()));
        for (QList<QSslError>::const_iterator err(errors.begin());
             err!=errors.end(); ++err) {
          LOG<<"SSL-Error: "<<(int)err->error()<<": "<<err->errorString();
          LOG<<"Certificate Issuer: "
             <<"O="
             <<err->certificate().issuerInfo(QSslCertificate::Organization)
             <<"CN="<<err->certificate().issuerInfo(QSslCertificate::CommonName)
             <<"L="
             <<err->certificate().issuerInfo(QSslCertificate::LocalityName)
             <<"OU="
             <<err->certificate()
            .issuerInfo(QSslCertificate::OrganizationalUnitName)
             <<"C="<<err->certificate().issuerInfo(QSslCertificate::CountryName)
             <<"ST="
             <<err->certificate()
            .issuerInfo(QSslCertificate::StateOrProvinceName);
          LOG<<"Certificate Subject: "
             <<"O="
             <<err->certificate().subjectInfo(QSslCertificate::Organization)
             <<"CN="
             <<err->certificate().subjectInfo(QSslCertificate::CommonName)
             <<"L="
             <<err->certificate().subjectInfo(QSslCertificate::LocalityName)
             <<"OU="
             <<err->certificate()
            .subjectInfo(QSslCertificate::OrganizationalUnitName)
             <<"C="
             <<err->certificate().subjectInfo(QSslCertificate::CountryName)
             <<"ST="
             <<err->certificate()
            .subjectInfo(QSslCertificate::StateOrProvinceName);
          LOG<<"Certificate:\n"<<err->certificate().toPem();
          switch (err->error()) {
            case QSslError::SelfSignedCertificate:
            case QSslError::SelfSignedCertificateInChain: {
              QSslConfiguration sslConfig
                (QSslConfiguration::defaultConfiguration());
              QList<QSslCertificate> certs(sslConfig.caCertificates());
              for (QList<QSslCertificate>::iterator cert(certs.begin());
                   cert!=certs.end(); ++cert) {
                if (err->certificate().subjectInfo(QSslCertificate::CommonName)
                    == cert->subjectInfo(QSslCertificate::CommonName)) {
                  LOG<<"Found matching CN:"
                     <<cert->subjectInfo(QSslCertificate::CommonName);
                  if (err->certificate()==*cert) {
                    LOG<<"QT-BUG! Certificate matches known certificate";
                    //! @bug work around qt bug (Qt Mac 4.8.2)
                    reply->ignoreSslErrors(errors);
                  } else {
                    LOG<<"CERTIFICATE ERROR! Certificates are different";
                  }
                }
              }
            } break;
            default:; // ignore
          }
          _downloads[reply].error +=
            tr("<h1>SSL Error</h1>"
               "<dl><dt>URL:</dt><dd>%1</dd>"
               "<dt>Error Code:</dt><dd>%3</dd>"
               "<dt>Error Details:</dt><dd>%2</dd></dl>"
               "<table>"
               "<caption>Certificate Issuer</caption>"
               "<tr><th>Organization:</th><td>%4</td></tr>"
               "<tr><th>Common Name:</th><td>%5</td></tr>"
               "<tr><th>Location:</th><td>%6</td></tr>"
               "<tr><th>Organizational Unit:</th><td>%7</td></tr>"
               "<tr><th>Country:</th><td>%8</td></tr>"
               "<tr><th>State or Provive:</th><td>%9</td></tr>"
               "</table>"
               "<table>"
               "<caption>Certificate Subject</caption>"
               "<tr><th>Organization:</th><td>%10</td></tr>"
               "<tr><th>Common Name:</th><td>%11</td></tr>"
               "<tr><th>Location:</th><td>%12</td></tr>"
               "<tr><th>Organizational Unit:</th><td>%13</td></tr>"
               "<tr><th>Country:</th><td>%14</td></tr>"
               "<tr><th>State or Province:</th><td>%15</td></tr>"
               "</table>")
            .arg(reply->url().toString())
            .arg(err->errorString())
            .arg(err->error())
            .arg(err->certificate().issuerInfo(QSslCertificate::Organization).join("<br/>"))
            .arg(err->certificate().issuerInfo(QSslCertificate::CommonName).join("<br/>"))
            .arg(err->certificate().issuerInfo(QSslCertificate::LocalityName).join("<br/>"))
            .arg(err->certificate()
                 .issuerInfo(QSslCertificate::OrganizationalUnitName).join("<br/>"))
            .arg(err->certificate().issuerInfo(QSslCertificate::CountryName).join("<br/>"))
            .arg(err->certificate()
                 .issuerInfo(QSslCertificate::StateOrProvinceName).join("<br/>"))
            .arg(err->certificate().subjectInfo(QSslCertificate::Organization).join("<br/>"))
            .arg(err->certificate().subjectInfo(QSslCertificate::CommonName).join("<br/>"))
            .arg(err->certificate().subjectInfo(QSslCertificate::LocalityName).join("<br/>"))
            .arg(err->certificate()
                 .subjectInfo(QSslCertificate::OrganizationalUnitName).join("<br/>"))
            .arg(err->certificate().subjectInfo(QSslCertificate::CountryName).join("<br/>"))
            .arg(err->certificate()
                 .subjectInfo(QSslCertificate::StateOrProvinceName).join("<br/>"));
        }
      }
      
      void uploadProgress(qint64 bytesSent, qint64 bytesTotal) {
        //TRC; LOG<<bytesSent<<bytesTotal;
        _downloads[qobject_cast<QNetworkReply*>(sender())].progress.first
          = bytesSent;
        _downloads[qobject_cast<QNetworkReply*>(sender())].progress.second
          = bytesTotal;
        calcProgress();
      }
      
    private:
      
      typedef std::pair<qint64, qint64> Progress;
      struct Download {
          Progress progress;
          QString error;
      };
      typedef std::map<QNetworkReply*, Download> Downloads;
      
      Downloads _downloads;
  };

}  
//! @}

#endif
