#include "parameter-wrappers.hpp"
#include "log-helper.hpp"

#include <QFileInfo>
#include <source-helpers.hpp>

namespace advss {

bool PatternMatchParameters::Save(obs_data_t *obj) const
{
	auto data = obs_data_create();
	obs_data_set_bool(data, "useForChangedCheck", useForChangedCheck);
	threshold.Save(data, "threshold");
	obs_data_set_bool(data, "useAlphaAsMask", useAlphaAsMask);
	obs_data_set_int(data, "matchMode", matchMode);
	obs_data_set_int(data, "version", 1);
	obs_data_set_obj(obj, "patternMatchData", data);
	obs_data_release(data);
	return true;
}

bool PatternMatchParameters::Load(obs_data_t *obj)
{
	// TODO: Remove this fallback in a future version
	if (!obs_data_has_user_value(obj, "patternMatchData")) {
		useForChangedCheck =
			obs_data_get_bool(obj, "usePatternForChangedCheck");
		threshold = obs_data_get_double(obj, "threshold");
		useAlphaAsMask = obs_data_get_bool(obj, "useAlphaAsMask");
		return true;
	}
	auto data = obs_data_get_obj(obj, "patternMatchData");
	useForChangedCheck = obs_data_get_bool(data, "useForChangedCheck");
	threshold.Load(data, "threshold");
	// TODO: Remove this fallback in a future version
	if (!obs_data_has_user_value(data, "version")) {
		threshold = obs_data_get_double(data, "threshold");
	}
	useAlphaAsMask = obs_data_get_bool(data, "useAlphaAsMask");
	// TODO: Remove this fallback in a future version
	if (!obs_data_has_user_value(data, "matchMode")) {
		matchMode = cv::TM_CCORR_NORMED;
	} else {
		matchMode = static_cast<cv::TemplateMatchModes>(
			obs_data_get_int(data, "matchMode"));
	}
	obs_data_release(data);
	return true;
}

static std::shared_ptr<cv::CascadeClassifier>
initObjectCascade(std::string &path)
{
	auto cascade = std::make_shared<cv::CascadeClassifier>();
	try {
		cascade->load(path);
	} catch (...) {
		blog(LOG_WARNING, "failed to load model data \"%s\"",
		     path.c_str());
	}
	return cascade;
}

bool ObjDetectParameters::LoadModelData()
{
	const auto path = QString::fromStdString(modelPath);
	if (!QFileInfo(path).exists(path)) {
		cascade.reset();
		return false;
	}

	cascade = initObjectCascade(modelPath);
	return !cascade->empty();
}

bool ObjDetectParameters::Save(obs_data_t *obj) const
{
	auto data = obs_data_create();
	obs_data_set_string(data, "modelPath", modelPath.c_str());
	scaleFactor.Save(data, "scaleFactor");
	obs_data_set_int(data, "minNeighbors", minNeighbors);
	minSize.Save(data, "minSize");
	maxSize.Save(data, "maxSize");
	obs_data_set_obj(obj, "objectMatchData", data);
	obs_data_set_int(data, "version", 2);
	obs_data_release(data);
	return true;
}

static bool isScaleFactorValid(double scaleFactor)
{
	return scaleFactor > 1.;
}

static bool isMinNeighborsValid(int minNeighbors)
{
	return minNeighbors >= minMinNeighbors &&
	       minNeighbors <= maxMinNeighbors;
}

bool ObjDetectParameters::Load(obs_data_t *obj)
{
	// TODO: Remove this fallback in a future version
	if (!obs_data_has_user_value(obj, "patternMatchData")) {
		modelPath = obs_data_get_string(obj, "modelDataPath");
		scaleFactor = obs_data_get_double(obj, "scaleFactor");
		if (!isScaleFactorValid(scaleFactor)) {
			scaleFactor = 1.1;
		}
		minNeighbors = obs_data_get_int(obj, "minNeighbors");
		if (!isMinNeighborsValid(minNeighbors)) {
			minNeighbors = minMinNeighbors;
		}
		minSize.Load(obj, "minSize");
		maxSize.Load(obj, "maxSize");
		return true;
	}
	auto data = obs_data_get_obj(obj, "objectMatchData");
	modelPath = obs_data_get_string(data, "modelPath");
	scaleFactor.Load(data, "scaleFactor");
	// TODO: Remove this fallback in a future version
	if (!obs_data_has_user_value(data, "version")) {
		scaleFactor = obs_data_get_double(data, "scaleFactor");
	} else if (obs_data_get_int(data, "version") == 1) {
#ifdef _WIN32
		// On Windows the plugin directory moved from
		// "Program Files/.../data/obs-plugins/advanced-scene-switcher"
		// to "ProgramData/.../plugins/advanced-scene-switcher/data",
		// which invalidates previously saved default model paths.
		const std::string oldPrefix =
			"../../data/obs-plugins/advanced-scene-switcher/res/cascadeClassifiers/";
		if (modelPath.substr(0, oldPrefix.size()) == oldPrefix) {
			modelPath = std::string(obs_get_module_data_path(
					    obs_current_module())) +
				    "/res/cascadeClassifiers/" +
				    modelPath.substr(oldPrefix.size());
		}
#endif
	}
	if (scaleFactor.IsFixedType() && !isScaleFactorValid(scaleFactor)) {
		scaleFactor = 1.1;
	}
	minNeighbors = obs_data_get_int(data, "minNeighbors");
	if (!isMinNeighborsValid(minNeighbors)) {
		minNeighbors = minMinNeighbors;
	}
	minSize.Load(data, "minSize");
	maxSize.Load(data, "maxSize");
	obs_data_release(data);

	return true;
}

bool ObjDetectParameters::SetModelPath(const std::string &path)
{
	modelPath = path;
	return LoadModelData();
}

std::shared_ptr<cv::CascadeClassifier> ObjDetectParameters::GetModel()
{
	if (cascade && !cascade->empty()) {
		return cascade;
	}

	if (!LoadModelData()) {
		return {};
	}

	return cascade;
}

bool AreaParameters::Save(obs_data_t *obj) const
{
	auto data = obs_data_create();
	obs_data_set_bool(data, "enabled", enable);
	area.Save(data, "area");
	obs_data_set_obj(obj, "areaData", data);
	obs_data_release(data);
	return true;
}

bool AreaParameters::Load(obs_data_t *obj)
{
	// TODO: Remove this fallback in a future version
	if (!obs_data_has_user_value(obj, "areaData")) {
		enable = obs_data_get_bool(obj, "checkAreaEnabled");
		area.Load(obj, "checkArea");
		return true;
	}
	auto data = obs_data_get_obj(obj, "areaData");
	enable = obs_data_get_bool(data, "enabled");
	area.Load(data, "area");
	obs_data_release(data);
	return true;
}

bool VideoInput::Save(obs_data_t *obj) const
{
	auto data = obs_data_create();
	obs_data_set_int(data, "type", static_cast<int>(type));
	source.Save(data);
	scene.Save(data);
	obs_data_set_obj(obj, "videoInputData", data);
	obs_data_release(data);
	return true;
}

bool VideoInput::Load(obs_data_t *obj)
{
	// TODO: Remove this fallback in a future version
	if (obs_data_has_user_value(obj, "videoType")) {
		enum class VideoSelectionType {
			SOURCE,
			OBS_MAIN,
		};
		auto oldType = static_cast<VideoSelectionType>(
			obs_data_get_int(obj, "videoType"));
		if (oldType == VideoSelectionType::SOURCE) {
			type = Type::SOURCE;
			auto name = obs_data_get_string(obj, "video");
			source.SetSource(GetWeakSourceByName(name));
		} else {
			type = Type::OBS_MAIN_OUTPUT;
		}
		return true;
	}

	auto data = obs_data_get_obj(obj, "videoInputData");
	type = static_cast<Type>(obs_data_get_int(data, "type"));
	source.Load(data);
	scene.Load(data);
	obs_data_release(data);
	return true;
}

std::string VideoInput::ToString(bool resolve) const
{
	switch (type) {
	case VideoInput::Type::OBS_MAIN_OUTPUT:
		return obs_module_text("AdvSceneSwitcher.OBSVideoOutput");
	case VideoInput::Type::SOURCE:
		return source.ToString(resolve);
	case VideoInput::Type::SCENE:
		return scene.ToString(resolve);
	}
	return "";
}

bool VideoInput::ValidSelection() const
{
	switch (type) {
	case VideoInput::Type::OBS_MAIN_OUTPUT:
		return true;
	case VideoInput::Type::SOURCE:
		return !!source.GetSource();
	case VideoInput::Type::SCENE:
		return !!scene.GetScene();
	}
	return false;
}

OBSWeakSource VideoInput::GetVideo() const
{
	switch (type) {
	case VideoInput::Type::OBS_MAIN_OUTPUT:
		return nullptr;
	case VideoInput::Type::SOURCE:
		return source.GetSource();
	case VideoInput::Type::SCENE:
		return scene.GetScene();
	}
	return nullptr;
}

static void SaveColor(obs_data_t *obj, const char *name, const QColor &color)
{
	auto data = obs_data_create();
	obs_data_set_int(data, "red", color.red());
	obs_data_set_int(data, "green", color.green());
	obs_data_set_int(data, "blue", color.blue());
	obs_data_set_obj(obj, name, data);
	obs_data_release(data);
}

static QColor LoadColor(obs_data_t *obj, const char *name)
{
	QColor color = Qt::black;
	auto data = obs_data_get_obj(obj, name);
	color.setRed(obs_data_get_int(data, "red"));
	color.setGreen(obs_data_get_int(data, "green"));
	color.setBlue(obs_data_get_int(data, "blue"));
	obs_data_release(data);
	return color;
}

OCRParameters::OCRParameters()
{
	Setup();
}

OCRParameters::~OCRParameters()
{
	if (!initDone) {
		return;
	}
	ocr->End();
}

OCRParameters::OCRParameters(const OCRParameters &other)
	: text(other.text),
	  regex(other.regex),
	  color(other.color),
	  colorThreshold(other.colorThreshold),
	  pageSegMode(other.pageSegMode),
	  tesseractBasePath(other.tesseractBasePath),
	  languageCode(other.languageCode),
	  useConfig(other.useConfig),
	  configFile(other.configFile)
{
}

OCRParameters &OCRParameters::operator=(const OCRParameters &other)
{
	text = other.text;
	regex = other.regex;
	color = other.color;
	colorThreshold = other.colorThreshold;
	pageSegMode = other.pageSegMode;
	tesseractBasePath = other.tesseractBasePath;
	languageCode = other.languageCode;
	useConfig = other.useConfig;
	configFile = other.configFile;
	initDone = false;
	return *this;
}

bool OCRParameters::Save(obs_data_t *obj) const
{
	auto data = obs_data_create();
	text.Save(data, "pattern");
	regex.Save(data);
	tesseractBasePath.Save(data, "tesseractBasePath");
	languageCode.Save(data, "language");
	obs_data_set_bool(data, "useConfig", useConfig);
	obs_data_set_string(data, "configFile", configFile.c_str());
	SaveColor(data, "textColor", color);
	colorThreshold.Save(data, "colorThreshold");
	obs_data_set_int(data, "pageSegMode", static_cast<int>(pageSegMode));
	obs_data_set_int(data, "version", 3);
	obs_data_set_obj(obj, "ocrData", data);
	obs_data_release(data);
	return true;
}

bool OCRParameters::Load(obs_data_t *obj)
{
	auto data = obs_data_get_obj(obj, "ocrData");
	text.Load(data, "pattern");
	regex.Load(data);
	obs_data_set_default_string(data, "language", "eng");
	languageCode.Load(data, "language");
	tesseractBasePath.Load(data, "tesseractBasePath");
	if (!obs_data_has_user_value(data, "version") ||
	    obs_data_get_int(data, "version") < 2) {
		tesseractBasePath = std::string(obs_get_module_data_path(
					    obs_current_module())) +
				    "/res/ocr/";
	} else if (obs_data_get_int(data, "version") == 2) {
#ifdef _WIN32
		// On Windows the plugin directory moved from
		// "Program Files/.../data/obs-plugins/advanced-scene-switcher"
		// to "ProgramData/.../plugins/advanced-scene-switcher/data",
		// which invalidates the previously saved default OCR path.
		const std::string oldPath = tesseractBasePath;
		if (oldPath ==
			    "../../data/obs-plugins/advanced-scene-switcher/res/ocr/" ||
		    oldPath ==
			    "../../data/obs-plugins/advanced-scene-switcher/res/ocr") {
			tesseractBasePath =
				std::string(obs_get_module_data_path(
					obs_current_module())) +
				"/res/ocr/";
		}
#endif
	}
	useConfig = obs_data_get_bool(data, "useConfig");
	configFile = obs_data_get_string(data, "configFile");

	color = LoadColor(data, "textColor");
	if (obs_data_has_user_value(data, "version")) {
		colorThreshold.Load(data, "colorThreshold");
	}
	pageSegMode = static_cast<tesseract::PageSegMode>(
		obs_data_get_int(data, "pageSegMode"));
	obs_data_release(data);

	Setup();
	return true;
}

void OCRParameters::SetPageMode(tesseract::PageSegMode mode)
{
	pageSegMode = mode;
	if (ocr) {
		ocr->SetPageSegMode(mode);
	}
}

tesseract::TessBaseAPI *OCRParameters::GetOCR()
{
	if (!initDone) {
		Setup();
	}
	return initDone ? ocr.get() : nullptr;
}

bool OCRParameters::SetLanguageCode(const std::string &value)
{
	const auto dataPath = QString::fromStdString(tesseractBasePath) + "/" +
			      QString::fromStdString(value) + ".traineddata";
	QFileInfo fileInfo(dataPath);
	if (!fileInfo.exists(dataPath)) {
		return false;
	}
	languageCode = value;
	initDone = false;
	return true;
}

std::string OCRParameters::GetLanguageCode() const
{
	return languageCode;
}

bool OCRParameters::SetTesseractBasePath(const std::string &value)
{
	const auto dataPath = QString::fromStdString(value) + "/" +
			      QString::fromStdString(languageCode) +
			      ".traineddata";
	QFileInfo fileInfo(dataPath);
	if (!fileInfo.exists(dataPath)) {
		return false;
	}
	tesseractBasePath = value;
	initDone = false;
	return true;
}

std::string OCRParameters::GetTesseractBasePath() const
{
	return tesseractBasePath;
}

void OCRParameters::EnableCustomConfig(bool enable)
{
	useConfig = enable;
	initDone = false;
}

void OCRParameters::SetCustomConfigFile(const std::string &filename)
{
	configFile = filename;
	initDone = false;
}

void OCRParameters::Setup()
{
	ocr = std::make_unique<tesseract::TessBaseAPI>();
	if (!ocr) {
		initDone = false;
		return;
	}

	const std::string dataPath = std::string(tesseractBasePath) + "/";
	const std::string modelFile =
		std::string(languageCode) + ".traineddata";
	const auto modelFullPath = QString::fromStdString(dataPath) +
				   QString::fromStdString(modelFile);
	QFileInfo modelFileInfo(modelFullPath);
	if (!modelFileInfo.exists(modelFullPath)) {
		initDone = false;
		blog(LOG_WARNING,
		     "cannot init tesseract! Model path does not exists: %s",
		     modelFileInfo.absoluteFilePath().toStdString().c_str());
		return;
	}

	auto configPath = QString::fromStdString(configFile);
	QFileInfo configFileInfo(configPath);
	bool configFileExists = configFileInfo.exists(configPath);

	bool setupWithConfig = useConfig;
	if (useConfig && !configFileExists) {
		blog(LOG_WARNING,
		     "tesseract config file will be ignored! File does not exists: %s",
		     configFileInfo.absoluteFilePath().toStdString().c_str());
		setupWithConfig = false;
	}

	char *configs[] = {configFile.data()};
	if (ocr->Init(dataPath.c_str(), languageCode.c_str(),
		      tesseract::OEM_DEFAULT,
		      setupWithConfig ? configs : nullptr,
		      setupWithConfig ? 1 : 0, nullptr, nullptr, false) != 0) {
		blog(LOG_WARNING, "tesseract init failed!");
		initDone = false;
		return;
	}

	ocr->SetPageSegMode(pageSegMode);

	initDone = true;
}

bool ColorParameters::Save(obs_data_t *obj) const
{
	auto data = obs_data_create();
	SaveColor(data, "color", color);
	colorThreshold.Save(data, "colorThreshold");
	matchThreshold.Save(data, "matchThreshold");
	obs_data_set_obj(obj, "colorData", data);
	obs_data_release(data);
	return true;
}

bool ColorParameters::Load(obs_data_t *obj)
{
	auto data = obs_data_get_obj(obj, "colorData");
	color = LoadColor(data, "color");
	colorThreshold.Load(data, "colorThreshold");
	matchThreshold.Load(data, "matchThreshold");
	obs_data_release(data);
	return true;
}

} // namespace advss
