/***************************************************************************
 *   Copyright (C) 2006 by Petri Damsten                                   *
 *   damu@iki.fi                                                           *
 *                                                                         *
 *   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.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include "config.h"

#ifdef HAVE_LIBGPGME

#include "kgpgme.h"

#include <QtCore/QPointer>
#include <QtGui/QTreeWidget>
#include <QtGui/QLabel>
#include <QtGui/QPixmap>
#include <QtGui/QVBoxLayout>

#include <KDE/KApplication>
#include <KDE/KLocale>
#include <KDE/KMessageBox>
#include <KDE/KPasswordDialog>

#include <locale.h>         //For LC_ALL, etc.
#include <errno.h>          //For errno
#include <unistd.h>         //For write

// KGpgSelKey class based on class in KGpg with the same name

class KGpgSelKey : public KDialog
{
private:
    QTreeWidget* keysListpr;

public:

    KGpgSelKey(QWidget *parent, const char *name, QString preselected,
               const KGpgMe& gpg):
            KDialog(parent) {

        // Dialog options
        setObjectName(name);
        setModal(true);
        setCaption(i18n("Private Key List"));
        setButtons(Ok | Cancel);

        QString keyname;
        QVBoxLayout* vbox;
        QWidget* page = new QWidget(this);
        QLabel* labeltxt;
        QPixmap keyPair = KIcon("kgpg_key2").pixmap(20, 20);

        setMinimumSize(350, 100);
        keysListpr = new QTreeWidget(page);
        keysListpr->setRootIsDecorated(true);
        keysListpr->setColumnCount(3);
        QStringList headers;
        headers << i18n("Name") << i18n("Email") << i18n("ID");
        keysListpr->setHeaderLabels(headers);
        keysListpr->setAllColumnsShowFocus(true);

        labeltxt = new QLabel(i18n("Choose a secret key:"), page);
        vbox = new QVBoxLayout(page);

        KGpgKeyList list = gpg.keys(true);

        for (KGpgKeyList::iterator it = list.begin(); it != list.end(); ++it) {
            QString name = gpg.checkForUtf8((*it).name);
            QStringList values;
            values << name << (*it).email << (*it).id;
            QTreeWidgetItem *item = new
            QTreeWidgetItem(keysListpr, values, 3);
            item->setIcon(0, keyPair);
            if (preselected == (*it).id) {
                item->setSelected(true);
                keysListpr->setCurrentItem(item);
            }
        }
        if (!keysListpr->currentItem() && keysListpr->topLevelItemCount() > 0) {
            keysListpr->topLevelItem(0)->setSelected(true);
            keysListpr->setCurrentItem(keysListpr->topLevelItem(0));
        }
        vbox->addWidget(labeltxt);
        vbox->addWidget(keysListpr);
        setMainWidget(page);
    };

    QString key() {
        QTreeWidgetItem* item = keysListpr->currentItem();

        if (item)
            return item->text(2);
        return "";
    }
};

KGpgMe::KGpgMe() : m_ctx(0), m_useGnuPGAgent(true)
{
    init(GPGME_PROTOCOL_OpenPGP);
    if (gpgme_new(&m_ctx)) {
        m_ctx = 0;
    } else {
        gpgme_set_armor(m_ctx, 1);
        setPassphraseCb();
    }
}

KGpgMe::~KGpgMe()
{
    if (m_ctx)
        gpgme_release(m_ctx);
    clearCache();
}

void KGpgMe::clearCache()
{
    if (m_cache.size() > 0) {
        m_cache.fill('\0');
        m_cache.truncate(0);
    }
}

void KGpgMe::init(gpgme_protocol_t proto)
{
    gpgme_error_t err;

    gpgme_check_version(NULL);
    setlocale(LC_ALL, "");
    gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
#ifndef Q_WS_WIN 
    gpgme_set_locale(NULL, LC_MESSAGES, setlocale(LC_MESSAGES, NULL));
#endif
    err = gpgme_engine_check_version(proto);
    if (err) {
        static QString lastErrorText;

        QString text = QString("%1: %2").arg(gpgme_strsource(err), gpgme_strerror(err));
        if (text != lastErrorText) {
            KMessageBox::error(kapp->activeWindow(), text);
            lastErrorText = text;
        }
    }
}

QString KGpgMe::checkForUtf8(QString txt)
{
    // code borrowed from KGpg which borrowed it from gpa
    const char *s;

    // Make sure the encoding is UTF-8.
    // Test structure suggested by Werner Koch
    if (txt.isEmpty())
        return QString::null;

    for (s = txt.toAscii(); *s && !(*s & 0x80); s++)
        ;
    if (*s && !strchr(txt.toAscii(), 0xc3) && (txt.indexOf("\\x") == -1))
        return txt;

    // The string is not in UTF-8
    //if (strchr (txt.toAscii(), 0xc3)) return (txt+" +++");
    if (txt.indexOf("\\x") == -1)
        return QString::fromUtf8(txt.toAscii());
    //        if (!strchr (txt.toAscii(), 0xc3) || (txt.indexOf("\\x")!=-1)) {
    for (int idx = 0 ; (idx = txt.indexOf("\\x", idx)) >= 0 ; ++idx) {
        char str[2] = "x";
        str[0] = (char)QString(txt.mid(idx + 2, 2)).toShort(0, 16);
        txt.replace(idx, 4, str);
    }
    if (!strchr(txt.toAscii(), 0xc3))
        return QString::fromUtf8(txt.toAscii());
    else
        return QString::fromUtf8(QString::fromUtf8(txt.toAscii()).toAscii());
    // perform Utf8 twice, or some keys display badly
    return txt;
}

QString KGpgMe::selectKey(QString previous)
{
    QPointer<KGpgSelKey> dlg = new KGpgSelKey(kapp->activeWindow(), "", previous, *this);

    if (dlg->exec())
        return dlg->key();
    return "";
}

// Rest of the code is mainly based in gpgme examples

KGpgKeyList KGpgMe::keys(bool privateKeys /* = false */) const
{
    KGpgKeyList keys;
    gpgme_error_t err = 0, err2 = 0;
    gpgme_key_t key = 0;
    gpgme_keylist_result_t result = 0;

    if (m_ctx) {
        err = gpgme_op_keylist_start(m_ctx, NULL, privateKeys);
        if (!err) {
            while (!(err = gpgme_op_keylist_next(m_ctx, &key))) {
                KGpgKey gpgkey;

                if (!key->subkeys)
                    continue;
                gpgkey.id = key->subkeys->keyid;
                if (key->uids) {
                    gpgkey.name = key->uids->name;
                    gpgkey.email = key->uids->email;
                }
                keys.append(gpgkey);
                gpgme_key_unref(key);
            }

            if (gpg_err_code(err) == GPG_ERR_EOF)
                err = 0;
            err2 = gpgme_op_keylist_end(m_ctx);
            if (!err)
                err = err2;
        }
    }

    if (err) {
        KMessageBox::error(kapp->activeWindow(), QString("%1: %2")
                           .arg(gpgme_strsource(err)).arg(gpgme_strerror(err)));
    } else {
        result = gpgme_op_keylist_result(m_ctx);
        if (result->truncated) {
            KMessageBox::error(kapp->activeWindow(),
                               i18n("Key listing unexpectedly truncated."));
        }
    }
    return keys;
}

