#include"gpgop.h"

#include<qstringlist.h>
#include<qtimer.h>
#include<qdatetime.h>
#include<stdlib.h>
#include"gpgproc/gpgproc.h"

static QByteArray stringToArray(const QString &str)
{
	QCString cs = str.utf8();
	QByteArray a(cs.length());
	memcpy(a.data(), cs.data(), a.size());
	return a;
}

class GpgOp::Private
{
public:
	Private() {}

	QString bin;
	int op;
	GPGProc *proc;
	bool tryAgent;

	QByteArray outbuf, errbuf;
	bool didPassphrase;
	QString sigKeyID;
	QDateTime sigTS;
	int verType;

	OpenPGP::KeyList keys;
	QString keyring;
	bool badpp;

	QString enc;
	QByteArray dec;
};

GpgOp::GpgOp(const QString &bin, QObject *parent)
:QObject(parent)
{
	d = new Private;
	d->bin = bin;
	d->proc = 0;
	d->op = -1;
	d->tryAgent = true;
}

GpgOp::~GpgOp()
{
	reset();
	delete d;
}

void GpgOp::reset()
{
	if(d->proc) {
		d->proc->disconnect(this);
		d->proc->deleteLater();
		d->proc = 0;
	}

	d->sigKeyID = "";
	d->sigTS = QDateTime();
	d->didPassphrase = false;
	d->outbuf.resize(0);
	d->errbuf.resize(0);
	d->keys.clear();
	d->keyring = "";
	d->verType = OpenPGP::VerifyError;
	d->badpp = false;
	d->enc = "";
	d->dec.resize(0);
}

void GpgOp::stop()
{
	reset();
}

bool GpgOp::isActive() const
{
	return (d->proc ? true: false);
}

int GpgOp::op() const
{
	return d->op;
}

const OpenPGP::KeyList & GpgOp::keys() const
{
	return d->keys;
}

const QString & GpgOp::keyringFile() const
{
	return d->keyring;
}

const QString & GpgOp::keyID() const
{
	return d->sigKeyID;
}

const QDateTime & GpgOp::timestamp() const
{
	return d->sigTS;
}

int GpgOp::verifyResult() const
{
	return d->verType;
}

bool GpgOp::badPassphrase() const
{
	return d->badpp;
}

const QString & GpgOp::encrypted() const
{
	return d->enc;
}

const QByteArray & GpgOp::decrypted() const
{
	return d->dec;
}

const QString & GpgOp::signature() const
{
	return d->enc;
}

void GpgOp::setTryAgent(bool b)
{
	d->tryAgent = b;
}

void GpgOp::doCheck()
{
	reset();

	d->op = Check;
	QStringList args;
	args += "--version";
	if(!launchGPG(args, false)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}
}

void GpgOp::doSecretKeyringFile()
{
	reset();

	d->op = SecretKeyringFile;
	QStringList args;
	args += "--list-secret-keys";
	if(!launchGPG(args, false)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}
}

void GpgOp::doPublicKeyringFile()
{
	reset();

	d->op = PublicKeyringFile;
	QStringList args;
	args += "--list-public-keys";
	if(!launchGPG(args, false)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}
}

void GpgOp::doSecretKeys()
{
	reset();

	d->op = SecretKeys;
	QStringList args;
	args += "--fixed-list-mode";
	args += "--with-colons";
	args += "--list-secret-keys";
	if(!launchGPG(args, false)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}
}

void GpgOp::doPublicKeys()
{
	reset();

	d->op = PublicKeys;
	QStringList args;
	args += "--fixed-list-mode";
	args += "--with-colons";
	args += "--list-public-keys";
	if(!launchGPG(args, false)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}
}

