#include "RuntimeConfigStore.h"

#include "RuntimeJson.h"

#include <algorithm>
#include <fstream>
#include <sstream>
#include <vector>
#include <windows.h>

namespace
{
double Clamp01(double value)
{
	return (std::max)(0.0, (std::min)(1.0, value));
}

bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate)
{
	return std::filesystem::exists(candidate / "config" / "runtime-host.json") &&
		std::filesystem::exists(candidate / "runtime" / "templates" / "shader_wrapper.slang.in") &&
		std::filesystem::exists(candidate / "shaders");
}

bool LooksLikeRepoRoot(const std::filesystem::path& candidate)
{
	return std::filesystem::exists(candidate / "CMakeLists.txt") &&
		std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing");
}

std::filesystem::path FindRepoRootCandidate()
{
	std::vector<std::filesystem::path> rootsToTry;

	char currentDirectory[MAX_PATH] = {};
	if (GetCurrentDirectoryA(MAX_PATH, currentDirectory) > 0)
		rootsToTry.push_back(std::filesystem::path(currentDirectory));

	char modulePath[MAX_PATH] = {};
	DWORD moduleLength = GetModuleFileNameA(NULL, modulePath, MAX_PATH);
	if (moduleLength > 0 && moduleLength < MAX_PATH)
		rootsToTry.push_back(std::filesystem::path(modulePath).parent_path());

	for (const std::filesystem::path& startPath : rootsToTry)
	{
		std::filesystem::path candidate = startPath;
		for (int depth = 0; depth < 10 && !candidate.empty(); ++depth)
		{
			if (LooksLikePackagedRuntimeRoot(candidate) || LooksLikeRepoRoot(candidate))
				return candidate;

			candidate = candidate.parent_path();
		}
	}

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

bool RuntimeConfigStore::Initialize(std::string& error)
{
	if (!ResolvePaths(error))
		return false;
	if (!LoadConfig(error))
		return false;
	RefreshConfigDependentPaths();
	return true;
}

const RuntimeConfigStore::AppConfig& RuntimeConfigStore::GetConfig() const
{
	return mConfig;
}

const std::filesystem::path& RuntimeConfigStore::GetRepoRoot() const
{
	return mRepoRoot;
}

const std::filesystem::path& RuntimeConfigStore::GetUiRoot() const
{
	return mUiRoot;
}

const std::filesystem::path& RuntimeConfigStore::GetDocsRoot() const
{
	return mDocsRoot;
}

const std::filesystem::path& RuntimeConfigStore::GetShaderRoot() const
{
	return mShaderRoot;
}

const std::filesystem::path& RuntimeConfigStore::GetRuntimeRoot() const
{
	return mRuntimeRoot;
}

const std::filesystem::path& RuntimeConfigStore::GetPresetRoot() const
{
	return mPresetRoot;
}

const std::filesystem::path& RuntimeConfigStore::GetRuntimeStatePath() const
{
	return mRuntimeStatePath;
}

const std::filesystem::path& RuntimeConfigStore::GetWrapperPath() const
{
	return mWrapperPath;
}

const std::filesystem::path& RuntimeConfigStore::GetGeneratedGlslPath() const
{
	return mGeneratedGlslPath;
}

const std::filesystem::path& RuntimeConfigStore::GetPatchedGlslPath() const
{
	return mPatchedGlslPath;
}

void RuntimeConfigStore::SetBoundControlServerPort(unsigned short port)
{
	mConfig.serverPort = port;
}

bool RuntimeConfigStore::ResolvePaths(std::string& error)
{
	mRepoRoot = FindRepoRootCandidate();
	if (mRepoRoot.empty())
	{
		error = "Could not locate the repository root from the current runtime path.";
		return false;
	}

	const std::filesystem::path builtUiRoot = mRepoRoot / "ui" / "dist";
	mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRepoRoot / "ui");
	mDocsRoot = mRepoRoot / "docs";
	mConfigPath = mRepoRoot / "config" / "runtime-host.json";
	mRuntimeRoot = mRepoRoot / "runtime";
	mPresetRoot = mRuntimeRoot / "stack_presets";
	mRuntimeStatePath = mRuntimeRoot / "runtime_state.json";
	RefreshConfigDependentPaths();

	std::error_code fsError;
	std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError);
	std::filesystem::create_directories(mPresetRoot, fsError);
	return true;
}

bool RuntimeConfigStore::LoadConfig(std::string& error)
{
	if (!std::filesystem::exists(mConfigPath))
		return true;

	std::string configText = ReadTextFile(mConfigPath, error);
	if (configText.empty())
		return false;

	JsonValue configJson;
	if (!ParseJson(configText, configJson, error))
		return false;

	if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary"))
		mConfig.shaderLibrary = shaderLibraryValue->asString();
	if (const JsonValue* serverPortValue = configJson.find("serverPort"))
		mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mConfig.serverPort));
	if (const JsonValue* oscPortValue = configJson.find("oscPort"))
		mConfig.oscPort = static_cast<unsigned short>(oscPortValue->asNumber(mConfig.oscPort));
	if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress"))
		mConfig.oscBindAddress = oscBindAddressValue->asString();
	if (const JsonValue* oscSmoothingValue = configJson.find("oscSmoothing"))
		mConfig.oscSmoothing = Clamp01(oscSmoothingValue->asNumber(mConfig.oscSmoothing));
	if (const JsonValue* autoReloadValue = configJson.find("autoReload"))
		mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload);
	if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames"))
	{
		const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames));
		mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
	}
	if (const JsonValue* previewFpsValue = configJson.find("previewFps"))
	{
		const double configuredValue = previewFpsValue->asNumber(static_cast<double>(mConfig.previewFps));
		mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
	}
	if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
		mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying);
	if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
	{
		if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
		{
			mConfig.inputVideoFormat = videoFormatValue->asString();
			mConfig.outputVideoFormat = videoFormatValue->asString();
		}
	}
	if (const JsonValue* frameRateValue = configJson.find("frameRate"))
	{
		if (frameRateValue->isString() && !frameRateValue->asString().empty())
		{
			mConfig.inputFrameRate = frameRateValue->asString();
			mConfig.outputFrameRate = frameRateValue->asString();
		}
		else if (frameRateValue->isNumber())
		{
			std::ostringstream stream;
			stream << frameRateValue->asNumber();
			mConfig.inputFrameRate = stream.str();
			mConfig.outputFrameRate = stream.str();
		}
	}
	if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat"))
	{
		if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty())
			mConfig.inputVideoFormat = inputVideoFormatValue->asString();
	}
	if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate"))
	{
		if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty())
			mConfig.inputFrameRate = inputFrameRateValue->asString();
		else if (inputFrameRateValue->isNumber())
		{
			std::ostringstream stream;
			stream << inputFrameRateValue->asNumber();
			mConfig.inputFrameRate = stream.str();
		}
	}
	if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat"))
	{
		if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty())
			mConfig.outputVideoFormat = outputVideoFormatValue->asString();
	}
	if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate"))
	{
		if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty())
			mConfig.outputFrameRate = outputFrameRateValue->asString();
		else if (outputFrameRateValue->isNumber())
		{
			std::ostringstream stream;
			stream << outputFrameRateValue->asNumber();
			mConfig.outputFrameRate = stream.str();
		}
	}

	return true;
}

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

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

void RuntimeConfigStore::RefreshConfigDependentPaths()
{
	mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
	mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang";
	mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag";
	mPatchedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.frag";
}
