// SPDX-License-Identifier: LGPL-2.1

/*
 * Copyright (C) 2017 VMware Inc, Yordan Karadzhov <ykaradzhov@vmware.com>
 */

/**
 *  @file    KsCaptureDialog.cpp
 *  @brief   Dialog for trace data recording.
 */

// Qt
#include <QLocalSocket>

// KernelShark
#include "libkshark.h"
#include "libkshark-tepdata.h"
#include "KsUtils.hpp"
#include "KsCmakeDef.hpp"
#include "KsCaptureDialog.hpp"

extern "C" {
  // To get access to geteuid()
  #include <unistd.h>
  #include <sys/types.h>
}

/**
 * @brief Create KsCommandLineEdit.
 *
 * @param text: Defaulst text to be shown.
 * @param parent: The parent of this widget.
 */
KsCommandLineEdit::KsCommandLineEdit(QString text, QWidget *parent)
: QPlainTextEdit(text, parent) {}

QSize KsCommandLineEdit::minimumSizeHint() const
{
	return {FONT_WIDTH * 30, FONT_HEIGHT * 2};
}

QSize KsCommandLineEdit::sizeHint() const
{
	return {FONT_WIDTH * 30, FONT_HEIGHT * 3};
}

static kshark_data_stream *_initLocalStream()
{
	kshark_context *kshark_ctx(nullptr);
	kshark_data_stream *stream;
	int sd;

	if (!kshark_instance(&kshark_ctx))
		return nullptr;

	sd = kshark_add_stream(kshark_ctx);
	stream = kshark_ctx->stream[sd];

	kshark_tep_init_local(stream);

	return stream;
}

/** @brief Create KsCaptureControl widget. */
KsCaptureControl::KsCaptureControl(QWidget *parent)
: QWidget(parent),
  _stream(_initLocalStream()),
  _eventsWidget(_stream),
  _pluginsLabel("Plugin: ", this),
  _outputLabel("Output file: ", this),
  _commandLabel("Command: ", this),
  _outputLineEdit("trace.dat", this),
  _commandLineEdit("sleep 0.1", this),
  _settingsToolBar(this),
  _controlToolBar(this),
  _pluginsComboBox(this),
  _importSettingsButton("Import Settings", this),
  _exportSettingsButton("Export Settings", this),
  _outputBrowseButton("Browse", this),
  _commandCheckBox("Display output", this),
  _captureButton("Capture", &_controlToolBar),
  _applyButton("Apply", &_controlToolBar),
  _closeButton("Close", &_controlToolBar)
{
	QStringList pluginList = _getPlugins();
	int row(0);

	auto lamAddLine = [&] {
		QFrame* line = new QFrame();

		line->setFrameShape(QFrame::HLine);
		line->setFrameShadow(QFrame::Sunken);
		_topLayout.addWidget(line);
	};

	if (pluginList.count() == 0 || !kshark_get_tep(_stream)) {
		/*
		 * No plugins or events have been found. Most likely this is
		 * because the process has no Root privileges or because
		 * tracefs cannot be mounted.
		 */
		QString message("Error: No events or plugins found.\n");

		if (!kshark_get_tep(_stream))
			message += "Cannot find or mount tracing directory.\n";

		// geteuid() returns 0 if running as effective id of root
		if (!pluginList.count() && geteuid())
			message += "Root privileges are required.\n";

		QLabel *errorLabel = new QLabel(message);

		errorLabel->setStyleSheet("QLabel {color : red;}");
		_topLayout.addWidget(errorLabel);

		lamAddLine();

		/* Disable "Capture" buttons. */
		_captureButton.setDisabled(true);
	}

	pluginList.prepend("nop");

	_settingsToolBar.addWidget(&_importSettingsButton);
	_settingsToolBar.addSeparator();
	_settingsToolBar.addWidget(&_exportSettingsButton);
	_topLayout.addWidget(&_settingsToolBar);

	lamAddLine();

	_eventsWidget.setDefault(false);
	_eventsWidget.setMinimumHeight(25 * FONT_HEIGHT);
	_eventsWidget.removeSystem("ftrace");
	_topLayout.addWidget(&_eventsWidget);

	_pluginsLabel.adjustSize();
	_execLayout.addWidget(&_pluginsLabel, row, 0);

	_pluginsComboBox.addItems(pluginList);
	_execLayout.addWidget(&_pluginsComboBox, row++, 1);

	_outputLabel.adjustSize();
	_execLayout.addWidget(&_outputLabel, row, 0);
	_outputLineEdit.setFixedWidth(FONT_WIDTH * 30);
	_execLayout.addWidget(&_outputLineEdit, row, 1);
	_outputBrowseButton.adjustSize();
	_execLayout.addWidget(&_outputBrowseButton, row++, 2);

	_commandLabel.adjustSize();
	_commandLabel.setFixedWidth(_outputLabel.width());
	_execLayout.addWidget(&_commandLabel, row, 0);

	_commandLineEdit.setFixedWidth(FONT_WIDTH * 30);
	_commandLineEdit.setMinimumHeight(FONT_HEIGHT * 2);
	_commandLineEdit.setMaximumHeight(FONT_HEIGHT * 9);

	_execLayout.addWidget(&_commandLineEdit, row, 1);
	_commandCheckBox.setCheckState(Qt::Unchecked);
	_commandCheckBox.adjustSize();
	_execLayout.addWidget(&_commandCheckBox, row++, 2);

	_topLayout.addLayout(&_execLayout);

	lamAddLine();

	_captureButton.setFixedWidth(STRING_WIDTH("_Capture_") + FONT_WIDTH * 2);
	_applyButton.setFixedWidth(_captureButton.width());
	_closeButton.setFixedWidth(_captureButton.width());

	_controlToolBar.addWidget(&_captureButton);
	_controlToolBar.addWidget(&_applyButton);
	_controlToolBar.addWidget(&_closeButton);
	_topLayout.addWidget(&_controlToolBar);

	setLayout(&_topLayout);

	connect(&_importSettingsButton,	&QPushButton::pressed,
		this,			&KsCaptureControl::_importSettings);

	connect(&_exportSettingsButton,	&QPushButton::pressed,
		this,			&KsCaptureControl::_exportSettings);

	connect(&_outputBrowseButton,	&QPushButton::pressed,
		this,			&KsCaptureControl::_browse);

	connect(&_applyButton,		&QPushButton::pressed,
		this,			&KsCaptureControl::_apply);
}

