#include "macro.hpp"
#include "macro-action-factory.hpp"
#include "macro-condition-factory.hpp"
#include "macro-helpers.hpp"
#include "macro-settings.hpp"
#include "plugin-state-helpers.hpp"
#include "splitter-helpers.hpp"
#include "sync-helpers.hpp"

#include <chrono>
#include <limits>
#undef max
#include <obs-frontend-api.h>
#include <QAction>
#include <QMainWindow>
#include <unordered_map>

namespace advss {

static bool setup()
{
	AddPluginCleanupStep([]() {
		GetTopLevelMacros().clear();
		GetTemporaryMacros().clear();
	});
	return true;
}

static bool setupDone = setup();

Macro::Macro(const std::string &name) : _dockSettings(this)
{
	SetName(name);
}

Macro::Macro(const std::string &name, const GlobalMacroSettings &settings)
	: Macro(name)
{
	if (settings._newMacroRegisterHotkeys) {
		SetupHotkeys();
	}
	_registerHotkeys = settings._newMacroRegisterHotkeys;
	_checkInParallel = settings._newMacroCheckInParallel;
	_useShortCircuitEvaluation =
		settings._newMacroUseShortCircuitEvaluation;
}

Macro::~Macro()
{
	_die = true;
	Stop();
	ClearHotkeys();
}

std::shared_ptr<Macro>
Macro::CreateGroup(const std::string &name,
		   std::vector<std::shared_ptr<Macro>> &children)
{
	auto group = std::make_shared<Macro>(name);
	group->_registerHotkeys = false;
	for (auto &c : children) {
		c->SetParent(group);
	}
	group->_isGroup = true;
	group->_groupSize = children.size();
	return group;
}

void Macro::RemoveGroup(std::shared_ptr<Macro> group)
{
	auto &macros = GetTopLevelMacros();
	auto it = std::find(macros.begin(), macros.end(), group);
	if (it == macros.end()) {
		return;
	}

	auto size = group->GroupSize();
	for (uint32_t i = 1; i <= size; i++) {
		auto m = std::next(it, i);
		(*m)->SetParent(nullptr);
	}

	macros.erase(it);
}

void Macro::PrepareMoveToGroup(Macro *group, std::shared_ptr<Macro> item)
{
	auto &macros = GetTopLevelMacros();
	for (const auto &m : macros) {
		if (m.get() == group) {
			PrepareMoveToGroup(m, item);
			return;
		}
	}
	PrepareMoveToGroup(std::shared_ptr<Macro>(), item);
}

void Macro::PrepareMoveToGroup(std::shared_ptr<Macro> group,
			       std::shared_ptr<Macro> item)
{
	if (!item) {
		return;
	}

	// Potentially remove from old group
	auto oldGroup = item->Parent();
	if (oldGroup) {
		oldGroup->_groupSize--;
	}

	item->SetParent(group);
	if (group) {
		group->_groupSize++;
	}
}

static bool checkCondition(const std::shared_ptr<MacroCondition> &condition)
{
	using namespace std::chrono_literals;
	static constexpr auto perfLogThreshold = 300ms;

	const auto startTime = std::chrono::high_resolution_clock::now();
	bool conditionMatched = false;
	condition->WithLock([&condition, &conditionMatched]() {
		conditionMatched = condition->EvaluateCondition();
	});
	condition->ApplyVarMappings();
	const auto endTime = std::chrono::high_resolution_clock::now();
	const auto timeSpent = endTime - startTime;

	if (timeSpent >= perfLogThreshold) {
		blog(LOG_WARNING,
		     "spent %lld ms in %s condition check of macro '%s'!",
		     (long long)std::chrono::duration_cast<
			     std::chrono::milliseconds>(timeSpent)
			     .count(),
		     condition->GetId().c_str(),
		     condition->GetMacro()->Name().c_str());
	}

	return conditionMatched;
}

bool Macro::CheckConditionHelper(
	const std::shared_ptr<MacroCondition> &condition) const
{
	bool conditionMatched = false;
	bool wasEvaluated = false;

	const auto evaluateCondition = [&condition, &conditionMatched,
					&wasEvaluated]() -> bool {
		conditionMatched = checkCondition(condition);
		conditionMatched =
			condition->CheckDurationModifier(conditionMatched);
		wasEvaluated = true;
		return conditionMatched;
	};

	if (!condition->Enabled()) {
		vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
		      condition->GetId().c_str(), _name.c_str());
		if (!_useShortCircuitEvaluation) {
			(void)evaluateCondition();
		}
		return _matched;
	}

	const auto logicType = condition->GetLogicType();
	if (logicType == Logic::Type::NONE) {
		vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
		      condition->GetId().c_str(), _name.c_str());
		if (!_useShortCircuitEvaluation) {
			(void)evaluateCondition();
		}
		return _matched;
	}

	bool result = _useShortCircuitEvaluation
			      // Evaluate the condition result if needed
			      ? Logic::ApplyConditionLogic(logicType, _matched,
							   evaluateCondition,
							   _name.c_str())
			      // Evaluate the condition result right away
			      : Logic::ApplyConditionLogic(logicType, _matched,
							   evaluateCondition(),
							   _name.c_str());

	const bool isNegativeLogicType = Logic::IsNegationType(logicType);
	if (wasEvaluated && ((conditionMatched && !isNegativeLogicType) ||
			     (!conditionMatched && isNegativeLogicType))) {
		condition->EnableHighlight();
	}

	if (VerboseLoggingEnabled()) {
		if (wasEvaluated) {
			blog(LOG_INFO, "condition %s returned %d",
			     condition->GetId().c_str(), conditionMatched);
		} else {
			blog(LOG_INFO,
			     "condition %s evaluation skipped (short circuit)",
			     condition->GetId().c_str());
		}
	}

	return result;
}

