#include "AppConfigProvider.h"

#include "RuntimeJson.h"

#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <vector>
#include <windows.h>

namespace RenderCadenceCompositor
{
namespace
{
std::filesystem::path ExecutableDirectory()
{
	char path[MAX_PATH] = {};
	const DWORD length = GetModuleFileNameA(nullptr, path, static_cast<DWORD>(sizeof(path)));
	if (length == 0 || length >= sizeof(path))
		return std::filesystem::current_path();
	return std::filesystem::path(path).parent_path();
}

std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
{
	std::ifstream input(path, std::ios::binary);
	if (!input)
	{
		error = "Could not open config file: " + path.string();
		return std::string();
	}

	std::ostringstream buffer;
	buffer << input.rdbuf();
	return buffer.str();
}

const JsonValue* Find(const JsonValue& root, const char* key)
{
	return root.find(key);
}

void ApplyString(const JsonValue& root, const char* key, std::string& target)
{
	const JsonValue* value = Find(root, key);
	if (value && value->isString())
		target = value->asString();
}

void ApplyBool(const JsonValue& root, const char* key, bool& target)
{
	const JsonValue* value = Find(root, key);
	if (value && value->isBoolean())
		target = value->asBoolean();
}

void ApplyDouble(const JsonValue& root, const char* key, double& target)
{
	const JsonValue* value = Find(root, key);
	if (value && value->isNumber())
		target = value->asNumber();
}

void ApplySize(const JsonValue& root, const char* key, std::size_t& target)
{
	const JsonValue* value = Find(root, key);
	if (value && value->isNumber() && value->asNumber() >= 0.0)
		target = static_cast<std::size_t>(value->asNumber());
}

void ApplyPort(const JsonValue& root, const char* key, unsigned short& target)
{
	const JsonValue* value = Find(root, key);
	if (!value || !value->isNumber())
		return;

	const double port = value->asNumber();
	if (port >= 1.0 && port <= 65535.0)
		target = static_cast<unsigned short>(port);
}
}

AppConfigProvider::AppConfigProvider() :
	mConfig(DefaultAppConfig())
{
}

bool AppConfigProvider::LoadDefault(std::string& error)
{
	const std::filesystem::path path = FindConfigFile();
	if (path.empty())
	{
		error = "Could not locate config/runtime-host.json from current directory or executable directory.";
		return false;
	}
	return Load(path, error);
}

bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& error)
{
	mConfig = DefaultAppConfig();
	mSourcePath = path;
	mLoadedFromFile = false;

	std::string fileError;
	const std::string text = ReadTextFile(path, fileError);
	if (!fileError.empty())
	{
		error = fileError;
		return false;
	}

	JsonValue root;
	std::string parseError;
	if (!ParseJson(text, root, parseError) || !root.isObject())
	{
		error = parseError.empty() ? "Config root must be a JSON object." : parseError;
		return false;
	}

	ApplyString(root, "shaderLibrary", mConfig.shaderLibrary);
	ApplyPort(root, "serverPort", mConfig.http.preferredPort);
	ApplyString(root, "oscBindAddress", mConfig.oscBindAddress);
	ApplyPort(root, "oscPort", mConfig.oscPort);
	ApplyDouble(root, "oscSmoothing", mConfig.oscSmoothing);
	ApplyString(root, "inputVideoFormat", mConfig.inputVideoFormat);
	ApplyString(root, "inputFrameRate", mConfig.inputFrameRate);
	ApplyString(root, "outputVideoFormat", mConfig.outputVideoFormat);
	ApplyString(root, "outputFrameRate", mConfig.outputFrameRate);
	ApplyBool(root, "autoReload", mConfig.autoReload);
	ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames);
	ApplyBool(root, "previewEnabled", mConfig.previewEnabled);
	ApplyDouble(root, "previewFps", mConfig.previewFps);
	ApplyBool(root, "enableExternalKeying", mConfig.deckLink.externalKeyingEnabled);

	mLoadedFromFile = true;
	error.clear();
	return true;
}

void AppConfigProvider::ApplyCommandLine(int argc, char** argv)
{
	for (int index = 1; index < argc; ++index)
	{
		const std::string argument = argv[index];
		if (argument == "--shader" && index + 1 < argc)
		{
			mConfig.runtimeShaderId = argv[++index];
			continue;
		}
		if (argument == "--no-shader")
		{
			mConfig.runtimeShaderId.clear();
			continue;
		}
		if (argument == "--port" && index + 1 < argc)
		{
			const int port = std::atoi(argv[++index]);
			if (port >= 1 && port <= 65535)
				mConfig.http.preferredPort = static_cast<unsigned short>(port);
			continue;
		}
	}
}

std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath)
{
	return FindRepoPath(relativePath);
}

std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath)
{
	std::vector<std::filesystem::path> starts;
	starts.push_back(std::filesystem::current_path());
	starts.push_back(ExecutableDirectory());

	for (std::filesystem::path start : starts)
	{
		for (;;)
		{
			const std::filesystem::path candidate = start / relativePath;
			if (std::filesystem::exists(candidate))
				return candidate;

			const std::filesystem::path parent = start.parent_path();
			if (parent.empty() || parent == start)
				break;
			start = parent;
		}
	}

	return std::filesystem::path();
}
}