/**
 * Use the settings of the Control panel to generate a list of command line
 * arguments for trace-cmd.
 */
QStringList KsCaptureControl::getArgs()
{
	QStringList argv;

	argv << "record";

	if (_pluginsComboBox.currentText() != "nop")
		argv << "-p" << _pluginsComboBox.currentText();

	if (_eventsWidget.all())
		argv << "-e" << "all";
	else
		argv << _eventsWidget.getCheckedEvents(true);

	argv << "-o" << outputFileName();

	argv << KsUtils::splitArguments(_commandLineEdit.toPlainText());

	return argv;
}

QStringList KsCaptureControl::_getPlugins()
{
	QStringList pluginList;
	char **all_tracers;

	all_tracers = kshark_tracecmd_local_plugins();

	if (!all_tracers)
		return pluginList;

	for (int i = 0; all_tracers[i]; ++i)
		pluginList << all_tracers[i];

	kshark_tracecmd_plugin_list_free(all_tracers);
	std::sort(pluginList.begin(), pluginList.end());

	return pluginList;
}

void KsCaptureControl::_importSettings()
{
	QVector<int> event_ids = _eventsWidget.getIds();
	QVector<int> status(_stream->n_events, 0);
	kshark_config_doc *conf, *jevents, *temp;
	kshark_hash_id *eventHash;
	QString fileName;
	int nIds;

	auto lamImportError = [this] () {
		emit print("ERROR: Unable to load the configuration file.\n");
	};

	/* Get the configuration document. */
	fileName = KsUtils::getFile(this, "Import from Filter",
				    "Kernel Shark Config files (*.json);;",
				    _lastFilePath);

	if (fileName.isEmpty()) {
		lamImportError();
		return;
	}

	conf = kshark_open_config_file(fileName.toStdString().c_str(),
				       "kshark.config.record");
	if (!conf) {
		lamImportError();
		return;
	}

	/*
	 * Load the hash table of selected events from the configuration
	 * document.
	 */
	jevents = kshark_config_alloc(KS_CONFIG_JSON);
	if (!kshark_config_doc_get(conf, "Events", jevents)) {
		lamImportError();
		return;
	}

	eventHash = kshark_get_filter(_stream, KS_SHOW_EVENT_FILTER);
	nIds = kshark_import_event_filter(_stream, KS_SHOW_EVENT_FILTER,
					  "Events", jevents);
	if (nIds < 0) {
		QString err("WARNING: ");
		err += "Some of the imported events are not available on this system.\n";
		err += "All missing events are ignored.\n";
		emit print(err);
	}

	for (int i = 0; i < _stream->n_events; ++i) {
		if (kshark_hash_id_find(eventHash, event_ids[i]))
			status[i] = true;
	}

	_eventsWidget.set(status);

	/** Get all available plugins. */
	temp = kshark_string_config_alloc();

	if (kshark_config_doc_get(conf, "Plugin", temp)) {
		const char *plugin = KS_C_STR_CAST(temp->conf_doc);
		int pluginIndex = _pluginsComboBox.findText(plugin);

		if (pluginIndex >= 0) {
			_pluginsComboBox.setCurrentText(KS_C_STR_CAST(temp->conf_doc));
		} else {
			QString err("WARNING: The tracer plugin \"");
			err += plugin;
			err += "\" is not available on this machine!\n";
			emit print(err);
		}
	}

	if (kshark_config_doc_get(conf, "Output", temp))
		_outputLineEdit.setText(KS_C_STR_CAST(temp->conf_doc));

	if (kshark_config_doc_get(conf, "Command", temp))
		_commandLineEdit.setPlainText(KS_C_STR_CAST(temp->conf_doc));
}

