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

#ifndef PROXYFACE_HXX
#define PROXYFACE_HXX

#if HAVE_QT == 1
#include <QtNetwork/QNetworkProxy>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QAuthenticator>
#include <QtNetwork/QSslError>
#include <QtNetwork/QHostInfo>
#include <QtCore/QTimer>
#include <QtCore/QDebug>
#include <QtCore/QThread>
#include <map>
#ifndef PROXYFACE_LOG
#define PROXYFACE_LOG qDebug()<<__PRETTY_FUNCTION__
#endif
#endif

#include <list>
#include <string>
#include <sstream>

//! auto proxy configuration
namespace proxy {
  
  std::string version();

  //! exceptions
  namespace exc {
    
    //! unspecific error
    class error: std::exception {
      public:
        virtual const char* what() const throw() {
          return "Auto Proxy Detection Error";
        }
    };

  }

  //! Proxy types
  enum Type {
    DIRECT,  //< direct connection, no proxy
    DEFAULT, //< default proxy
    HTTP,    //< HTTP proxy host
    SOCKS    //<SOCKS5 proxy
  };

  //! One proxy
  struct Proxy {
      Proxy(): type(DIRECT), port(0) {}
      Proxy(Type t, std::string h, unsigned int p): type(t), host(h), port(p) {}
      Proxy(Type t, std::string h, std::string p): type(t), host(h), port(0) {
        std::stringstream(p)>>port;
      }
      Type type;
      std::string host;
      unsigned int port;
  };

  //! List of proxies, type -> url and port
  typedef std::list<Proxy> List;

  /*! @page dyn Interaction Diagrams

      @section dynState State Diagram

      @dot
      digraph A {
        // actors
        "start" [shape="circle"];

        // loops
        subgraph clusterMainLoop {
          label="Main Loop";
          "«thread loop»";
          "«timer loop»";
          "«network loop»";
        }

        node [shape=box];
      
      
        // method declarations
        "Interface::proxy"            [URL="\ref Interface::proxy"];
        "Interface::ping"             [URL="\ref Interface::ping"];
        "Interface::run"              [URL="\ref Interface::run"];
        "Interface::setupProxyCheck"  [URL="\ref Interface::setupProxyCheck"];

        // slot declarations
        {
          node [shape=octagon];
          "Interface::timeout"          [URL="\ref Interface::timeout"];
          "Interface::threadFinished"   [URL="\ref Interface::threadFinished"];
          "Interface::replyFinished"    [URL="\ref Interface::replyFinished"];
          "Interface::uploadProgress"   [URL="\ref Interface::uploadProgress"];
          "Interface::downloadProgress"
                                       [URL="\ref Interface::downloadProgress"];
          "Interface::sslErrors"        [URL="\ref Interface::sslErrors"];
        }

        // signal declarations
        {
          node [shape=invhouse];
          "Interface::ProxyError"       [URL="\ref Interface::ProxyError"];
          "Interface::authenticationRequired"
                                 [URL="\ref Interface::authenticationRequired"];
          "Interface::proxyAuthenticationRequired"
                            [URL="\ref Interface::proxyAuthenticationRequired"];
          "Interface::temporaryError"   [URL="\ref Interface::temporaryError"];
          "Interface::proxyFound"       [URL="\ref Interface::proxyFound"];
        }
      
        // method interactions
        "start" -> { "Interface::proxy" "Interface::ping"};
        "Interface::ping" -> "Interface::setupProxyCheck";
        "Interface::proxy" -> "Interface::timeout" [label="for test only:\ndirect call, no _timeout1"];
         "Interface::run" -> "«thread loop»" [label="proxies(url)",URL="\ref Interface::proxies"];
        "Interface::setupProxyCheck" -> "Interface::threadFinished";

        // networkmanager
        "Interface::setupProxyCheck" -> "QNetworkAccessManager" [label="new"];
        "QNetworkAccessManager" -> "«network loop»" [label="QNetworkReply* QNetworkAccessManager::get()"];
     
        // slot interactions
        "Interface::timeout" -> "Interface::run" [label="first time\n(own thread)"];
        "Interface::timeout" -> "Interface::ProxyError" [label="second time"];
        "Interface::threadFinished" -> "Interface::setupProxyCheck" [label="1.) for all proxies"];
        "Interface::threadFinished" -> "«timer loop»" [label="2.) _timeout2.start()"];
        "Interface::replyFinished" -> "Interface::temporaryError" [label="error"];
        "Interface::temporaryError" -> "«network loop»" [label="wait for timeout"];
        "Interface::replyFinished" -> "Interface::proxyFound" [label="success"];

        // signaling objects
        "«timer loop»" -> "Interface::timeout" [label="_timeout1",URL="\ref Interface::_timeout1"];
        "«timer loop»" -> "Interface::timeout" [label="_timeout2",URL="\ref Interface::_timeout2"];
        "«thread loop»" -> {
          "Interface::threadFinished";
        } [label="finished"];
        "«network loop»" -> {
          "Interface::replyFinished";
          "Interface::uploadProgress";
          "Interface::downloadProgress";
          "Interface::authenticationRequired";
          "Interface::proxyAuthenticationRequired";
          "Interface::sslErrors";
        }

      }
      @enddot

      @section dynMSC Message State Chart

      @msc
      |||;
      application, Interface, QNetworkAccessManager, QNetworkReply;
      application -> Interface [label="Interface()"];
      application -> Interface [label="proxy(url)"];
      |||;
      --- [label="for each proxy"];
      Interface -> QNetworkAccessManager  [label="new"];
      QNetworkAccessManager -> QNetworkReply [label="get(url)"];
      Interface -> Interface [label="_timeout2.start()"];
      --- [label=""];
      |||;
      QNetworkReply -> Interface [label="replyFinished()"];
      Interface -> application [label="proxyFound()"];
      |||;
      @endmsc */
  
