#include "macro-condition-stream-deck.hpp"
#include "layout-helpers.hpp"
#include "websocket-api.hpp"

#include <obs.hpp>

namespace advss {

const std::string MacroConditionStreamdeck::id = "streamdeck";

bool MacroConditionStreamdeck::_registered = MacroConditionFactory::Register(
	MacroConditionStreamdeck::id,
	{MacroConditionStreamdeck::Create, MacroConditionStreamdeckEdit::Create,
	 "AdvSceneSwitcher.condition.streamDeck"});

constexpr char streamDeckMessage[] = "StreamDeckKeyEvent";

static StreamDeckMessageDispatcher messageDispatcher;

static bool setup();
static bool setupDone = setup();
static void receiveStreamDeckMessage(obs_data_t *request_data, obs_data_t *);

bool setup()
{
	RegisterWebsocketRequest(streamDeckMessage, receiveStreamDeckMessage);
	return true;
}

static void printParseError(obs_data_t *data)
{
	blog(LOG_INFO, "received unexpected stream deck message: %s",
	     obs_data_get_json(data) ? obs_data_get_json(data) : "''");
}

void receiveStreamDeckMessage(obs_data_t *request_data, obs_data_t *)
{
	StreamDeckMessage message;
	OBSDataAutoRelease data = obs_data_get_obj(request_data, "data");
	if (!data) {
		printParseError(request_data);
		return;
	}

	if (!obs_data_has_user_value(data, "isKeyDownEvent")) {
		printParseError(request_data);
		return;
	}
	message.keyDown = obs_data_get_bool(data, "isKeyDownEvent");

	OBSDataAutoRelease coordinates = obs_data_get_obj(data, "coordinates");
	if (!coordinates) {
		printParseError(request_data);
		return;
	}

	message.row = obs_data_get_int(coordinates, "row");
	message.column = obs_data_get_int(coordinates, "column");

	OBSDataAutoRelease settings = obs_data_get_obj(data, "settings");
	if (!settings) {
		printParseError(request_data);
		return;
	}
	message.data = obs_data_get_string(settings, "data");

	messageDispatcher.DispatchMessage(message);
}

StreamDeckMessageBuffer RegisterForStreamDeckMessages()
{
	return messageDispatcher.RegisterClient();
}

MacroConditionStreamdeck::MacroConditionStreamdeck(Macro *m)
	: MacroCondition(m, true)
{
	_messageBuffer = RegisterForStreamDeckMessages();
}

bool MacroConditionStreamdeck::MessageMatches(const StreamDeckMessage &message)
{
	const bool keyStateMatches = !_pattern.checkKeyState ||
				     ((message.keyDown && _pattern.keyDown) ||
				      (!message.keyDown && !message.keyDown));
	const bool positionMatches = !_pattern.checkPosition ||
				     (message.row == _pattern.row &&
				      message.column == _pattern.column);
	const bool dataMatches =
		!_pattern.checkData ||
		(_pattern.regex.Enabled()
			 ? _pattern.regex.Matches(message.data, _pattern.data)
			 : message.data == std::string(_pattern.data));
	return keyStateMatches && positionMatches && dataMatches;
}

bool MacroConditionStreamdeck::CheckCondition()
{
	while (!_messageBuffer->Empty()) {
		auto message = _messageBuffer->ConsumeMessage();
		if (!message) {
			continue;
		}

		if (!message->keyDown) {
			_lastMatchingKeyIsStillPressed = false;
		}

		if (MessageMatches(*message)) {
			_lastMatchingKeyIsStillPressed = message->keyDown;
			SetTempVarValues(*message);
			return true;
		}
	}

	if (_lastMatchingKeyIsStillPressed) {
		return true;
	}

	return false;
}

bool MacroConditionStreamdeck::Save(obs_data_t *obj) const
{
	MacroCondition::Save(obj);
	_pattern.Save(obj);
	return true;
}

bool MacroConditionStreamdeck::Load(obs_data_t *obj)
{
	MacroCondition::Load(obj);
	_pattern.Load(obj);
	return true;
}

void MacroConditionStreamdeck::StreamDeckMessagePattern::Save(
	obs_data_t *obj) const
{
	OBSDataAutoRelease dataObj = obs_data_create();
	obs_data_set_bool(dataObj, "checkKeyState", checkKeyState);
	obs_data_set_bool(dataObj, "keyDown", keyDown);
	obs_data_set_bool(dataObj, "checkPosition", checkPosition);
	row.Save(dataObj, "row");
	column.Save(dataObj, "column");
	obs_data_set_bool(dataObj, "checkData", checkData);
	data.Save(dataObj, "data");
	regex.Save(dataObj);
	obs_data_set_obj(obj, "pattern", dataObj);
}

void MacroConditionStreamdeck::StreamDeckMessagePattern::Load(obs_data_t *obj)
{
	OBSDataAutoRelease dataObj = obs_data_get_obj(obj, "pattern");
	checkKeyState = obs_data_get_bool(dataObj, "checkKeyState");
	keyDown = obs_data_get_bool(dataObj, "keyDown");
	checkPosition = obs_data_get_bool(dataObj, "checkPosition");
	row.Load(dataObj, "row");
	column.Load(dataObj, "column");
	checkData = obs_data_get_bool(dataObj, "checkData");
	data.Load(dataObj, "data");
	regex.Load(dataObj);
	obs_data_set_obj(obj, "pattern", dataObj);
}

void MacroConditionStreamdeck::SetTempVarValues(const StreamDeckMessage &message)
{
	SetTempVarValue("keyPressed", std::to_string(message.keyDown));
	SetTempVarValue("row", std::to_string(message.row));
	SetTempVarValue("column", std::to_string(message.column));
	SetTempVarValue("data", message.data);
}

void MacroConditionStreamdeck::SetupTempVars()
{
	AddTempvar(
		"keyPressed",
		obs_module_text(
			"AdvSceneSwitcher.tempVar.streamDeck.keyPressed"),
		obs_module_text(
			"AdvSceneSwitcher.tempVar.streamDeck.keyPressed.description"));
	AddTempvar("row",
		   obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.row"));
	AddTempvar(
		"column",
		obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.column"));
	AddTempvar(
		"data",
		obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.data"),
		obs_module_text(
			"AdvSceneSwitcher.tempVar.streamDeck.data.description"));
}

MacroConditionStreamdeckEdit::MacroConditionStreamdeckEdit(
	QWidget *parent, std::shared_ptr<MacroConditionStreamdeck> entryData)
	: QWidget(parent),
	  _checkKeyState(new QCheckBox(obs_module_text(
		  "AdvSceneSwitcher.condition.streamDeck.checkKeyState"))),
	  _keyState(new QComboBox(this)),
	  _checkPosition(new QCheckBox(obs_module_text(
		  "AdvSceneSwitcher.condition.streamDeck.checkKeyPosition"))),
	  _row(new VariableSpinBox(this)),
	  _column(new VariableSpinBox(this)),
	  _checkData(new QCheckBox(obs_module_text(
		  "AdvSceneSwitcher.condition.streamDeck.checkData"))),
	  _data(new VariableTextEdit(this)),
	  _regex(new RegexConfigWidget(this)),
	  _listen(new QPushButton(
		  obs_module_text(
			  "AdvSceneSwitcher.condition.streamDeck.startListen"),
		  this))
{
	_keyState->addItem(
		obs_module_text(
			"AdvSceneSwitcher.condition.streamDeck.keyState.pressed"),
		true);
	_keyState->addItem(
		obs_module_text(
			"AdvSceneSwitcher.condition.streamDeck.keyState.released"),
		false);

	connect(_checkKeyState, &QCheckBox::stateChanged, this,
		&MacroConditionStreamdeckEdit::CheckKeyStateChanged);
	connect(_keyState, QOverload<int>::of(&QComboBox::currentIndexChanged),
		this, &MacroConditionStreamdeckEdit::KeyStateChanged);
	connect(_checkPosition, &QCheckBox::stateChanged, this,
		&MacroConditionStreamdeckEdit::CheckPositionChanged);
	connect(_row,
		static_cast<void (VariableSpinBox::*)(const IntVariable &)>(
			&VariableSpinBox::NumberVariableChanged),
		this, &MacroConditionStreamdeckEdit::RowChanged);
	connect(_column,
		static_cast<void (VariableSpinBox::*)(const IntVariable &)>(
			&VariableSpinBox::NumberVariableChanged),
		this, &MacroConditionStreamdeckEdit::ColumnChanged);
	connect(_checkData, &QCheckBox::stateChanged, this,
		&MacroConditionStreamdeckEdit::CheckDataChanged);
	connect(_regex, &RegexConfigWidget::RegexConfigChanged, this,
		&MacroConditionStreamdeckEdit::RegexChanged);
	connect(_data, &VariableTextEdit::textChanged, this,
		&MacroConditionStreamdeckEdit::DataChanged);
	connect(_listen, &QPushButton::clicked, this,
		&MacroConditionStreamdeckEdit::ListenClicked);

	auto keyStateLayout = new QHBoxLayout();
	keyStateLayout->addWidget(_checkKeyState);
	keyStateLayout->addWidget(_keyState);
	keyStateLayout->addStretch();

	auto positionLayout = new QHBoxLayout();
	positionLayout->addWidget(_row);
	positionLayout->addWidget(_column);

	auto dataLayout = new QHBoxLayout();
	dataLayout->addWidget(_checkData);
	dataLayout->addWidget(_regex);
	dataLayout->addStretch();

	auto layout = new QVBoxLayout();
	layout->addLayout(keyStateLayout);
	layout->addWidget(_checkPosition);
	layout->addLayout(positionLayout);
	layout->addLayout(dataLayout);
	layout->addWidget(_data);
	layout->addWidget(_listen);
	layout->addWidget(new QLabel(obs_module_text(
		"AdvSceneSwitcher.condition.streamDeck.pluginDownload")));
	setLayout(layout);

	_entryData = entryData;
	UpdateEntryData();
	_loading = false;
}

void MacroConditionStreamdeckEdit::UpdateEntryData()
{
	if (!_entryData) {
		return;
	}

	_checkKeyState->setChecked(_entryData->_pattern.checkKeyState);
	_keyState->setCurrentIndex(
		_keyState->findData(_entryData->_pattern.keyDown));
	_checkPosition->setChecked(_entryData->_pattern.checkPosition);
	_row->SetValue(_entryData->_pattern.row);
	_column->SetValue(_entryData->_pattern.column);
	_checkData->setChecked(_entryData->_pattern.checkData);
	_data->setPlainText(_entryData->_pattern.data);
	_regex->SetRegexConfig(_entryData->_pattern.regex);

	SetWidgetVisibility();
}

void MacroConditionStreamdeckEdit::CheckKeyStateChanged(int state)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_pattern.checkKeyState = state;
	SetWidgetVisibility();
}

void MacroConditionStreamdeckEdit::KeyStateChanged(int index)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_pattern.keyDown = _keyState->itemData(index).toBool();
}

void MacroConditionStreamdeckEdit::CheckPositionChanged(int state)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_pattern.checkPosition = state;
	SetWidgetVisibility();
}

