#include "scripts.hpp"
#include "frontend-tools-config.h"
#include "../../properties-view.hpp"

#include <QFileDialog>
#include <QPlainTextEdit>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QScrollBar>
#include <QPushButton>
#include <QFontDatabase>
#include <QFont>
#include <QDialogButtonBox>
#include <QResizeEvent>
#include <QAction>

#include <obs.hpp>
#include <obs-module.h>
#include <obs-frontend-api.h>
#include <obs-scripting.h>

#include <util/config-file.h>
#include <util/platform.h>
#include <util/util.hpp>

#include <string>

#include "ui_scripts.h"

#if COMPILE_PYTHON && (defined(_WIN32) || defined(__APPLE__))
#define PYTHON_UI 1
#else
#define PYTHON_UI 0
#endif

#if ARCH_BITS == 64
#define ARCH_NAME "64bit"
#else
#define ARCH_NAME "32bit"
#endif

#define PYTHONPATH_LABEL_TEXT "PythonSettings.PythonInstallPath" ARCH_NAME

/* ----------------------------------------------------------------- */

using OBSScript = OBSObj<obs_script_t*, obs_script_destroy>;

struct ScriptData {
	std::vector<OBSScript> scripts;

	inline obs_script_t *FindScript(const char *path)
	{
		for (OBSScript &script : scripts) {
			const char *script_path = obs_script_get_path(script);
			if (strcmp(script_path, path) == 0) {
				return script;
			}
		}

		return nullptr;
	}

	bool ScriptOpened(const char *path)
	{
		for (OBSScript &script : scripts) {
			const char *script_path = obs_script_get_path(script);
			if (strcmp(script_path, path) == 0) {
				return true;
			}
		}

		return false;
	}
};

static ScriptData *scriptData = nullptr;
static ScriptsTool *scriptsWindow = nullptr;
static ScriptLogWindow *scriptLogWindow = nullptr;
static QPlainTextEdit *scriptLogWidget = nullptr;

/* ----------------------------------------------------------------- */

ScriptLogWindow::ScriptLogWindow() : QWidget(nullptr)
{
	const QFont fixedFont =
		QFontDatabase::systemFont(QFontDatabase::FixedFont);

	QPlainTextEdit *edit = new QPlainTextEdit();
	edit->setReadOnly(true);
	edit->setFont(fixedFont);
	edit->setWordWrapMode(QTextOption::NoWrap);

	QHBoxLayout *buttonLayout = new QHBoxLayout();
	QPushButton *clearButton = new QPushButton(tr("Clear"));
	connect(clearButton, &QPushButton::clicked,
			this, &ScriptLogWindow::ClearWindow);
	QPushButton *closeButton = new QPushButton(tr("Close"));
	connect(closeButton, &QPushButton::clicked,
			this, &QDialog::hide);

	buttonLayout->addStretch();
	buttonLayout->addWidget(clearButton);
	buttonLayout->addWidget(closeButton);

	QVBoxLayout *layout = new QVBoxLayout();
	layout->addWidget(edit);
	layout->addLayout(buttonLayout);

	setLayout(layout);
	scriptLogWidget = edit;

	resize(600, 400);

	config_t *global_config = obs_frontend_get_global_config();
	const char *geom = config_get_string(global_config,
			"ScriptLogWindow", "geometry");
	if (geom != nullptr) {
		QByteArray ba = QByteArray::fromBase64(QByteArray(geom));
		restoreGeometry(ba);
	}

	setWindowTitle(obs_module_text("ScriptLogWindow"));

	connect(edit->verticalScrollBar(), &QAbstractSlider::sliderMoved,
			this, &ScriptLogWindow::ScrollChanged);
}

ScriptLogWindow::~ScriptLogWindow()
{
	config_t *global_config = obs_frontend_get_global_config();
	config_set_string(global_config,
			"ScriptLogWindow", "geometry",
			saveGeometry().toBase64().constData());
}

void ScriptLogWindow::ScrollChanged(int val)
{
	QScrollBar *scroll = scriptLogWidget->verticalScrollBar();
	bottomScrolled = (val == scroll->maximum());
}