void KsCaptureControl::_exportSettings()
{
	kshark_config_doc *conf, *events;
	json_object *jplugin;
	QString plugin, out, comm;
	QVector<int> ids;
	QString fileName;

	fileName = KsUtils::getSaveFile(this, "Export to File",
					"Kernel Shark Config files (*.json);;",
					".json",
					_lastFilePath);

	if (fileName.isEmpty())
		return;

	/* Create a configuration document. */
	conf = kshark_record_config_new(KS_CONFIG_JSON);
	events = kshark_filter_config_new(KS_CONFIG_JSON);

	/*
	 * Use the kshark_hash_id to save all selected events in the
	 * configuration file.
	 */
	ids = _eventsWidget.getCheckedIds();
	kshark_hash_id *eventHash = kshark_get_filter(_stream,
						      KS_SHOW_EVENT_FILTER);

	for (auto const &id: ids)
		kshark_hash_id_add(eventHash, id);

	kshark_export_event_filter(_stream, KS_SHOW_EVENT_FILTER, "Events", events);
	kshark_config_doc_add(conf, "Events", events);

	kshark_hash_id_free(eventHash);

	/* Save the plugin. */
	plugin = _pluginsComboBox.currentText();
	jplugin = json_object_new_string(plugin.toStdString().c_str());
	kshark_config_doc_add(conf, "Plugin", kshark_json_to_conf(jplugin));

	/* Save the output file. */
	out = outputFileName();
	json_object *jout = json_object_new_string(out.toStdString().c_str());
	kshark_config_doc_add(conf, "Output", kshark_json_to_conf(jout));

	/* Save the command. */
	comm = _commandLineEdit.toPlainText();
	json_object *jcomm = json_object_new_string(comm.toStdString().c_str());
	kshark_config_doc_add(conf, "Command", kshark_json_to_conf(jcomm));

	kshark_save_config_file(fileName.toStdString().c_str(), conf);
}

void KsCaptureControl::_browse()
{
	QString fileName =
		KsUtils::getSaveFile(this, "Save File",
				     "trace-cmd files (*.dat);;All files (*)",
				     ".dat",
				     _lastFilePath);

	if (!fileName.isEmpty())
		_outputLineEdit.setText(fileName);
}

