#include "connection-manager.hpp"
#include "layout-helpers.hpp"
#include "macro-export-extensions.hpp"
#include "name-dialog.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"

#include <algorithm>
#include <QAction>
#include <QMenu>

Q_DECLARE_METATYPE(advss::WSConnection *);

namespace advss {

static std::deque<std::shared_ptr<Item>> connections;
static void saveConnections(obs_data_t *obj);
static void loadConnections(obs_data_t *obj);
static bool setup();
static bool setupDone = setup();

bool setup()
{
	AddSaveStep(saveConnections);
	AddLoadStep(loadConnections);
	AddPluginCleanupStep([]() { connections.clear(); });
	AddMacroExportExtension(
		{"AdvSceneSwitcher.macroTab.export.websocketConnections",
		 "websocketConnections",
		 [](obs_data_t *data, const QStringList &selectedIds) {
			 OBSDataArrayAutoRelease array =
				 obs_data_array_create();
			 for (const auto &c : connections) {
				 if (!selectedIds.isEmpty() &&
				     !selectedIds.contains(
					     QString::fromStdString(c->Name())))
					 continue;
				 OBSDataAutoRelease item = obs_data_create();
				 c->Save(item);
				 obs_data_array_push_back(array, item);
			 }
			 obs_data_set_array(data, "websocketConnections",
					    array);
		 },
		 [](obs_data_t *data, const QStringList &) {
			 OBSDataArrayAutoRelease array = obs_data_get_array(
				 data, "websocketConnections");
			 const size_t count = obs_data_array_count(array);
			 for (size_t i = 0; i < count; ++i) {
				 OBSDataAutoRelease item =
					 obs_data_array_item(array, i);
				 auto con = WSConnection::Create();
				 con->Load(item);
				 if (!GetWeakConnectionByName(con->Name())
					      .expired())
					 continue;
				 connections.emplace_back(con);
				 ConnectionSelectionSignalManager::Instance()
					 ->Add(QString::fromStdString(
						 con->Name()));
			 }
		 },
		 []() -> QList<QPair<QString, QString>> {
			 QList<QPair<QString, QString>> items;
			 for (const auto &c : connections) {
				 const QString name =
					 QString::fromStdString(c->Name());
				 items.append({name, name});
			 }
			 return items;
		 }});
	return true;
}

static void saveConnections(obs_data_t *obj)
{
	obs_data_array_t *connectionArray = obs_data_array_create();
	for (const auto &c : connections) {
		obs_data_t *array_obj = obs_data_create();
		c->Save(array_obj);
		obs_data_array_push_back(connectionArray, array_obj);
		obs_data_release(array_obj);
	}
	obs_data_set_array(obj, "websocketConnections", connectionArray);
	obs_data_array_release(connectionArray);
}

static void loadConnections(obs_data_t *obj)
{
	connections.clear();

	obs_data_array_t *connectionArray = nullptr;
	// TODO: remove this fallback at some point in the future
	if (!obs_data_has_user_value(obj, "websocketConnections")) {
		connectionArray = obs_data_get_array(obj, "connections");
	} else {
		connectionArray =
			obs_data_get_array(obj, "websocketConnections");
	}
	size_t count = obs_data_array_count(connectionArray);

	for (size_t i = 0; i < count; i++) {
		obs_data_t *array_obj = obs_data_array_item(connectionArray, i);
		auto con = WSConnection::Create();
		connections.emplace_back(con);
		connections.back()->Load(array_obj);
		obs_data_release(array_obj);
	}
	obs_data_array_release(connectionArray);
}

WSConnection::WSConnection(bool useCustomURI, std::string customURI,
			   std::string name, std::string address, uint64_t port,
			   std::string pass, bool connectOnStart,
			   bool reconnect, int reconnectDelay,
			   bool useOBSWebsocketProtocol)
	: Item(name),
	  _useCustomURI(useCustomURI),
	  _customURI(customURI),
	  _address(address),
	  _port(port),
	  _password(pass),
	  _connectOnStart(connectOnStart),
	  _reconnect(reconnect),
	  _reconnectDelay(reconnectDelay),
	  _useOBSWSProtocol(useOBSWebsocketProtocol),
	  _client(useOBSWebsocketProtocol)
{
}

WSConnection::WSConnection(const WSConnection &other) : Item(other)
{
	_useCustomURI = other._useCustomURI;
	_customURI = other._customURI;
	_name = other._name;
	_address = other._address;
	_port = other._port;
	_password = other._password;
	_connectOnStart = other._connectOnStart;
	_reconnect = other._reconnect;
	_reconnectDelay = other._reconnectDelay;
	_useOBSWSProtocol = other._useOBSWSProtocol;
	_client.UseOBSWebsocketProtocol(_useOBSWSProtocol);
}

WSConnection &WSConnection::operator=(const WSConnection &other)
{
	if (this != &other) {
		_useCustomURI = other._useCustomURI;
		_customURI = other._customURI;
		_name = other._name;
		_address = other._address;
		_port = other._port;
		_password = other._password;
		_connectOnStart = other._connectOnStart;
		_reconnect = other._reconnect;
		_reconnectDelay = other._reconnectDelay;
		_client.UseOBSWebsocketProtocol(_useOBSWSProtocol);
		_useOBSWSProtocol = other._useOBSWSProtocol;
		_client.Disconnect();
	}
	return *this;
}

WSConnection::~WSConnection()
{
	_client.Disconnect();
}

static std::string constructUri(std::string addr, int port)
{
	return "ws://" + addr + ":" + std::to_string(port);
}

std::string WSConnection::GetURI() const
{
	if (_useCustomURI) {
		return _customURI;
	}
	return constructUri(_address, _port);
}

void WSConnection::Reconnect()
{
	_client.Disconnect();
	_client.Connect(GetURI(), _password, _reconnect, _reconnectDelay);
}

void WSConnection::SendMsg(const std::string &msg)
{
	const auto status = _client.GetStatus();
	if (status == WSClientConnection::Status::DISCONNECTED) {
		_client.Connect(GetURI(), _password, _reconnect,
				_reconnectDelay);
		blog(LOG_WARNING,
		     "could not send message '%s' (connection to '%s' not established)",
		     msg.c_str(), GetURI().c_str());
		return;
	}

	if (status == WSClientConnection::Status::AUTHENTICATED) {
		_client.SendRequest(msg);
	}
}

void WSConnection::Load(obs_data_t *obj)
{
	Item::Load(obj);

	if (obs_data_has_user_value(obj, "version")) {
		UseOBSWebsocketProtocol(
			obs_data_get_bool(obj, "useOBSWSProtocol"));
	} else {
		// TODO: Remove this fallback in future version
		_useOBSWSProtocol = true;
	}
	_client.UseOBSWebsocketProtocol(_useOBSWSProtocol);

	_useCustomURI = obs_data_get_bool(obj, "useCustomURI");
	_customURI = obs_data_get_string(obj, "customURI");
	_address = obs_data_get_string(obj, "address");
	_port = obs_data_get_int(obj, "port");
	_password = obs_data_get_string(obj, "password");
	_connectOnStart = obs_data_get_bool(obj, "connectOnStart");
	_reconnect = obs_data_get_bool(obj, "reconnect");
	_reconnectDelay = obs_data_get_int(obj, "reconnectDelay");

	if (_connectOnStart) {
		_client.Connect(GetURI(), _password, _reconnect,
				_reconnectDelay);
	}
}

void WSConnection::Save(obs_data_t *obj) const
{
	Item::Save(obj);
	obs_data_set_bool(obj, "useCustomURI", _useCustomURI);
	obs_data_set_string(obj, "customURI", _customURI.c_str());
	obs_data_set_bool(obj, "useOBSWSProtocol", _useOBSWSProtocol);
	obs_data_set_string(obj, "address", _address.c_str());
	obs_data_set_int(obj, "port", _port);
	obs_data_set_string(obj, "password", _password.c_str());
	obs_data_set_bool(obj, "connectOnStart", _connectOnStart);
	obs_data_set_bool(obj, "reconnect", _reconnect);
	obs_data_set_int(obj, "reconnectDelay", _reconnectDelay);
	obs_data_set_int(obj, "version", 1);
}

WebsocketMessageBuffer WSConnection::RegisterForEvents()
{
	return _client.RegisterForEvents();
}

void WSConnection::UseOBSWebsocketProtocol(bool useOBSWSProtocol)
{
	_useOBSWSProtocol = useOBSWSProtocol;
	_client.UseOBSWebsocketProtocol(useOBSWSProtocol);
}

WSConnection *GetConnectionByName(const QString &name)
{
	return GetConnectionByName(name.toStdString());
}

WSConnection *GetConnectionByName(const std::string &name)
{
	for (auto &con : connections) {
		if (con->Name() == name) {
			return dynamic_cast<WSConnection *>(con.get());
		}
	}
	return nullptr;
}

std::weak_ptr<WSConnection> GetWeakConnectionByName(const std::string &name)
{
	for (const auto &c : connections) {
		if (c->Name() == name) {
			std::weak_ptr<WSConnection> wp =
				std::dynamic_pointer_cast<WSConnection>(c);
			return wp;
		}
	}
	return std::weak_ptr<WSConnection>();
}

std::weak_ptr<WSConnection> GetWeakConnectionByQString(const QString &name)
{
	return GetWeakConnectionByName(name.toStdString());
}

std::string GetWeakConnectionName(std::weak_ptr<WSConnection> connection)
{
	auto con = connection.lock();
	if (!con) {
		return obs_module_text("AdvSceneSwitcher.connection.invalid");
	}
	return con->Name();
}

std::deque<std::shared_ptr<Item>> &GetConnections()
{
	return connections;
}

static bool ConnectionNameAvailable(const QString &name)
{
	return !GetConnectionByName(name);
}

static bool ConnectionNameAvailable(const std::string &name)
{
	return ConnectionNameAvailable(QString::fromStdString(name));
}

static bool AskForSettingsWrapper(QWidget *parent, Item &settings)
{
	WSConnection &ConnectionSettings =
		dynamic_cast<WSConnection &>(settings);
	return WSConnectionSettingsDialog::AskForSettings(parent,
							  ConnectionSettings);
}

WSConnectionSelection::WSConnectionSelection(QWidget *parent)
	: ItemSelection(connections, WSConnection::Create,
			AskForSettingsWrapper,
			"AdvSceneSwitcher.connection.select",
			"AdvSceneSwitcher.connection.add",
			"AdvSceneSwitcher.item.nameNotAvailable",
			"AdvSceneSwitcher.connection.configure", parent)
{
	// Connect to slots
	QWidget::connect(ConnectionSelectionSignalManager::Instance(),
			 SIGNAL(Rename(const QString &, const QString &)), this,
			 SLOT(RenameItem(const QString &, const QString &)));
	QWidget::connect(ConnectionSelectionSignalManager::Instance(),
			 SIGNAL(Add(const QString &)), this,
			 SLOT(AddItem(const QString &)));
	QWidget::connect(ConnectionSelectionSignalManager::Instance(),
			 SIGNAL(Remove(const QString &)), this,
			 SLOT(RemoveItem(const QString &)));

	// Forward signals
	QWidget::connect(this,
			 SIGNAL(ItemRenamed(const QString &, const QString &)),
			 ConnectionSelectionSignalManager::Instance(),
			 SIGNAL(Rename(const QString &, const QString &)));
	QWidget::connect(this, SIGNAL(ItemAdded(const QString &)),
			 ConnectionSelectionSignalManager::Instance(),
			 SIGNAL(Add(const QString &)));
	QWidget::connect(this, SIGNAL(ItemRemoved(const QString &)),
			 ConnectionSelectionSignalManager::Instance(),
			 SIGNAL(Remove(const QString &)));
}

void WSConnectionSelection::SetConnection(const std::string &con)
{
	if (!!GetConnectionByName(con)) {
		SetItem(con);
	} else {
		SetItem("");
	}
}

void WSConnectionSelection::SetConnection(
	const std::weak_ptr<WSConnection> &connection_)
{
	auto connection = connection_.lock();
	if (connection) {
		SetItem(connection->Name());
	} else {
		SetItem("");
	}
}

WSConnectionSettingsDialog::WSConnectionSettingsDialog(
	QWidget *parent, const WSConnection &settings)
	: ItemSettingsDialog(settings, connections,
			     "AdvSceneSwitcher.connection.select",
			     "AdvSceneSwitcher.connection.add",
			     "AdvSceneSwitcher.item.nameNotAvailable", true,
			     parent),
	  _useCustomURI(new QCheckBox()),
	  _customUri(new QLineEdit()),
	  _address(new QLineEdit()),
	  _port(new QSpinBox()),
	  _password(new QLineEdit()),
	  _showPassword(new QPushButton()),
	  _connectOnStart(new QCheckBox()),
	  _reconnect(new QCheckBox()),
	  _reconnectDelay(new QSpinBox()),
	  _useOBSWSProtocol(new QCheckBox()),
	  _test(new QPushButton(
		  obs_module_text("AdvSceneSwitcher.connection.test"))),
	  _status(new QLabel()),
	  _layout(new QGridLayout())
{
	_port->setMaximum(65535);
	_showPassword->setMaximumWidth(22);
	_showPassword->setFlat(true);
	_showPassword->setStyleSheet(
		"QPushButton { background-color: transparent; border: 0px }");
	_reconnectDelay->setMaximum(9999);
	_reconnectDelay->setSuffix("s");

	_useCustomURI->setChecked(settings._useCustomURI);
	_customUri->setText(QString::fromStdString(settings._customURI));
	_address->setText(QString::fromStdString(settings._address));
	_port->setValue(settings._port);
	_password->setText(QString::fromStdString(settings._password));
	_connectOnStart->setChecked(settings._connectOnStart);
	_reconnect->setChecked(settings._reconnect);
	_reconnectDelay->setValue(settings._reconnectDelay);
	_useOBSWSProtocol->setChecked(settings._useOBSWSProtocol);

	QWidget::connect(_useCustomURI, SIGNAL(stateChanged(int)), this,
			 SLOT(UseCustomURIChanged(int)));
	QWidget::connect(_useOBSWSProtocol, SIGNAL(stateChanged(int)), this,
			 SLOT(ProtocolChanged(int)));
	QWidget::connect(_reconnect, SIGNAL(stateChanged(int)), this,
			 SLOT(ReconnectChanged(int)));
	QWidget::connect(_showPassword, SIGNAL(pressed()), this,
			 SLOT(ShowPassword()));
	QWidget::connect(_showPassword, SIGNAL(released()), this,
			 SLOT(HidePassword()));
	QWidget::connect(_test, SIGNAL(clicked()), this,
			 SLOT(TestConnection()));

	int row = 0;
	_layout->addWidget(
		new QLabel(obs_module_text("AdvSceneSwitcher.connection.name")),
		row, 0);
	QHBoxLayout *nameLayout = new QHBoxLayout;
	nameLayout->addWidget(_name);
	nameLayout->addWidget(_nameHint);
	_layout->addLayout(nameLayout, row, 1);
	++row;
	_layout->addWidget(new QLabel(obs_module_text(
				   "AdvSceneSwitcher.connection.useCustomURI")),
			   row, 0);
	_layout->addWidget(_useCustomURI, row, 1);
	++row;
	_layout->addWidget(new QLabel(obs_module_text(
				   "AdvSceneSwitcher.connection.customURI")),
			   row, 0);
	_layout->addWidget(_customUri, row, 1);
	_customURIRow = row;
	++row;
	_layout->addWidget(new QLabel(obs_module_text(
				   "AdvSceneSwitcher.connection.address")),
			   row, 0);
	_layout->addWidget(_address, row, 1);
	_addressRow = row;
	++row;
	_layout->addWidget(
		new QLabel(obs_module_text("AdvSceneSwitcher.connection.port")),
		row, 0);
	_layout->addWidget(_port, row, 1);
	_portRow = row;
	++row;
	_layout->addWidget(new QLabel(obs_module_text(
				   "AdvSceneSwitcher.connection.password")),
			   row, 0);
	auto passLayout = new QHBoxLayout;
	passLayout->addWidget(_password);
	passLayout->addWidget(_showPassword);
	_layout->addLayout(passLayout, row, 1);
	++row;
	_layout->addWidget(
		new QLabel(obs_module_text(
			"AdvSceneSwitcher.connection.connectOnStart")),
		row, 0);
	_layout->addWidget(_connectOnStart, row, 1);
	++row;
	_layout->addWidget(new QLabel(obs_module_text(
				   "AdvSceneSwitcher.connection.reconnect")),
			   row, 0);
	_layout->addWidget(_reconnect, row, 1);
	++row;
	_layout->addWidget(
		new QLabel(obs_module_text(
			"AdvSceneSwitcher.connection.reconnectDelay")),
		row, 0);
	_layout->addWidget(_reconnectDelay, row, 1);
	++row;
	auto label = new QLabel(obs_module_text(
		"AdvSceneSwitcher.connection.useOBSWebsocketProtocol"));
	label->setTextInteractionFlags(Qt::TextBrowserInteraction);
	label->setOpenExternalLinks(true);
	_layout->addWidget(label, row, 0);
	_layout->addWidget(_useOBSWSProtocol, row, 1);
	++row;
	_layout->addWidget(_test, row, 0);
	_layout->addWidget(_status, row, 1);
	++row;
	_layout->addWidget(_buttonbox, row, 0, 1, -1);
	setLayout(_layout);

	MinimizeSizeOfColumn(_layout, 0);
	ReconnectChanged(_reconnect->isChecked());
	ProtocolChanged(_useOBSWSProtocol->isChecked());
	HidePassword();
	UseCustomURIChanged(settings._useCustomURI);
}

void WSConnectionSettingsDialog::UseCustomURIChanged(int state)
{
	SetGridLayoutRowVisible(_layout, _addressRow, !state);
	SetGridLayoutRowVisible(_layout, _portRow, !state);
	SetGridLayoutRowVisible(_layout, _customURIRow, state);

	adjustSize();
	updateGeometry();
}

void WSConnectionSettingsDialog::ProtocolChanged(int state)
{
	_password->setEnabled(state);
	_showPassword->setEnabled(state);
}

void WSConnectionSettingsDialog::ReconnectChanged(int state)
{
	_reconnectDelay->setEnabled(state);
}

void WSConnectionSettingsDialog::SetStatus()
{
	switch (_testConnection.GetStatus()) {
	case WSClientConnection::Status::DISCONNECTED:
		_status->setText(obs_module_text(
			"AdvSceneSwitcher.connection.status.disconnected"));
		break;
	case WSClientConnection::Status::CONNECTING:
		_status->setText(obs_module_text(
			"AdvSceneSwitcher.connection.status.connecting"));
		break;
	case WSClientConnection::Status::CONNECTED:
		_status->setText(obs_module_text(
			"AdvSceneSwitcher.connection.status.connected"));
		break;
	case WSClientConnection::Status::AUTHENTICATED:
		_status->setText(obs_module_text(
			"AdvSceneSwitcher.connection.status.authenticated"));
		break;
	default:
		break;
	}
}

void WSConnectionSettingsDialog::ShowPassword()
{
	SetButtonIcon(_showPassword, GetThemeTypeName() == "Light"
					     ? ":res/images/visible.svg"
					     : "theme:Dark/visible.svg");
	_password->setEchoMode(QLineEdit::Normal);
}

void WSConnectionSettingsDialog::HidePassword()
{
	SetButtonIcon(_showPassword, ":res/images/invisible.svg");
	_password->setEchoMode(QLineEdit::PasswordEchoOnEdit);
}

void WSConnectionSettingsDialog::TestConnection()
{
	_testConnection.UseOBSWebsocketProtocol(_useOBSWSProtocol->isChecked());
	_testConnection.Disconnect();
	std::string uri = _useCustomURI->isChecked()
				  ? _customUri->text().toStdString()
				  : constructUri(_address->text().toStdString(),
						 _port->value());
	_testConnection.Connect(uri, _password->text().toStdString(), false);
	_statusTimer.setInterval(1000);
	QWidget::connect(&_statusTimer, &QTimer::timeout, this,
			 &WSConnectionSettingsDialog::SetStatus);
	_statusTimer.start();
}

bool WSConnectionSettingsDialog::AskForSettings(QWidget *parent,
						WSConnection &settings)
{
	WSConnectionSettingsDialog dialog(parent, settings);
	dialog.setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
	if (dialog.exec() != DialogCode::Accepted) {
		return false;
	}

	settings._name = dialog._name->text().toStdString();
	settings._useCustomURI = dialog._useCustomURI->isChecked();
	settings._customURI = dialog._customUri->text().toStdString();
	settings._address = dialog._address->text().toStdString();
	settings._port = dialog._port->value();
	settings._password = dialog._password->text().toStdString();
	settings._connectOnStart = dialog._connectOnStart->isChecked();
	settings._reconnect = dialog._reconnect->isChecked();
	settings._reconnectDelay = dialog._reconnectDelay->value();
	settings.UseOBSWebsocketProtocol(dialog._useOBSWSProtocol->isChecked());
	settings.Reconnect();
	return true;
}

ConnectionSelectionSignalManager::ConnectionSelectionSignalManager(
	QObject *parent)
	: QObject(parent)
{
}

ConnectionSelectionSignalManager *ConnectionSelectionSignalManager::Instance()
{
	static ConnectionSelectionSignalManager manager;
	return &manager;
}

} // namespace advss
