/***************************************************************************
                          rkfrontendtransmitter  -  description
                             -------------------
    begin                : Thu Nov 04 2010
    copyright            : (C) 2010-2019 by Thomas Friedrichsmeier
    email                : thomas.friedrichsmeier@kdemail.net
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "rkfrontendtransmitter.h"

#include "rkrbackendprotocol_frontend.h"
#include "rkwarddevice/rkgraphicsdevice_frontendtransmitter.h"
#include "../misc/rkcommonfunctions.h"
#include "../settings/rksettingsmodulegeneral.h"
#include "../rkglobals.h"

#include <KLocalizedString>
#include <krandom.h>

#include <QCoreApplication>
#include <QProcess>
#include <QLocalServer>
#include <QLocalSocket>
#include <QDir>
#include <QStandardPaths>
#include <QTime>

#include "../version.h"
#include "../debug.h"

QString findBackendAtPath (const QString &path) {
	QDir dir (path);
	dir.makeAbsolute ();
#ifdef Q_OS_WIN
	QString ret = dir.filePath ("rkward.rbackend.exe");
#else
	QString ret = dir.filePath ("rkward.rbackend");
#endif
	RK_DEBUG (RBACKEND, DL_DEBUG, "Looking for backend at %s", qPrintable (ret));
	QFileInfo fi (ret);
	if (fi.exists () && fi.isExecutable ()) return ret;
	return QString ();
}

RKFrontendTransmitter::RKFrontendTransmitter () : RKAbstractTransmitter () {
	RK_TRACE (RBACKEND);

	rkd_transmitter = new RKGraphicsDeviceFrontendTransmitter ();
	start ();
}

RKFrontendTransmitter::~RKFrontendTransmitter () {
	RK_TRACE (RBACKEND);

	delete rkd_transmitter;
	RK_ASSERT (!server->isListening ());
}

QString localeDir () {
	// adapted from KCatalog::catalogLocaleDir()
	QString relpath = QStringLiteral ("%1/LC_MESSAGES/rkward.mo").arg (QLocale ().name ().section ('_', 0, 0));
	QString file = QStandardPaths::locate (QStandardPaths::GenericDataLocation, QStringLiteral ("locale/") + relpath);
	if (file.isEmpty ()) return QString ();
	return QFileInfo (file.left (file.size() - relpath.size ())).absolutePath ();
}

void RKFrontendTransmitter::run () {
	RK_TRACE (RBACKEND);

	// start server
	qsrand (QTime::currentTime ().msec ()); // Workaround for some versions of kcoreaddons (5.21.0 through at least 5.34.0). See https://phabricator.kde.org/D5966
	server = new QLocalServer (this);
	// we add a bit of randomness to the servername, as in general the servername must be unique
	// there could be conflicts with concurrent or with previous crashed rkward sessions.
	if (!server->listen ("rkward" + KRandom::randomString (8))) handleTransmissionError ("Failure to start frontend server: " + server->errorString ());
	connect (server, &QLocalServer::newConnection, this, &RKFrontendTransmitter::connectAndEnterLoop, Qt::QueuedConnection);
	// start backend
	backend = new QProcess (this);

	// Try to synchronize language selection in frontend and backend
	QStringList env = QProcess::systemEnvironment ();
	int index = env.indexOf (QRegExp("^LANGUAGE=.*", Qt::CaseInsensitive));
	if (index >= 0) env.removeAt (index);
	env.append ("LANGUAGE=" + QLocale ().name ().section ('_', 0, 0));
	backend->setEnvironment (env);

	QStringList args;
	args.append ("--debug-level=" + QString::number (RK_Debug::RK_Debug_Level));
	// NOTE: QProcess quotes its arguments, *but* properly passing all spaces and quotes through the R CMD wrapper, seems near(?) impossible on Windows. Instead, we use percent encoding, internally.
	args.append ("--server-name=" + server->fullServerName ().toUtf8 ().toPercentEncoding ());
	args.append ("--rkd-server-name=" + rkd_transmitter->serverName ().toUtf8 ().toPercentEncoding ());
	args.append ("--data-dir=" + RKSettingsModuleGeneral::filesPath ().toUtf8 ().toPercentEncoding ());
	args.append ("--locale-dir=" + localeDir ().toUtf8 ().toPercentEncoding ());
	connect (backend, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &RKFrontendTransmitter::backendExit);
	QString backend_executable = findBackendAtPath (QCoreApplication::applicationDirPath ());
#ifdef Q_OS_MACOS
	if (backend_executable.isEmpty ()) backend_executable = findBackendAtPath (QCoreApplication::applicationDirPath () + "../Resources"); // an appropriate location in a standalone app-bundle
#endif
	if (backend_executable.isEmpty ()) backend_executable = findBackendAtPath (QCoreApplication::applicationDirPath () + "/rbackend");	// for running directly from the build-dir
	if (backend_executable.isEmpty ()) backend_executable = findBackendAtPath (QCoreApplication::applicationDirPath () + "../lib/libexec");
#ifdef Q_OS_MACOS
	if (backend_executable.isEmpty ()) backend_executable = findBackendAtPath (QCoreApplication::applicationDirPath () + "/../../../rbackend");
	if (backend_executable.isEmpty ()) backend_executable = findBackendAtPath (QCoreApplication::applicationDirPath () + "/../Frameworks/libexec");  // For running from .dmg created by craft --package rkward
#endif
	if (backend_executable.isEmpty ()) backend_executable = findBackendAtPath (RKWARD_BACKEND_PATH);
	if (backend_executable.isEmpty ()) handleTransmissionError (i18n ("The backend executable could not be found. This is likely to be a problem with your installation."));
	QString debugger = RKGlobals::startup_options["backend-debugger"].toString ();
	args.prepend (RKCommonFunctions::windowsShellScriptSafeCommand (backend_executable));
	if (!debugger.isEmpty ()) {
		args = debugger.split (' ') + args;
	}
#ifdef Q_OS_MACOS
	// Resolving libR.dylib and friends is a pain on MacOS, and running through R CMD does not always seem to be enough.
	// (Apparently DYLIB_FALLBACK_LIBRARY_PATH is ignored on newer versions of MacOS). Safest best seems to be to start in the lib directory, itself.
	QProcess dummy;
	dummy.start (qgetenv ("R_BINARY"), QStringList() << "--slave" << "--no-save" << "--no-init-file" << "-e" << "cat(R.home('lib'))");
	dummy.waitForFinished ();
	QString r_home = QString::fromLocal8Bit (dummy.readAllStandardOutput ());
	RK_DEBUG(RBACKEND, DL_INFO, "Setting working directory to %s", qPrintable (r_home));
	backend->setWorkingDirectory (r_home);
#endif
	args.prepend ("CMD");
	if (DL_DEBUG >= RK_Debug::RK_Debug_Level) {
		qDebug ("%s", qPrintable (qgetenv ("R_BINARY")));
		qDebug ("%s", qPrintable (args.join ("\n")));
	}
	backend->start (qgetenv ("R_BINARY"), args, QIODevice::ReadOnly);

	if (!backend->waitForStarted ()) {
		handleTransmissionError (i18n ("The backend executable could not be started. Error message was: %1", backend->errorString ()));
	}

	waitForCanReadLine (backend, 3000);
	token = QString::fromLocal8Bit (backend->readLine ()).trimmed ();
	backend->closeReadChannel (QProcess::StandardError);
	backend->closeReadChannel (QProcess::StandardOutput);

	exec ();

	// It's ok to only give backend a short time to finish. We only get here, after QuitCommand has been handled by the backend
	backend->waitForFinished(1000);

	if (!connection) {
		RK_ASSERT (false);
		return;
	}
}

void RKFrontendTransmitter::waitForCanReadLine (QIODevice* con, int msecs) {
	RK_TRACE (RBACKEND);

	// NOTE: On Qt5+Windows, readyReady may actually come in char by char, so calling waitForReadyRead() does not guarantee we will
	//       see the full line, at all. But also, of course, we want to put some cap on trying. Using a time threshold for this.
	QTime time;
	time.start ();
	while ((!con->canReadLine ()) && (time.elapsed () < msecs)) con->waitForReadyRead (500);
}

void RKFrontendTransmitter::connectAndEnterLoop () {
	RK_TRACE (RBACKEND);
	RK_ASSERT (server->hasPendingConnections ());

	QLocalSocket *con = server->nextPendingConnection ();
	server->close ();

	// handshake
	waitForCanReadLine (con, 1000);
	QString token_c = QString::fromLocal8Bit (con->readLine ()). trimmed ();
	if (token_c != token) handleTransmissionError (i18n ("Error during handshake with backend process. Expected token '%1', received token '%2'", token, token_c));
	waitForCanReadLine (con, 1000);
	QString version_c = QString::fromLocal8Bit (con->readLine ().trimmed ());
	if (version_c != RKWARD_VERSION) handleTransmissionError (i18n ("Version mismatch during handshake with backend process. Frontend is version '%1' while backend is '%2'.\nPlease fix your installation.", QString (RKWARD_VERSION), version_c));

	setConnection (con);
}

void RKFrontendTransmitter::requestReceived (RBackendRequest* request) {
	RK_TRACE (RBACKEND);

	if (request->type == RBackendRequest::Output) {
		ROutputList* list = request->output;
		for (int i = 0; i < list->size (); ++i) {
			ROutput *out = (*list)[i];

			if (handleOutput (out->output, out->output.length (), out->type)) {
				RKRBackendEvent* event = new RKRBackendEvent (new RBackendRequest (false, RBackendRequest::OutputStartedNotification));
				qApp->postEvent (RKRBackendProtocolFrontend::instance (), event);
			}

			delete (out);
		}
		delete list;
		request->output = 0;
		RK_ASSERT (request->synchronous);
		writeRequest (request);	// to tell the backend, that we are keeping up. Also deletes the request.
		return;
	}

	RKRBackendEvent* event = new RKRBackendEvent (request);
	qApp->postEvent (RKRBackendProtocolFrontend::instance (), event);
}

void RKFrontendTransmitter::backendExit (int exitcode) {
	RK_TRACE (RBACKEND);

	if (!exitcode && token.isEmpty ()) handleTransmissionError (i18n ("The backend process could not be started. Please check your installation."));
	else if (token.isEmpty ()) handleTransmissionError (i18n ("The backend process failed to start with exit code %1.", exitcode));
	else handleTransmissionError (i18n ("Backend process has exited with code %1.", exitcode));
}

void RKFrontendTransmitter::writeRequest (RBackendRequest *request) {
	RK_TRACE (RBACKEND);

	transmitRequest (request);
	connection->flush ();
	delete request;
}

void RKFrontendTransmitter::handleTransmissionError (const QString &message) {
	RK_TRACE (RBACKEND);

	if (connection) connection->close ();
	RBackendRequest* req = new RBackendRequest (false, RBackendRequest::BackendExit);
	req->params["message"] = message;
	RKRBackendEvent* event = new RKRBackendEvent (req);
	qApp->postEvent (RKRBackendProtocolFrontend::instance (), event);

	exit ();
}