void KsCaptureControl::_apply()
{
	QStringList argv = getArgs();

	if (argv.count())
		emit argsReady(argv.join(" "));
}

/** @brief Create KsCaptureMonitor widget. */
KsCaptureMonitor::KsCaptureMonitor(QWidget *parent)
: QWidget(parent),
  _mergedChannels(false),
  _argsModified(false),
  _panel(this),
  _name("Output display", this),
  _space("max size ", this),
  _readOnlyCB("read only", this),
  _maxLinNumEdit(QString("%1").arg(KS_CAP_MON_MAX_LINE_NUM), this),
  _consolOutput("", this)
{
	_panel.setMaximumHeight(FONT_HEIGHT * 1.75);
	_panel.addWidget(&_name);

	_space.setAlignment(Qt::AlignRight);
	_panel.addWidget(&_space);

	_maxLinNumEdit.setFixedWidth(FONT_WIDTH * 7);
	_panel.addWidget(&_maxLinNumEdit);
	_panel.addSeparator();
	_readOnlyCB.setCheckState(Qt::Checked);
	_panel.addWidget(&_readOnlyCB);
	_layout.addWidget(&_panel);

	_consolOutput.setStyleSheet("QLabel {background-color : white;}");
	_consolOutput.setMinimumWidth(FONT_WIDTH * 60);
	_consolOutput.setMinimumHeight(FONT_HEIGHT * 10);
	_consolOutput.setMaximumBlockCount(KS_CAP_MON_MAX_LINE_NUM);

	_space.setMinimumWidth(FONT_WIDTH * 50 - _name.width() - _readOnlyCB.width());
	_consolOutput.setReadOnly(true);
	_layout.addWidget(&_consolOutput);

	this->setLayout(&_layout);

	connect(&_maxLinNumEdit,	&QLineEdit::textChanged,
		this,			&KsCaptureMonitor::_maxLineNumber);

	connect(&_readOnlyCB,		&QCheckBox::stateChanged,
		this,			&KsCaptureMonitor::_readOnly);

	connect(&_consolOutput,		&QPlainTextEdit::textChanged,
		this,			&KsCaptureMonitor::_argVModified);

	this->show();
}

void KsCaptureMonitor::_maxLineNumber(const QString &test)
{
	bool ok;
	int max = test.toInt(&ok);

	if (ok)
		_consolOutput.setMaximumBlockCount(max);
}

void KsCaptureMonitor::_readOnly(int state)
{
	if (state == Qt::Checked)
		_consolOutput.setReadOnly(true);
	else
		_consolOutput.setReadOnly(false);
}

void KsCaptureMonitor::_argsReady(const QString &args)
{
	_name.setText("Capture options:");
	_consolOutput.setPlainText(args);
	_argsModified = false;
}

void KsCaptureMonitor::_argVModified()
{
	_argsModified = true;
}

void KsCaptureMonitor::_printAllStandardError()
{
	QProcess *_capture = (QProcess*) sender();

	_consolOutput.moveCursor(QTextCursor::End);
	_consolOutput.insertPlainText(_capture->readAllStandardError());
	_consolOutput.moveCursor(QTextCursor::End);
	QCoreApplication::processEvents();
}

void KsCaptureMonitor::_printAllStandardOutput()
{
	QProcess *_capture = (QProcess*) sender();

	if (!_mergedChannels)
		return;

	_consolOutput.appendPlainText(_capture->readAllStandardOutput());
	QCoreApplication::processEvents();
}

/**
 * Connect the Capture monitor widget to the signals of the recording process.
 */
void KsCaptureMonitor::connectMe(QProcess *proc, KsCaptureControl *ctrl)
{
	connect(proc,	&QProcess::started,
		this,	&KsCaptureMonitor::_captureStarted);

	/* Using the old Signal-Slot syntax because QProcess::finished has overloads. */
	connect(proc,	SIGNAL(finished(int, QProcess::ExitStatus)),
		this,	SLOT(_captureFinished(int, QProcess::ExitStatus)));

	connect(proc,	&QProcess::readyReadStandardError,
		this,	&KsCaptureMonitor::_printAllStandardError);

	connect(proc,	&QProcess::readyReadStandardOutput,
		this,	&KsCaptureMonitor::_printAllStandardOutput);

	connect(ctrl,	&KsCaptureControl::argsReady,
		this,	&KsCaptureMonitor::_argsReady);

	connect(ctrl,	&KsCaptureControl::print,
		this,	&KsCaptureMonitor::print);
}

