#include <QMainWindow>
#include <QAction>
#include <QFileDialog>
#include <QDirIterator>
#include <regex>
#include <filesystem>

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

#include "advanced-scene-switcher.hpp"
#include "status-control.hpp"
#include "curl-helper.hpp"
#include "utility.hpp"
#include "version.h"

const char *obs_module_text(const char *text)
{
	return switcher->translate(text);
}

obs_module_t *obs_current_module()
{
	return switcher->modulePtr;
}

SwitcherData *switcher = nullptr;
SwitcherData *GetSwitcher()
{
	return switcher;
}

AdvSceneSwitcher *AdvSceneSwitcher::window = nullptr;

/******************************************************************************
 * Create the Advanced Scene Switcher settings window
 ******************************************************************************/
AdvSceneSwitcher::AdvSceneSwitcher(QWidget *parent)
	: QDialog(parent), ui(new Ui_AdvSceneSwitcher)
{
	switcher->settingsWindowOpened = true;
	ui->setupUi(this);

	std::lock_guard<std::mutex> lock(switcher->m);

	switcher->Prune();
	loadUI();
}

AdvSceneSwitcher::~AdvSceneSwitcher()
{
	if (switcher) {
		switcher->settingsWindowOpened = false;
		switcher->lastOpenedTab = ui->tabWidget->currentIndex();
	}
}

bool translationAvailable()
{
	return !!strcmp(obs_module_text("AdvSceneSwitcher.pluginName"),
			"AdvSceneSwitcher.pluginName");
}

void AdvSceneSwitcher::loadUI()
{
	if (!translationAvailable()) {
		QString msg = "Failed to find plug-in's 'data' directory.\n"
			      "Please check installation instructions!\n\n"
			      "Data most likely expected at:\n\n";
#ifdef _WIN32
		msg += QString::fromStdString(
			(std::filesystem::current_path().string()));
		msg += "/";
#endif
		msg += obs_get_module_data_path(obs_current_module());
		(void)DisplayMessage(msg);
	}

	setupGeneralTab();
	setupTitleTab();
	setupExecutableTab();
	setupRegionTab();
	setupPauseTab();
	setupSequenceTab();
	setupTransitionsTab();
	setupIdleTab();
	setupRandomTab();
	setupMediaTab();
	setupFileTab();
	setupTimeTab();
	setupAudioTab();
	setupVideoTab();
	setupNetworkTab();
	setupSceneGroupTab();
	setupTriggerTab();
	setupMacroTab();

	setDeprecationWarnings();
	setTabOrder();
	setCurrentTab();
	restoreWindowGeo();
	checkFirstTimeSetup();

	loading = false;
}

/******************************************************************************
 * Saving and loading
 ******************************************************************************/
static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *)
{
	if (saving) {
		std::lock_guard<std::mutex> lock(switcher->m);
		switcher->Prune();
		obs_data_t *obj = obs_data_create();
		switcher->saveSettings(obj);
		obs_data_set_obj(save_data, "advanced-scene-switcher", obj);
		obs_data_release(obj);
	} else {
		// Stop the scene switcher at least once to
		// avoid scene duplication issues with scene collection changes
		switcher->Stop();

		switcher->m.lock();
		obs_data_t *obj =
			obs_data_get_obj(save_data, "advanced-scene-switcher");
		if (!obj) {
			obj = obs_data_create();
		}
		if (switcher->versionChanged(obj, g_GIT_SHA1)) {
			AskForBackup(obj);
		}
		switcher->loadSettings(obj);
		obs_data_release(obj);
		switcher->m.unlock();

		if (!switcher->stop) {
			switcher->Start();
		}
	}
}

/******************************************************************************
 * Main switcher thread
 ******************************************************************************/