void ScriptLogWindow::resizeEvent(QResizeEvent *event)
{
	QWidget::resizeEvent(event);

	if (bottomScrolled) {
		QScrollBar *scroll = scriptLogWidget->verticalScrollBar();
		scroll->setValue(scroll->maximum());
	}
}

void ScriptLogWindow::AddLogMsg(int log_level, QString msg)
{
	QScrollBar *scroll = scriptLogWidget->verticalScrollBar();
	bottomScrolled = scroll->value() == scroll->maximum();

	lines += QStringLiteral("\n");
	lines += msg;
	scriptLogWidget->setPlainText(lines);

	if (bottomScrolled)
		scroll->setValue(scroll->maximum());

	if (log_level <= LOG_WARNING) {
		show();
		raise();
	}
}

void ScriptLogWindow::ClearWindow()
{
	Clear();
	scriptLogWidget->setPlainText(QString());
}

void ScriptLogWindow::Clear()
{
	lines.clear();
}

/* ----------------------------------------------------------------- */

ScriptsTool::ScriptsTool()
	: QWidget (nullptr),
	  ui      (new Ui_ScriptsTool)
{
	ui->setupUi(this);
	RefreshLists();

#if PYTHON_UI
	config_t *config = obs_frontend_get_global_config();
	const char *path = config_get_string(config, "Python",
			"Path" ARCH_NAME);
	ui->pythonPath->setText(path);
	ui->pythonPathLabel->setText(obs_module_text(PYTHONPATH_LABEL_TEXT));
#else
	delete ui->pythonSettingsTab;
	ui->pythonSettingsTab = nullptr;
#endif

	delete propertiesView;
	propertiesView = new QWidget();
	propertiesView->setSizePolicy(QSizePolicy::Expanding,
			QSizePolicy::Expanding);
	ui->propertiesLayout->addWidget(propertiesView);
}

ScriptsTool::~ScriptsTool()
{
	delete ui;
}

void ScriptsTool::RemoveScript(const char *path)
{
	for (size_t i = 0; i < scriptData->scripts.size(); i++) {
		OBSScript &script = scriptData->scripts[i];

		const char *script_path = obs_script_get_path(script);
		if (strcmp(script_path, path) == 0) {
			scriptData->scripts.erase(
					scriptData->scripts.begin() + i);
			break;
		}
	}
}

void ScriptsTool::ReloadScript(const char *path)
{
	for (OBSScript &script : scriptData->scripts) {
		const char *script_path = obs_script_get_path(script);
		if (strcmp(script_path, path) == 0) {
			obs_script_reload(script);

			OBSData settings = obs_data_create();
			obs_data_release(settings);

			obs_properties_t *prop =
				obs_script_get_properties(script);
			obs_properties_apply_settings(prop, settings);
			obs_properties_destroy(prop);

			break;
		}
	}
}

void ScriptsTool::RefreshLists()
{
	ui->scripts->clear();

	for (OBSScript &script : scriptData->scripts) {
		const char *script_file = obs_script_get_file(script);
		const char *script_path = obs_script_get_path(script);

		QListWidgetItem *item = new QListWidgetItem(script_file);
		item->setData(Qt::UserRole, QString(script_path));
		ui->scripts->addItem(item);
	}
}

void ScriptsTool::on_close_clicked()
{
	close();
}

