/*
 * Copyright (c) 2010-2020 Belledonne Communications SARL.
 *
 * This file is part of linphone-desktop
 * (see https://www.linphone.org).
 *
 * 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 3 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 program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <iostream>

#include <QQuickWindow>

#include "config.h"

#include "app/App.hpp"
#include "components/calls/CallsListModel.hpp"
#include "components/core/CoreHandlers.hpp"
#include "components/core/CoreManager.hpp"
#include "components/settings/SettingsModel.hpp"
#include "utils/Utils.hpp"

#include "Cli.hpp"

// =============================================================================

using namespace std;

// =============================================================================
// API.
// =============================================================================

static void cliShow(QHash<QString, QString> &args) {
	App *app = App::getInstance();
	if (args.size() > 0) {
		app->processArguments(args);
		app->initContentApp();
	}
	app->smartShowWindow(app->getMainWindow());
}

static void cliCall(QHash<QString, QString> &args) {
	QString addressToCall = args["sip-address"];
	if (args.size() > 1) { // Call with options
		App *app = App::getInstance();
		args["call"] = args["sip-address"]; // Swap cli def to parser
		args.remove("sip-address");
		app->processArguments(args);
		app->initContentApp();
	} else CoreManager::getInstance()->getCallsListModel()->launchAudioCall(args["sip-address"], "");
}

static void cliAccept(QHash<QString, QString> &args) {
	auto currentCall = CoreManager::getInstance()->getCore()->getCurrentCall();
	App *app = App::getInstance();
	if (args.size() > 0) {
		app->processArguments(args);
		app->initContentApp();
	}
	if (currentCall) {
		currentCall->accept();
	}
}

static void cliDecline(QHash<QString, QString> &args) {
	auto currentCall = CoreManager::getInstance()->getCore()->getCurrentCall();
	App *app = App::getInstance();
	if (args.size() > 0) {
		app->processArguments(args);
		app->initContentApp();
	}
	if (currentCall) {
		currentCall->decline(linphone::Reason::Declined);
	}
}

static void cliBye(QHash<QString, QString> &args) {
	auto currentCall = CoreManager::getInstance()->getCore()->getCurrentCall();
	if (args.size() > 0) {
		if (args["sip-address"] == "*") // Call with options
			CoreManager::getInstance()->getCallsListModel()->terminateAllCalls();
		else if (args["sip-address"] == "") {
			if (currentCall) currentCall->terminate();
		} else CoreManager::getInstance()->getCallsListModel()->terminateCall(args["sip-address"]);
	} else if (currentCall) {
		currentCall->terminate();
	}
}

static void cliJoinConference(QHash<QString, QString> &args) {
	const QString sipAddress = args.take("sip-address");

	CoreManager *coreManager = CoreManager::getInstance();
	const shared_ptr<linphone::Core> core = coreManager->getCore();

	{
		shared_ptr<linphone::Address> address = core->createPrimaryContactParsed();
		address->setDisplayName(Utils::appStringToCoreString(args.take("display-name")));
		core->setPrimaryContact(address->asString());
	}

	args["method"] = QStringLiteral("join-conference");
	coreManager->getCallsListModel()->launchAudioCall(sipAddress, "", args);
}

static void cliJoinConferenceAs(QHash<QString, QString> &args) {
	const QString fromSipAddress = args.take("guest-sip-address");
	const QString toSipAddress = args.take("sip-address");
	CoreManager *coreManager = CoreManager::getInstance();
	/*shared_ptr<linphone::ProxyConfig> proxyConfig = coreManager->getCore()->getDefaultProxyConfig();
	if (!proxyConfig) {
		qWarning() << QStringLiteral("You have no proxy config.");
		return;
	}

	const shared_ptr<const linphone::Address> currentSipAddress = proxyConfig->getIdentityAddress();
	*/
	shared_ptr<linphone::Account> account = coreManager->getCore()->getDefaultAccount();
	if (!account) {
		qWarning() << QStringLiteral("You have no account.");
		return;
	}

	const shared_ptr<const linphone::Address> currentSipAddress = account->getParams()->getIdentityAddress();
	const shared_ptr<const linphone::Address> askedSipAddress =
		linphone::Factory::get()->createAddress(Utils::appStringToCoreString(fromSipAddress));
	if (!currentSipAddress->weakEqual(askedSipAddress)) {
		qWarning() << QStringLiteral("Guest sip address `%1` doesn't match with default account.").arg(fromSipAddress);
		return;
	}

	args["method"] = QStringLiteral("join-conference");
	coreManager->getCallsListModel()->launchAudioCall(toSipAddress, "", args);
}

