/*
 * Copyright (C) 2003-2007  Justin Karneges <justin@affinix.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 */

#include "gpgproc_p.h"

#ifdef Q_OS_MAC
#define QT_PIPE_HACK
#endif


using namespace QCA;

namespace gpgQCAPlugin {

void releaseAndDeleteLater(QObject *owner, QObject *obj)
{
	obj->disconnect(owner);
	obj->setParent(nullptr);
	obj->deleteLater();
}


GPGProc::Private::Private(GPGProc *_q)
	: QObject(_q)
	, q(_q)
	, pipeAux(this)
	, pipeCommand(this)
	, pipeStatus(this)
	, startTrigger(this)
	, doneTrigger(this)
{
	qRegisterMetaType<gpgQCAPlugin::GPGProc::Error>("gpgQCAPlugin::GPGProc::Error");

	proc = nullptr;
	proc_relay = nullptr;
	startTrigger.setSingleShot(true);
	doneTrigger.setSingleShot(true);

	connect(&pipeAux.writeEnd(), &QCA::QPipeEnd::bytesWritten, this, &GPGProc::Private::aux_written);
	connect(&pipeAux.writeEnd(), &QCA::QPipeEnd::error, this, &GPGProc::Private::aux_error);
	connect(&pipeCommand.writeEnd(), &QCA::QPipeEnd::bytesWritten, this, &GPGProc::Private::command_written);
	connect(&pipeCommand.writeEnd(), &QCA::QPipeEnd::error, this, &GPGProc::Private::command_error);
	connect(&pipeStatus.readEnd(), &QCA::QPipeEnd::readyRead, this, &GPGProc::Private::status_read);
	connect(&pipeStatus.readEnd(), &QCA::QPipeEnd::error, this, &GPGProc::Private::status_error);
	connect(&startTrigger, &QCA::SafeTimer::timeout, this, &GPGProc::Private::doStart);
	connect(&doneTrigger, &QCA::SafeTimer::timeout, this, &GPGProc::Private::doTryDone);

	reset(ResetSessionAndData);
}

GPGProc::Private::~Private()
{
	reset(ResetSession);
}

void GPGProc::Private::closePipes()
{
#ifdef QT_PIPE_HACK
	pipeAux.readEnd().reset();
	pipeCommand.readEnd().reset();
	pipeStatus.writeEnd().reset();
#endif

	pipeAux.reset();
	pipeCommand.reset();
	pipeStatus.reset();
}

void GPGProc::Private::reset(ResetMode mode)
{
#ifndef QT_PIPE_HACK
	closePipes();
#endif

	if(proc)
	{
		proc->disconnect(this);

		if(proc->state() != QProcess::NotRunning)
		{
			// Before try to correct end proccess
			// Terminate if failed
			proc->close();
			bool finished = proc->waitForFinished(5000);
			if (!finished)
				proc->terminate();
		}

		proc->setParent(nullptr);
		releaseAndDeleteLater(this, proc_relay);
		proc_relay = nullptr;
		delete proc; // should be safe to do thanks to relay
		proc = nullptr;
	}

#ifdef QT_PIPE_HACK
	closePipes();
#endif

	startTrigger.stop();
	doneTrigger.stop();

	pre_stdin.clear();
	pre_aux.clear();
	pre_command.clear();
	pre_stdin_close = false;
	pre_aux_close = false;
	pre_command_close = false;

	need_status = false;
	fin_process = false;
	fin_status = false;

	if(mode >= ResetSessionAndData)
	{
		statusBuf.clear();
		statusLines.clear();
		leftover_stdout.clear();
		leftover_stderr.clear();
		error = GPGProc::FailedToStart;
		exitCode = -1;
	}
}

bool GPGProc::Private::setupPipes(bool makeAux)
{
	if(makeAux && !pipeAux.create())
	{
		closePipes();
		emit q->debug(QStringLiteral("Error creating pipeAux"));
		return false;
	}

#ifdef QPIPE_SECURE
	if(!pipeCommand.create(true)) // secure
#else
		if(!pipeCommand.create())
#endif
		{
			closePipes();
			emit q->debug(QStringLiteral("Error creating pipeCommand"));
			return false;
		}

	if(!pipeStatus.create())
	{
		closePipes();
		emit q->debug(QStringLiteral("Error creating pipeStatus"));
		return false;
	}

	return true;
}

void GPGProc::Private::setupArguments()
{
	QStringList fullargs;
	fullargs += QStringLiteral("--no-tty");
	fullargs += QStringLiteral("--pinentry-mode");
	fullargs += QStringLiteral("loopback");

	if(mode == ExtendedMode)
	{
		fullargs += QStringLiteral("--enable-special-filenames");

		fullargs += QStringLiteral("--status-fd");
		fullargs += QString::number(pipeStatus.writeEnd().idAsInt());

		fullargs += QStringLiteral("--command-fd");
		fullargs += QString::number(pipeCommand.readEnd().idAsInt());
	}

	for(int n = 0; n < args.count(); ++n)
	{
		QString a = args[n];
		if(mode == ExtendedMode && a == QLatin1String("-&?"))
			fullargs += QStringLiteral("-&") + QString::number(pipeAux.readEnd().idAsInt());
		else
			fullargs += a;
	}

	QString fullcmd = fullargs.join(QStringLiteral(" "));
	emit q->debug(QStringLiteral("Running: [") + bin + QLatin1Char(' ') + fullcmd + QLatin1Char(']'));

	args = fullargs;
}

void GPGProc::Private::doStart()
{
#ifdef Q_OS_WIN
	// Note: for unix, inheritability is set in SProcess
	if(pipeAux.readEnd().isValid())
		pipeAux.readEnd().setInheritable(true);
	if(pipeCommand.readEnd().isValid())
		pipeCommand.readEnd().setInheritable(true);
	if(pipeStatus.writeEnd().isValid())
		pipeStatus.writeEnd().setInheritable(true);
#endif

	setupArguments();

	proc->start(bin, args);
	proc->waitForStarted();

	pipeAux.readEnd().close();
	pipeCommand.readEnd().close();
	pipeStatus.writeEnd().close();
}

void GPGProc::Private::aux_written(int x)
{
	emit q->bytesWrittenAux(x);
}

void GPGProc::Private::aux_error(QCA::QPipeEnd::Error)
{
	emit q->debug(QStringLiteral("Aux: Pipe error"));
	reset(ResetSession);
	emit q->error(GPGProc::ErrorWrite);
}

void GPGProc::Private::command_written(int x)
{
	emit q->bytesWrittenCommand(x);
}

void GPGProc::Private::command_error(QCA::QPipeEnd::Error)
{
	emit q->debug(QStringLiteral("Command: Pipe error"));
	reset(ResetSession);
	emit q->error(GPGProc::ErrorWrite);
}

void GPGProc::Private::status_read()
{
	if(readAndProcessStatusData())
		emit q->readyReadStatusLines();
}

void GPGProc::Private::status_error(QCA::QPipeEnd::Error e)
{
	if(e == QPipeEnd::ErrorEOF)
		emit q->debug(QStringLiteral("Status: Closed (EOF)"));
	else
		emit q->debug(QStringLiteral("Status: Closed (gone)"));

	fin_status = true;
	doTryDone();
}

void GPGProc::Private::proc_started()
{
	emit q->debug(QStringLiteral("Process started"));

	// Note: we don't close these here anymore.  instead we
	//   do it just after calling proc->start().
	// close these, we don't need them
	/*pipeAux.readEnd().close();
	  pipeCommand.readEnd().close();
	  pipeStatus.writeEnd().close();*/

	// do the pre* stuff
	if(!pre_stdin.isEmpty())
	{
		proc->write(pre_stdin);
		pre_stdin.clear();
	}
	if(!pre_aux.isEmpty())
	{
		pipeAux.writeEnd().write(pre_aux);
		pre_aux.clear();
	}
	if(!pre_command.isEmpty())
	{
#ifdef QPIPE_SECURE
		pipeCommand.writeEnd().writeSecure(pre_command);
#else
		pipeCommand.writeEnd().write(pre_command);
#endif
		pre_command.clear();
	}

	if(pre_stdin_close)
	{
		proc->waitForBytesWritten();
		proc->closeWriteChannel();
	}

	if(pre_aux_close)
		pipeAux.writeEnd().close();
	if(pre_command_close)
		pipeCommand.writeEnd().close();
}

void GPGProc::Private::proc_readyReadStandardOutput()
{
	emit q->readyReadStdout();
}

void GPGProc::Private::proc_readyReadStandardError()
{
	emit q->readyReadStderr();
}

void GPGProc::Private::proc_bytesWritten(qint64 lx)
{
	int x = (int)lx;
	emit q->bytesWrittenStdin(x);
}

void GPGProc::Private::proc_finished(int x)
{
	emit q->debug(QStringLiteral("Process finished: %1").arg(x));
	exitCode = x;

	fin_process = true;
	fin_process_success = true;

	if(need_status && !fin_status)
	{
		pipeStatus.readEnd().finalize();
		fin_status = true;
		if(readAndProcessStatusData())
		{
			doneTrigger.start();
			emit q->readyReadStatusLines();
			return;
		}
	}

	doTryDone();
}

void GPGProc::Private::proc_error(QProcess::ProcessError x)
{
	QMap<int, QString> errmap;
	errmap[QProcess::FailedToStart] = QStringLiteral("FailedToStart");
	errmap[QProcess::Crashed]       = QStringLiteral("Crashed");
	errmap[QProcess::Timedout]      = QStringLiteral("Timedout");
	errmap[QProcess::WriteError]    = QStringLiteral("WriteError");
	errmap[QProcess::ReadError]     = QStringLiteral("ReadError");
	errmap[QProcess::UnknownError]  = QStringLiteral("UnknownError");

	emit q->debug(QStringLiteral("Process error: %1").arg(errmap[x]));

	if(x == QProcess::FailedToStart)
		error = GPGProc::FailedToStart;
	else if(x == QProcess::WriteError)
		error = GPGProc::ErrorWrite;
	else
		error = GPGProc::UnexpectedExit;

	fin_process = true;
	fin_process_success = false;

#ifdef QT_PIPE_HACK
	// If the process fails to start, then the ends of the pipes
	// intended for the child process are still open.  Some Mac
	// users experience a lockup if we close our ends of the pipes
	// when the child's ends are still open.  If we ensure the
	// child's ends are closed, we prevent this lockup.  I have no
	// idea why the problem even happens or why this fix should
	// work.
	pipeAux.readEnd().reset();
	pipeCommand.readEnd().reset();
	pipeStatus.writeEnd().reset();
#endif

	if(need_status && !fin_status)
	{
		pipeStatus.readEnd().finalize();
		fin_status = true;
		if(readAndProcessStatusData())
		{
			doneTrigger.start();
			emit q->readyReadStatusLines();
			return;
		}
	}

	doTryDone();
}

void GPGProc::Private::doTryDone()
{
	if(!fin_process)
		return;

	if(need_status && !fin_status)
		return;

	emit q->debug(QStringLiteral("Done"));

	// get leftover data
	proc->setReadChannel(QProcess::StandardOutput);
	leftover_stdout = proc->readAll();

	proc->setReadChannel(QProcess::StandardError);
	leftover_stderr = proc->readAll();

	reset(ResetSession);
	if(fin_process_success)
		emit q->finished(exitCode);
	else
		emit q->error(error);
}

bool GPGProc::Private::readAndProcessStatusData()
{
	QByteArray buf = pipeStatus.readEnd().read();
	if(buf.isEmpty())
		return false;

	return processStatusData(buf);
}

// return true if there are newly parsed lines available
bool GPGProc::Private::processStatusData(const QByteArray &buf)
{
	statusBuf.append(buf);

	// extract all lines
	QStringList list;
	while(true)
	{
		int n = statusBuf.indexOf('\n');
		if(n == -1)
			break;

		// extract the string from statusbuf
		++n;
		char *p = (char *)statusBuf.data();
		QByteArray cs(p, n);
		int newsize = statusBuf.size() - n;
		memmove(p, p + n, newsize);
		statusBuf.resize(newsize);

		// convert to string without newline
		QString str = QString::fromUtf8(cs);
		str.truncate(str.length() - 1);

		// ensure it has a proper header
		if(str.left(9) != QLatin1String("[GNUPG:] "))
			continue;

		// take it off
		str = str.mid(9);

		// add to the list
		list += str;
	}

	if(list.isEmpty())
		return false;

	statusLines += list;
	return true;
}

GPGProc::GPGProc(QObject *parent)
:QObject(parent)
{
	d = new Private(this);
}

GPGProc::~GPGProc()
{
	delete d;
}

void GPGProc::reset()
{
	d->reset(ResetAll);
}

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

void GPGProc::start(const QString &bin, const QStringList &args, Mode mode)
{
	if(isActive())
		d->reset(ResetSessionAndData);

	if(mode == ExtendedMode)
	{
		if(!d->setupPipes(args.contains(QStringLiteral("-&?"))))
		{
			d->error = FailedToStart;

			// emit later
			QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, Q_ARG(gpgQCAPlugin::GPGProc::Error, d->error));
			return;
		}

		d->need_status = true;

		emit debug(QStringLiteral("Pipe setup complete"));
	}