bool Macro::CheckConditions(bool ignorePause)
{
	if (_isGroup) {
		return false;
	}

	const auto checkConditionsTask =
		[this,
		 ignorePause](const std::deque<std::shared_ptr<MacroCondition>>
				      &conditions) {
			for (auto &condition : conditions) {
				if (!condition) {
					continue;
				}

				if (_paused && !ignorePause) {
					vblog(LOG_INFO, "Macro %s is paused",
					      _name.c_str());
					return false;
				}

				_matched = CheckConditionHelper(condition);
			}
			return _matched;
		};

	if (CheckInParallel()) {
		if (!_conditionCheckFuture.valid()) {
			_stop = false;
			_matched = false;
			_conditionCheckFuture = std::async(
				std::launch::async,
				[this, checkConditionsTask]() {
					// Copy to avoid settings modifications
					// causing issues
					const auto conditionsCopy = _conditions;
					checkConditionsTask(conditionsCopy);
				});
			return false;
		}
		if (_conditionCheckFuture.wait_for(std::chrono::seconds(0)) !=
		    std::future_status::ready) {
			vblog(LOG_INFO,
			      "Macro %s still waiting for condition check result",
			      _name.c_str());
			return false;
		}
		_conditionCheckFuture.get();
	} else {
		_stop = false;
		_matched = false;
		_matched = checkConditionsTask(_conditions);
	}

	vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched);

	_actionModeMatch = false;
	switch (_actionTriggerMode) {
	case Macro::ActionTriggerMode::ALWAYS:
		_actionModeMatch = true;
		break;
	case Macro::ActionTriggerMode::MACRO_RESULT_CHANGED:
		_actionModeMatch = _lastMatched != _matched;
		break;
	case Macro::ActionTriggerMode::ANY_CONDITION_CHANGED:
		for (const auto &condition : _conditions) {
			if (condition->HasChanged()) {
				_actionModeMatch = true;
			}
		}
		break;
	case Macro::ActionTriggerMode::ANY_CONDITION_TRIGGERED:
		for (const auto &condition : _conditions) {
			if (condition->IsRisingEdge()) {
				_actionModeMatch = true;
			}
		}
		break;
	default:
		break;
	}

	const bool hasActionsToExecute = _matched ? (_actions.size() > 0)
						  : (_elseActions.size() > 0);
	if (!_actionModeMatch && hasActionsToExecute && !_paused) {
		_lastActionRunModePreventTime =
			std::chrono::high_resolution_clock::now();
	}

	_lastMatched = _matched;
	_lastCheckTime = std::chrono::high_resolution_clock::now();
	return _matched;
}

bool Macro::PerformActions(bool match, bool forceParallel, bool ignorePause)
{
	if (_actionRunFuture.valid() &&
	    _actionRunFuture.wait_for(std::chrono::seconds(0)) !=
		    std::future_status::ready) {
		vblog(LOG_INFO, "Macro %s already running", _name.c_str());

		if (!_stopActionsIfNotDone) {
			return !forceParallel;
		}

		Stop();
		vblog(LOG_INFO, "Stopped macro %s actions to rerun them",
		      _name.c_str());
	}

	std::function<bool(bool)> runFunc =
		match ? std::bind(&Macro::RunActions, this,
				  std::placeholders::_1)
		      : std::bind(&Macro::RunElseActions, this,
				  std::placeholders::_1);
	_stop = false;
	bool ret = true;
	if (_runInParallel || forceParallel) {
		if (_actionRunFuture.valid()) {
			_actionRunFuture.get();
		}
		_actionRunFuture = std::async(std::launch::async,
					      [this, runFunc, ignorePause] {
						      runFunc(ignorePause);
					      });
	} else {
		ret = runFunc(ignorePause);
	}

	_lastExecutionTime = std::chrono::high_resolution_clock::now();
	auto group = _parent.lock();
	if (group) {
		group->_lastExecutionTime = _lastExecutionTime;
	}
	if (_runCount != std::numeric_limits<int>::max()) {
		_runCount++;
	}
	return ret;
}

bool Macro::WasExecutedSince(const TimePoint &time) const
{
	return _lastExecutionTime > time;
}

bool Macro::ActionTriggerModePreventedActionsSince(const TimePoint &time) const
{
	return _lastActionRunModePreventTime > time;
}

Macro::TimePoint Macro::GetLastExecutionTime() const
{
	return _lastExecutionTime;
}