static void cliInitiateConference(QHash<QString, QString> &args) {
	shared_ptr<linphone::Core> core = CoreManager::getInstance()->getCore();

	// Check identity.
	{
		shared_ptr<linphone::Address> address = core->interpretUrl(Utils::appStringToCoreString(args["sip-address"]));
		if (!address || address->getUsername().empty()) {
			qWarning() << QStringLiteral("Unable to parse invalid sip address.");
			return;
		}

		address->clean();

		shared_ptr<linphone::Account> account = core->getDefaultAccount();
		if (!account) {
			qWarning() << QStringLiteral("Not connected to an account");
			return;
		}
		if (!account->getParams()->getIdentityAddress()->weakEqual(address)) {
			qWarning() << QStringLiteral("Received different sip address from identity : `%1 != %2`.")
							  .arg(Utils::coreStringToAppString(account->getParams()->getIdentityAddress()->asString()))
							  .arg(Utils::coreStringToAppString(address->asString()));
			return;
		}
	}

	shared_ptr<linphone::Conference> conference = core->getConference();
	const QString id = args["conference-id"];

	auto updateCallsWindow = []() {
		QQuickWindow *callsWindow = App::getInstance()->getCallsWindow();
		if (!callsWindow) return;

		// TODO: Set the view to the "waiting call view".
		if (CoreManager::getInstance()->getSettingsModel()->getKeepCallsWindowInBackground()) {
			if (!callsWindow->isVisible()) callsWindow->showMinimized();
		} else App::smartShowWindow(callsWindow);
	};

	if (conference) {
		if (conference->getId() == Utils::appStringToCoreString(id)) {
			qInfo() << QStringLiteral("Conference `%1` already exists.").arg(id);
			updateCallsWindow();
			return;
		}

		qInfo() << QStringLiteral("Remove existing conference with id: `%1`.")
					   .arg(Utils::coreStringToAppString(conference->getId()));
		core->terminateConference();
	}

	qInfo() << QStringLiteral("Create conference with id: `%1`.").arg(id);
	auto confParameters = core->createConferenceParams(conference);
	confParameters->enableVideo(false); // Video is not yet fully supported by the application in conference
	conference = core->createConferenceWithParams(confParameters);
	conference->setId(Utils::appStringToCoreString(id));

	if (core->enterConference() == -1) {
		qWarning() << QStringLiteral("Unable to join created conference: `%1`.").arg(id);
		return;
	}

	updateCallsWindow();
}

// =============================================================================
// Helpers.
// =============================================================================

static QString splitWord(QString word, int &curPos, const int lineLength, const QString &padding) {
	QString out;
	out += word.mid(0, lineLength - curPos) + "\n" + padding;
	curPos = padding.length();
	word = word.mid(lineLength - curPos);
	while (word.length() > lineLength - curPos) {
		out += word.mid(0, lineLength - curPos);
		word = word.mid(lineLength - curPos);
		out += "\n" + padding;
	}
	out += word;
	curPos = word.length() + padding.length();
	return out;
}

static QString indentedWord(QString word, int &curPos, const int lineLength, const QString &padding) {
	QString out;
	if (curPos + word.length() > lineLength) {
		if (padding.length() + word.length() > lineLength) {
			out += splitWord(word, curPos, lineLength, padding);
		} else {
			out += QStringLiteral("\n");
			out += padding + word;
			curPos = padding.length();
		}
	} else {
		out += word;
		curPos += word.length();
	}
	return out;
}

static string multilineIndent(const QString &str, int indentationNumber = 0) {
	constexpr int lineLength(80);

	static const QRegExp spaceRegexp("(\\s)");
	const QString padding(indentationNumber * 2, ' ');

	QString out = padding;

	int indentedTextCurPos = padding.length();

	int spacePos = 0;
	int wordPos = spacePos;
	QString word;

	while ((spacePos = spaceRegexp.indexIn(str, spacePos)) != -1) {
		word = str.mid(wordPos, spacePos - wordPos);
		out += indentedWord(word, indentedTextCurPos, lineLength, padding);
		switch (str[spacePos].unicode()) {
			case '\n':
				out += "\n" + padding;
				indentedTextCurPos = padding.length();
				break;
			case '\t': // TAB as space.
			case ' ':
				if (indentedTextCurPos == lineLength) {
					out += "\n" + padding;
					indentedTextCurPos = padding.length();
				} else {
					out += " ";
					indentedTextCurPos += 1;
				}
				break;

			default:
				break;
		}
		spacePos += 1;
		wordPos = spacePos;
	}
	word = str.mid(wordPos);
	out += indentedWord(word, indentedTextCurPos, lineLength, padding);
	out += "\n";

	return Utils::appStringToCoreString(out);
}

