/*
 * gnupg.cpp - OpenPGP interface to GnuPG
 * Copyright (C) 2003  Justin Karneges
 *
 * 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 library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include"gnupg.h"

#include<qptrlist.h>
#include<qstringlist.h>
#include<qapplication.h>
#include<qtimer.h>
#include<qguardedptr.h>
#include<qfileinfo.h>
#include"gpgop.h"
#include"dirwatch.h"

#ifdef Q_WS_WIN
#include<windows.h>

static QString find_reg_gpgProgram()
{
	HKEY root;
	root = HKEY_CURRENT_USER;

	HKEY hkey;
	const char *path = "Software\\GNU\\GnuPG";
	if(RegOpenKeyExA(HKEY_CURRENT_USER, path, 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS) {
		if(RegOpenKeyExA(HKEY_LOCAL_MACHINE, path, 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS)
			return QString::null;
	}

	char szValue[256];
	DWORD dwLen = 256;
	if(RegQueryValueExA(hkey, "gpgProgram", NULL, NULL, (LPBYTE)szValue, &dwLen) != ERROR_SUCCESS) {
		RegCloseKey(hkey);
		return QString::null;
	}
	RegCloseKey(hkey);

	return QString::fromLatin1(szValue);
}
#endif

using namespace OpenPGP;

//----------------------------------------------------------------------------
// GnuPG
//----------------------------------------------------------------------------
class GnuPG::Item
{
public:
	Item() {}

	QGuardedPtr<Request> r;
	Request *r_ng;

	int op;
	QByteArray data;
	QString verSig, keyID;
	QStringList keys;
	QString str;
};

class GnuPG::Private
{
public:
	Private(const QString &bin) : gpg(bin) {}

	bool available, initialized;
	QString name;
	GpgOp gpg;

	// item list
	QPtrList<Item> list;

	KeyList secretKeys, publicKeys;
	QString secretRing, publicRing;
	FileWatch secretWatch, publicWatch;
	bool secretDirty, publicDirty;
};

GnuPG::GnuPG(QObject *parent)
:Engine(parent)
{
	QString bin = "gpg";
#ifdef Q_WS_WIN
	QString s = find_reg_gpgProgram();
	if(!s.isNull())
		bin = s;
#endif

#ifdef Q_OS_MAC
	// mac-gpg
	QFileInfo fi("/usr/local/bin/gpg");
	if(fi.exists())
		bin = fi.filePath();
#endif

#ifdef GPG_DEBUG
	printf("gpg bin=[%s]\n", bin.latin1());
#endif
	d = new Private(bin);
	d->available = false;
	d->initialized = false;
	d->name = "GNU Privacy Guard (GPG)";

	connect(&d->gpg, SIGNAL(finished(bool)), SLOT(gpg_finished(bool)));
	connect(&d->gpg, SIGNAL(needPassphrase()), SLOT(gpg_needPassphrase()));

	connect(&d->secretWatch, SIGNAL(changed()), SLOT(fileChanged()));
	connect(&d->publicWatch, SIGNAL(changed()), SLOT(fileChanged()));
}

GnuPG::~GnuPG()
{
	d->gpg.stop();

	// unhook all requests (send errors)
	QPtrListIterator<Item> it(d->list);
	Item *i;
	while((i = it.current())) {
		if(i->op == Sign)
			signFinished(i->r, false, "");
		else if(i->op == Verify)
			verifyFinished(i->r, VerifyError, "", QDateTime());
		unhook(i);
	}

	delete d;
}

void GnuPG::setTryAgent(bool b)
{
	d->gpg.setTryAgent(b);
}

bool GnuPG::checkAvailability()
{
	if(d->available)
		return true;

	d->gpg.doCheck();

	// loop here until we are done executing
	while(d->gpg.isActive())
		qApp->processEvents();

	return d->available;
}

QString GnuPG::id() const
{
	return "gpg";
}

QString GnuPG::name() const
{
	return d->name;
}

void GnuPG::init()
{
	if(d->initialized)
		return;

	d->available = true;

	// first step: get secret keys
	d->gpg.doSecretKeys();
}

KeyList GnuPG::secretKeys() const
{
	return d->secretKeys;
}

KeyList GnuPG::publicKeys() const
{
	return d->publicKeys;
}

void GnuPG::encrypt(Request *r, const QByteArray &in, const QStringList &keys)
{
	if(!d->initialized)
		return;

	Item *i = new Item;
	i->r = r;
	i->r_ng = r;
	i->op = Encrypt;
	i->data = in;
	i->keys = keys;
	hook(i);

	if(d->list.count() == 1)
		tryNext();
}

void GnuPG::decrypt(Request *r, const QString &in)
{
	if(!d->initialized)
		return;

	Item *i = new Item;
	i->r = r;
	i->r_ng = r;
	i->op = Decrypt;
	i->str = in;
	hook(i);

	if(d->list.count() == 1)
		tryNext();
}

void GnuPG::sign(Request *r, const QByteArray &in, const QString &keyID)
{
	if(!d->initialized)
		return;

	Item *i = new Item;
	i->r = r;
	i->r_ng = r;
	i->op = Sign;
	i->data = in;
	i->keyID = keyID;
	hook(i);

	if(d->list.count() == 1)
		tryNext();
}

void GnuPG::verify(Request *r, const QByteArray &in, const QString &sig)
{
	if(!d->initialized)
		return;

	Item *i = new Item;
	i->r = r;
	i->r_ng = r;
	i->op = Verify;
	i->data = in;
	i->verSig = sig;
	hook(i);

	if(d->list.count() == 1)
		tryNext();
}

void GnuPG::submitPassphrase(Request *, const QString &pp)
{
	d->gpg.submitPassphrase(pp);
}

void GnuPG::tryNext()
{
	QTimer::singleShot(0, this, SLOT(doNext()));
}

void GnuPG::doNext()
{
	// busy?  don't do anything then
	if(d->gpg.isActive())
		return;
	if(d->list.isEmpty()) {
		handleDirtyRings();
		return;
	}

	Item *i = d->list.getFirst();
	if(i->op == Encrypt)
		d->gpg.doEncrypt(i->data, i->keys);
	else if(i->op == Decrypt)
		d->gpg.doDecrypt(i->str);
	else if(i->op == Sign)
		d->gpg.doSign(i->data, i->keyID);
	else if(i->op == Verify)
		d->gpg.doVerify(i->data, i->verSig);
}

void GnuPG::doInitFinished(bool b, const QString &err)
{
#ifdef GPG_DEBUG
	printf("GnuPG initialization complete\n");
	printf(" Secret Keyring: [%s]\n", d->secretWatch.fileName().latin1());
	printf(" Public Keyring: [%s]\n", d->publicWatch.fileName().latin1());
#endif
	initFinished(b, err);
}

void GnuPG::gpg_finished(bool b)
{
	GpgOp *gpg = (GpgOp *)sender();
	int op = gpg->op();

	if(op == GpgOp::Check)
		d->available = b;
	else if(op == GpgOp::SecretKeyringFile) {
		if(!gpg->keyringFile().isEmpty())
			d->secretWatch.setFileName(gpg->keyringFile());

		if(!d->initialized) {
			// get public keys
			d->gpg.doPublicKeys();
		}
		else
			keysUpdated();

		handleDirtyRings();
	}
	else if(op == GpgOp::PublicKeyringFile) {
		if(!gpg->keyringFile().isEmpty())
			d->publicWatch.setFileName(gpg->keyringFile());

		if(!d->initialized) {
			// init finished
			d->initialized = true;
			doInitFinished(true, "");
		}
		else
			keysUpdated();

		handleDirtyRings();
	}
	else if(op == GpgOp::SecretKeys) {
		if(!b) {
			doInitFinished(false, tr("Unable to retrieve secret key list."));
			return;
		}

		d->secretKeys = gpg->keys();
		d->secretDirty = false;
		// We may need to do a second request to obtain the keyring
		// filename, but only do this once.
		if(!d->initialized && gpg->keyringFile().isEmpty()) {
			d->gpg.doSecretKeyringFile();
			return;
		}
		d->secretWatch.setFileName(gpg->keyringFile());

		if(!d->initialized) {
			// get public keys
			d->gpg.doPublicKeys();
		}
		else
			keysUpdated();

		handleDirtyRings();
	}
	else if(op == GpgOp::PublicKeys) {
		if(!b) {
			doInitFinished(false, tr("Unable to retrieve public key list."));
			return;
		}

		d->publicKeys = gpg->keys();
		d->publicDirty = false;
		// We may need to do a second request to obtain the keyring
		// filename, but only do this once.
		if(!d->initialized && gpg->keyringFile().isEmpty()) {
			d->gpg.doPublicKeyringFile();
			return;
		}
		d->publicWatch.setFileName(gpg->keyringFile());

		if(!d->initialized) {
			// init finished
			d->initialized = true;
			doInitFinished(true, "");
		}
		else
			keysUpdated();

		handleDirtyRings();
	}
	else {
		Request *r = unhookFirst();
		setBadPassphrase(r, gpg->badPassphrase());

		if(op == GpgOp::Encrypt)
			encryptFinished(r, b, gpg->encrypted());
		else if(op == GpgOp::Decrypt)
			decryptFinished(r, b, gpg->decrypted());
		else if(op == GpgOp::Sign)
			signFinished(r, b, gpg->signature());
		else if(op == GpgOp::Verify)
			verifyFinished(r, gpg->verifyResult(), gpg->keyID(), gpg->timestamp());

		tryNext();
	}
}

void GnuPG::gpg_needPassphrase()
{
	needPassphrase(d->list.getFirst()->r);
}

void GnuPG::req_destroyed()
{
	Request *r = (Request *)sender();
	Item *i = find(r);
	if(i) {
		bool first = (r == d->list.getFirst()->r_ng) ? true: false;
		unhook(i);
		if(first) {
			d->gpg.stop();
			tryNext();
		}
	}
}

GnuPG::Item *GnuPG::find(Request *r) const
{
	QPtrListIterator<Item> it(d->list);
	for(Item *i; (i = it.current()); ++it) {
		if((Request *)i->r_ng == r)
			return i;
	}
	return 0;
}

void GnuPG::hook(Item *i)
{
	connect(i->r, SIGNAL(destroyed()), this, SLOT(req_destroyed()));
	d->list.append(i);
}

Request *GnuPG::unhook(Item *i)
{
	Request *r = i->r;
	if(r)
		disconnect(r, SIGNAL(destroyed()), this, SLOT(req_destroyed()));
	d->list.removeRef(i);
	delete i;
	return r;
}

Request *GnuPG::unhookFirst()
{
	return unhook(d->list.getFirst());
}

void GnuPG::fileChanged()
{
	FileWatch *w = (FileWatch *)sender();
	if(w == &d->secretWatch) {
		d->secretDirty = true;
#ifdef GPG_DEBUG
		printf("Secret keys updated!\n");
#endif
	}
	else if(w == &d->publicWatch) {
		d->publicDirty = true;
#ifdef GPG_DEBUG
		printf("Public keys updated!\n");
#endif
	}
	handleDirtyRings();
}

void GnuPG::handleDirtyRings()
{
	if(!d->initialized || d->gpg.isActive())
		return;

	if(d->secretDirty)
		d->gpg.doSecretKeys();
	else if(d->publicDirty)
		d->gpg.doPublicKeys();
}
