#ifndef SMARTCARDAUTH_H
#define SMARTCARDAUTH_H

#include <qbrowserlib/log.hxx>
#include <QtCore/QMutex>
#include <QtCore/QWaitCondition>
#include <QtNetwork/QSslSocket>
#include <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslCertificate>
#include <QtNetwork/QSslKey>
#include <QMessageBox>

#include <pinentry.hxx>

#include <suisseid.hxx>
#include <openssl-engine.hxx>

#include <memory>

class CryptokiEngine: public QObject, public openssl::Engine {

    Q_OBJECT;

  Q_SIGNALS:

    void certRequired();
    
  public:
    
    CryptokiEngine() {
      TRC;
    }

    operator bool() {
      TRC; LOG<<"Status of CryptokiEngine: "
         <<(_privateKey.get()
            ?"privateKey defined, ":"privateKey undefined");
      return _privateKey.get();
    }

    void cert(cryptoki::Object& privateKey, const std::string& certVal) {
      TRC;
      _privateKey = std::auto_ptr<cryptoki::Object>
        (new cryptoki::Object(privateKey));
      try { // new
        QSslConfiguration sslConfig(QSslConfiguration::defaultConfiguration());
        QSslCertificate localcert(QByteArray(certVal.data(),
                                             certVal.size()),
                                  QSsl::Der);
        sslConfig.setLocalCertificate(localcert);
        
        //RSA_set_default_method(ENGINE_get_RSA(_e));
        QByteArray pem // empty dummy key for qt object instantiation
          ("-----BEGIN RSA PRIVATE KEY-----\n"
           "MIIBOwIBAAJBAMH2yqAGeVNPdgeZ2GoHo31m9aUxZ7QfK2Go2qLTahLpQ3UL1C8G\n"
           "LkuMS8SNK0ZGfRMalIpIhv6bW5l3kjogOncCAwEAAQJABVGECtFCoGMsZFb2lSmy\n"
           "dOzOzYHGSy0TnnDn1dEgNnZ8sIljElPtUzm9dyXs2P3ICL1sOd7qjpzfJeyxknDL\n"
           "AQIhAO5iKdLmhyuW+EDEH19vDs1Pmqs3/ZnT5UgUiJnTJqz3AiEA0ExIfUOCnxq2\n"
           "a3Z46KEivcr8JB2P9VqouBbVryiq/oECIQDj8bPCejMoiEzMSX0iWWTTB9qC/KAg\n"
           "FtF4skHIrXKfEwIgPCs86Uo+Ch2aQjKHvJMHSRHAgeI0OmiEwiB+e0lhE4ECIQDd\n"
           "IbUmHIXt6oHLJmoGFX46bCcfil5eE5FXfiaw7Q9iPw==\n"
           "-----END RSA PRIVATE KEY-----\n");
        QSslKey privkey(pem, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
        RSA* rsa((RSA*)privkey.handle());
        if (!ENGINE_init(_e)) return;
        rsa->engine=_e;
        rsa->meth=ENGINE_get_RSA(_e);
        if (!CRYPTO_new_ex_data(CRYPTO_EX_INDEX_RSA, rsa, &rsa->ex_data)) {
          ENGINE_finish(_e);
          return;
        }
        set(rsa->n, privateKey, CKA_MODULUS, "CKA_MODULUS");
        set(rsa->e, privateKey, CKA_PUBLIC_EXPONENT, "CKA_PUBLIC_EXPONENT");
        set(rsa->d, privateKey, CKA_PRIVATE_EXPONENT, "CKA_PRIVATE_EXPONENT");
        set(rsa->p, privateKey, CKA_PRIME_1, "CKA_PRIME_1");
        set(rsa->q, privateKey, CKA_PRIME_2, "CKA_PRIME_2");
        set(rsa->dmp1, privateKey, CKA_EXPONENT_1, "CKA_EXPONENT_1");
        set(rsa->dmq1, privateKey, CKA_EXPONENT_2, "CKA_EXPONENT_2");
        set(rsa->iqmp, privateKey, CKA_COEFFICIENT, "CKA_COEFFICIENT");
        rsa->flags |= RSA_FLAG_SIGN_VER; // don't emulate with encrypt/decrypt
        assert(!privkey.isNull());
        LOG<<"Setup RSA finished";
        sslConfig.setPrivateKey(privkey);
        QSslConfiguration::setDefaultConfiguration(sslConfig);
      } catch (const std::exception& e) {
        LOG<<"SETUP ERROR: "<<e.what();
      }
    }

  protected:
    
    void set(BIGNUM*& num, cryptoki::Object& key, int type, std::string name) {
      TRC;
      try {
        std::string value(key.attribute(type).value);
        num = BN_bin2bn((const unsigned char*)value.data(),
                        value.size(), num);
      } catch (const std::exception& x) {
        LOG<<"**** ERROR: key attribute missing:"<<name.c_str()<<x.what();
      }
    }

    virtual const char* id() {
      TRC;
      return "SuisseID Engine ID";
    }
    
    virtual const char* name() {
      TRC;
      return "SuisseID Engine NAME";
    }
    
    virtual std::string rsaSign(const std::string& in, unsigned int type) try {
      TRC; LOG<<"log; type="<<type<<"; size="<<in.size();
      LOG<<crypto::readable(in).c_str();
      if (type != NID_md5_sha1) throw std::runtime_error("wrong sign type");
      if (in.size() != 36) throw std::runtime_error("wrong msg size to sign");
      LOG<<"ready to sign";
      try {
        return _privateKey->sign(in, CKM_RSA_PKCS);
      } catch (const std::exception& x) {
        LOG<<"signature failed, reason: "<<x.what();
        certRequired(); // get new certificate
        return _privateKey->sign(in, CKM_RSA_PKCS); // try again
      }
    } catch (const std::exception& x) {
      TRC; LOG<<"rsaSign failed, reason: "<<x.what();
      throw;
    }

    
  private:
    
    std::auto_ptr<cryptoki::Object> _privateKey;
    
};

class SmartCardAuth: public QObject {
    Q_OBJECT;
    