// =============================================================================

Cli::Command::Command(const QString &functionName,
                      const char *functionDescription,
                      Cli::Function function,
                      const QHash<QString, Cli::Argument> &argsScheme,
                      const bool &genericArguments)
    : mFunctionName(functionName), mFunctionDescription(functionDescription), mFunction(function),
      mArgsScheme(argsScheme), mGenericArguments(genericArguments) {
}

void Cli::Command::execute(QHash<QString, QString> &args) const {
	if (!mGenericArguments) { // Check arguments validity.
		for (const auto &argName : args.keys()) {
			if (!mArgsScheme.contains(argName)) {
				qWarning()
					<< QStringLiteral("Command with invalid argument: `%1 (%2)`.").arg(mFunctionName).arg(argName);

				return;
			}
		}
	}
	// Check missing arguments.
	for (const auto &argName : mArgsScheme.keys()) {
		if (!mArgsScheme[argName].isOptional && (!args.contains(argName) || args[argName].isEmpty())) {
			qWarning() << QStringLiteral("Missing argument for command: `%1 (%2)`.").arg(mFunctionName).arg(argName);
			return;
		}
	}

	// Execute!
	App *app = App::getInstance();
	if (app->isOpened()) {
		qInfo() << QStringLiteral("Execute command:") << args;
		(*mFunction)(args);
	} else {
		Function f = mFunction;
		QObject *context = new QObject();
		QObject::connect(app, &App::opened, [f, args, context]() mutable {
			if (context) {
				delete context;
				context = nullptr;
				qInfo() << QStringLiteral("Execute deferred command:") << args;
				QHash<QString, QString> fuckConst = args;
				(*f)(fuckConst);
			}
		});
	}
}

void Cli::Command::executeUri(QString address, QHash<QString, QString> args) const {
	QUrl url(address);
	QString query = url.query();

	QStringList parameters = query.split('&');
	for (int i = 0; i < parameters.size(); ++i) {
		QStringList parameter = parameters[i].split('=');
		if (parameter[0] != "" && parameter[0] != "method") {
			if (parameter.size() > 1) args[parameter[0]] = QByteArray::fromBase64(parameter[1].toUtf8());
			else args[parameter[0]] = "";
		}
	}

	args["sip-address"] = address;
	execute(args);
}

// pUrl can be `anytoken?p1=x&p2=y` or `p1=x&p2=y`. It will only use p1 and p2
void Cli::Command::executeUrl(const QString &pUrl) const {
	QHash<QString, QString> args;
	QStringList urlParts = pUrl.split('?');
	QString query = (urlParts.size() > 1 ? urlParts[1] : urlParts[0]);
	QString authority = (urlParts.size() > 1 && urlParts[0].contains(':') ? urlParts[0].split(':')[1] : "");

	QStringList parameters = query.split('&');
	for (int i = 0; i < parameters.size(); ++i) {
		QStringList parameter = parameters[i].split('=');
		if (parameter[0] != "method") {
			if (parameter.size() > 1) args[parameter[0]] = QByteArray::fromBase64(parameter[1].toUtf8());
			else args[parameter[0]] = "";
		}
	}
	if (!authority.isEmpty()) args["sip-address"] = authority;
	execute(args);
}

QString Cli::Command::getFunctionSyntax() const {
	QString functionSyntax;
	functionSyntax += QStringLiteral("\"");
	functionSyntax += mFunctionName;
	for (auto &argName : mArgsScheme.keys()) {
		functionSyntax += QStringLiteral(" ");
		functionSyntax += mArgsScheme[argName].isOptional ? QStringLiteral("[") : QStringLiteral("");
		functionSyntax += argName;
		functionSyntax += QStringLiteral("=<");
		switch (mArgsScheme[argName].type) {
			case String:
				functionSyntax += QStringLiteral("str");
				break;
			default:
				functionSyntax += QStringLiteral("value");
				break;
		}
		functionSyntax += QString(">");
		functionSyntax += mArgsScheme[argName].isOptional ? QStringLiteral("]") : QStringLiteral("");
	}
	functionSyntax += QStringLiteral("\"");
	return functionSyntax;
}

// =============================================================================

// FIXME: Do not accept args without value like: cmd toto.
// In the future `toto` could be a boolean argument.
QRegExp Cli::mRegExpArgs("(?:(?:([\\w-]+)\\s*)=\\s*(?:\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"|([^\\s]+)\\s*))");
QRegExp Cli::mRegExpFunctionName("^\\s*([a-z-]+)\\s*");