void ScriptsTool::on_addScripts_clicked()
{
	const char **formats = obs_scripting_supported_formats();
	const char **cur_format = formats;
	QString extensions;
	QString filter;

	while (*cur_format) {
		if (!extensions.isEmpty())
			extensions += QStringLiteral(" ");

		extensions += QStringLiteral("*.");
		extensions += *cur_format;

		cur_format++;
	}

	if (!extensions.isEmpty()) {
		filter += obs_module_text("FileFilter.ScriptFiles");
		filter += QStringLiteral(" (");
		filter += extensions;
		filter += QStringLiteral(")");
	}

	if (filter.isEmpty())
		return;

	static std::string lastBrowsedDir;

	if (lastBrowsedDir.empty()) {
		BPtr<char> baseScriptPath = obs_module_file("scripts");
		lastBrowsedDir = baseScriptPath;
	}

	QFileDialog dlg(this, obs_module_text("AddScripts"));
	dlg.setFileMode(QFileDialog::ExistingFiles);
	dlg.setDirectory(QDir(lastBrowsedDir.c_str()));
	dlg.setNameFilter(filter);
	dlg.exec();

	QStringList files = dlg.selectedFiles();
	if (!files.count())
		return;

	lastBrowsedDir = dlg.directory().path().toUtf8().constData();

	for (const QString &file : files) {
		QByteArray pathBytes = file.toUtf8();
		const char *path = pathBytes.constData();

		if (scriptData->ScriptOpened(path)) {
			continue;
		}

		obs_script_t *script = obs_script_create(path, NULL);
		if (script) {
			const char *script_file = obs_script_get_file(script);

			scriptData->scripts.emplace_back(script);

			QListWidgetItem *item = new QListWidgetItem(script_file);
			item->setData(Qt::UserRole, QString(file));
			ui->scripts->addItem(item);

			OBSData settings = obs_data_create();
			obs_data_release(settings);

			obs_properties_t *prop =
				obs_script_get_properties(script);
			obs_properties_apply_settings(prop, settings);
			obs_properties_destroy(prop);
		}
	}
}

void ScriptsTool::on_removeScripts_clicked()
{
	QList<QListWidgetItem *> items = ui->scripts->selectedItems();

	for (QListWidgetItem *item : items)
		RemoveScript(item->data(Qt::UserRole).toString()
				.toUtf8().constData());
	RefreshLists();
}

void ScriptsTool::on_reloadScripts_clicked()
{
	QList<QListWidgetItem *> items = ui->scripts->selectedItems();
	for (QListWidgetItem *item : items)
		ReloadScript(item->data(Qt::UserRole).toString()
				.toUtf8().constData());

	on_scripts_currentRowChanged(ui->scripts->currentRow());
}

void ScriptsTool::on_scriptLog_clicked()
{
	scriptLogWindow->show();
	scriptLogWindow->raise();
}

void ScriptsTool::on_pythonPathBrowse_clicked()
{
	QString curPath = ui->pythonPath->text();
	QString newPath = QFileDialog::getExistingDirectory(
			this,
			ui->pythonPathLabel->text(),
			curPath);

	if (newPath.isEmpty())
		return;

	QByteArray array = newPath.toUtf8();
	const char *path = array.constData();

	config_t *config = obs_frontend_get_global_config();
	config_set_string(config, "Python", "Path" ARCH_NAME, path);

	ui->pythonPath->setText(newPath);

	if (obs_scripting_python_loaded())
		return;
	if (!obs_scripting_load_python(path))
		return;

	for (OBSScript &script : scriptData->scripts) {
		enum obs_script_lang lang = obs_script_get_lang(script);
		if (lang == OBS_SCRIPT_LANG_PYTHON) {
			obs_script_reload(script);
		}
	}

	on_scripts_currentRowChanged(ui->scripts->currentRow());
}

void ScriptsTool::on_scripts_currentRowChanged(int row)
{
	ui->propertiesLayout->removeWidget(propertiesView);
	delete propertiesView;

	if (row == -1) {
		propertiesView = new QWidget();
		propertiesView->setSizePolicy(QSizePolicy::Expanding,
				QSizePolicy::Expanding);
		ui->propertiesLayout->addWidget(propertiesView);
		ui->description->setText(QString());
		return;
	}

	QByteArray array = ui->scripts->item(row)->data(Qt::UserRole)
		.toString().toUtf8();
	const char *path = array.constData();

	obs_script_t *script = scriptData->FindScript(path);
	if (!script) {
		propertiesView = nullptr;
		return;
	}

	OBSData settings = obs_script_get_settings(script);
	obs_data_release(settings);

	propertiesView = new OBSPropertiesView(settings, script,
			(PropertiesReloadCallback)obs_script_get_properties,
			(PropertiesUpdateCallback)obs_script_update);
	ui->propertiesLayout->addWidget(propertiesView);
	ui->description->setText(obs_script_get_description(script));
}

