#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 <QtGui/QMessageBox>

#include <pinentry.hxx>

#include <cryptoki.hxx>
#include <pcsc.hxx>
#include <openssl-engine.hxx>
#include <openssl.hxx>

#include <memory>

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

    Q_OBJECT;

  Q_SIGNALS:

    void certRequired();
    
  public:
    
    CryptokiEngine(std::string lib):
        _cryptoki(lib) {
      TRC;
    }

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

    cryptoki::Init& cryptoki() {
      return _cryptoki;
    }

    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);
        assert(localcert.isValid());
        
        //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 "CryptokiEngine_ID";
    }
    
    virtual const char* name() {
      TRC;
      return "CryptokiEngine_NAME";
    }
    
    virtual std::string rsaSign(const std::string& in, unsigned int type) {
      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) {
        certRequired(); // get new certificate
        return _privateKey->sign(in, CKM_RSA_PKCS); // try again
      }
    }
    
  private:
    
    cryptoki::Init _cryptoki;
    std::auto_ptr<cryptoki::Object> _privateKey;
    
};

class SmartCardAuth: public QObject {
    Q_OBJECT;
    
  public:

    SmartCardAuth(const QString& lib, QWidget* p=0, bool loginAtStart=true):
        _parent(p), _e(new CryptokiEngine(lib.toStdString())), _reg(_e) {
      TRC;
      if (loginAtStart) login();
      assert(connect(_e, SIGNAL(certRequired()), SLOT(login())));
    }

  public Q_SLOTS:

    void login(bool force=true) {
      TRC;
      try {
        Lock lock;
        LOG<<"got lock";
        if (!_e || (!force && *_e)) return; // no smartcard or already logged in
        LOG<<"start login to smartcard";
        QList<CertInfo> authcerts;
        QList<CertInfo> allcerts;
        QSslConfiguration sslConfig(QSslConfiguration::defaultConfiguration());
        _slots = _e->cryptoki().slotList();
        LOG<<"number of slots"<<_slots.size();
        // look for login certificates ----------------------------------------
        for (cryptoki::SlotList::iterator slot(_slots.begin());
             slot!=_slots.end(); ++slot) {
          cryptoki::Session session(*slot);
          cryptoki::ObjectList certs(session.find
                                     (cryptoki::Attribute(CKA_CLASS)
                                      .from<CK_OBJECT_CLASS>(CKO_CERTIFICATE)));
          for (cryptoki::ObjectList::iterator cert(certs.begin());
               cert!=certs.end(); ++cert) {
            cryptoki::Attribute label(cert->attribute(CKA_LABEL));
            cryptoki::Attribute id(cert->attribute(CKA_ID));
            LOG<<"**** FOUND CERTIFICATE: "<<label.value.c_str();
            cryptoki::ObjectList keys
              (session.find(cryptoki::Attribute(CKA_CLASS)
                            .from<CK_OBJECT_CLASS>(CKO_PUBLIC_KEY),
                            id));
            LOG<<"**** with keys: "<<keys.size();
            std::string data(cert->attribute(CKA_VALUE).value);
            if (!keys.size()) { // add CA-certificate
              LOG<<"**** add to CA-certificates";
            } else {
              LOG<<"**** user cert, check for authentictaion";
              if (label.value.find("auth")==0 ||
                  label.value.find("Authentication")!=std::string::npos) {
                LOG<<"**** it's our authentication cert";
                authcerts.push_back(CertInfo(data, slot, id));
              } else {
                LOG<<"**** it's an unknown cert";
                allcerts.push_back(CertInfo(data, slot, id));
              }
            }
          }
        }
        // get pin and install client certificate ------------------------------
        LOG<<"00000000000000000000000000000000000000000000000000";
        if (!authcerts.isEmpty() || !allcerts.isEmpty()) {
          LOG<<"11111111111111111111111111111111111111111111111111";
          CertInfo c(authcerts.size()?authcerts[0]:allcerts[0]);
          PinEntry pinEntry(QSslCertificate(QByteArray(c.data.data(),
                                                       c.data.size()),
                                            QSsl::Der), _parent);
          while (true) try {
              LOG<<"******************************************1*******";
              pinEntry.retries(retries(c.slot->slotinfo().slotDescription));
              LOG<<"******************************************2*******";
              int res(pinEntry.myexec());
              LOG<<"******************************************3*******";
              if (res!=PinEntry::Accepted) return;
              LOG<<"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
              _session = // session login with pin
                std::auto_ptr<cryptoki::Session>
                (new cryptoki::Session(*c.slot));
              LOG<<"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
              _session->login(pinEntry.pin().toStdString());
              LOG<<"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
              cryptoki::ObjectList keys
                (_session->find(cryptoki::Attribute(CKA_CLASS)
                                .from<CK_OBJECT_CLASS>(CKO_PRIVATE_KEY),
                                c.id));
              LOG<<"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD";
              if (keys.size()==1) {
                LOG<<"**** found one private key";
                _e->cert(keys[0], c.data); // install client cert
                LOG<<"==================================================";
                break;
              }
              LOG<<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
            } 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."));
            }
        }
        LOG<<"YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY";
      } catch (std::exception& x) {
        LOG<<"**** ERROR"<<x.what();
        throw;
      }
      LOG<<"ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ";
    }

  private:
    
    int retries(const std::string& name) {
      TRC; LOG<<name.c_str();
      try {
        pcsc::Connection pcsc;
        pcsc::Connection::Reader& reader(pcsc.reader(name));
#ifndef Q_OS_MAC
        pcsc::Connection::Reader::Transaction lock(reader);
#endif
        // first try to read version info
        if (reader.transmit(0x00, 0xA4, 0x08, 0x0C, "\x3f\x00\x56\x49", 4)
            != std::string("\x90\x00", 2) || !reader) {
          LOG<<"Select File failed";
          return -2;
        }
        std::string res(reader.transmit(0x00, 0xB0, 0x00, 0x00));
        if (res.substr(res.size()-2)!=std::string("\x90\x00", 2)) {
          LOG<<"read error";
          return -2;
        }
        LOG<<"version text is: "<<res.substr(4, res[3]).c_str();
        // if (res.substr(4, res[3]) != "RAPost 2009" &&
        //     res.substr(4, res[3]) != "RAPost 2010") {
        //   LOG<<"unsupported card";
        //   return -2;
        // }
        if (retCode(reader.transmit(0x00, 0xA4, 0x00, 0x0C)) == 0x9000) {
          int value(retCode(reader.transmit(0x00, 0x20, 0x00, 0x81)));
          if ((value&0x63C0)==0x63C0) return value&0x0F;
        } else {
          LOG<<"**** ERROR in select MF while reading pin status";
        }
        return -1; // locked
      } catch (const std::exception& x) {
        LOG<<"**** ERROR while reading pin status: "<<x.what();
        return -2;
      }
    }
    
    int retCode(const std::string& res) {
      if (res.size()>=2)
        return ((((unsigned int)(unsigned char)res[res.size()-2])*256)
                +((unsigned int)(unsigned char)res[res.size()-1]));
      else
        return -1;
    }
    
  private:

    struct CertInfo {
        CertInfo(std::string d, cryptoki::SlotList::iterator s,
                 cryptoki::Attribute i):
            data(d), slot(s), id(i) {
        }
        std::string data;
        cryptoki::SlotList::iterator slot;
        cryptoki::Attribute id;
    };
    
    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;
    cryptoki::SlotList _slots;
    std::auto_ptr<cryptoki::Session> _session;

};

#endif // SMARTCARDAUTH_H