QMap<QString, Cli::Command> Cli::mCommands = {
    createCommand("show", QT_TR_NOOP("showFunctionDescription"), cliShow, QHash<QString, Argument>(), true),
    createCommand("call", QT_TR_NOOP("callFunctionDescription"), cliCall, {{"sip-address", {}}}, true),
    createCommand("initiate-conference",
                  QT_TR_NOOP("initiateConferenceFunctionDescription"),
                  cliInitiateConference,
                  {{"sip-address", {}}, {"conference-id", {}}}),
    createCommand("join-conference",
                  QT_TR_NOOP("joinConferenceFunctionDescription"),
                  cliJoinConference,
                  {{"sip-address", {}}, {"conference-id", {}}, {"display-name", {}}}),
    createCommand("join-conference-as",
                  QT_TR_NOOP("joinConferenceAsFunctionDescription"),
                  cliJoinConferenceAs,
                  {{"sip-address", {}}, {"conference-id", {}}, {"guest-sip-address", {}}}),
    createCommand("bye", QT_TR_NOOP("byeFunctionDescription"), cliBye, QHash<QString, Argument>(), true),
    createCommand("accept", QT_TR_NOOP("acceptFunctionDescription"), cliAccept, QHash<QString, Argument>(), true),
    createCommand("decline", QT_TR_NOOP("declineFunctionDescription"), cliDecline, QHash<QString, Argument>(), true),
};

// -----------------------------------------------------------------------------
/*
string Cli::getScheme(const QString& address){
    QStringList tempSipAddress = address->split(':');
    if( tempSipAddress.size() > 0)
        return tempSipAddress[0].toStdString();
    else
        return "";
}
bool Cli::changeScheme(QString * address){
    QStringList tempSipAddress = address->split(':');
    string scheme;
    bool ok = false;
    if(tempSipAddress.size() > 1) {
        scheme = tempSipAddress[0].toStdString();
        for (const string &validScheme : { string("sip"), "sip-"+string(EXECUTABLE_NAME), string("sips"),
"sips-"+string(EXECUTABLE_NAME), string("tel"), string("callto"), string(EXECUTABLE_NAME)+ "-config" }) if (scheme ==
validScheme) ok = true; if( !ok){ qWarning() << QStringLiteral("Not a valid uri: `%1` Unsupported scheme:
`%2`.").arg(*address).arg(Utils::coreStringToAppString(scheme)); }else{ tempSipAddress[0] = "sip";// In order to pass
bellesip parsing. *address = tempSipAddress.join(':');
        }
    }
    return ok;
}
*/
void Cli::executeCommand(const QString &command, CommandFormat *format) {

	// Detect if command is a CLI by testing commands
	const QString &functionName = parseFunctionName(command);
	const std::string configURI = string(EXECUTABLE_NAME) + "-config";
	if (!functionName.isEmpty()) { // It is a CLI
		qInfo() << QStringLiteral("Detecting cli command: `%1`...").arg(command);
		QHash<QString, QString> args = parseArgs(command);
		mCommands[functionName].execute(args);
		if (format) *format = CliFormat;
		return;
	} else { // It is a URI
		QStringList tempSipAddress = command.split(':');
		string scheme = "sip";
		QString transformedCommand; // In order to pass bellesip parsing, set scheme to 'sip:'.
		if (tempSipAddress.size() == 1) {
			transformedCommand = "sip:" + command;
		} else {
			scheme = tempSipAddress[0].toStdString();
			bool ok = false;
			for (const string &validScheme :
				 {string("sip"), "sip-" + string(EXECUTABLE_NAME), string("sips"), "sips-" + string(EXECUTABLE_NAME),
				  string("tel"), string("callto"), configURI})
				if (scheme == validScheme) ok = true;
			if (!ok) {
				qWarning() << QStringLiteral("Not a valid URI: `%1` Unsupported scheme: `%2`.")
								  .arg(command)
								  .arg(Utils::coreStringToAppString(scheme));
				return;
			}
			tempSipAddress[0] = "sip";
			transformedCommand = tempSipAddress.join(':');
		}
		if (scheme == configURI) {
			QHash<QString, QString> args = parseArgs(command);
			QString fetchUrl;
			if (args.contains("fetch-config")) fetchUrl = QByteArray::fromBase64(args["fetch-config"].toUtf8());
			else {
				QUrl url(command.mid(configURI.size() + 1)); // Remove 'exec-config:'
				if (url.scheme().isEmpty()) url.setScheme("https");
				fetchUrl = url.toString();
			}
			if (format) *format = CliFormat;
			QHash<QString, QString> dummy;
			mCommands["show"].execute(dummy); // Just open the app.
			App::getInstance()->useFetchConfig(fetchUrl);
		} else {
			shared_ptr<linphone::Address> address;
			QString qAddress = transformedCommand;
			if (Utils::isUsername(transformedCommand)) {
				address = linphone::Factory::get()->createAddress(
					Utils::appStringToCoreString(transformedCommand + "@to.remove"));
				address->setDomain("");
				qAddress = Utils::coreStringToAppString(address->asString());
				if (address && qAddress.isEmpty()) qAddress = transformedCommand;
			} else
				address = linphone::Factory::get()->createAddress(
					Utils::appStringToCoreString(transformedCommand)); // Test if command is an address
			if (format) *format = UriFormat;
			qInfo() << QStringLiteral("Detecting URI command: `%1`...").arg(command);
			QString functionName;
			if (address) {
				functionName = Utils::coreStringToAppString(address->getHeader("method")).isEmpty()
								   ? QStringLiteral("call")
								   : Utils::coreStringToAppString(address->getHeader("method"));
			} else {
				QStringList fields = command.split('?');
				if (fields.size() > 1) {
					fields = fields[1].split('&');
					for (int i = 0; i < fields.size() && functionName.isEmpty(); ++i) {
						QStringList data = fields[i].split('=');
						if (data[0] == "method" && data.size() > 1) functionName = data[1];
					}
					if (functionName.isEmpty()) functionName = "call";
				}
			}
			functionName = functionName.toLower();
			if (functionName.isEmpty()) {
				qWarning() << QStringLiteral("There is no method set in `%1`.").arg(command);
				return;
			} else if (!mCommands.contains(functionName)) {
				qWarning() << QStringLiteral("This command doesn't exist: `%1`.").arg(functionName);
				return;
			}
			if (address) {
				// TODO: check if there is too much headers.
				QHash<QString, QString> headers;
				for (const auto &argName : mCommands[functionName].mArgsScheme.keys()) {
					const string header = address->getHeader(Utils::appStringToCoreString(argName));
					headers[argName] = QByteArray::fromBase64(QByteArray(header.c_str(), int(header.length())));
				}
				mCommands[functionName].executeUri(qAddress, headers);
			} else mCommands[functionName].executeUrl(command);
		}
	}
}