void SwitcherData::Thread()
{
	blog(LOG_INFO, "started");
	int sleep = 0;
	int linger = 0;
	std::chrono::milliseconds duration;
	auto startTime = std::chrono::high_resolution_clock::now();
	auto endTime = std::chrono::high_resolution_clock::now();
	switcher->firstIntervalAfterStop = true;

	while (true) {
		std::unique_lock<std::mutex> lock(m);
		mainLoopLock = &lock;

		bool match = false;
		OBSWeakSource scene;
		OBSWeakSource transition;
		// The previous scene might have changed during the linger duration,
		// if a longer transition is used than the configured check interval
		bool setPrevSceneAfterLinger = false;
		bool macroMatch = false;
		macroSceneSwitched = false;
		endTime = std::chrono::high_resolution_clock::now();
		auto runTime =
			std::chrono::duration_cast<std::chrono::milliseconds>(
				endTime - startTime);

		if (sleep) {
			duration = std::chrono::milliseconds(sleep);
		} else {
			duration = std::chrono::milliseconds(interval) +
				   std::chrono::milliseconds(linger) - runTime;
			if (duration.count() < 1) {
				blog(LOG_INFO,
				     "detected busy loop - refusing to sleep less than 1ms");
				duration = std::chrono::milliseconds(50);
			}
		}

		vblog(LOG_INFO, "try to sleep for %ld", duration.count());
		setWaitScene();
		cv.wait_for(lock, duration);

		startTime = std::chrono::high_resolution_clock::now();
		sleep = 0;
		linger = 0;

		Prune();
		if (stop) {
			break;
		}
		if (checkPause()) {
			continue;
		}
		setPreconditions();
		match = checkForMatch(scene, transition, linger,
				      setPrevSceneAfterLinger, macroMatch);
		if (stop) {
			break;
		}
		checkNoMatchSwitch(match, scene, transition, sleep);
		checkSwitchCooldown(match);

		if (linger) {
			duration = std::chrono::milliseconds(linger);
			vblog(LOG_INFO, "sleep for %ld before switching scene",
			      duration.count());

			setWaitScene();
			cv.wait_for(lock, duration);

			if (stop) {
				break;
			}

			if (sceneChangedDuringWait()) {
				vblog(LOG_INFO,
				      "scene was changed manually - ignoring match");

				match = false;
				linger = 0;
			} else if (setPrevSceneAfterLinger) {
				scene = previousScene;
			}
		}

		ClearWebsocketMessages();

		if (match) {
			if (macroMatch) {
				runMacros();
			} else {
				switchScene({scene, transition, 0});
			}
		}

		writeSceneInfoToFile();
		switcher->firstInterval = false;
		switcher->firstIntervalAfterStop = false;
	}

	mainLoopLock = nullptr;
	blog(LOG_INFO, "stopped");
}

void SwitcherData::setPreconditions()
{
	// Window title
	lastTitle = currentTitle;
	std::string title;
	GetCurrentWindowTitle(title);
	for (auto &window : ignoreWindowsSwitches) {
		bool equals = (title == window);
		bool matches = false;
		if (!equals) {
			try {
				std::regex expr(window);
				matches = std::regex_match(title, expr);

			} catch (const std::regex_error &) {
			}
		}
		if (equals || matches) {
			title = lastTitle;
			break;
		}
	}
	currentTitle = title;

	// Process name
	GetForegroundProcessName(currentForegroundProcess);

	// Cursor
	std::pair<int, int> cursorPos = getCursorPos();
	cursorPosChanged = cursorPos.first != switcher->lastCursorPos.first ||
			   cursorPos.second != switcher->lastCursorPos.second;
	lastCursorPos = getCursorPos();
}