bool Macro::ConditionsShouldBeChecked() const
{
	if (!_useCustomConditionCheckInterval) {
		return true;
	}

	const auto timePassed = std::chrono::high_resolution_clock::now() -
				LastConditionCheckTime();
	const auto timePassedMs =
		std::chrono::duration_cast<std::chrono::milliseconds>(
			timePassed);
	return timePassedMs.count() >=
	       _customConditionCheckInterval.Milliseconds();
}

bool Macro::ShouldRunActions() const
{
	if (CheckInParallel() && _conditionCheckFuture.valid()) {
		vblog(LOG_INFO,
		      "%s not ready to perform actions as condition check is still running",
		      _name.c_str());
		return false;
	}

	const bool hasActionsToExecute =
		!_paused && (_matched || _elseActions.size() > 0) &&
		_actionModeMatch;

	if (VerboseLoggingEnabled() && !_actionModeMatch && !_paused) {
		if (_matched && _actions.size() > 0) {
			blog(LOG_INFO, "skip actions for Macro %s (on change)",
			     _name.c_str());
		}
		if (!_matched && _elseActions.size() > 0) {
			blog(LOG_INFO,
			     "skip else actions for Macro %s (on change)",
			     _name.c_str());
		}
	}

	return hasActionsToExecute;
}

void Macro::SetName(const std::string &name)
{
	_name = name;
	SetHotkeysDesc();
	_dockSettings.HandleMacroNameChange();
}

void Macro::ResetTimers()
{
	for (auto &c : _conditions) {
		c->ResetDuration();
	}
	_lastCheckTime = {};
	_lastExecutionTime = {};
}

void Macro::SetActionTriggerMode(ActionTriggerMode mode)
{
	_actionTriggerMode = mode;
}

Macro::ActionTriggerMode Macro::GetActionTriggerMode() const
{
	return _actionTriggerMode;
}

bool Macro::RunActionsHelper(
	const std::deque<std::shared_ptr<MacroAction>> &actionsToRun,
	bool ignorePause)
{
	if (_paused && !ignorePause) {
		return true;
	}

	// Create copy of action list as elements might be removed, inserted, or
	// reordered while actions are currently being executed.
	auto actions = actionsToRun;

	bool actionsExecutedSuccessfully = true;
	for (auto &action : actions) {
		if (!action) {
			continue;
		}
		if (action->Enabled()) {
			action->LogAction();
			bool actionResult = false;
			action->WithLock([&action, &actionResult]() {
				actionResult = action->PerformAction();
			});
			action->ApplyVarMappings();
			actionsExecutedSuccessfully =
				actionsExecutedSuccessfully && actionResult;
		} else {
			vblog(LOG_INFO, "skipping disabled action %s",
			      action->GetId().c_str());
		}
		if (!actionsExecutedSuccessfully || (_paused && !ignorePause) ||
		    _stop || _die) {
			break;
		}
		if (action->Enabled()) {
			action->EnableHighlight();
		}
	}
	return actionsExecutedSuccessfully;
}

bool Macro::RunActions(bool ignorePause)
{
	mblog(LOG_INFO, "running actions of %s", _name.c_str());
	return RunActionsHelper(_actions, ignorePause);
}

bool Macro::RunElseActions(bool ignorePause)
{
	mblog(LOG_INFO, "running else actions of %s", _name.c_str());
	return RunActionsHelper(_elseActions, ignorePause);
}

bool Macro::WasPausedSince(const TimePoint &time) const
{
	return _lastUnpauseTime > time;
}

void Macro::SetStopActionsIfNotDone(bool stopActionsIfNotDone)
{
	_stopActionsIfNotDone = stopActionsIfNotDone;
}

void Macro::SetShortCircuitEvaluation(bool useShortCircuitEvaluation)
{
	_useShortCircuitEvaluation = useShortCircuitEvaluation;
}

bool Macro::ShortCircuitEvaluationEnabled() const
{
	return _useShortCircuitEvaluation;
}

void Macro::SetCustomConditionCheckIntervalEnabled(bool enable)
{
	_useCustomConditionCheckInterval = enable;
}

bool Macro::CustomConditionCheckIntervalEnabled() const
{
	return _useCustomConditionCheckInterval;
}

void Macro::SetCustomConditionCheckInterval(const Duration &duration)
{
	_customConditionCheckInterval = duration;
}

Duration Macro::GetCustomConditionCheckInterval() const
{
	return _customConditionCheckInterval;
}

void Macro::SetPaused(bool pause)
{
	if (_paused && !pause) {
		_lastUnpauseTime = std::chrono::high_resolution_clock::now();
		ResetTimers();
	}
	_paused = pause;
}

void Macro::AddHelperThread(std::thread &&newThread)
{
	for (unsigned int i = 0; i < _helperThreads.size(); i++) {
		if (!_helperThreads[i].joinable()) {
			_helperThreads[i] = std::move(newThread);
			return;
		}
	}
	_helperThreads.push_back(std::move(newThread));
}

void Macro::SetPauseStateSaveBehavior(PauseStateSaveBehavior behavior)
{
	_pauseSaveBehavior = behavior;
}

Macro::PauseStateSaveBehavior Macro::GetPauseStateSaveBehavior() const
{
	return _pauseSaveBehavior;
}

