#include "process-config.hpp"
#include "layout-helpers.hpp"
#include "log-helper.hpp"
#include "name-dialog.hpp"

#include <QFileDialog>

namespace advss {

bool ProcessConfig::Save(obs_data_t *obj) const
{
	auto data = obs_data_create();
	_path.Save(data, "path");
	_workingDirectory.Save(data, "workingDirectory");
	_args.Save(data, "args", "arg");
	obs_data_set_obj(obj, "processConfig", data);
	obs_data_release(data);

	return true;
}

bool ProcessConfig::Load(obs_data_t *obj)
{
	// TODO: Remove this fallback in a future version
	if (!obs_data_has_user_value(obj, "processConfig")) {
		_path = obs_data_get_string(obj, "path");
		_workingDirectory =
			obs_data_get_string(obj, "workingDirectory");
		_args.Load(obj, "args", "arg");

		return true;
	}

	auto data = obs_data_get_obj(obj, "processConfig");
	_path.Load(data, "path");
	_workingDirectory.Load(data, "workingDirectory");
	_args.Load(data, "args", "arg");
	obs_data_release(data);

	return true;
}

QStringList ProcessConfig::Args() const
{
	QStringList result;
	for (auto &arg : _args) {
		result << QString::fromStdString(arg);
	}

	return result;
}

bool ProcessConfig::StartProcessDetached() const
{
	return QProcess::startDetached(QString::fromStdString(Path()), Args(),
				       QString::fromStdString(WorkingDir()));
}

void ProcessConfig::ResolveVariables()
{
	_path.ResolveVariables();
	_workingDirectory.ResolveVariables();
	_args.ResolveVariables();
}

std::variant<int, ProcessConfig::ProcStartError>
ProcessConfig::StartProcessAndWait(int timeout)
{
	ResetFinishedProcessData();

	QProcess process;
	process.setWorkingDirectory(QString::fromStdString(WorkingDir()));
	process.start(QString::fromStdString(Path()), Args());
	SetProcessId(QString::number(process.processId()).toStdString());
	vblog(LOG_INFO, "run \"%s\" with a timeout of %d ms", Path().c_str(),
	      timeout);

	if (!process.waitForFinished(timeout)) {
		if (process.error() == QProcess::FailedToStart) {
			vblog(LOG_INFO, "failed to start \"%s\"!",
			      Path().c_str());
			return ProcStartError::FAILED_TO_START;
		}

		SetFinishedProcessData(process);
		vblog(LOG_INFO,
		      "timeout while running \"%s\"\nAttempting to kill process!",
		      Path().c_str());
		process.kill();
		process.waitForFinished();

		return ProcStartError::TIMEOUT;
	}

	SetFinishedProcessData(process);

	if (process.exitStatus() == QProcess::NormalExit) {
		return process.exitCode();
	}

	vblog(LOG_INFO, "process \"%s\" crashed!", Path().c_str());
	return ProcStartError::CRASH;
}

void ProcessConfig::SetFinishedProcessData(QProcess &process)
{
	static const QRegularExpression regex("(\\r\\n|\\r|\\n)$");
	_processExitCode = QString::number(process.exitCode()).toStdString();
	// Qt reads extra newline, at least on Windows, hence the workaround
	_processOutputStream = QString(process.readAllStandardOutput())
				       .remove(regex)
				       .toStdString();
	_processErrorStream = QString(process.readAllStandardError())
				      .remove(regex)
				      .toStdString();
}

void ProcessConfig::ResetFinishedProcessData()
{
	_processExitCode = "";
	_processOutputStream = "";
	_processErrorStream = "";
}

ProcessConfigEdit::ProcessConfigEdit(QWidget *parent)
	: QWidget(parent),
	  _filePath(new FileSelection()),
	  _showAdvancedSettings(new QPushButton(
		  obs_module_text("AdvSceneSwitcher.process.showAdvanced"))),
	  _advancedSettingsLayout(new QVBoxLayout()),
	  _argList(new StringListEdit(
		  this, obs_module_text("AdvSceneSwitcher.process.addArgument"),
		  obs_module_text(
			  "AdvSceneSwitcher.process.addArgumentDescription"),
		  4096)),
	  _workingDirectory(new FileSelection(FileSelection::Type::FOLDER))
{
	_advancedSettingsLayout->setContentsMargins(0, 0, 0, 0);

	QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
			 SLOT(PathChanged(const QString &)));
	QWidget::connect(_showAdvancedSettings, SIGNAL(clicked()), this,
			 SLOT(ShowAdvancedSettingsClicked()));
	QWidget::connect(_argList,
			 SIGNAL(StringListChanged(const StringList &)), this,
			 SLOT(ArgsChanged(const StringList &)));
	QWidget::connect(_workingDirectory,
			 SIGNAL(PathChanged(const QString &)), this,
			 SLOT(WorkingDirectoryChanged(const QString &)));

	auto *entryLayout = new QHBoxLayout;
	std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
		{"{{filePath}}", _filePath},
		{"{{workingDirectory}}", _workingDirectory},
		{"{{advancedSettings}}", _showAdvancedSettings},
	};
	PlaceWidgets(obs_module_text("AdvSceneSwitcher.process.entry"),
		     entryLayout, widgetPlaceholders, false);

	auto workingDirectoryLayout = new QHBoxLayout;
	PlaceWidgets(obs_module_text(
			     "AdvSceneSwitcher.process.entry.workingDirectory"),
		     workingDirectoryLayout, widgetPlaceholders, false);

	_advancedSettingsLayout->addWidget(new QLabel(
		obs_module_text("AdvSceneSwitcher.process.arguments")));
	_advancedSettingsLayout->addWidget(_argList);
	_advancedSettingsLayout->addLayout(workingDirectoryLayout);

	auto mainLayout = new QVBoxLayout;
	mainLayout->setContentsMargins(0, 0, 0, 0);
	mainLayout->addLayout(entryLayout);
	mainLayout->addLayout(_advancedSettingsLayout);
	setLayout(mainLayout);

	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
}

void ProcessConfigEdit::SetProcessConfig(const ProcessConfig &conf)
{
	_conf = conf;
	_filePath->SetPath(conf._path);
	_argList->SetStringList(conf._args);
	_workingDirectory->SetPath(conf._workingDirectory);
	ShowAdvancedSettings(
		!_conf._args.empty() ||
		!_conf._workingDirectory.UnresolvedValue().empty());
}

void ProcessConfigEdit::PathChanged(const QString &text)
{
	_conf._path = text.toStdString();
	emit ConfigChanged(_conf);
}

void ProcessConfigEdit::ShowAdvancedSettingsClicked()
{
	ShowAdvancedSettings(true);
	emit ConfigChanged(_conf); // Just to make sure resizing is handled
}

void ProcessConfigEdit::WorkingDirectoryChanged(const QString &path)
{
	_conf._workingDirectory = path.toStdString();
	emit ConfigChanged(_conf);
}

void ProcessConfigEdit::ArgsChanged(const StringList &args)
{
	_conf._args = args;
	adjustSize();
	updateGeometry();
	emit ConfigChanged(_conf);
}

void ProcessConfigEdit::ShowAdvancedSettings(bool showAdvancedSettings)
{
	SetLayoutVisible(_advancedSettingsLayout, showAdvancedSettings);
	_showAdvancedSettings->setVisible(!showAdvancedSettings);
	adjustSize();
	updateGeometry();
	if (showAdvancedSettings) {
		emit AdvancedSettingsEnabled();
	}
}

} // namespace advss