void Cli::showHelp() {
	cout << multilineIndent(tr("appCliDescription").arg(APPLICATION_NAME), 0) << endl
		 << "Usage: " << endl
		 << multilineIndent(tr("uriCommandLineSyntax").arg(EXECUTABLE_NAME), 0)
		 << multilineIndent(tr("cliCommandLineSyntax").arg(EXECUTABLE_NAME), 0) << endl
		 << multilineIndent(tr("commandsName")) << endl;

	for (const auto &method : mCommands.keys())
		cout << multilineIndent(mCommands[method].getFunctionSyntax(), 1)
			 << multilineIndent(tr(mCommands[method].getFunctionDescription()), 2) << endl;
}

// -----------------------------------------------------------------------------

pair<QString, Cli::Command> Cli::createCommand(const QString &functionName,
                                               const char *functionDescription,
                                               Function function,
                                               const QHash<QString, Argument> &argsScheme,
                                               const bool &genericArguments) {
    return {functionName.toLower(),
            Cli::Command(functionName.toLower(), functionDescription, function, argsScheme, genericArguments)};
}

// -----------------------------------------------------------------------------

QString Cli::parseFunctionName(const QString &command) {
	mRegExpFunctionName.indexIn(command.toLower());
	if (mRegExpFunctionName.pos(1) == -1) {
		qWarning() << QStringLiteral("Unable to parse function name of command: `%1`.").arg(command);
		return QString("");
	}

	const QStringList texts = mRegExpFunctionName.capturedTexts();

	const QString functionName = texts[1];
	if (!mCommands.contains(functionName)) {
		qWarning() << QStringLiteral("This command doesn't exist: `%1`.").arg(functionName);
		return QString("");
	}

	return functionName;
}

QHash<QString, QString> Cli::parseArgs(const QString &command) {
	QHash<QString, QString> args;
	int pos = 0;

	while ((pos = mRegExpArgs.indexIn(command.toLower(), pos)) != -1) {
		pos += mRegExpArgs.matchedLength();
		args[mRegExpArgs.cap(1)] = (mRegExpArgs.cap(2).isEmpty() ? mRegExpArgs.cap(3) : mRegExpArgs.cap(2));
	}

	return args;
}