void MacroConditionStreamdeckEdit::RowChanged(const IntVariable &value)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_pattern.row = value;
}

void MacroConditionStreamdeckEdit::ColumnChanged(const IntVariable &value)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_pattern.column = value;
}

void MacroConditionStreamdeckEdit::CheckDataChanged(int state)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_pattern.checkData = state;
	SetWidgetVisibility();
}

void MacroConditionStreamdeckEdit::RegexChanged(const RegexConfig &regex)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_pattern.regex = regex;
}

void MacroConditionStreamdeckEdit::DataChanged()
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_pattern.data = _data->toPlainText().toStdString();
}

void MacroConditionStreamdeckEdit::ListenClicked()
{
	_isListening = !_isListening;

	_keyState->setDisabled(_isListening);
	_row->setDisabled(_isListening);
	_column->setDisabled(_isListening);
	_data->setDisabled(_isListening);
	_regex->setDisabled(_isListening);

	if (_isListening) {
		_messageBuffer = RegisterForStreamDeckMessages();
		_listen->setText(obs_module_text(
			"AdvSceneSwitcher.condition.streamDeck.stopListen"));
	} else {
		_updateListenSettings.stop();
		_messageBuffer.reset();
		_listen->setText(obs_module_text(
			"AdvSceneSwitcher.condition.streamDeck.startListen"));
		return;
	}

	connect(&_updateListenSettings, &QTimer::timeout, this, [this]() {
		if (_messageBuffer->Empty()) {
			return;
		}

		StreamDeckMessage lastMessageInBuffer;
		while (!_messageBuffer->Empty()) {
			auto message = _messageBuffer->ConsumeMessage();
			if (!message) {
				continue;
			}
			lastMessageInBuffer = *message;
		}

		_keyState->setCurrentIndex(
			_keyState->findData(lastMessageInBuffer.keyDown));
		_row->SetFixedValue(lastMessageInBuffer.row);
		_column->SetFixedValue(lastMessageInBuffer.column);
		_data->setPlainText(lastMessageInBuffer.data);
	});

	_updateListenSettings.start(300);
}

void MacroConditionStreamdeckEdit::SetWidgetVisibility()
{
	_keyState->setVisible(_checkKeyState->isChecked());
	_row->setVisible(_checkPosition->isChecked());
	_column->setVisible(_checkPosition->isChecked());
	_data->setVisible(_checkData->isChecked());
	_regex->setVisible(_checkData->isChecked());

	adjustSize();
	updateGeometry();
}

} // namespace advss