void KsCaptureMonitor::_captureStarted()
{
	_name.setText("Terminal output:");
	_readOnlyCB.setCheckState(Qt::Checked);

	QCoreApplication::processEvents();
}

void KsCaptureMonitor::_captureFinished(int exit, QProcess::ExitStatus status)
{
	QProcess *_capture = (QProcess *)sender();

	if (exit != 0 || status != QProcess::NormalExit) {
		QString errMessage("Capture process failed: ");

		errMessage += _capture->errorString();
		_consolOutput.appendPlainText(errMessage);

		QCoreApplication::processEvents();
	}
}

/** Print a message. */
void KsCaptureMonitor::print(const QString &message)
{
	_consolOutput.appendPlainText(message);
}

/** @brief Create KsCaptureDialog widget. */
KsCaptureDialog::KsCaptureDialog(QWidget *parent)
: QWidget(parent),
  _captureCtrl(this),
  _captureMon(this),
  _captureProc(this)
{
	QString captureExe;

	this->setWindowTitle("Capture");
	_layout.addWidget(&_captureCtrl);
	_layout.addWidget(&_captureMon);
	this->setLayout(&_layout);

	connect(&_captureCtrl._commandCheckBox,	&QCheckBox::stateChanged,
		this,				&KsCaptureDialog::_setChannelMode);

	connect(&_captureCtrl._captureButton,	&QPushButton::pressed,
		this,				&KsCaptureDialog::_capture);

	connect(&_captureCtrl._closeButton,	&QPushButton::pressed,
		this,				&KsCaptureDialog::close);

	if (!KsUtils::isInstalled())
		captureExe = QString(_INSTALL_PREFIX) + QString("/bin/trace-cmd");
	else
		captureExe = TRACECMD_EXECUTABLE;

	_captureProc.setProgram(captureExe);

	_captureMon.connectMe(&_captureProc, &_captureCtrl);
}

void KsCaptureDialog::_capture()
{
	QStringList argv;
	int argc;

	if(_captureMon._argsModified) {
		argv = KsUtils::splitArguments(_captureMon.text());
	} else {
		argv = _captureCtrl.getArgs();
	}

	_captureMon.print("\n");
	_captureMon.print(QString("trace-cmd " + argv.join(" ") + "\n"));
	_captureProc.setArguments(argv);
	_captureProc.start();
	_captureProc.waitForFinished();

	/* Reset the _argsModified flag. */
	_captureMon._argsModified = false;

	if (_captureProc.exitCode() != 0 ||
	    _captureProc.exitStatus() != QProcess::NormalExit)
		return;

	/*
	 * Capture finished successfully. Open the produced tracing data file
	 * in KernelShark.
	 */
	argc = argv.count();
	for (int i = 0; i < argc; ++i) {
		if (argv[i] == "-o") {
			_sendOpenReq(argv[i + 1]);
			break;
		}
	}
}

void KsCaptureDialog::_setChannelMode(int state)
{
	if (state > 0) {
		_captureMon._mergedChannels = true;
	} else {
		_captureMon._mergedChannels = false;
	}
}

void KsCaptureDialog::_sendOpenReq(const QString &fileName)
{
	QLocalSocket *socket;

	socket = new QLocalSocket(this);
	socket->connectToServer("KSCapture", QIODevice::WriteOnly);
	if (socket->waitForConnected()) {
		QByteArray block;
		QDataStream out(&block, QIODevice::WriteOnly);
		const QString message = fileName;

		out << quint32(message.size());
		out << message;

		socket->write(block);
		socket->flush();
		socket->disconnectFromServer();
	} else {
		QString error(socket->errorString());

		error += "\n(maybe KernelShark GUI is not open)";
		_captureMon.print(error);
	}
}