bool SwitcherData::checkForMatch(OBSWeakSource &scene,
				 OBSWeakSource &transition, int &linger,
				 bool &setPrevSceneAfterLinger,
				 bool &macroMatch)
{
	bool match = false;

	if (uninterruptibleSceneSequenceActive) {
		match = checkSceneSequence(scene, transition, linger,
					   setPrevSceneAfterLinger);
		if (match) {
			return match;
		}
	}

	for (int switchFuncName : functionNamesByPriority) {
		switch (switchFuncName) {
		case read_file_func:
			match = checkSwitchInfoFromFile(scene, transition) ||
				checkFileContent(scene, transition);
			break;
		case idle_func:
			match = checkIdleSwitch(scene, transition);
			break;
		case exe_func:
			match = checkExeSwitch(scene, transition);
			break;
		case screen_region_func:
			match = checkScreenRegionSwitch(scene, transition);
			break;
		case window_title_func:
			match = checkWindowTitleSwitch(scene, transition);
			break;
		case round_trip_func:
			match = checkSceneSequence(scene, transition, linger,
						   setPrevSceneAfterLinger);
			break;
		case media_func:
			match = checkMediaSwitch(scene, transition);
			break;
		case time_func:
			match = checkTimeSwitch(scene, transition);
			break;
		case audio_func:
			match = checkAudioSwitch(scene, transition);
			break;
		case video_func:
			match = checkVideoSwitch(scene, transition);
			break;
		case macro_func:
			if (checkMacros()) {
				match = true;
				macroMatch = true;
			}
			break;
		}

		if (stop) {
			return false;
		}
		if (match) {
			break;
		}
	}
	return match;
}

void switchScene(const sceneSwitchInfo &sceneSwitch, bool force)
{
	if (!sceneSwitch.scene && switcher->verbose) {
		blog(LOG_INFO, "nothing to switch to");
		return;
	}

	obs_source_t *source = obs_weak_source_get_source(sceneSwitch.scene);
	obs_source_t *currentSource = obs_frontend_get_current_scene();

	if (source && (source != currentSource || force)) {
		transitionData currentTransitionData;
		setNextTransition(sceneSwitch, currentSource,
				  currentTransitionData);
		obs_frontend_set_current_scene(source);
		if (switcher->transitionOverrideOverride) {
			restoreTransitionOverride(source,
						  currentTransitionData);
		}

		if (switcher->verbose) {
			blog(LOG_INFO, "switched scene");
		}

		if (switcher->networkConfig.ShouldSendSceneChange()) {
			switcher->server.sendMessage(sceneSwitch);
		}
	}
	obs_source_release(currentSource);
	obs_source_release(source);
}

void switchPreviewScene(const OBSWeakSource &ws)
{
	auto source = obs_weak_source_get_source(ws);
	obs_frontend_set_current_preview_scene(source);
	obs_source_release(source);
}

void SwitcherData::Start()
{
	if (!(th && th->isRunning())) {
		stop = false;
		th = new SwitcherThread();
		th->start((QThread::Priority)threadPriority);

		// Will be overwritten quickly but might be useful
		writeToStatusFile("Advanced Scene Switcher running");
	}

	if (networkConfig.ServerEnabled) {
		server.start(networkConfig.ServerPort,
			     networkConfig.LockToIPv4);
	}

	if (networkConfig.ClientEnabled) {
		client.connect(networkConfig.GetClientUri());
	}

	if (showSystemTrayNotifications) {
		DisplayTrayMessage(
			obs_module_text("AdvSceneSwitcher.pluginName"),
			obs_module_text("AdvSceneSwitcher.running"));
	}
}

void ResetMacros()
{
	for (auto &m : switcher->macros) {
		m->ResetRunCount();
		m->ResetTimers();
	}
}

void SwitcherData::Stop()
{
	if (th && th->isRunning()) {
		stop = true;
		cv.notify_all();
		abortMacroWait = true;
		macroWaitCv.notify_all();
		macroTransitionCv.notify_all();
		th->wait();
		delete th;
		th = nullptr;

		writeToStatusFile("Advanced Scene Switcher stopped");
		ResetMacros();
	}

	server.stop();
	client.disconnect();

	if (showSystemTrayNotifications) {
		DisplayTrayMessage(
			obs_module_text("AdvSceneSwitcher.pluginName"),
			obs_module_text("AdvSceneSwitcher.stopped"));
	}
}

void SwitcherData::setWaitScene()
{
	waitScene = obs_frontend_get_current_scene();
	obs_source_release(waitScene);
}