  //! Unified Interface for accessing proxy::Face
  /*! Abstract interface, impementation for Unix and Windoze differs.
      
      Instanciate proxy::Face, which is a typedef to your
      platform's implementation.
      
      @code
      proxy::Face pf;        // keep for program life time (because of caching)
      proxy::List pf.proxies("http://swisssign.com");
      [...]                  // set proxy, read from http://swisssign.com
      @endcode

      @example getproxylist.cxx */
  class Interface
#if HAVE_QT == 1
    : public QThread
#endif
  {
#if HAVE_QT == 1
      Q_OBJECT
#endif
          
    public:
          
      //! Keep your instance as long as possible, because of caching.
      Interface()
#if HAVE_QT == 1
          : _timeout1Paused(false), _timeout2Paused(false)
#endif
      {
#if HAVE_QT == 1
        PROXYFACE_LOG;
        if (!connect(&_timeout1, SIGNAL(timeout()), SLOT(timeout())))
          qFatal("connect failed");
        if (!connect(&_timeout2, SIGNAL(timeout()), SLOT(timeout())))
          qFatal("connect failed");
#ifndef Q_OS_WIN32
        if (!connect(this, SIGNAL(finished()), SLOT(threadFinished())))
          qFatal("connect failed");
#endif
#endif
      }
      
      virtual ~Interface() {}
      
      //! Get list of proxies for a given URL.
      virtual List proxies(const std::string& url) = 0;
#if HAVE_QT == 1
      //! Reset, stop all outstanding checks
      void reset() {
        _timeout1.stop();
        _timeout2.stop();
        for (Requests::iterator it(_requests.begin());
             it!=_requests.end(); ++it)
          clean(it->second.first);
        _requests.clear();
      }

      //! Pause timeouts and restart them later.
      /*! Use while waiting for user input at proxy authentication.
          @see restart() */
      void pause() {
        if (_timeout1.isActive()) {
          _timeout1.stop();
          _timeout1Paused = true;
        }
        if (_timeout2.isActive()) {
          _timeout2.stop();
          _timeout2Paused = true;
        }
      }
      
      //! Restart paused timeouts.
      /*! Use while waiting for user input at proxy authentication.
          @see pause() */
      void restart() {
        if (_timeout1Paused) {
          _timeout1.start();
          _timeout1Paused = false;
        }
        if (_timeout2Paused) {
          _timeout2.start();
          _timeout2Paused = false;
        }
      }
      
      //! Network Ping: Check access to a given address using default proxy.
      void ping(const std::string& url, int timeout2=30000) {
        if (_requests.size() || _timeout1.isActive() || _timeout2.isActive())
          // detection is already running, wait for that, don't restart
          return;
        reset();
        _timeout2.setSingleShot(true);
        _timeout2.setInterval(timeout2);
        _url = url;
        _direct = false; // simulate second try
        QNetworkProxy defaultProxy;
        setupProxyCheck(defaultProxy, url);
        _timeout2.start();
      }
      
      void ping(const QUrl& url, int timeout2=30000) {
        ping(url.toString().toStdString(), timeout2);
      }
      
      void ping(const QString& url, int timeout2=30000) {
        ping(url.toStdString(), timeout2);
      }
      
      //! If Qt Network is available you may check the proxies found
      /*! First checks for direct access to the target. If this is not
          possible, starts scan for proxies.

          Returns immediately and signals proxyFound() if a valid
          configuration has been found, or proxyError() in case of
          timeout. You should wait for either of the signals, before you
          can call this method again.

          @param url the url to find a proxy for
          @param timeout [ms] time to give up search */
      void proxy(const std::string& url,
                 int timeout1=5000, int timeout2=30000) {
        PROXYFACE_LOG;
        qDebug()<<"Search proxy for URL, direct and default"
                <<"url="<<url.data()<<"timeout1="<<timeout1;
        reset();
        _url = url;
        _direct = true; // first try direct access
        if (getenv("HTTP_PROXY")) {
          QUrl proxy(QString(getenv("HTTP_PROXY")));
          QNetworkProxy envProxy(QNetworkProxy::HttpProxy,
                                 proxy.host(), proxy.port(),
                                 proxy.userName(), proxy.password());
          setupProxyCheck(envProxy, url);
        }
        QNetworkProxy directProxy(QNetworkProxy::NoProxy);
        setupProxyCheck(directProxy, url);
        QNetworkProxy defaultProxy;
        setupProxyCheck(defaultProxy, url);
        _timeout2.setSingleShot(true);
        _timeout2.setInterval(timeout2);
        _timeout1.setSingleShot(true);
        _timeout1.setInterval(timeout1);
        _timeout1.start();
      }
      
      //! If Qt Network is available you may check the proxies found
      /*! @copydoc proxy(const std::string&, int) */
      void proxy(const QUrl& url, int timeout1=5000, int timeout2=30000) {
        PROXYFACE_LOG;
        proxy(url.toString().toStdString(), timeout1, timeout2);
      }
      
      //! If Qt Network is available you may check the proxies found
      /*! @copydoc proxy(const std::string&, int) */
      void proxy(const QString& url, int timeout1=5000, int timeout2=30000) {
        PROXYFACE_LOG;
        proxy(url.toStdString(), timeout1, timeout2);
      }
      
      static QString toString(const QNetworkProxy& p) {
        switch (p.type()) {
          case QNetworkProxy::NoProxy:
            return QString("direct://");
          case QNetworkProxy::DefaultProxy:
            if (QNetworkProxy::applicationProxy().type()
                !=QNetworkProxy::DefaultProxy)
              return QString("Default Proxy: %1")
                .arg(toString(QNetworkProxy::applicationProxy()));
            else
              return QString("Default Proxy: **ERROR**"
                             " application proxy is configured as"
                             " QNetworkProxy::DefaultProxy");
          case QNetworkProxy::Socks5Proxy:
            return QString("socks://%1:%2").arg(p.hostName()).arg(p.port());
          case QNetworkProxy::HttpProxy:
            return QString("http://%1:%2").arg(p.hostName()).arg(p.port());
          case QNetworkProxy::HttpCachingProxy:
            return QString("http-cache://%1:%2").arg(p.hostName())
              .arg(p.port());
          case QNetworkProxy::FtpCachingProxy:
            return QString("ftp-cache://%1:%2").arg(p.hostName()).arg(p.port());
        }
        return QString("**ERROR** illegal proxy type");
      }
      
    Q_SIGNALS:
        
      //! Signals a valid proxy for the URL requested earlier.
      void proxyFound(const QUrl&, const QNetworkProxy&);
      //! Signals a timeout, and no valid proxy for the URL requested earlier.
      void proxyError(QNetworkReply::NetworkError);
      //! Signals an error during proxy detection.
      void temporaryError(QNetworkReply::NetworkError, QString, QString);
      void authenticationRequired(QNetworkReply*, QAuthenticator*);
      void proxyAuthenticationRequired(const QNetworkProxy&, QAuthenticator*);
      
    private:
      
      void setupProxyCheck(const QNetworkProxy& prxy, const std::string& url) {
        PROXYFACE_LOG;
        qDebug()<<"Testing proxy for url:"<<toString(prxy)<<"url="<<url.data();
        QNetworkAccessManager* manager(new QNetworkAccessManager);
        QNetworkProxy p(prxy.type()==QNetworkProxy::DefaultProxy
                        ?QNetworkProxy::applicationProxy():prxy);
        qDebug()<<"Testing proxy for url:"<<toString(p)<<"url="<<url.data();
        manager->setProxy(p);
        if (!connect(manager, SIGNAL(finished(QNetworkReply*)),
                     SLOT(replyFinished(QNetworkReply*))))
          qFatal("connect failed");
        if (!connect(manager,
                     SIGNAL(authenticationRequired(QNetworkReply*,
                                                   QAuthenticator*)),
                     SIGNAL(authenticationRequired(QNetworkReply*,
                                                   QAuthenticator*))))
          qFatal("connect failed");
        if (!connect(manager,
                     SIGNAL(proxyAuthenticationRequired
                            (const QNetworkProxy&, QAuthenticator*)),
                     SIGNAL(proxyAuthenticationRequired
                            (const QNetworkProxy&, QAuthenticator*))))
          qFatal("connect failed");
        if (!connect(manager,
                     SIGNAL(proxyAuthenticationRequired
                            (const QNetworkProxy&, QAuthenticator*)),
                     SLOT(proxyAuthenticationRequiredLog
                          (const QNetworkProxy&, QAuthenticator*))))
          qFatal("connect failed");
        if (!connect(manager,
                     SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError>&)),
                     SLOT(sslErrors(QNetworkReply*, const QList<QSslError>&))))
          qFatal("connect failed");
        QNetworkReply* reply(0);
        _requests.insert
          (std::make_pair(reply=manager->get
                          (QNetworkRequest
                           (QUrl(QString::fromStdString(url)))),
                          std::make_pair(manager, prxy)));
        if (!connect(reply, SIGNAL(uploadProgress(qint64, qint64)),
                     SLOT(uploadProgress(qint64, qint64))))
          qFatal("connect failed");
        if (!connect(reply, SIGNAL(downloadProgress(qint64, qint64)),
                     SLOT(downloadProgress(qint64, qint64))))
          qFatal("connect failed");
      }
      
      void clean(QNetworkAccessManager* manager) {
        if (!disconnect(manager, SIGNAL(finished(QNetworkReply*)),
                        this, SLOT(replyFinished(QNetworkReply*))))
          qFatal("disconnect failed");
        disconnect(manager,
                   SIGNAL(authenticationRequired(QNetworkReply*,
                                                 QAuthenticator*)),
                   this,
                   SIGNAL(authenticationRequired(QNetworkReply*,
                                                 QAuthenticator*)));
        disconnect(manager,
                   SIGNAL(proxyAuthenticationRequired(const QNetworkProxy&,
                                                      QAuthenticator*)),
                   this,
                   SIGNAL(proxyAuthenticationRequired(const QNetworkProxy&,
                                                      QAuthenticator*)));
        disconnect(manager,
                   SIGNAL(proxyAuthenticationRequired(const QNetworkProxy&,
                                                      QAuthenticator*)),
                   this,
                   SLOT(proxyAuthenticationRequiredLog(const QNetworkProxy&,
                                                       QAuthenticator*)));
        disconnect(manager,
                   SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError>&)),
                   this,
                   SLOT(sslErrors(QNetworkReply*, const QList<QSslError>&)));
        //delete manager;
      }
      
    private Q_SLOTS:

      void uploadProgress(qint64 a, qint64 b) {
        qDebug()<<"upload: "<<a<<"/"<<b;
      }
      
      void downloadProgress(qint64 a, qint64 b) {
        qDebug()<<"download: "<<a<<"/"<<b;
      }
      
      void proxyAuthenticationRequiredLog(const QNetworkProxy&,
                                          QAuthenticator* auth) {
        qDebug()<<"proxyAuthenticationRequired for "<<auth->realm();
      }
      
      void timeout() {
        PROXYFACE_LOG;
        reset();
        qDebug()<<"Proxy detection timed out"<<"url="<<_url.data();
        if (_direct) {
          qDebug()<<"Direct or preconfigured proxy not available,"
            " try autoproxy negotiation";
          _direct = false;
#ifndef Q_OS_WIN32
          start(); // autoproxy detection in own thread
#else
          run();
#endif
        } else {
          qDebug()<<"No proxy at all, giving up - offline?";
          proxyError(QNetworkReply::TimeoutError);
        }
      }
      
      void replyFinished(QNetworkReply* reply) {
        PROXYFACE_LOG;
        qDebug()<<"Proxydetection got reply with status:"
                <<reply->error()<<reply->errorString();
        if (reply->error()!=QNetworkReply::NoError) {
          temporaryError(reply->error(), reply->errorString(),
                         toString(_requests[reply].second));
          return; // wait for timeout
        }
        QNetworkProxy prxy(_requests[reply].second);
        QUrl url(reply->url());
        reset();
        qDebug()<<"SUCCESS - Valid proxy found for url:"
                <<"url="<<url<<"proxy="<<toString(prxy);
        proxyFound(url, prxy);
      }
      
      void sslErrors(QNetworkReply*, const QList<QSslError>& l) {
        PROXYFACE_LOG;
        qDebug()<<"## "<<__PRETTY_FUNCTION__;
        for (QList<QSslError>::const_iterator it(l.begin()); it!=l.end(); ++it)
          qDebug()<<" SSL-Error -> "<<it->errorString();
      }
      
      void threadFinished() {
        PROXYFACE_LOG;
        for (List::const_iterator it(_proxies.begin());
             it!=_proxies.end(); ++it) {
          QNetworkProxy prxy((it->type==DEFAULT?QNetworkProxy::DefaultProxy
                              :(it->type==HTTP?QNetworkProxy::HttpProxy
                                :(it->type==SOCKS?QNetworkProxy::Socks5Proxy
                                  :QNetworkProxy::NoProxy))),
                             QString::fromStdString(it->host), it->port);
          setupProxyCheck(prxy, _url);
        }
        QNetworkProxy directProxy(QNetworkProxy::NoProxy);
        setupProxyCheck(directProxy, _url);
        QNetworkProxy defaultProxy;
        setupProxyCheck(defaultProxy, _url);
        _timeout2.start();
      }
      
    protected:
      
      void run() {
        PROXYFACE_LOG;
        _proxies = proxies(_url);
#ifdef Q_OS_WIN32
        threadFinished();
#endif
      }
      
    private:
      
      typedef std::map
          <QNetworkReply*, std::pair<QNetworkAccessManager*, QNetworkProxy> >
          Requests;
      Requests _requests;
      bool _direct;
      QTimer _timeout1;
      QTimer _timeout2;
      bool _timeout1Paused;
      bool _timeout2Paused;
      List _proxies;
      std::string _url;
#endif
  };
}

#  ifdef WIN32
#    ifdef QT_NETWORK_LIB___HAS_A_BUG /// @bug Crashes on Windows Qt 5.2.1
//     use Qt if available
#      include <proxyface/qtproxy.hxx>
namespace proxy {
  typedef QtProxy Face;
}
#    else
//     use windoze proprietary winhttp
#      include <proxyface/windoze.hxx>
namespace proxy {
  typedef Windoze Face;
}
#    endif
#  else
#    if HAVE_QT == 1
//     use Qt if available (not yet linux)
#      include <proxyface/qtproxy.hxx>
namespace proxy {
  typedef QtProxy Face;
}
#    else
//     no Qt support, nor proprietary: use http://code.google.com/p/libproxy
#      include <proxyface/unix.hxx>
namespace proxy {
  typedef Unix Face;
}
#    endif
#  endif

#endif