bool KGpgMe::encrypt(const QByteArray& inBuffer, unsigned long length,
                     QByteArray* outBuffer, QString keyid /* = QString::null */)
{
    gpgme_error_t err = 0;
    gpgme_data_t in = 0, out = 0;
    gpgme_key_t keys[2] = { NULL, NULL };
    gpgme_key_t* key = NULL;
    gpgme_encrypt_result_t result = 0;

    outBuffer->resize(0);
    if (m_ctx) {
        err = gpgme_data_new_from_mem(&in, inBuffer.data(), length, 1);
        if (!err) {
            err = gpgme_data_new(&out);
            if (!err) {
                if (keyid.isNull()) {
                    key = NULL;
                } else {
                    err = gpgme_get_key(m_ctx, keyid.toAscii(), &keys[0], 0);
                    key = keys;
                }

                if (!err) {
                    err = gpgme_op_encrypt(m_ctx, key, GPGME_ENCRYPT_ALWAYS_TRUST,
                                           in, out);
                    if (!err) {
                        result = gpgme_op_encrypt_result(m_ctx);
                        if (result->invalid_recipients) {
                            KMessageBox::error(kapp->activeWindow(), QString("%1: %2")
                                               .arg(i18n("That public key is not meant for encryption"))
                                               .arg(result->invalid_recipients->fpr));
                        } else {
                            err = readToBuffer(out, outBuffer);
                        }
                    }
                }
            }
        }
    }
    if (err != GPG_ERR_NO_ERROR && err != GPG_ERR_CANCELED) {
        KMessageBox::error(kapp->activeWindow(), QString("%1: %2")
                           .arg(gpgme_strsource(err)).arg(gpgme_strerror(err)));
    }
    if (err != GPG_ERR_NO_ERROR)
        clearCache();
    if (keys[0])
        gpgme_key_unref(keys[0]);
    if (in)
        gpgme_data_release(in);
    if (out)
        gpgme_data_release(out);
    return (err == GPG_ERR_NO_ERROR);
}

