// Copyright 2010-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "SocketRPC.h"

#include "Channel.h"
#include "ClientUser.h"
#include "MainWindow.h"
#include "ServerHandler.h"
#include "Global.h"

#include <QtCore/QProcessEnvironment>
#include <QtCore/QUrlQuery>
#include <QtNetwork/QLocalServer>
#include <QtXml/QDomDocument>

SocketRPCClient::SocketRPCClient(QLocalSocket *s, QObject *p) : QObject(p), qlsSocket(s), qbBuffer(nullptr) {
	qlsSocket->setParent(this);

	connect(qlsSocket, SIGNAL(disconnected()), this, SLOT(disconnected()));
	connect(qlsSocket, SIGNAL(error(QLocalSocket::LocalSocketError)), this,
			SLOT(error(QLocalSocket::LocalSocketError)));
	connect(qlsSocket, SIGNAL(readyRead()), this, SLOT(readyRead()));

	qxsrReader.setDevice(qlsSocket);
	qxswWriter.setAutoFormatting(true);

	qbBuffer = new QBuffer(&qbaOutput, this);
	qbBuffer->open(QIODevice::WriteOnly);
	qxswWriter.setDevice(qbBuffer);
}

void SocketRPCClient::disconnected() {
	deleteLater();
}

void SocketRPCClient::error(QLocalSocket::LocalSocketError) {
}

void SocketRPCClient::readyRead() {
	forever {
		switch (qxsrReader.readNext()) {
			case QXmlStreamReader::Invalid: {
				if (qxsrReader.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
					qWarning() << "Malformed" << qxsrReader.error();
					qlsSocket->abort();
				}
				return;
			} break;
			case QXmlStreamReader::EndDocument: {
				qxswWriter.writeCurrentToken(qxsrReader);

				processXml();

				qxsrReader.clear();
				qxsrReader.setDevice(qlsSocket);

				qxswWriter.setDevice(nullptr);
				delete qbBuffer;
				qbaOutput = QByteArray();
				qbBuffer  = new QBuffer(&qbaOutput, this);
				qbBuffer->open(QIODevice::WriteOnly);
				qxswWriter.setDevice(qbBuffer);
			} break;
			default:
				qxswWriter.writeCurrentToken(qxsrReader);
				break;
		}
	}
}

void SocketRPCClient::processXml() {
	QDomDocument qdd;
	qdd.setContent(qbaOutput, false);

	QDomElement request = qdd.firstChildElement();

	if (!request.isNull()) {
		bool ack = false;
		QMap< QString, QVariant > qmRequest;
		QMap< QString, QVariant > qmReply;
		QMap< QString, QVariant >::const_iterator iter;

		QDomNamedNodeMap attributes = request.attributes();
		for (int i = 0; i < attributes.count(); ++i) {
			QDomAttr attr = attributes.item(i).toAttr();
			qmRequest.insert(attr.name(), attr.value());
		}
		QDomNodeList childNodes = request.childNodes();
		for (int i = 0; i < childNodes.count(); ++i) {
			QDomElement child = childNodes.item(i).toElement();
			if (!child.isNull())
				qmRequest.insert(child.nodeName(), child.text());
		}

		iter = qmRequest.find(QLatin1String("reqid"));
		if (iter != qmRequest.constEnd())
			qmReply.insert(iter.key(), iter.value());

		if (request.nodeName() == QLatin1String("focus")) {
			Global::get().mw->show();
			Global::get().mw->raise();
			Global::get().mw->activateWindow();

			ack = true;
		} else if (request.nodeName() == QLatin1String("self")) {
			iter = qmRequest.find(QLatin1String("mute"));
			if (iter != qmRequest.constEnd()) {
				bool set = iter.value().toBool();
				if (set != Global::get().s.bMute) {
					Global::get().mw->qaAudioMute->setChecked(!set);
					Global::get().mw->qaAudioMute->trigger();
				}
			}
			iter = qmRequest.find(QLatin1String("unmute"));
			if (iter != qmRequest.constEnd()) {
				bool set = iter.value().toBool();
				if (set == Global::get().s.bMute) {
					Global::get().mw->qaAudioMute->setChecked(set);
					Global::get().mw->qaAudioMute->trigger();
				}
			}
			iter = qmRequest.find(QLatin1String("togglemute"));
			if (iter != qmRequest.constEnd()) {
				bool set = iter.value().toBool();
				if (set == Global::get().s.bMute) {
					Global::get().mw->qaAudioMute->setChecked(set);
					Global::get().mw->qaAudioMute->trigger();
				} else {
					Global::get().mw->qaAudioMute->setChecked(!set);
					Global::get().mw->qaAudioMute->trigger();
				}
			}
			iter = qmRequest.find(QLatin1String("deaf"));
			if (iter != qmRequest.constEnd()) {
				bool set = iter.value().toBool();
				if (set != Global::get().s.bDeaf) {
					Global::get().mw->qaAudioDeaf->setChecked(!set);
					Global::get().mw->qaAudioDeaf->trigger();
				}
			}
			iter = qmRequest.find(QLatin1String("undeaf"));
			if (iter != qmRequest.constEnd()) {
				bool set = iter.value().toBool();
				if (set == Global::get().s.bDeaf) {
					Global::get().mw->qaAudioDeaf->setChecked(set);
					Global::get().mw->qaAudioDeaf->trigger();
				}
			}
			iter = qmRequest.find(QLatin1String("toggledeaf"));
			if (iter != qmRequest.constEnd()) {
				bool set = iter.value().toBool();
				if (set == Global::get().s.bDeaf) {
					Global::get().mw->qaAudioDeaf->setChecked(set);
					Global::get().mw->qaAudioDeaf->trigger();
				} else {
					Global::get().mw->qaAudioDeaf->setChecked(!set);
					Global::get().mw->qaAudioDeaf->trigger();
				}
			}
			iter = qmRequest.find(QLatin1String("starttalking"));
			if (iter != qmRequest.constEnd()) {
				Global::get().mw->on_PushToTalk_triggered(true, QVariant());
			}
			iter = qmRequest.find(QLatin1String("stoptalking"));
			if (iter != qmRequest.constEnd()) {
				Global::get().mw->on_PushToTalk_triggered(false, QVariant());
			}
			ack = true;
		} else if (request.nodeName() == QLatin1String("url")) {
			if (Global::get().sh && Global::get().sh->isRunning() && Global::get().uiSession) {
				QString host, user, pw;
				unsigned short port;
				QUrl u;

				Global::get().sh->getConnectionInfo(host, port, user, pw);
				u.setScheme(QLatin1String("mumble"));
				u.setHost(host);
				u.setPort(port);
				u.setUserName(user);

				QUrlQuery query;
				query.addQueryItem(QLatin1String("version"), QLatin1String("1.2.0"));
				u.setQuery(query);

				QStringList path;
				Channel *c = ClientUser::get(Global::get().uiSession)->cChannel;
				while (c->cParent) {
					path.prepend(c->qsName);
					c = c->cParent;
				}
				u.setPath(path.join(QLatin1String("/")));
				qmReply.insert(QLatin1String("href"), u);
			}

			iter = qmRequest.find(QLatin1String("href"));
			if (iter != qmRequest.constEnd()) {
				QUrl u = iter.value().toUrl();
				if (u.isValid() && u.scheme() == QLatin1String("mumble")) {
					OpenURLEvent *oue = new OpenURLEvent(u);
					qApp->postEvent(Global::get().mw, oue);
					ack = true;
				}
			} else {
				ack = true;
			}
		}

		QDomDocument replydoc;
		QDomElement reply = replydoc.createElement(QLatin1String("reply"));

		qmReply.insert(QLatin1String("succeeded"), ack);

		for (iter = qmReply.constBegin(); iter != qmReply.constEnd(); ++iter) {
			QDomElement elem = replydoc.createElement(iter.key());
			QDomText text    = replydoc.createTextNode(iter.value().toString());
			elem.appendChild(text);
			reply.appendChild(elem);
		}

		replydoc.appendChild(reply);

		qlsSocket->write(replydoc.toByteArray());
	}
}