void GpgOp::doEncrypt(const QByteArray &in, const QStringList &keys)
{
	reset();

	d->op = Encrypt;
	QStringList args;
	args += "--armor";
	args += "--always-trust";
	args += "--encrypt";

	// recipients
	for(QStringList::ConstIterator it = keys.begin(); it != keys.end(); ++it) {
		args += "--recipient";
		args += QString("0x") + *it;
	}

	if(!launchGPG(args)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}

	if(!in.isEmpty())
		d->proc->writeToStdin(in);
	else
		d->proc->closeStdin();
}

void GpgOp::doDecrypt(const QString &in)
{
	reset();

	d->op = Decrypt;
	QStringList args;
	if(!d->tryAgent)
		args += "--no-use-agent";
	args += "--armor";
	args += "--decrypt";
	if(!launchGPG(args)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}

	if(!in.isEmpty())
		d->proc->writeToStdin(stringToArray(fixOutgoingLines(in)));
	else
		d->proc->closeStdin();
}

void GpgOp::doSign(const QByteArray &in, const QString &keyID)
{
	reset();

	d->op = Sign;
	QStringList args;
	if(!d->tryAgent)
		args += "--no-use-agent";
	args += "--armor";
	args += "--default-key";
	args += QString("0x") + keyID;
	args += "--detach-sign";
	if(!launchGPG(args)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}

	if(!in.isEmpty())
		d->proc->writeToStdin(in);
	else
		d->proc->closeStdin();
}

void GpgOp::doVerify(const QByteArray &in, const QString &sig)
{
	reset();

	d->op = Verify;
	d->verType = OpenPGP::VerifyError;
	QStringList args;
	args += "--armor";
	args += "--verify";
	args += "-";
	args += "-&?";

	if(!launchGPG(args)) {
		QTimer::singleShot(0, this, SLOT(doFail()));
		return;
	}

	// sig goes into stdin
	if(!sig.isEmpty())
		d->proc->writeToStdin(stringToArray(fixOutgoingLines(sig)));
	else
		d->proc->closeStdin();

	// data goes into aux
	d->proc->writeToAux(in);
	d->proc->closeAux();
}

void GpgOp::submitPassphrase(const QString &pp)
{
	QCString cs = pp.local8Bit() + '\n';
	QByteArray a(cs.length());
	memcpy(a.data(), cs.data(), a.size());
	d->proc->writeToCommand(a);
}

void GpgOp::doFail()
{
	finished(false);
}

bool GpgOp::launchGPG(const QStringList &args, bool useExtra)
{
	d->proc = new GPGProc;
	connect(d->proc, SIGNAL(readyReadStdout()), SLOT(proc_readyReadStdout()));
	connect(d->proc, SIGNAL(readyReadStderr()), SLOT(proc_readyReadStderr()));
	connect(d->proc, SIGNAL(processExited()), SLOT(proc_processExited()));
	connect(d->proc, SIGNAL(wroteToStdin()), SLOT(proc_wroteToStdin()));
	connect(d->proc, SIGNAL(statusLine(const QString &)), SLOT(proc_statusLine(const QString &)));

	if(!d->proc->start(d->bin, args, useExtra)) {
		d->proc->disconnect(this);
		d->proc->deleteLater();
		d->proc = 0;
		return false;
	}

	return true;
}

void GpgOp::proc_readyReadStdout()
{
	QByteArray block = d->proc->readStdout();
	int oldsize = d->outbuf.size();
	d->outbuf.resize(oldsize + block.size());
	memcpy(d->outbuf.data() + oldsize, block.data(), block.size());
}

void GpgOp::proc_readyReadStderr()
{
	QByteArray block = d->proc->readStderr();
	int oldsize = d->errbuf.size();
	d->errbuf.resize(oldsize + block.size());
	memcpy(d->errbuf.data() + oldsize, block.data(), block.size());
}

void GpgOp::proc_wroteToStdin()
{
	d->proc->closeStdin();
}

