#include "RuntimeStore.h"

#include "RuntimeStatePresenter.h"

#include <cctype>
#include <fstream>
#include <mutex>
#include <random>
#include <sstream>

namespace
{
std::string ToLowerCopy(std::string text)
{
	std::transform(text.begin(), text.end(), text.begin(),
		[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
	return text;
}

double GenerateStartupRandom()
{
	std::random_device randomDevice;
	std::uniform_real_distribution<double> distribution(0.0, 1.0);
	return distribution(randomDevice);
}

std::string PersistenceTargetKindName(PersistenceTargetKind targetKind)
{
	switch (targetKind)
	{
	case PersistenceTargetKind::RuntimeState:
		return "runtime-state";
	case PersistenceTargetKind::StackPreset:
		return "stack-preset";
	case PersistenceTargetKind::RuntimeConfig:
		return "runtime-config";
	default:
		return "unknown";
	}
}

}

RuntimeStore::RuntimeStore() :
	mRenderSnapshotBuilder(*this),
	mReloadRequested(false),
	mCompileSucceeded(false),
	mStartupRandom(GenerateStartupRandom()),
	mServerPort(8080),
	mAutoReloadEnabled(true),
	mStartTime(std::chrono::steady_clock::now()),
	mLastScanTime((std::chrono::steady_clock::time_point::min)())
{
	mPersistenceWriter.SetResultCallback([this](const PersistenceWriteResult& result) {
		mHealthTelemetry.RecordPersistenceWriteResult(
			result.succeeded,
			PersistenceTargetKindName(result.targetKind),
			result.targetPath,
			result.reason,
			result.errorMessage,
			result.newerRequestPending);
	});
}

HealthTelemetry& RuntimeStore::GetHealthTelemetry()
{
	return mHealthTelemetry;
}

const HealthTelemetry& RuntimeStore::GetHealthTelemetry() const
{
	return mHealthTelemetry;
}

RenderSnapshotBuilder& RuntimeStore::GetRenderSnapshotBuilder()
{
	return mRenderSnapshotBuilder;
}

const RenderSnapshotBuilder& RuntimeStore::GetRenderSnapshotBuilder() const
{
	return mRenderSnapshotBuilder;
}

bool RuntimeStore::InitializeStore(std::string& error)
{
	try
	{
		std::lock_guard<std::mutex> lock(mMutex);

		if (!mConfigStore.Initialize(error))
			return false;
		if (!LoadPersistentState(error))
			return false;
		if (!ScanShaderPackages(error))
			return false;
		mCommittedLiveState.NormalizeLayerIds();
		mCommittedLiveState.EnsureDefaultsForAllLayers(mShaderCatalog);
		mCommittedLiveState.EnsureDefaultLayer(mShaderCatalog);

		mServerPort = mConfigStore.GetConfig().serverPort;
		mAutoReloadEnabled = mConfigStore.GetConfig().autoReload;
		mReloadRequested = true;
		mCompileMessage = "Waiting for shader compile.";
		return true;
	}
	catch (const std::exception& exception)
	{
		error = std::string("RuntimeStore::InitializeStore exception: ") + exception.what();
		return false;
	}
	catch (...)
	{
		error = "RuntimeStore::InitializeStore threw a non-standard exception.";
		return false;
	}
}

std::string RuntimeStore::BuildPersistentStateJson() const
{
	return RuntimeStatePresenter::BuildRuntimeStateJson(*this);
}

PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshot(const PersistenceRequest& request) const
{
	std::lock_guard<std::mutex> lock(mMutex);
	return BuildRuntimeStatePersistenceSnapshotLocked(request);
}

bool RuntimeStore::RequestPersistence(const PersistenceRequest& request, std::string& error)
{
	if (request.targetKind != PersistenceTargetKind::RuntimeState)
	{
		error = "Unsupported persistence request target: " + PersistenceTargetKindName(request.targetKind);
		mHealthTelemetry.RecordPersistenceWriteResult(
			false,
			PersistenceTargetKindName(request.targetKind),
			std::string(),
			request.reason,
			error,
			false);
		return false;
	}

	const PersistenceSnapshot snapshot = BuildRuntimeStatePersistenceSnapshot(request);
	if (mPersistenceWriter.EnqueueSnapshot(snapshot, error))
		return true;

	mHealthTelemetry.RecordPersistenceWriteResult(
		false,
		PersistenceTargetKindName(request.targetKind),
		snapshot.targetPath.string(),
		request.reason,
		error,
		false);
	return false;
}

bool RuntimeStore::FlushPersistenceForShutdown(std::chrono::milliseconds timeout, std::string& error)
{
	if (mPersistenceWriter.StopAndFlush(timeout, error))
		return true;

	mHealthTelemetry.RecordPersistenceWriteResult(
		false,
		PersistenceTargetKindName(PersistenceTargetKind::RuntimeState),
		std::string(),
		"shutdown-flush",
		error,
		true);
	return false;
}

PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshotLocked(const PersistenceRequest& request) const
{
	PersistenceSnapshot snapshot;
	snapshot.targetKind = PersistenceTargetKind::RuntimeState;
	snapshot.targetPath = mConfigStore.GetRuntimeStatePath();
	snapshot.contents = SerializeJson(mCommittedLiveState.BuildPersistentStateValue(mShaderCatalog), true);
	snapshot.reason = request.reason;
	snapshot.debounceKey = request.debounceKey;
	snapshot.debounceAllowed = request.debounceAllowed;
	snapshot.flushRequested = request.flushRequested;
	snapshot.generation = request.sequence;
	return snapshot;
}

bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error)
{
	try
	{
		std::lock_guard<std::mutex> lock(mMutex);
		registryChanged = false;
		reloadRequested = false;

		if (!mAutoReloadEnabled)
		{
			reloadRequested = mReloadRequested;
			return true;
		}

		const auto now = std::chrono::steady_clock::now();
		if (mLastScanTime != (std::chrono::steady_clock::time_point::min)() &&
			std::chrono::duration_cast<std::chrono::milliseconds>(now - mLastScanTime).count() < 250)
		{
			reloadRequested = mReloadRequested;
			return true;
		}

		mLastScanTime = now;

		std::string scanError;
		const ShaderPackageCatalog::Snapshot previousCatalog = mShaderCatalog.CaptureSnapshot();
		if (!ScanShaderPackages(scanError))
		{
			error = scanError;
			return false;
		}

		registryChanged = mShaderCatalog.HasCatalogChangedSince(previousCatalog);

		mCommittedLiveState.EnsureDefaultsForAllLayers(mShaderCatalog);
		for (RuntimeStore::LayerPersistentState& layer : mCommittedLiveState.Layers())
		{
			const ShaderPackage* active = mShaderCatalog.FindPackage(layer.shaderId);
			if (!active)
				continue;
			if (mShaderCatalog.HasPackageChangedSince(previousCatalog, layer.shaderId))
				mReloadRequested = true;
		}

		reloadRequested = mReloadRequested;
		if (registryChanged || reloadRequested)
			MarkRenderStateDirtyLocked();
		return true;
	}
	catch (const std::exception& exception)
	{
		error = std::string("RuntimeStore::PollStoredFileChanges exception: ") + exception.what();
		return false;
	}
	catch (...)
	{
		error = "RuntimeStore::PollStoredFileChanges threw a non-standard exception.";
		return false;
	}
}

bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& error)
{
	std::lock_guard<std::mutex> lock(mMutex);
	if (!mCommittedLiveState.CreateLayer(mShaderCatalog, shaderId, error))
		return false;

	mReloadRequested = true;
	MarkRenderStateDirtyLocked();
	return true;
}

bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error)
{
	std::lock_guard<std::mutex> lock(mMutex);
	if (!mCommittedLiveState.DeleteLayer(layerId, error))
		return false;

	mReloadRequested = true;
	MarkRenderStateDirtyLocked();
	return true;
}

bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error)
{
	std::lock_guard<std::mutex> lock(mMutex);
	bool shouldMove = false;
	if (!mCommittedLiveState.ResolveLayerMove(layerId, direction, shouldMove, error))
		return false;
	if (!shouldMove)
		return true;

	if (!mCommittedLiveState.MoveLayer(layerId, direction, error))
		return false;

	mReloadRequested = true;
	MarkRenderStateDirtyLocked();
	return true;
}

bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
{
	std::lock_guard<std::mutex> lock(mMutex);
	bool shouldMove = false;
	if (!mCommittedLiveState.ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error))
		return false;
	if (!shouldMove)
		return true;

	if (!mCommittedLiveState.MoveLayerToIndex(layerId, targetIndex, error))
		return false;

	mReloadRequested = true;
	MarkRenderStateDirtyLocked();
	return true;
}

bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
{
	std::lock_guard<std::mutex> lock(mMutex);
	if (!mCommittedLiveState.SetLayerBypassState(layerId, bypassed, error))
		return false;

	mReloadRequested = true;
	MarkParameterStateDirtyLocked();
	return true;
}

bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error)
{
	std::lock_guard<std::mutex> lock(mMutex);
	if (!mCommittedLiveState.SetLayerShaderSelection(mShaderCatalog, layerId, shaderId, error))
		return false;

	mReloadRequested = true;
	MarkRenderStateDirtyLocked();
	return true;
}

bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, bool persistState, std::string& error)
{
	(void)persistState;
	std::lock_guard<std::mutex> lock(mMutex);

	if (!mCommittedLiveState.SetParameterValue(layerId, parameterId, value, error))
		return false;

	MarkParameterStateDirtyLocked();
	return true;
}

bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error)
{
	std::lock_guard<std::mutex> lock(mMutex);

	if (!mCommittedLiveState.ResetLayerParameterValues(mShaderCatalog, layerId, error))
		return false;

	MarkParameterStateDirtyLocked();
	return true;
}

bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const
{
	std::lock_guard<std::mutex> lock(mMutex);
	const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName);
	if (safeStem.empty())
	{
		error = "Preset name must include at least one letter or number.";
		return false;
	}

	return mPersistenceWriter.WriteSnapshot(BuildStackPresetPersistenceSnapshot(presetName), error);
}

bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error)
{
	std::lock_guard<std::mutex> lock(mMutex);
	const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName);
	if (safeStem.empty())
	{
		error = "Preset name must include at least one letter or number.";
		return false;
	}

	const std::filesystem::path presetPath = mConfigStore.GetPresetRoot() / (safeStem + ".json");
	std::string presetText = ReadTextFile(presetPath, error);
	if (presetText.empty())
		return false;

	JsonValue root;
	if (!ParseJson(presetText, root, error))
		return false;

	if (!mCommittedLiveState.LoadStackPresetValue(mShaderCatalog, root, error))
		return false;

	mReloadRequested = true;
	MarkRenderStateDirtyLocked();
	return true;
}

bool RuntimeStore::HasStoredLayer(const std::string& layerId) const
{
	std::lock_guard<std::mutex> lock(mMutex);
	return mCommittedLiveState.HasLayer(layerId);
}

bool RuntimeStore::HasStoredShader(const std::string& shaderId) const
{
	std::lock_guard<std::mutex> lock(mMutex);
	return mShaderCatalog.HasPackage(shaderId);
}

bool RuntimeStore::TryGetStoredParameterById(const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const
{
	std::lock_guard<std::mutex> lock(mMutex);

	return mCommittedLiveState.TryGetParameterById(mShaderCatalog, layerId, parameterId, snapshot, error);
}

bool RuntimeStore::TryGetStoredParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const
{
	std::lock_guard<std::mutex> lock(mMutex);

	return mCommittedLiveState.TryGetParameterByControlKey(mShaderCatalog, layerKey, parameterKey, snapshot, error);
}

bool RuntimeStore::ResolveStoredLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const
{
	std::lock_guard<std::mutex> lock(mMutex);
	return mCommittedLiveState.ResolveLayerMove(layerId, direction, shouldMove, error);
}

bool RuntimeStore::ResolveStoredLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const
{
	std::lock_guard<std::mutex> lock(mMutex);
	return mCommittedLiveState.ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error);
}

bool RuntimeStore::IsValidStackPresetName(const std::string& presetName) const
{
	return !LayerStackStore::MakeSafePresetFileStem(presetName).empty();
}