void Macro::Stop()
{
	_stop = true;
	GetMacroWaitCV().notify_all();
	for (auto &t : _helperThreads) {
		if (t.joinable()) {
			t.join();
		}
	}
	if (_actionRunFuture.valid()) {
		_actionRunFuture.get();
	}
	if (_conditionCheckFuture.valid()) {
		_conditionCheckFuture.get();
	}
}

void Macro::SetCheckInParallel(bool parallel)
{
	_checkInParallel = parallel;
	_conditionCheckFuture = {};
}

bool Macro::ParallelTasksCompleted() const
{
	// A parallel action run might be triggered by RunInParallel() or the
	// "Run Macro" button, so checking just for RunInParallel() will not
	// suffice
	if (!CheckInParallel() && !_actionRunFuture.valid()) {
		return true;
	}
	if (_actionRunFuture.valid()) {
		return false;
	}
	if (CheckInParallel() && _conditionCheckFuture.valid()) {
		return false;
	}
	return true;
}

MacroInputVariables Macro::GetInputVariables() const
{
	return _inputVariables;
}

void Macro::SetInputVariables(const MacroInputVariables &inputVariables)
{
	_inputVariables = inputVariables;
}

std::vector<TempVariable> Macro::GetTempVars(const MacroSegment *filter) const
{
	std::vector<TempVariable> res;

	auto addTempVars = [&res](const std::deque<std::shared_ptr<MacroSegment>>
					  &segments) {
		for (const auto &s : segments) {
			const auto &tempVars = s->_tempVariables;
			res.insert(res.end(), tempVars.begin(), tempVars.end());
		}
	};

	addTempVars({_conditions.begin(), _conditions.end()});
	addTempVars({_actions.begin(), _actions.end()});
	addTempVars({_elseActions.begin(), _elseActions.end()});

	if (!filter) {
		return res;
	}

	auto isCondition = [this](const MacroSegment *segment) -> bool {
		return std::find_if(_conditions.begin(), _conditions.end(),
				    [segment](
					    const std::shared_ptr<MacroSegment>
						    &ptr) {
					    return ptr.get() == segment;
				    }) != _conditions.end();
	};
	auto isAction = [this](const MacroSegment *segment) -> bool {
		return std::find_if(_actions.begin(), _actions.end(),
				    [segment](
					    const std::shared_ptr<MacroSegment>
						    &ptr) {
					    return ptr.get() == segment;
				    }) != _actions.end();
	};
	auto isElseAction = [this](const MacroSegment *segment) -> bool {
		return std::find_if(_elseActions.begin(), _elseActions.end(),
				    [segment](
					    const std::shared_ptr<MacroSegment>
						    &ptr) {
					    return ptr.get() == segment;
				    }) != _elseActions.end();
	};

	const int filterIndex = filter->GetIndex();
	// Remove all actions and else actions and conditions after filterIndex
	if (isCondition(filter)) {
		for (auto it = res.begin(); it != res.end();) {
			auto segment = it->Segment().lock().get();
			if (isCondition(segment) &&
			    segment->GetIndex() >= filterIndex) {
				it = res.erase(it);
				continue;
			}
			if (isAction(segment) || isElseAction(segment)) {
				it = res.erase(it);
				continue;
			}
			++it;
		}
		return res;
	}
	// Remove all else actions and actions after filterIndex
	if (isAction(filter)) {
		for (auto it = res.begin(); it != res.end();) {
			auto segment = it->Segment().lock().get();
			if (isAction(segment) &&
			    segment->GetIndex() >= filterIndex) {
				it = res.erase(it);
				continue;
			}
			if (isElseAction(segment)) {
				it = res.erase(it);
				continue;
			}
			++it;
		}
		return res;
	}
	// Remove all actions and elseActions after filterIndex
	for (auto it = res.begin(); it != res.end();) {
		auto segment = it->Segment().lock().get();
		if (isElseAction(segment) &&
		    segment->GetIndex() >= filterIndex) {
			it = res.erase(it);
			continue;
		}
		if (isAction(segment)) {
			it = res.erase(it);
			continue;
		}
		++it;
	}
	return res;
}

std::optional<const TempVariable> Macro::GetTempVar(const MacroSegment *segment,
						    const std::string &id) const
{
	if (!segment) {
		return {};
	}
	return segment->GetTempVar(id);
}

void Macro::InvalidateTempVarValues() const
{
	auto invalidateHelper =
		[](const std::deque<std::shared_ptr<MacroSegment>> &segments) {
			for (const auto &s : segments) {
				s->InvalidateTempVarValues();
			}
		};

	invalidateHelper({_conditions.begin(), _conditions.end()});
	invalidateHelper({_actions.begin(), _actions.end()});
	invalidateHelper({_elseActions.begin(), _elseActions.end()});
}

std::deque<std::shared_ptr<MacroCondition>> &Macro::Conditions()
{
	return _conditions;
}

const std::deque<std::shared_ptr<MacroCondition>> &Macro::Conditions() const
{
	return _conditions;
}

std::deque<std::shared_ptr<MacroAction>> &Macro::Actions()
{
	return _actions;
}

const std::deque<std::shared_ptr<MacroAction>> &Macro::Actions() const
{
	return _actions;
}

std::deque<std::shared_ptr<MacroAction>> &Macro::ElseActions()
{
	return _elseActions;
}