  public:

    SmartCardAuth(suisseid::Cards cards, QWidget* p=0, bool loginAtStart=true):
        _parent(p), _e(new CryptokiEngine()), _reg(_e), _cards(cards) {
      TRC;
      if (loginAtStart) login();
      assert(connect(_e, SIGNAL(certRequired()), SLOT(login())));
    }

  public Q_SLOTS:

    void login(bool force=true) {
      TRC;
      Lock lock;
      LOG<<"got lock";
      if (!_e || (!force && *_e)) return; // no smartcard or already logged in
      LOG<<"get new certificate";
      try {
        for (suisseid::Cards::iterator card(_cards.begin());
             card!=_cards.end(); ++card) {
          suisseid::Certificate cert((*card)->authenticationCertificate());
          PinEntry pinEntry(QSslCertificate(QByteArray(cert.data(),
                                                       cert.size()),
                                            QSsl::Der), _parent);
          while (true) try {
              if (pinEntry
                  .tokeninfo((*card)->minimalPinLength(),
                             (*card)->maximalPinLength())
                  .retries((*card)->pkcs15PinRetries())
                  .myexec()
                  !=PinEntry::Accepted)
                return;
              _session = // session login with pin
                std::shared_ptr<cryptoki::Session>
                (new cryptoki::Session((*card)->session()));
              _session->login(pinEntry.pin().toStdString());
              cryptoki::ObjectList keys
                (_session->find(cryptoki::Attribute(CKA_CLASS)
                                .from<CK_OBJECT_CLASS>(CKO_PRIVATE_KEY),
                                cert.id()));
              if (keys.size()==1) {
                _e->cert(keys[0], cert); // install client cert
                break;
              }
            } catch (std::exception& x) {
              pinEntry.pin().clear();
              LOG<<"**** ERROR"<<x.what();
              QMessageBox::critical(0, QMessageBox::tr("Wrong PIN"),
                                    QMessageBox::tr("Authentication failed,"
                                                    " please try again."));
            }
        }
      } catch (std::exception& x) {
        LOG<<"**** ERROR"<<x.what();
        throw;
      }
    }

  private:

  private:

    class Lock {
      public:
        Lock() {
          TRC; LOG<<loops().size();
          loops().append(new QEventLoop); // add to queue
          if (loops().size()>1) loops().back()->exec(); // wait
        }
        ~Lock() {
          TRC; LOG<<loops().size();
          delete loops().front(); // mine is the first;
          loops().erase(loops().begin()); // mine is the first;
          if (loops().begin()!=loops().end())
            loops().front()->exit(0); // wake up next
        }
      private:
        QList<QEventLoop*>& loops() { // same as a static member variable
          static QList<QEventLoop*> _loops;
          return _loops;
        }
    };

  private:
    
    QWidget* _parent;
    CryptokiEngine* _e;
    openssl::RegisterEngine<CryptokiEngine> _reg;
    suisseid::Cards _cards;
    std::shared_ptr<cryptoki::Session> _session;

};

#endif // SMARTCARDAUTH_H