bool KGpgMe::decrypt(const QByteArray& inBuffer, QByteArray* outBuffer)
{
    gpgme_error_t err = 0;
    gpgme_data_t in = 0, out = 0;
    gpgme_decrypt_result_t result = 0;

    outBuffer->resize(0);
    if (m_ctx) {
        err = gpgme_data_new_from_mem(&in, inBuffer.data(), inBuffer.size(), 1);
        if (!err) {
            err = gpgme_data_new(&out);
            if (!err) {
                err = gpgme_op_decrypt(m_ctx, in, out);
                if (!err) {
                    result = gpgme_op_decrypt_result(m_ctx);
                    if (result->unsupported_algorithm) {
                        KMessageBox::error(kapp->activeWindow(), QString("%1: %2")
                                           .arg(i18n("Unsupported algorithm"))
                                           .arg(result->unsupported_algorithm));
                    } else {
                        err = readToBuffer(out, outBuffer);
                    }
                }
            }
        }
    }
    if (err != GPG_ERR_NO_ERROR && err != GPG_ERR_CANCELED) {
        KMessageBox::error(kapp->activeWindow(), QString("%1: %2")
                           .arg(gpgme_strsource(err)).arg(gpgme_strerror(err)));
    }
    if (err != GPG_ERR_NO_ERROR)
        clearCache();
    if (in)
        gpgme_data_release(in);
    if (out)
        gpgme_data_release(out);
    return (err == GPG_ERR_NO_ERROR);
}

#define BUF_SIZE (32 * 1024)

gpgme_error_t KGpgMe::readToBuffer(gpgme_data_t in, QByteArray* outBuffer) const
{
    int ret;
    gpgme_error_t err = GPG_ERR_NO_ERROR;

    ret = gpgme_data_seek(in, 0, SEEK_SET);
    if (ret) {
        err = gpgme_err_code_from_errno(errno);
    } else {
        char* buf = new char[BUF_SIZE + 2];

        if (buf) {
            while ((ret = gpgme_data_read(in, buf, BUF_SIZE)) > 0) {
                uint size = outBuffer->size();
                outBuffer->resize(size + ret);
                memcpy(outBuffer->data() + size, buf, ret);
            }
            if (ret < 0)
                err = gpgme_err_code_from_errno(errno);
            delete[] buf;
        }
    }
    return err;
}

bool KGpgMe::isGnuPGAgentAvailable()
{
    QString agent_info = qgetenv("GPG_AGENT_INFO");

    if (agent_info.indexOf(':') > 0)
        return true;
    return false;
}

void KGpgMe::setPassphraseCb()
{
    bool agent = false;
    QString agent_info;

    agent_info = qgetenv("GPG_AGENT_INFO");

    if (m_useGnuPGAgent) {
        if (agent_info.indexOf(':'))
            agent = true;
        if (agent_info.startsWith(QLatin1String("disable:")))
            setenv("GPG_AGENT_INFO", agent_info.mid(8).toAscii(), 1);
    } else {
        if (!agent_info.startsWith(QLatin1String("disable:")))
            setenv("GPG_AGENT_INFO", "disable:" + agent_info.toAscii(), 1);
    }
    if (agent)
        gpgme_set_passphrase_cb(m_ctx, 0, 0);
    else
        gpgme_set_passphrase_cb(m_ctx, passphraseCb, this);
}

gpgme_error_t KGpgMe::passphraseCb(void* hook, const char* uid_hint,
                                   const char* passphrase_info,
                                   int last_was_bad, int fd)
{
    KGpgMe* gpg = static_cast<KGpgMe*>(hook);
    return gpg->passphrase(uid_hint, passphrase_info, last_was_bad, fd);
}

gpgme_error_t KGpgMe::passphrase(const char* uid_hint,
                                 const char* /*passphrase_info*/,
                                 int last_was_bad, int fd)
{
    gpgme_error_t res = GPG_ERR_CANCELED;
    QString s;
    QString gpg_hint = checkForUtf8(uid_hint);
    int result;

    if (last_was_bad) {
        s += "<b>" + i18n("Wrong password.") + "</b><br><br>\n\n";
        clearCache();
    }

    if (!m_text.isEmpty())
        s += m_text + "<br>";

    if (!gpg_hint.isEmpty())
        s += gpg_hint;

    if (m_cache.isEmpty()) {
        KPasswordDialog dlg;
        dlg.setPrompt(s);

        if (m_saving)
            dlg.setWindowTitle(i18n("Please enter a new password:"));

        if (dlg.exec())
            m_cache = dlg.password();
    } else
        result = KPasswordDialog::Accepted;

    if (result == KPasswordDialog::Accepted) {
        write(fd, m_cache.data(), m_cache.length());
        res = 0;
    }
    write(fd, "\n", 1);
    return res;
}
#endif  // HAVE_LIBGPGME