const std::deque<std::shared_ptr<MacroAction>> &Macro::ElseActions() const
{
	return _elseActions;
}

static void updateIndicesHelper(std::deque<std::shared_ptr<MacroSegment>> &list)
{
	int idx = 0;
	for (auto segment : list) {
		segment->SetIndex(idx);
		idx++;
	}
}

void Macro::UpdateActionIndices()
{
	std::deque<std::shared_ptr<MacroSegment>> list(_actions.begin(),
						       _actions.end());
	updateIndicesHelper(list);
}

void Macro::UpdateElseActionIndices()
{
	std::deque<std::shared_ptr<MacroSegment>> list(_elseActions.begin(),
						       _elseActions.end());
	updateIndicesHelper(list);
}

void Macro::UpdateConditionIndices()
{
	std::deque<std::shared_ptr<MacroSegment>> list(_conditions.begin(),
						       _conditions.end());
	updateIndicesHelper(list);
}

std::shared_ptr<Macro> Macro::Parent() const
{
	return _parent.lock();
}

bool Macro::Save(obs_data_t *obj, bool saveForCopy) const
{
	if (!saveForCopy) {
		obs_data_set_string(obj, "name", _name.c_str());
	}

	obs_data_set_bool(obj, "group", _isGroup);
	if (_isGroup) {
		OBSDataAutoRelease groupData = obs_data_create();
		obs_data_set_bool(groupData, "collapsed", _isCollapsed);
		obs_data_set_int(groupData, "size", _groupSize);
		obs_data_set_obj(obj, "groupData", groupData);
		return true;
	}

	obs_data_set_int(obj, "pauseSaveBehavior",
			 static_cast<int>(_pauseSaveBehavior));
	obs_data_set_bool(obj, "pause", _paused);
	obs_data_set_bool(obj, "parallel", _runInParallel);
	obs_data_set_bool(obj, "checkConditionsInParallel", _checkInParallel);
	obs_data_set_int(obj, "actionTriggerMode",
			 static_cast<int>(_actionTriggerMode));
	obs_data_set_bool(obj, "skipExecOnStart", _skipExecOnStart);
	obs_data_set_bool(obj, "stopActionsIfNotDone", _stopActionsIfNotDone);
	obs_data_set_bool(obj, "useShortCircuitEvaluation",
			  _useShortCircuitEvaluation);
	obs_data_set_bool(obj, "useCustomConditionCheckInterval",
			  _useCustomConditionCheckInterval);
	_customConditionCheckInterval.Save(obj, "customConditionCheckInterval");

	_dockSettings.Save(obj, saveForCopy);

	SaveSplitterPos(_actionConditionSplitterPosition, obj,
			"macroActionConditionSplitterPosition");
	SaveSplitterPos(_elseActionSplitterPosition, obj,
			"macroElseActionSplitterPosition");

	obs_data_set_bool(obj, "registerHotkeys", _registerHotkeys);
	OBSDataArrayAutoRelease pauseHotkey = obs_hotkey_save(_pauseHotkey);
	obs_data_set_array(obj, "pauseHotkey", pauseHotkey);
	OBSDataArrayAutoRelease unpauseHotkey = obs_hotkey_save(_unpauseHotkey);
	obs_data_set_array(obj, "unpauseHotkey", unpauseHotkey);
	OBSDataArrayAutoRelease togglePauseHotkey =
		obs_hotkey_save(_togglePauseHotkey);
	obs_data_set_array(obj, "togglePauseHotkey", togglePauseHotkey);

	OBSDataArrayAutoRelease conditions = obs_data_array_create();
	for (auto &c : _conditions) {
		OBSDataAutoRelease arrayObj = obs_data_create();
		c->Save(arrayObj);
		obs_data_array_push_back(conditions, arrayObj);
	}
	obs_data_set_array(obj, "conditions", conditions);

	OBSDataArrayAutoRelease actions = obs_data_array_create();
	for (auto &a : _actions) {
		OBSDataAutoRelease arrayObj = obs_data_create();
		a->Save(arrayObj);
		obs_data_array_push_back(actions, arrayObj);
	}
	obs_data_set_array(obj, "actions", actions);

	OBSDataArrayAutoRelease elseActions = obs_data_array_create();
	for (auto &a : _elseActions) {
		OBSDataAutoRelease arrayObj = obs_data_create();
		a->Save(arrayObj);
		obs_data_array_push_back(elseActions, arrayObj);
	}
	obs_data_set_array(obj, "elseActions", elseActions);

	_inputVariables.Save(obj);

	return true;
}