SocketRPC::SocketRPC(const QString &basename, QObject *p) : QObject(p) {
	qlsServer = new QLocalServer(this);

	QString pipepath;

#ifdef Q_OS_WIN
	pipepath = basename;
#else
	{
		QString xdgRuntimePath = QProcessEnvironment::systemEnvironment().value(QLatin1String("XDG_RUNTIME_DIR"));
		QDir xdgRuntimeDir     = QDir(xdgRuntimePath);

		if (!xdgRuntimePath.isNull() && xdgRuntimeDir.exists()) {
			pipepath = xdgRuntimeDir.absoluteFilePath(basename + QLatin1String("Socket"));
		} else {
			pipepath = QDir::home().absoluteFilePath(QLatin1String(".") + basename + QLatin1String("Socket"));
		}
	}

	{
		QFile f(pipepath);
		if (f.exists()) {
			qWarning() << "SocketRPC: Removing old socket on" << pipepath;
			f.remove();
		}
	}
#endif

	if (!qlsServer->listen(pipepath)) {
		qWarning() << "SocketRPC: Listen failed";
		delete qlsServer;
		qlsServer = nullptr;
	} else {
		connect(qlsServer, SIGNAL(newConnection()), this, SLOT(newConnection()));
	}
}

void SocketRPC::newConnection() {
	while (true) {
		QLocalSocket *qls = qlsServer->nextPendingConnection();
		if (!qls)
			break;
		new SocketRPCClient(qls, this);
	}
}

bool SocketRPC::send(const QString &basename, const QString &request, const QMap< QString, QVariant > &param) {
	QString pipepath;

#ifdef Q_OS_WIN
	pipepath = basename;
#else
	{
		QString xdgRuntimePath = QProcessEnvironment::systemEnvironment().value(QLatin1String("XDG_RUNTIME_DIR"));
		QDir xdgRuntimeDir     = QDir(xdgRuntimePath);

		if (!xdgRuntimePath.isNull() && xdgRuntimeDir.exists()) {
			pipepath = xdgRuntimeDir.absoluteFilePath(basename + QLatin1String("Socket"));
		} else {
			pipepath = QDir::home().absoluteFilePath(QLatin1String(".") + basename + QLatin1String("Socket"));
		}
	}
#endif

	QLocalSocket qls;
	qls.connectToServer(pipepath);
	if (!qls.waitForConnected(1000)) {
		return false;
	}

	QDomDocument requestdoc;
	QDomElement req = requestdoc.createElement(request);
	for (QMap< QString, QVariant >::const_iterator iter = param.constBegin(); iter != param.constEnd(); ++iter) {
		QDomElement elem = requestdoc.createElement(iter.key());
		QDomText text    = requestdoc.createTextNode(iter.value().toString());
		elem.appendChild(text);
		req.appendChild(elem);
	}
	requestdoc.appendChild(req);

	qls.write(requestdoc.toByteArray());
	qls.flush();

	if (!qls.waitForReadyRead(2000)) {
		return false;
	}

	QByteArray qba = qls.readAll();

	QDomDocument replydoc;
	replydoc.setContent(qba);

	QDomElement succ = replydoc.firstChildElement(QLatin1String("reply"));
	succ             = succ.firstChildElement(QLatin1String("succeeded"));
	if (succ.isNull())
		return false;

	return QVariant(succ.text()).toBool();
}