void GpgOp::proc_statusLine(const QString &str)
{
#ifdef GPG_DEBUG
	printf("{%s}\n", str.latin1());
#endif
	QString s, rest;
	int n = str.find(' ');
	if(n == -1) {
		s = str;
		rest = "";
	}
	else {
		s = str.mid(0, n);
		rest = str.mid(n+1);
	}

	if(s == "NEED_PASSPHRASE") {
		if(!(d->tryAgent && getenv("GPG_AGENT_INFO"))) {
			if(!d->didPassphrase) {
				d->didPassphrase = true;
				needPassphrase();
			}
			else {
				QByteArray a(1);
				a[0] = '\n';
				d->proc->writeToCommand(a);
			}
		}
	}
	else if(s == "BAD_PASSPHRASE") {
		d->badpp = true;
	}
	else if(s == "GOOD_PASSPHRASE") {
		d->badpp = false;
	}
	else if(s == "GOODSIG") {
		QString keyID;
		int n = rest.find(' ');
		if(n == -1)
			keyID = rest;
		else
			keyID = rest.mid(0, n);
		d->sigKeyID = keyID;
		d->verType = OpenPGP::VerifyGood;
	}
	else if(s == "BADSIG") {
		QString keyID;
		int n = rest.find(' ');
		if(n == -1)
			keyID = rest;
		else
			keyID = rest.mid(0, n);
		d->sigKeyID = keyID;
		d->verType = OpenPGP::VerifyBad;
	}
	else if(s == "ERRSIG") {
		QStringList list = QStringList::split(' ', rest, false);
		d->sigKeyID = list[0];
		d->sigTS.setTime_t(list[4].toInt());
		d->verType = OpenPGP::VerifyNoKey;
	}
	else if(s == "VALIDSIG") {
		QStringList list = QStringList::split(' ', rest, false);
		d->sigTS.setTime_t(list[2].toInt());
	}
}

void GpgOp::proc_processExited()
{
	if(!d->proc)
		return;

#ifdef GPG_DEBUG
	printf("GPG Finished: ");
#endif

	bool clean = true;
	int exitStatus = -1;
	if(!d->proc->normalExit()) {
		clean = false;
#ifdef GPG_DEBUG
		printf("bad exit.. crash?\n");
#endif
	}
	else {
		exitStatus = d->proc->exitStatus();
#ifdef GPG_DEBUG
		printf("exitStatus=%d\n", exitStatus);
#endif
	}

	d->proc->disconnect(this);
	d->proc->deleteLater();
	d->proc = 0;

	processResult(clean, exitStatus, d->outbuf, d->errbuf);
}

void GpgOp::processResult(bool clean, int code, const QByteArray &out, const QByteArray &err)
{
	// put stdout and stderr into QStrings
	QCString cs;
	cs.resize(out.size()+1);
	memcpy(cs.data(), out.data(), out.size());
	QString outstr = QString::fromLatin1(cs);
	cs.resize(err.size()+1);
	memcpy(cs.data(), err.data(), err.size());
	QString errstr = QString::fromLatin1(cs);

#ifdef GPG_DEBUG
	printf("stdout: [%s]\n", outstr.latin1());
	printf("stderr: [%s]\n", errstr.latin1());
#endif

	if(d->op == Check) {
		if(!clean || code != 0) {
			doFail();
			return;
		}
		finished(true);
	}
	else if(d->op == SecretKeyringFile || d->op == PublicKeyringFile) {
		if(!clean || code != 0) {
			doFail();
			return;
		}
		if(!findKeyringFilename(fixIncomingLines(outstr), &d->keyring)) {
			doFail();
			return;
		}
		finished(true);
	}
	else if(d->op == SecretKeys || d->op == PublicKeys) {
		if(!clean || code != 0) {
			doFail();
			return;
		}
		if(!stringToKeyList(fixIncomingLines(outstr), &d->keys, &d->keyring)) {
			doFail();
			return;
		}
		finished(true);
	}
	else if(d->op == Encrypt) {
		if(!clean || code != 0) {
			doFail();
			return;
		}
		d->enc = fixIncomingLines(outstr);
		finished(true);
	}
	else if(d->op == Decrypt) {
		if(!clean || code != 0) {
			doFail();
			return;
		}
		d->dec = out;
		finished(true);
	}
	else if(d->op == Sign) {
		if(!clean || code != 0) {
			doFail();
			return;
		}
		d->enc = fixIncomingLines(outstr);
		finished(true);
	}
	else if(d->op == Verify) {
		if(!clean || code != 0) {
			doFail();
			return;
		}
		finished(true);
	}
}