bool Macro::Load(obs_data_t *obj)
{
	_name = obs_data_get_string(obj, "name");

	_isGroup = obs_data_get_bool(obj, "group");
	if (_isGroup) {
		OBSDataAutoRelease groupData =
			obs_data_get_obj(obj, "groupData");
		_isCollapsed = obs_data_get_bool(groupData, "collapsed");
		_groupSize = obs_data_get_int(groupData, "size");

		return true;
	}

	_pauseSaveBehavior = static_cast<PauseStateSaveBehavior>(
		obs_data_get_int(obj, "pauseSaveBehavior"));
	switch (_pauseSaveBehavior) {
	case PauseStateSaveBehavior::PERSIST:
		_paused = obs_data_get_bool(obj, "pause");
		break;
	case PauseStateSaveBehavior::PAUSE:
		_paused = true;
		break;
	case PauseStateSaveBehavior::UNPAUSE:
		_paused = false;
		break;
	default:
		_paused = obs_data_get_bool(obj, "pause");
		break;
	}
	_runInParallel = obs_data_get_bool(obj, "parallel");
	_checkInParallel = obs_data_get_bool(obj, "checkConditionsInParallel");
	if (obs_data_has_user_value(obj, "onChange")) {
		const bool onChange = obs_data_get_bool(obj, "onChange");
		_actionTriggerMode =
			onChange ? ActionTriggerMode::MACRO_RESULT_CHANGED
				 : ActionTriggerMode::ALWAYS;
	} else {
		_actionTriggerMode = static_cast<ActionTriggerMode>(
			obs_data_get_int(obj, "actionTriggerMode"));
	}
	_skipExecOnStart = obs_data_get_bool(obj, "skipExecOnStart");
	_stopActionsIfNotDone = obs_data_get_bool(obj, "stopActionsIfNotDone");
	_useShortCircuitEvaluation =
		obs_data_get_bool(obj, "useShortCircuitEvaluation");
	_useCustomConditionCheckInterval =
		obs_data_get_bool(obj, "useCustomConditionCheckInterval");
	_customConditionCheckInterval.Load(obj, "customConditionCheckInterval");

	_dockSettings.Load(obj);

	LoadSplitterPos(_actionConditionSplitterPosition, obj,
			"macroActionConditionSplitterPosition");
	LoadSplitterPos(_elseActionSplitterPosition, obj,
			"macroElseActionSplitterPosition");

	obs_data_set_default_bool(obj, "registerHotkeys", true);
	_registerHotkeys = obs_data_get_bool(obj, "registerHotkeys");
	if (_registerHotkeys) {
		SetupHotkeys();
	}
	OBSDataArrayAutoRelease pauseHotkey =
		obs_data_get_array(obj, "pauseHotkey");
	obs_hotkey_load(_pauseHotkey, pauseHotkey);
	OBSDataArrayAutoRelease unpauseHotkey =
		obs_data_get_array(obj, "unpauseHotkey");
	obs_hotkey_load(_unpauseHotkey, unpauseHotkey);
	OBSDataArrayAutoRelease togglePauseHotkey =
		obs_data_get_array(obj, "togglePauseHotkey");
	obs_hotkey_load(_togglePauseHotkey, togglePauseHotkey);
	SetHotkeysDesc();

	bool root = true;
	OBSDataArrayAutoRelease conditions =
		obs_data_get_array(obj, "conditions");
	size_t count = obs_data_array_count(conditions);
	for (size_t i = 0; i < count; i++) {
		OBSDataAutoRelease arrayObj =
			obs_data_array_item(conditions, i);
		std::string id = obs_data_get_string(arrayObj, "id");
		auto newEntry = MacroConditionFactory::Create(id, this);
		if (newEntry) {
			_conditions.emplace_back(newEntry);
			auto condition = _conditions.back().get();
			condition->WithLock([&]() {
				condition->Load(arrayObj);
				condition->ValidateLogicSelection(
					root, Name().c_str());
			});
		} else {
			blog(LOG_WARNING,
			     "discarding condition entry with unknown id (%s) for macro %s",
			     id.c_str(), _name.c_str());
		}
		root = false;
	}
	UpdateConditionIndices();

	OBSDataArrayAutoRelease actions = obs_data_get_array(obj, "actions");
	count = obs_data_array_count(actions);
	for (size_t i = 0; i < count; i++) {
		OBSDataAutoRelease arrayObj = obs_data_array_item(actions, i);
		std::string id = obs_data_get_string(arrayObj, "id");
		auto newEntry = MacroActionFactory::Create(id, this);
		if (newEntry) {
			_actions.emplace_back(newEntry);
			auto action = _actions.back().get();
			action->WithLock([action, &arrayObj]() {
				action->Load(arrayObj);
			});
		} else {
			blog(LOG_WARNING,
			     "discarding action entry with unknown id (%s) for macro %s",
			     id.c_str(), _name.c_str());
		}
	}
	UpdateActionIndices();

	OBSDataArrayAutoRelease elseActions =
		obs_data_get_array(obj, "elseActions");
	count = obs_data_array_count(elseActions);
	for (size_t i = 0; i < count; i++) {
		OBSDataAutoRelease arrayObj =
			obs_data_array_item(elseActions, i);
		std::string id = obs_data_get_string(arrayObj, "id");
		auto newEntry = MacroActionFactory::Create(id, this);
		if (newEntry) {
			_elseActions.emplace_back(newEntry);
			auto action = _elseActions.back().get();
			action->WithLock([action, &arrayObj]() {
				action->Load(arrayObj);
			});
		} else {
			blog(LOG_WARNING,
			     "discarding elseAction entry with unknown id (%s) for macro %s",
			     id.c_str(), _name.c_str());
		}
	}
	UpdateElseActionIndices();

	_inputVariables.Load(obj);

	return true;
}