double RuntimeStore::GetRuntimeElapsedSeconds() const
{
	return std::chrono::duration_cast<std::chrono::duration<double>>(
		std::chrono::steady_clock::now() - mStartTime).count();
}

const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const
{
	return mConfigStore.GetRepoRoot();
}

const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const
{
	return mConfigStore.GetUiRoot();
}

const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const
{
	return mConfigStore.GetDocsRoot();
}

const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const
{
	return mConfigStore.GetRuntimeRoot();
}

unsigned short RuntimeStore::GetConfiguredControlServerPort() const
{
	return mServerPort;
}

unsigned short RuntimeStore::GetConfiguredOscPort() const
{
	return mConfigStore.GetConfig().oscPort;
}

const std::string& RuntimeStore::GetConfiguredOscBindAddress() const
{
	return mConfigStore.GetConfig().oscBindAddress;
}

double RuntimeStore::GetConfiguredOscSmoothing() const
{
	return mConfigStore.GetConfig().oscSmoothing;
}

unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const
{
	return mConfigStore.GetConfig().maxTemporalHistoryFrames;
}

unsigned RuntimeStore::GetConfiguredPreviewFps() const
{
	return mConfigStore.GetConfig().previewFps;
}

bool RuntimeStore::IsExternalKeyingConfigured() const
{
	return mConfigStore.GetConfig().enableExternalKeying;
}

const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const
{
	return mConfigStore.GetConfig().inputVideoFormat;
}

const std::string& RuntimeStore::GetConfiguredInputFrameRate() const
{
	return mConfigStore.GetConfig().inputFrameRate;
}

const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const
{
	return mConfigStore.GetConfig().outputVideoFormat;
}

const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const
{
	return mConfigStore.GetConfig().outputFrameRate;
}

void RuntimeStore::SetBoundControlServerPort(unsigned short port)
{
	std::lock_guard<std::mutex> lock(mMutex);
	mServerPort = port;
	mConfigStore.SetBoundControlServerPort(port);
}

void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message)
{
	std::lock_guard<std::mutex> lock(mMutex);
	mCompileSucceeded = succeeded;
	mCompileMessage = message;
}

void RuntimeStore::ClearReloadRequest()
{
	std::lock_guard<std::mutex> lock(mMutex);
	mReloadRequested = false;
}

bool RuntimeStore::LoadPersistentState(std::string& error)
{
	if (!std::filesystem::exists(mConfigStore.GetRuntimeStatePath()))
		return true;

	std::string stateText = ReadTextFile(mConfigStore.GetRuntimeStatePath(), error);
	if (stateText.empty())
		return false;

	JsonValue root;
	if (!ParseJson(stateText, root, error))
		return false;

	return mCommittedLiveState.LoadPersistentStateValue(root);
}

PersistenceSnapshot RuntimeStore::BuildStackPresetPersistenceSnapshot(const std::string& presetName) const
{
	const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName);

	PersistenceSnapshot snapshot;
	snapshot.targetKind = PersistenceTargetKind::StackPreset;
	snapshot.targetPath = mConfigStore.GetPresetRoot() / (safeStem + ".json");
	snapshot.contents = SerializeJson(mCommittedLiveState.BuildStackPresetValue(mShaderCatalog, presetName), true);
	snapshot.reason = "SaveStackPreset";
	snapshot.debounceKey = "stack-preset:" + safeStem;
	snapshot.debounceAllowed = false;
	snapshot.flushRequested = true;
	snapshot.generation = 0;
	return snapshot;
}

bool RuntimeStore::ScanShaderPackages(std::string& error)
{
	if (!mShaderCatalog.Scan(mConfigStore.GetShaderRoot(), mConfigStore.GetConfig().maxTemporalHistoryFrames, error))
		return false;

	mCommittedLiveState.RemoveLayersWithMissingPackages(mShaderCatalog);

	MarkRenderStateDirtyLocked();
	return true;
}

std::string RuntimeStore::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();
}