bool SwitcherData::sceneChangedDuringWait()
{
	obs_source_t *currentSource = obs_frontend_get_current_scene();
	if (!currentSource) {
		return true;
	}
	obs_source_release(currentSource);
	return (waitScene && currentSource != waitScene);
}

// Relies on the fact that switcher->currentScene will only be updated on event
// OBS_FRONTEND_EVENT_SCENE_CHANGED but obs_frontend_get_current_scene() will
// already return the scene to be transitioned to.
bool SwitcherData::anySceneTransitionStarted()
{
	auto currentSceneSrouce = obs_frontend_get_current_scene();
	auto currentScene = obs_source_get_weak_source(currentSceneSrouce);
	bool ret = switcher->currentScene != currentScene;
	obs_weak_source_release(currentScene);
	obs_source_release(currentSceneSrouce);
	return ret;
}

/******************************************************************************
 * OBS module setup
 ******************************************************************************/
extern "C" void FreeSceneSwitcher()
{
	PlatformCleanup();

	delete switcher;
	switcher = nullptr;
}

void handleSceneChange()
{
	switcher->lastSceneChangeTime =
		std::chrono::high_resolution_clock::now();

	// Stop waiting if scene was changed
	if (switcher->sceneChangedDuringWait()) {
		switcher->cv.notify_one();
	}

	// Set current and previous scene
	obs_source_t *source = obs_frontend_get_current_scene();
	obs_weak_source_t *ws = obs_source_get_weak_source(source);

	if (ws && ws != switcher->currentScene) {
		switcher->previousScene = switcher->currentScene;
		switcher->currentScene = ws;
		vblog(LOG_INFO, "current scene:  %s",
		      GetWeakSourceName(switcher->currentScene).c_str());
		vblog(LOG_INFO, "previous scene: %s",
		      GetWeakSourceName(switcher->previousScene).c_str());
	}

	obs_source_release(source);
	obs_weak_source_release(ws);

	switcher->checkTriggers();
	switcher->checkDefaultSceneTransitions();

	if (switcher->networkConfig.ShouldSendFrontendSceneChange()) {
		switcher->server.sendMessage({ws, nullptr, 0});
	}
}

void setLiveTime()
{
	switcher->liveTime = QDateTime::currentDateTime();
}

void resetLiveTime()
{
	switcher->liveTime = QDateTime();
}

void checkAutoStartRecording()
{
	if (switcher->autoStartEvent == AutoStartEvent::RECORDING ||
	    switcher->autoStartEvent == AutoStartEvent::RECORINDG_OR_STREAMING)
		switcher->Start();
}

void checkAutoStartStreaming()
{
	if (switcher->autoStartEvent == AutoStartEvent::STREAMING ||
	    switcher->autoStartEvent == AutoStartEvent::RECORINDG_OR_STREAMING)
		switcher->Start();
}

void handlePeviewSceneChange()
{
	if (switcher->networkConfig.ShouldSendPrviewSceneChange()) {
		auto source = obs_frontend_get_current_preview_scene();
		auto weak = obs_source_get_weak_source(source);
		switcher->server.sendMessage({weak, nullptr, 0}, true);
		obs_weak_source_release(weak);
		obs_source_release(source);
	}
}

void setReplayBufferSaved()
{
	switcher->replayBufferSaved = true;
}

void setTranstionEnd()
{
	switcher->lastTransitionEndTime =
		std::chrono::high_resolution_clock::now();
	switcher->macroTransitionCv.notify_all();
}

void setStreamStarting()
{
	switcher->lastStreamStartingTime =
		std::chrono::high_resolution_clock::now();
}

void setStreamStopping()
{
	switcher->lastStreamStoppingTime =
		std::chrono::high_resolution_clock::now();
}

void handleExit()
{
	switcher->obsIsShuttingDown = true;
	if (switcher && switcher->obsIsShuttingDown &&
	    switcher->shutdownConditionCount) {
		switcher->Stop();
		switcher->checkMacros();
		switcher->runMacros();
	}
	FreeSceneSwitcher();
}