bool Macro::PostLoad()
{
	for (auto &c : _conditions) {
		c->WithLock([c]() { c->PostLoad(); });
	}
	for (auto &a : _actions) {
		a->WithLock([a]() { a->PostLoad(); });
	}
	for (auto &a : _elseActions) {
		a->WithLock([a]() { a->PostLoad(); });
	}
	return true;
}

bool Macro::SwitchesScene() const
{
	for (const auto &a : _actions) {
		if (a->GetId() == MacroAction::GetDefaultID()) {
			return true;
		}
	}
	for (const auto &a : _elseActions) {
		if (a->GetId() == MacroAction::GetDefaultID()) {
			return true;
		}
	}
	return false;
}

const QList<int> &Macro::GetActionConditionSplitterPosition() const
{
	return _actionConditionSplitterPosition;
}

void Macro::SetActionConditionSplitterPosition(const QList<int> sizes)
{
	_actionConditionSplitterPosition = sizes;
}

const QList<int> &Macro::GetElseActionSplitterPosition() const
{
	return _elseActionSplitterPosition;
}

void Macro::SetElseActionSplitterPosition(const QList<int> sizes)
{
	_elseActionSplitterPosition = sizes;
}

bool Macro::HasValidSplitterPositions() const
{
	return !_actionConditionSplitterPosition.empty() &&
	       !_elseActionSplitterPosition.empty();
}

void Macro::ResetUIHelpers()
{
	for (auto c : _conditions) {
		c->GetHighlightAndReset();
	}
	for (auto a : _actions) {
		a->GetHighlightAndReset();
	}
}

void Macro::EnablePauseHotkeys(bool value)
{
	if (_registerHotkeys == value) {
		return;
	}

	if (_registerHotkeys) {
		ClearHotkeys();
	} else {
		SetupHotkeys();
	}
	_registerHotkeys = value;
}

bool Macro::PauseHotkeysEnabled() const
{
	return _registerHotkeys;
}

static void pauseCB(void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed)
{
	if (pressed) {
		auto m = static_cast<Macro *>(data);
		m->SetPaused(true);
	}
}

static void unpauseCB(void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed)
{
	if (pressed) {
		auto m = static_cast<Macro *>(data);
		m->SetPaused(false);
	}
}

static void togglePauseCB(void *data, obs_hotkey_id, obs_hotkey_t *,
			  bool pressed)
{
	if (pressed) {
		auto m = static_cast<Macro *>(data);
		m->SetPaused(!m->Paused());
	}
}

static int macroHotkeyID = 0;

static obs_hotkey_id registerHotkeyHelper(const std::string prefix,
					  const char *formatModuleText,
					  Macro *macro, obs_hotkey_func func)
{
	macroHotkeyID++;

	std::string hotkeyName = prefix + std::to_string(macroHotkeyID);
	QString format{obs_module_text(formatModuleText)};
	QString hotkeyDesc = format.arg(QString::fromStdString(macro->Name()));
	return obs_hotkey_register_frontend(hotkeyName.c_str(),
					    hotkeyDesc.toStdString().c_str(),
					    func, macro);
}

void Macro::SetupHotkeys()
{
	if (_pauseHotkey != OBS_INVALID_HOTKEY_ID ||
	    _unpauseHotkey != OBS_INVALID_HOTKEY_ID ||
	    _togglePauseHotkey != OBS_INVALID_HOTKEY_ID) {
		ClearHotkeys();
	}

	_pauseHotkey = registerHotkeyHelper(
		"macro_pause_hotkey_", "AdvSceneSwitcher.hotkey.macro.pause",
		this, pauseCB);
	_unpauseHotkey = registerHotkeyHelper(
		"macro_unpause_hotkey_",
		"AdvSceneSwitcher.hotkey.macro.unpause", this, unpauseCB);
	_togglePauseHotkey = registerHotkeyHelper(
		"macro_toggle_pause_hotkey_",
		"AdvSceneSwitcher.hotkey.macro.togglePause", this,
		togglePauseCB);
}

void Macro::ClearHotkeys() const
{
	obs_hotkey_unregister(_pauseHotkey);
	obs_hotkey_unregister(_unpauseHotkey);
	obs_hotkey_unregister(_togglePauseHotkey);
}

void setHotkeyDescriptionHelper(const char *formatModuleText,
				const std::string name, const obs_hotkey_id id)
{
#ifndef UNIT_TEST
	QString format{obs_module_text(formatModuleText)};
	QString hotkeyDesc = format.arg(QString::fromStdString(name));
	obs_hotkey_set_description(id, hotkeyDesc.toStdString().c_str());
#endif // !UNIT_TEST
}

void Macro::SetHotkeysDesc() const
{
	setHotkeyDescriptionHelper("AdvSceneSwitcher.hotkey.macro.pause", _name,
				   _pauseHotkey);
	setHotkeyDescriptionHelper("AdvSceneSwitcher.hotkey.macro.unpause",
				   _name, _unpauseHotkey);
	setHotkeyDescriptionHelper("AdvSceneSwitcher.hotkey.macro.togglePause",
				   _name, _togglePauseHotkey);
}