/* ----------------------------------------------------------------- */

extern "C" void FreeScripts()
{
	obs_scripting_unload();
}

static void obs_event(enum obs_frontend_event event, void *)
{
	if (event == OBS_FRONTEND_EVENT_EXIT) {
		delete scriptData;
		delete scriptsWindow;
		delete scriptLogWindow;

	} else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP) {
		scriptLogWindow->hide();
		scriptLogWindow->Clear();

		delete scriptData;
		scriptData = new ScriptData;
	}
}

static void load_script_data(obs_data_t *load_data, bool, void *)
{
	obs_data_array_t *array = obs_data_get_array(load_data,
			"scripts-tool");

	delete scriptData;
	scriptData = new ScriptData;

	size_t size = obs_data_array_count(array);
	for (size_t i = 0; i < size; i++) {
		obs_data_t *obj = obs_data_array_item(array, i);
		const char *path = obs_data_get_string(obj, "path");
		obs_data_t *settings = obs_data_get_obj(obj, "settings");

		obs_script_t *script = obs_script_create(path, settings);
		if (script) {
			scriptData->scripts.emplace_back(script);
		}

		obs_data_release(settings);
		obs_data_release(obj);
	}

	if (scriptsWindow)
		scriptsWindow->RefreshLists();

	obs_data_array_release(array);
}

static void save_script_data(obs_data_t *save_data, bool saving, void *)
{
	if (!saving)
		return;

	obs_data_array_t *array = obs_data_array_create();

	for (OBSScript &script : scriptData->scripts) {
		const char *script_path = obs_script_get_path(script);
		obs_data_t *settings = obs_script_save(script);

		obs_data_t *obj = obs_data_create();
		obs_data_set_string(obj, "path", script_path);
		obs_data_set_obj(obj, "settings", settings);
		obs_data_array_push_back(array, obj);
		obs_data_release(obj);

		obs_data_release(settings);
	}

	obs_data_set_array(save_data, "scripts-tool", array);
	obs_data_array_release(array);
}

static void script_log(void *, obs_script_t *script, int log_level,
		const char *message)
{
	QString qmsg;

	if (script) {
		qmsg = QStringLiteral("[%1] %2").arg(
				obs_script_get_file(script),
				message);
	} else {
		qmsg = QStringLiteral("[Unknown Script] %1").arg(message);
	}

	QMetaObject::invokeMethod(scriptLogWindow, "AddLogMsg",
			Q_ARG(int, log_level),
			Q_ARG(QString, qmsg));
}

extern "C" void InitScripts()
{
	scriptLogWindow = new ScriptLogWindow();

	obs_scripting_load();
	obs_scripting_set_log_callback(script_log, nullptr);

	QAction *action = (QAction*)obs_frontend_add_tools_menu_qaction(
			obs_module_text("Scripts"));

#if PYTHON_UI
	config_t *config = obs_frontend_get_global_config();
	const char *python_path = config_get_string(config, "Python",
			"Path" ARCH_NAME);

	if (!obs_scripting_python_loaded() && python_path && *python_path)
		obs_scripting_load_python(python_path);
#endif

	scriptData = new ScriptData;

	auto cb = [] ()
	{
		obs_frontend_push_ui_translation(obs_module_get_string);

		if (!scriptsWindow) {
			scriptsWindow = new ScriptsTool();
			scriptsWindow->show();
		} else {
			scriptsWindow->show();
			scriptsWindow->raise();
		}

		obs_frontend_pop_ui_translation();
	};

	obs_frontend_add_save_callback(save_script_data, nullptr);
	obs_frontend_add_preload_callback(load_script_data, nullptr);
	obs_frontend_add_event_callback(obs_event, nullptr);

	action->connect(action, &QAction::triggered, cb);
}