QString GpgOp::fixIncomingLines(const QString &str)
{
#ifdef Q_WS_WIN
	QString out;
	for(int n = 0; n < (int)str.length(); ++n) {
		if(str.at(n) == '\r' && (n + 1) < (int)str.length() && str.at(n+1) == '\n') {
			out += '\n';
			++n;
		}
		else
			out += str.at(n);
	}
	return out;
#else
	return str;
#endif
}

QString GpgOp::fixOutgoingLines(const QString &str)
{
#ifdef Q_WS_WIN
	QString out;
	for(int n = 0; n < (int)str.length(); ++n) {
		if(str.at(n) == '\n' && (n == 0 || str.at(n-1) != '\r'))
			out += "\r\n";
		else
			out += str.at(n);
	}
	return out;
#else
	return str;
#endif
}

bool GpgOp::stringToKeyList(const QString &outstr, OpenPGP::KeyList *_keylist, QString *_keyring)
{
	OpenPGP::KeyList keyList;
	QStringList lines = QStringList::split('\n', outstr, true);

	if(lines.count() < 1)
		return false;

	QStringList::ConstIterator it = lines.begin();

	// first line is keyring file
	QString keyring = *(it++);

	// if the second line isn't a divider, we are dealing
	// with a new version of gnupg that doesn't give us
	// the keyring file on gpg --list-keys --with-colons
	if(it == lines.end() || (*it).at(0) != '-') {
		// first line wasn't the keyring name...
		keyring = "";
		// ...so read the first line again
		it--;
	}
	else {
		// this was the divider line - skip it
		it++;
	}

	OpenPGP::Key *k = 0;
	for(; it != lines.end(); ++it) {
		QStringList f = QStringList::split(':', *it, true);
		QString type = f[0];

		if(type == "pub") {
			if(k) {
				keyList += (*k);
				delete k;
				k = 0;
			}
			k = new OpenPGP::Key;

			k->setKeyID(f[4]);
		}
		else if(type == "sec") {
			if(k) {
				keyList += (*k);
				delete k;
				k = 0;
			}
			k = new OpenPGP::Key;

			k->setKeyID(f[4]);
		}
		else if(type == "uid") {
			if(!k)
				continue;
			// ignore a uid if we already have one
			if(!k->userID().isEmpty())
				continue;
			QString s = f[9];
			QCString uid;
			// convert the "backslash" C-string syntax
			for(int n = 0; n < (int)s.length(); ++n) {
				if(s.at(n) == '\\' && n + 1 < (int)s.length()) {
					++n;
					unsigned char c = (unsigned char)s.at(n).latin1();
					if(c == '\\')
						uid += '\\';
					else if(c == 'x' && n + 2 < (int)s.length()) {
						++n;
						QString hex = s.mid(n, 2);
						bool ok;
						uint val = hex.toInt(&ok, 16);
						uid += (unsigned char)val;
						++n; // only skip one, the for-loop will skip the next
					}
				}
				else {
					uid += (unsigned char)s.at(n).latin1();
				}
			}
			k->setUserID(QString::fromUtf8(uid));
		}
	}
	if(k) {
		keyList += (*k);
		delete k;
		k = 0;
	}

	if(_keylist)
		*_keylist = keyList;
	if(_keyring)
		*_keyring = keyring;

	return true;
}

bool GpgOp::findKeyringFilename(const QString &outstr, QString *_keyring)
{
	QStringList lines = QStringList::split('\n', outstr, true);
	if(lines.count() < 1)
		return false;

	*_keyring = lines[0];
	return true;
}