	d->proc = new SProcess(d);

#ifdef Q_OS_UNIX
	QList<int> plist;
	if(d->pipeAux.readEnd().isValid())
		plist += d->pipeAux.readEnd().id();
	if(d->pipeCommand.readEnd().isValid())
		plist += d->pipeCommand.readEnd().id();
	if(d->pipeStatus.writeEnd().isValid())
		plist += d->pipeStatus.writeEnd().id();
	d->proc->setInheritPipeList(plist);
#endif

	// enable the pipes we want
	if(d->pipeAux.writeEnd().isValid())
		d->pipeAux.writeEnd().enable();
	if(d->pipeCommand.writeEnd().isValid())
		d->pipeCommand.writeEnd().enable();
	if(d->pipeStatus.readEnd().isValid())
		d->pipeStatus.readEnd().enable();

	d->proc_relay = new QProcessSignalRelay(d->proc, d);
	connect(d->proc_relay, &QProcessSignalRelay::started, d, &GPGProc::Private::proc_started);
	connect(d->proc_relay, &QProcessSignalRelay::readyReadStandardOutput, d, &GPGProc::Private::proc_readyReadStandardOutput);
	connect(d->proc_relay, &QProcessSignalRelay::readyReadStandardError, d, &GPGProc::Private::proc_readyReadStandardError);
	connect(d->proc_relay, &QProcessSignalRelay::bytesWritten, d, &GPGProc::Private::proc_bytesWritten);
	connect(d->proc_relay, &QProcessSignalRelay::finished, d, &GPGProc::Private::proc_finished);
	connect(d->proc_relay, &QProcessSignalRelay::error, d, &GPGProc::Private::proc_error);