std::vector<std::string> RuntimeStore::GetStackPresetNamesLocked() const
{
	std::vector<std::string> presetNames;
	std::error_code fsError;
	if (!std::filesystem::exists(mConfigStore.GetPresetRoot(), fsError))
		return presetNames;

	for (const auto& entry : std::filesystem::directory_iterator(mConfigStore.GetPresetRoot(), fsError))
	{
		if (!entry.is_regular_file())
			continue;
		if (ToLowerCopy(entry.path().extension().string()) != ".json")
			continue;
		presetNames.push_back(entry.path().stem().string());
	}

	std::sort(presetNames.begin(), presetNames.end());
	return presetNames;
}

bool RuntimeStore::CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const
{
	std::lock_guard<std::mutex> lock(mMutex);
	const RuntimeStore::LayerPersistentState* layer = mCommittedLiveState.FindLayerById(layerId);
	if (!layer)
	{
		error = "Unknown layer id: " + layerId;
		return false;
	}

	if (!mShaderCatalog.CopyPackage(layer->shaderId, shaderPackage))
	{
		error = "Unknown shader id: " + layer->shaderId;
		return false;
	}

	return true;
}

ShaderCompilerInputs RuntimeStore::GetShaderCompilerInputs() const
{
	std::lock_guard<std::mutex> lock(mMutex);
	ShaderCompilerInputs inputs;
	inputs.repoRoot = mConfigStore.GetRepoRoot();
	inputs.wrapperPath = mConfigStore.GetWrapperPath();
	inputs.generatedGlslPath = mConfigStore.GetGeneratedGlslPath();
	inputs.patchedGlslPath = mConfigStore.GetPatchedGlslPath();
	inputs.maxTemporalHistoryFrames = mConfigStore.GetConfig().maxTemporalHistoryFrames;
	return inputs;
}

CommittedLiveStateReadModel RuntimeStore::BuildCommittedLiveStateReadModel() const
{
	std::lock_guard<std::mutex> lock(mMutex);
	return mCommittedLiveState.BuildReadModel(mShaderCatalog);
}

RenderSnapshotReadModel RuntimeStore::BuildRenderSnapshotReadModel() const
{
	RenderSnapshotReadModel model;
	model.signalStatus = mHealthTelemetry.GetSignalStatusSnapshot();
	model.committedLiveState = BuildCommittedLiveStateReadModel();
	std::lock_guard<std::mutex> lock(mMutex);
	model.timing.startTime = mStartTime;
	model.timing.startupRandom = mStartupRandom;
	return model;
}

std::vector<RuntimeStore::LayerPersistentState> RuntimeStore::CopyCommittedLiveLayerStates() const
{
	std::lock_guard<std::mutex> lock(mMutex);
	return mCommittedLiveState.CopyLayerStates();
}

std::vector<RuntimeStore::LayerPersistentState> RuntimeStore::CopyLayerStates() const
{
	return CopyCommittedLiveLayerStates();
}

RenderTimingSnapshot RuntimeStore::GetRenderTimingSnapshot() const
{
	std::lock_guard<std::mutex> lock(mMutex);
	RenderTimingSnapshot snapshot;
	snapshot.startTime = mStartTime;
	snapshot.startupRandom = mStartupRandom;
	return snapshot;
}

RuntimeStatePresentationReadModel RuntimeStore::BuildRuntimeStatePresentationReadModel() const
{
	RuntimeStatePresentationReadModel model;
	model.telemetry = mHealthTelemetry.GetSnapshot();
	std::lock_guard<std::mutex> lock(mMutex);
	model.config = mConfigStore.GetConfig();
	model.layerStack = mCommittedLiveState.LayerStack();
	model.shaderCatalog = mShaderCatalog.CaptureSnapshot();
	model.packageStatuses = mShaderCatalog.PackageStatuses();
	model.stackPresetNames = GetStackPresetNamesLocked();
	model.serverPort = mServerPort;
	model.autoReloadEnabled = mAutoReloadEnabled;
	model.compileSucceeded = mCompileSucceeded;
	model.compileMessage = mCompileMessage;
	return model;
}

void RuntimeStore::MarkRenderStateDirtyLocked()
{
	mRenderSnapshotBuilder.MarkRenderStateDirty();
}

void RuntimeStore::MarkParameterStateDirtyLocked()
{
	mRenderSnapshotBuilder.MarkParameterStateDirty();
}