void handleSceneCollectionChanging()
{
	if (switcher->settingsWindowOpened) {
		AdvSceneSwitcher::window->close();
	}
	if (!switcher->stop) {
		switcher->sceneColletionStop = true;
		switcher->Stop();
	}
}

// Note to future self:
// be careful using switcher->m here as there is potential for deadlocks when using
// frontend functions such as obs_frontend_set_current_scene()
static void OBSEvent(enum obs_frontend_event event, void *switcher)
{
	if (!switcher) {
		return;
	}

	switch (event) {
	case OBS_FRONTEND_EVENT_EXIT:
		handleExit();
		break;
	case OBS_FRONTEND_EVENT_SCENE_CHANGED:
		handleSceneChange();
		break;
	case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
		handlePeviewSceneChange();
		break;
	case OBS_FRONTEND_EVENT_RECORDING_STARTED:
		setLiveTime();
		checkAutoStartRecording();
		break;
	case OBS_FRONTEND_EVENT_STREAMING_STARTED:
		setLiveTime();
		checkAutoStartStreaming();
		break;
	case OBS_FRONTEND_EVENT_RECORDING_STOPPED:
	case OBS_FRONTEND_EVENT_STREAMING_STOPPED:
		resetLiveTime();
		break;
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(26, 0, 0)
	case OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED:
		setReplayBufferSaved();
		break;
#endif
	case OBS_FRONTEND_EVENT_TRANSITION_STOPPED:
		setTranstionEnd();
		break;
	case OBS_FRONTEND_EVENT_STREAMING_STARTING:
		setStreamStarting();
		break;
	case OBS_FRONTEND_EVENT_STREAMING_STOPPING:
		setStreamStopping();
		break;
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(27, 2, 0)
	case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING:
		handleSceneCollectionChanging();
		break;
#endif
	default:
		break;
	}
}

void LoadPlugins()
{
	QFileInfo libPath(
		QString(obs_get_module_binary_path(obs_current_module())));
	QString pluginDir(libPath.absolutePath() + "/adv-ss-plugins");
#ifdef _WIN32
	QString libPattern = "*.dll";
	SetDllDirectory(pluginDir.toStdWString().c_str());
#else
	QString libPattern = "*.so";
#endif
	QDirIterator it(pluginDir, QStringList() << libPattern, QDir::Files);
	while (it.hasNext()) {
		auto file = it.next();
		blog(LOG_INFO, "attempting to load \"%s\"",
		     file.toStdString().c_str());
		auto lib = new QLibrary(file, nullptr);
		if (lib->load()) {
			blog(LOG_INFO, "successfully loaded \"%s\"",
			     file.toStdString().c_str());
		} else {
			blog(LOG_WARNING, "failed to load \"%s\": %s",
			     file.toStdString().c_str(),
			     lib->errorString().toStdString().c_str());
		}
	}
}

void OpenSettingsWindow()
{
	if (switcher->settingsWindowOpened) {
		AdvSceneSwitcher::window->show();
		AdvSceneSwitcher::window->raise();
		AdvSceneSwitcher::window->activateWindow();
	} else {
		AdvSceneSwitcher::window =
			new AdvSceneSwitcher(static_cast<QMainWindow *>(
				obs_frontend_get_main_window()));
		AdvSceneSwitcher::window->setAttribute(Qt::WA_DeleteOnClose);
		AdvSceneSwitcher::window->show();
	}
}

extern "C" void InitSceneSwitcher(obs_module_t *m, translateFunc t)
{
	blog(LOG_INFO, "version: %s", g_GIT_TAG);
	blog(LOG_INFO, "version: %s", g_GIT_SHA1);

	switcher = new SwitcherData;
	switcher->modulePtr = m;
	switcher->translate = t;

	PlatformInit();
	LoadPlugins();
	SetupDock();

	obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
	obs_frontend_add_event_callback(OBSEvent, switcher);

	QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(
		obs_module_text("AdvSceneSwitcher.pluginName"));
	action->connect(action, &QAction::triggered, OpenSettingsWindow);
}