void SaveMacros(obs_data_t *obj)
{
	obs_data_array_t *macroArray = obs_data_array_create();
	for (const auto &m : GetTopLevelMacros()) {
		obs_data_t *array_obj = obs_data_create();

		m->Save(array_obj);
		obs_data_array_push_back(macroArray, array_obj);

		obs_data_release(array_obj);
	}
	obs_data_set_array(obj, "macros", macroArray);
	obs_data_array_release(macroArray);
}

void LoadMacros(obs_data_t *obj)
{
	auto &macros = GetTopLevelMacros();
	macros.clear();

	obs_data_array_t *macroArray = obs_data_get_array(obj, "macros");
	size_t count = obs_data_array_count(macroArray);

	for (size_t i = 0; i < count; i++) {
		obs_data_t *array_obj = obs_data_array_item(macroArray, i);
		macros.emplace_back(std::make_shared<Macro>());
		macros.back()->Load(array_obj);
		obs_data_release(array_obj);
	}
	obs_data_array_release(macroArray);

	int groupCount = 0;
	std::shared_ptr<Macro> group;
	std::vector<std::shared_ptr<Macro>> invalidGroups;
	for (const auto &m : macros) {
		if (groupCount && m->IsGroup()) {
			blog(LOG_ERROR,
			     "nested group detected - will delete \"%s\"",
			     m->Name().c_str());
			invalidGroups.emplace_back(m);
			continue;
		}
		if (groupCount) {
			m->SetParent(group);
			groupCount--;
		}
		if (m->IsGroup()) {
			groupCount = m->GroupSize();
			group = m;
		}
		m->PostLoad();
	}

	if (groupCount) {
		blog(LOG_ERROR,
		     "invalid group size detected - will delete \"%s\"",
		     group->Name().c_str());
		invalidGroups.emplace_back(group);
	}

	for (auto &m : invalidGroups) {
		auto it = std::find(macros.begin(), macros.end(), m);
		if (it == macros.end()) {
			continue;
		}
		macros.erase(it);
	}
}

bool CheckMacros()
{
	bool matchFound = false;
	for (const auto &m : GetTopLevelMacros()) {
		if (!m->ConditionsShouldBeChecked()) {
			vblog(LOG_INFO,
			      "skipping condition check for macro \"%s\" "
			      "(custom check interval)",
			      m->Name().c_str());
			continue;
		}

		if (m->CheckConditions() || m->ElseActions().size() > 0) {
			matchFound = true;
			// This has to be performed here for now as actions are
			// not performed immediately after checking conditions.
			if (m->SwitchesScene()) {
				SetMacroSwitchedScene(true);
			}
		}
	}
	return matchFound;
}

bool RunMacros()
{
	// Create copy of macro list as elements might be removed, inserted, or
	// reordered while macros are currently being executed.
	// For example, this can happen if a macro is performing a wait action,
	// as the main lock will be unlocked during this time.
	auto runPhaseMacros = GetTopLevelMacros();

	// Avoid deadlocks when opening settings window and calling frontend
	// API functions at the same time.
	//
	// If the timing is just right, the frontend API call will call
	// QMetaObject::invokeMethod(...) with  Qt::BlockingQueuedConnection
	// while holding the main switcher mutex.
	// But this invokeMethod call itself will be blocked as it is waiting
	// the constructor of AdvSceneSwitcher() to complete.
	// The constructor of AdvSceneSwitcher() cannot continue however as it
	// cannot lock the main switcher mutex.
	auto lock = GetLoopLock();
	if (lock) {
		lock->unlock();
	}

	for (auto &m : runPhaseMacros) {
		if (!m || !m->ShouldRunActions()) {
			continue;
		}
		if (IsFirstInterval() && m->SkipExecOnStart()) {
			blog(LOG_INFO,
			     "skip execution of macro \"%s\" at startup",
			     m->Name().c_str());
			continue;
		}
		vblog(LOG_INFO, "running macro: %s", m->Name().c_str());
		if (!m->PerformActions(m->ConditionsMatched())) {
			blog(LOG_WARNING, "abort macro: %s", m->Name().c_str());
		}
	}
	if (lock) {
		lock->lock();
	}
	return true;
}

void SignalStopAllMacros()
{
	for (const auto &m : GetAllMacros()) {
		m->SignalStop();
	}
}

void WaitForAllMacros()
{
	for (const auto &m : GetAllMacros()) {
		m->Stop();
	}
}

void InvalidateMacroTempVarValues()
{
	for (const auto &m : GetTopLevelMacros()) {
		// Do not invalidate the temp vars set during condition checks
		// or action executions running in parallel to the "main" macro
		// loop, as otherwise access to the information stored in those
		// variables might get lost while those checks or actions are
		// still ongoing.
		if (!m->ParallelTasksCompleted()) {
			continue;
		}
		m->InvalidateTempVarValues();
	}
}

std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval()
{
	auto &macros = GetTopLevelMacros();

	if (macros.empty()) {
		return {};
	}

	for (const auto &macro : macros) {
		if (!macro) {
			continue;
		}
		if (!macro->CustomConditionCheckIntervalEnabled()) {
			continue;
		}

		if (macro->GetCustomConditionCheckInterval().Milliseconds() <
		    GetIntervalValue()) {
			return macro;
		}
	}

	return {};
}

} // namespace advss