	d->bin = bin;
	d->args = args;
	d->mode = mode;
	d->startTrigger.start();
}

QByteArray GPGProc::readStdout()
{
	if(d->proc)
	{
		d->proc->setReadChannel(QProcess::StandardOutput);
		return d->proc->readAll();
	}
	else
	{
		QByteArray a = d->leftover_stdout;
		d->leftover_stdout.clear();
		return a;
	}
}

QByteArray GPGProc::readStderr()
{
	if(d->proc)
	{
		d->proc->setReadChannel(QProcess::StandardError);
		return d->proc->readAll();
	}
	else
	{
		QByteArray a = d->leftover_stderr;
		d->leftover_stderr.clear();
		return a;
	}
}

QStringList GPGProc::readStatusLines()
{
	QStringList out = d->statusLines;
	d->statusLines.clear();
	return out;
}

void GPGProc::writeStdin(const QByteArray &a)
{
	if(!d->proc || a.isEmpty())
		return;

	if(d->proc->state() == QProcess::Running)
		d->proc->write(a);
	else
		d->pre_stdin += a;
}

void GPGProc::writeAux(const QByteArray &a)
{
	if(!d->proc || a.isEmpty())
		return;

	if(d->proc->state() == QProcess::Running)
		d->pipeAux.writeEnd().write(a);
	else
		d->pre_aux += a;
}

#ifdef QPIPE_SECURE
void GPGProc::writeCommand(const SecureArray &a)
#else
void GPGProc::writeCommand(const QByteArray &a)
#endif
{
	if(!d->proc || a.isEmpty())
		return;

	if(d->proc->state() == QProcess::Running)
#ifdef QPIPE_SECURE
		d->pipeCommand.writeEnd().writeSecure(a);
#else
		d->pipeCommand.writeEnd().write(a);
#endif
	else
		d->pre_command += a;
}

void GPGProc::closeStdin()
{
	if(!d->proc)
		return;

	if(d->proc->state() == QProcess::Running)
	{
		d->proc->waitForBytesWritten();
		d->proc->closeWriteChannel();
	}
	else
	{
		d->pre_stdin_close = true;
	}
}

void GPGProc::closeAux()
{
	if(!d->proc)
		return;

	if(d->proc->state() == QProcess::Running)
		d->pipeAux.writeEnd().close();
	else
		d->pre_aux_close = true;
}

void GPGProc::closeCommand()
{
	if(!d->proc)
		return;

	if(d->proc->state() == QProcess::Running)
		d->pipeCommand.writeEnd().close();
	else
		d->pre_command_close = true;
}

}