#include "dialogue/DialogueRuntime.h"

#include <algorithm>
#include <cmath>
#include <iostream>

namespace ZL::Dialogue {
	
void DialogueRuntime::setDatabase(const DialogueDatabase* value) {
	database = value;
}

bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
	if (!database) {
		std::cerr << "[dialogue] No database assigned to runtime\n";
		return false;
	}

	const DialogueDefinition* dialogue = database->findDialogue(dialogueId);
	if (!dialogue) {
		std::cerr << "[dialogue] Dialogue not found: " << dialogueId << "\n";
		return false;
	}

	activeDialogue = dialogue;
	activeCutscene = nullptr;
	currentNodeId.clear();
	pendingNodeAfterCutscene.clear();
	visibleChoices.clear();
	selectedChoice = -1;
	revealCharacters = 0.0f;
	currentCutsceneLine = -1;
	cutsceneTimerMs = 0;
	cutsceneElapsedMs = 0;
	cutsceneTotalDurationMs = 0;
	cutsceneContentDurationMs = 0;
	currentCutsceneBackground.clear();
	presentation = {};
	presentation.dialogueId = dialogue->id;

	return enterNode(dialogue->startNode);
}

bool DialogueRuntime::startStandaloneCutscene(const std::string& cutsceneId) {
	if (!database) {
		std::cerr << "[dialogue] No database assigned to runtime\n";
		return false;
	}
	const StaticCutsceneDefinition* def = database->findCutscene(cutsceneId);
	if (!def) {
		std::cerr << "[dialogue] Cutscene not found: " << cutsceneId << "\n";
		return false;
	}
	activeDialogue = nullptr;
	activeCutsceneId = cutsceneId;
	presentation = {};
	startCutscene(cutsceneId, "");
	return true;
}

void DialogueRuntime::setOnCutsceneFinished(std::function<void(const std::string&)> cb) {
	onCutsceneFinished = std::move(cb);
}

void DialogueRuntime::stop() {
	activeDialogue = nullptr;
	activeCutscene = nullptr;
	currentNodeId.clear();
	pendingNodeAfterCutscene.clear();
	visibleChoices.clear();
	selectedChoice = -1;
	revealCharacters = 0.0f;
	currentCutsceneLine = -1;
	cutsceneTimerMs = 0;
	cutsceneElapsedMs = 0;
	cutsceneTotalDurationMs = 0;
	cutsceneContentDurationMs = 0;
	currentCutsceneBackground.clear();
	mode = Mode::Inactive;
	presentation = {};
}

void DialogueRuntime::update(int deltaMs) {
	if (mode == Mode::PresentingLine) {
		if (!presentation.revealCompleted) {
			revealCharacters += revealSpeedCharsPerSecond * (static_cast<float>(deltaMs) / 1000.0f);
			const size_t fullLen = presentation.fullText.size();
			const size_t visibleLen = static_cast<size_t>(std::min<float>(revealCharacters, static_cast<float>(fullLen)));
			presentation.visibleText = presentation.fullText.substr(0, visibleLen);
			presentation.revealCompleted = (visibleLen >= fullLen);
		}
		return;
	}

	if (mode == Mode::PlayingCutscene && activeCutscene) {
		cutsceneElapsedMs += deltaMs;

		if (!activeCutscene->lines.empty() &&
			currentCutsceneLine >= 0 &&
			currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {

			const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
			if (!line.waitForConfirm) {
				cutsceneTimerMs += deltaMs;
				const int durationMs =
					(line.durationMs > 0)
					? line.durationMs
					: computeFallbackCutsceneDurationMs(line.text);

				if (cutsceneTimerMs >= durationMs) {
					advanceCutsceneLine();

					// ВАЖНО: после advance катсцена могла завершиться
					if (!activeCutscene || mode != Mode::PlayingCutscene) {
						return;
					}
				}
			}
		}

		if (!activeCutscene || mode != Mode::PlayingCutscene) {
			return;
		}

		refreshCutscenePresentation();

		if (!activeCutscene || mode != Mode::PlayingCutscene) {
			return;
		}

		const bool subtitlesFinished =
			activeCutscene->lines.empty() ||
			currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size());

		const bool durationFinished =
			cutsceneTotalDurationMs > 0 &&
			cutsceneElapsedMs >= cutsceneTotalDurationMs;

		if (activeCutscene->lines.empty()) {
			if (durationFinished) {
				finishCutscene();
			}
			return;
		}

		if (subtitlesFinished && (cutsceneTotalDurationMs <= 0 || durationFinished)) {
			finishCutscene();
			return;
		}
	}
}

void DialogueRuntime::confirmAdvance() {
	if (mode == Mode::PresentingLine) {
		if (!presentation.revealCompleted) {
			presentation.visibleText = presentation.fullText;
			presentation.revealCompleted = true;
			revealCharacters = static_cast<float>(presentation.fullText.size());
			return;
		}

		if (!activeDialogue) {
			stop();
			return;
		}

		auto it = activeDialogue->nodes.find(currentNodeId);
		if (it == activeDialogue->nodes.end()) {
			stop();
			return;
		}

		if (!it->second.next.empty()) {
			enterNode(it->second.next);
		}
		else {
			stop();
		}
		return;
	}

	if (mode == Mode::WaitingForChoice) {
		if (visibleChoices.empty() || selectedChoice < 0 || selectedChoice >= static_cast<int>(visibleChoices.size())) {
			return;
		}

		const Choice& choice = visibleChoices[selectedChoice];
		if (choice.consumeOnce && !choice.id.empty()) {
			consumedChoices.insert(choice.id);
		}
		applyEffects(choice.effects);
		enterNode(choice.next);
		return;
	}

	if (mode == Mode::PlayingCutscene) {
		return;
	}
}

void DialogueRuntime::moveSelection(int delta) {
	if (mode != Mode::WaitingForChoice || visibleChoices.empty()) {
		return;
	}

	const int count = static_cast<int>(visibleChoices.size());
	if (selectedChoice < 0 || selectedChoice >= count) {
		selectedChoice = (delta >= 0) ? 0 : (count - 1);
	}
	else {
		selectedChoice = (selectedChoice + delta) % count;
		if (selectedChoice < 0) {
			selectedChoice += count;
		}
	}
	presentation.selectedChoice = selectedChoice;
}

void DialogueRuntime::selectChoice(int index) {
	if (mode != Mode::WaitingForChoice || visibleChoices.empty()) {
		return;
	}
	if (index < 0 || index >= static_cast<int>(visibleChoices.size())) {
		return;
	}
	selectedChoice = index;
	presentation.selectedChoice = selectedChoice;
}

bool DialogueRuntime::canSkipCurrentCutscene() const {
	return mode == Mode::PlayingCutscene && activeCutscene && activeCutscene->skippable;
}

void DialogueRuntime::skipCurrentCutscene() {
	if (!canSkipCurrentCutscene()) {
		return;
	}

	// Multi-image cutscenes skip to the next image cue first.
	// This matches the desired behavior: skip advances the visual chapter,
	// not necessarily the whole cutscene immediately.
	if (!activeCutscene->images.empty()) {
		int nextImageStartMs = -1;

		for (const CutsceneImageCue& cue : activeCutscene->images) {
			if (cue.path.empty()) {
				continue;
			}
			if (cue.startMs > cutsceneElapsedMs) {
				if (nextImageStartMs < 0 || cue.startMs < nextImageStartMs) {
					nextImageStartMs = cue.startMs;
				}
			}
		}

		if (nextImageStartMs >= 0) {
			cutsceneElapsedMs = nextImageStartMs;
			syncCutsceneLineToElapsedTime();
			refreshCutscenePresentation();
			return;
		}
	}

	finishCutscene();
}

void DialogueRuntime::setFlag(const std::string& name, int value) {
	flags[name] = value;
}

int DialogueRuntime::getFlag(const std::string& name) const {
	auto it = flags.find(name);
	return (it != flags.end()) ? it->second : 0;
}

bool DialogueRuntime::evaluateConditions(const std::vector<Condition>& conditions) const {
	for (const Condition& condition : conditions) {
		const int currentValue = getFlag(condition.flag);
		switch (condition.op) {
		case ComparisonOp::Exists:
			if (currentValue == 0) return false;
			break;
		case ComparisonOp::Equals:
			if (currentValue != condition.value) return false;
			break;
		case ComparisonOp::NotEquals:
			if (currentValue == condition.value) return false;
			break;
		case ComparisonOp::GreaterOrEqual:
			if (currentValue < condition.value) return false;
			break;
		case ComparisonOp::LessOrEqual:
			if (currentValue > condition.value) return false;
			break;
		}
	}
	return true;
}

void DialogueRuntime::applyEffects(const std::vector<Effect>& effects) {
	for (const Effect& effect : effects) {
		if (effect.flag.empty()) {
			continue;
		}
		if (effect.relative) {
			flags[effect.flag] += effect.value;
		}
		else {
			flags[effect.flag] = effect.value;
		}
	}
}

bool DialogueRuntime::enterNode(const std::string& nodeId) {
	if (!activeDialogue) {
		stop();
		return false;
	}

	auto it = activeDialogue->nodes.find(nodeId);
	if (it == activeDialogue->nodes.end()) {
		std::cerr << "[dialogue] Node not found: " << nodeId << " in dialogue " << activeDialogue->id << "\n";
		stop();
		return false;
	}

	const Node& node = it->second;
	currentNodeId = node.id;
	presentation.dialogueId = activeDialogue->id;

	switch (node.type) {
	case NodeType::Line:
		presentLine(node);
		return true;

	case NodeType::Choice:
		presentChoices(node);
		return true;

	case NodeType::Condition:
		if (evaluateConditions(node.conditions)) {
			return enterNode(!node.trueNext.empty() ? node.trueNext : node.next);
		}
		return enterNode(node.falseNext);

	case NodeType::SetFlag:
		applyEffects(node.effects);
		if (!node.next.empty()) {
			return enterNode(node.next);
		}
		stop();
		return true;

	case NodeType::Jump:
		if (!node.next.empty()) {
			return enterNode(node.next);
		}
		stop();
		return true;

	case NodeType::End:
		stop();
		return true;

	case NodeType::CutsceneStart:
		activeCutsceneId = node.cutsceneId;
		startCutscene(node.cutsceneId, node.next);
		return true;
	}

	stop();
	return false;
}

void DialogueRuntime::presentLine(const Node& node) {
	mode = Mode::PresentingLine;
	revealCharacters = 0.0f;
	presentation.mode = PresentationMode::Dialogue;
	presentation.speaker = node.speaker;
	presentation.fullText = node.text;
	presentation.visibleText.clear();
	presentation.portraitPath = node.portrait;
	presentation.backgroundPath.clear();
	presentation.choices.clear();
	presentation.selectedChoice = -1;
	presentation.revealCompleted = node.text.empty();
	presentation.showCutsceneSubtitle = false;
	presentation.cutsceneSkippable = false;
	presentation.cutsceneCamera = {};
	presentation.cutsceneImages.clear();
	presentation.cutsceneGlobalFadeAlpha = 1.0f;
	presentation.cutsceneBlackAlpha = 0.0f;

	if (presentation.revealCompleted) {
		presentation.visibleText = node.text;
		revealCharacters = static_cast<float>(node.text.size());
	}
}

void DialogueRuntime::presentChoices(const Node& node) {
	visibleChoices.clear();
	presentation.choices.clear();

	for (const Choice& choice : node.choices) {
		if (!choice.id.empty() && consumedChoices.count(choice.id) > 0) {
			continue;
		}
		if (!evaluateConditions(choice.conditions)) {
			continue;
		}
		visibleChoices.push_back(choice);
		presentation.choices.push_back({ choice.id, choice.text, choice.kind });
	}

	if (visibleChoices.empty()) {
		if (!node.next.empty()) {
			enterNode(node.next);
		}
		else {
			stop();
		}
		return;
	}

	mode = Mode::WaitingForChoice;
	selectedChoice = -1;
	presentation.mode = PresentationMode::Choice;
	presentation.speaker = node.speaker;
	presentation.fullText = node.text;
	presentation.visibleText = node.text;
	presentation.portraitPath = node.portrait;
	presentation.backgroundPath.clear();
	presentation.selectedChoice = -1;
	presentation.revealCompleted = true;
	presentation.showCutsceneSubtitle = false;
	presentation.cutsceneSkippable = false;
	presentation.cutsceneCamera = {};
	presentation.cutsceneImages.clear();
	presentation.cutsceneGlobalFadeAlpha = 1.0f;
	presentation.cutsceneBlackAlpha = 0.0f;
}

void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) {
	if (!database) {
		stop();
		return;
	}

	const StaticCutsceneDefinition* cutscene = database->findCutscene(cutsceneId);
	if (!cutscene) {
		std::cerr << "[dialogue] Cutscene not found: " << cutsceneId << "\n";
		if (!nextNodeAfterCutscene.empty()) {
			enterNode(nextNodeAfterCutscene);
		}
		else {
			stop();
		}
		return;
	}

	activeCutscene = cutscene;
	pendingNodeAfterCutscene = nextNodeAfterCutscene;
	currentCutsceneBackground = cutscene->background;
	mode = Mode::PlayingCutscene;
	cutsceneElapsedMs = 0;
	cutsceneTimerMs = 0;
	currentCutsceneLine = activeCutscene->lines.empty() ? -1 : 0;
	int imageTrackDurationMs = 0;
	for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
		const CutsceneImageCue& cue = activeCutscene->images[i];
		int cueEnd = cue.endMs;
		if (cueEnd <= cue.startMs) {
			if (i + 1 < activeCutscene->images.size()) {
				cueEnd = std::max(activeCutscene->images[i + 1].startMs, cue.startMs);
			}
			else {
				cueEnd = cue.startMs + std::max(cue.fadeInMs, 0) + std::max(cue.fadeOutMs, 0) + 1000;
			}
		}
		imageTrackDurationMs = std::max(imageTrackDurationMs, cueEnd);
	} 
	cutsceneContentDurationMs = std::max({ activeCutscene->durationMs, computeCameraTrackDurationMs(*activeCutscene), imageTrackDurationMs });
	if (cutsceneContentDurationMs <= 0 && activeCutscene->lines.empty()) {
		cutsceneContentDurationMs = 3000;
	}
	cutsceneTotalDurationMs = cutsceneContentDurationMs + activeCutscene->endFadeOutMs + activeCutscene->endFadeInMs;
	refreshCutscenePresentation();

	std::cout << "[CUTSCENE] start id=" << cutsceneId
		<< " lines=" << activeCutscene->lines.size()
		<< " totalDuration=" << cutsceneTotalDurationMs
		<< std::endl;
}

void DialogueRuntime::finishCutscene() {

	std::cout << "[CUTSCENE] finish nextNode=" << pendingNodeAfterCutscene << std::endl;
	const std::string finishedId = activeCutsceneId;
	activeCutsceneId.clear();
	activeCutscene = nullptr;
	currentCutsceneLine = -1;
	cutsceneTimerMs = 0;
	cutsceneElapsedMs = 0;
	cutsceneTotalDurationMs = 0;
	cutsceneContentDurationMs = 0;
	currentCutsceneBackground.clear();
	if (onCutsceneFinished && !finishedId.empty())
		onCutsceneFinished(finishedId);

	if (!pendingNodeAfterCutscene.empty()) {
		const std::string nextNode = pendingNodeAfterCutscene;
		pendingNodeAfterCutscene.clear();
		enterNode(nextNode);
	}
	else {
		stop();
	}
}

void DialogueRuntime::syncCutsceneLineToElapsedTime() {
	if (!activeCutscene || activeCutscene->lines.empty()) {
		currentCutsceneLine = -1;
		cutsceneTimerMs = 0;
		return;
	}

	int elapsed = std::max(cutsceneElapsedMs, 0);
	int accumulatedMs = 0;

	for (size_t i = 0; i < activeCutscene->lines.size(); ++i) {
		const CutsceneLine& line = activeCutscene->lines[i];
		const int durationMs = (line.durationMs > 0)
			? line.durationMs
			: computeFallbackCutsceneDurationMs(line.text);

		if (elapsed < accumulatedMs + durationMs) {
			currentCutsceneLine = static_cast<int>(i);
			cutsceneTimerMs = std::max(0, elapsed - accumulatedMs);
			return;
		}

		accumulatedMs += durationMs;
	}

	currentCutsceneLine = -1;
	cutsceneTimerMs = 0;
}

void DialogueRuntime::advanceCutsceneLine() {
	if (!activeCutscene) {
		stop();
		return;
	}

	if (activeCutscene->lines.empty()) {
		return;
	}

	std::cout << "[CUTSCENE] advance before current=" << currentCutsceneLine << std::endl;
	++currentCutsceneLine;
	std::cout << "[CUTSCENE] advance after current=" << currentCutsceneLine << std::endl;
	cutsceneTimerMs = 0;

	if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
		refreshCutscenePresentation();
		if (cutsceneContentDurationMs <= 0 || cutsceneElapsedMs >= cutsceneContentDurationMs) {
			// Only finish immediately if there's no end transition to play
			if (activeCutscene->endFadeOutMs <= 0 && activeCutscene->endFadeInMs <= 0) {
				finishCutscene();
			}
		}
		return;
	}

	refreshCutscenePresentation();
}

CutsceneCameraBlendState DialogueRuntime::evaluateCutsceneCameraBlend() const {
	CutsceneCameraBlendState result;
	result.active = false;
	result.from = {};
	result.to = {};
	result.t = 1.0f;

	if (!activeCutscene || activeCutscene->cameraTrack.empty()) {
		return result;
	}

	int elapsed = cutsceneElapsedMs;
	for (const CutsceneCameraSegment& segment : activeCutscene->cameraTrack) {
		const int durationMs = std::max(segment.durationMs, 1);
		if (elapsed <= durationMs) {
			result.active = true;
			result.from = segment.from;
			result.to = segment.to;
			result.t = applyEasing(
				segment.easing,
				std::clamp(static_cast<float>(elapsed) / static_cast<float>(durationMs), 0.0f, 1.0f)
			);
			return result;
		}
		elapsed -= durationMs;
	}

	result.active = true;
	result.from = activeCutscene->cameraTrack.back().to;
	result.to = activeCutscene->cameraTrack.back().to;
	result.t = 1.0f;

	return result;
}

std::vector<PresentedCutsceneImage> DialogueRuntime::evaluateCutsceneImages() const {
	std::vector<PresentedCutsceneImage> result;
	if (!activeCutscene) {
		return result;
	}

	const std::string& fallbackPath = !currentCutsceneBackground.empty()
		? currentCutsceneBackground
		: activeCutscene->background;

	if (activeCutscene->images.empty()) {
		if (!fallbackPath.empty()) {
			result.push_back({ fallbackPath, 1.0f });
		}
		return result;
	}

	const int effectiveTotalDuration = (cutsceneContentDurationMs > 0) ? cutsceneContentDurationMs : std::max(activeCutscene->durationMs, 1);
	const int now = std::max(cutsceneElapsedMs, 0);

	for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
		const CutsceneImageCue& cue = activeCutscene->images[i];
		if (cue.path.empty()) {
			continue;
		}

		const int startMs = std::max(cue.startMs, 0);
		int endMs = cue.endMs;
		if (endMs <= startMs) {
			if (i + 1 < activeCutscene->images.size()) {
				endMs = std::max(activeCutscene->images[i + 1].startMs, startMs + 1);
			}
			else {
				endMs = effectiveTotalDuration;
			}
		}
		if (endMs <= startMs) {
			endMs = startMs + 1;
		}

		if (now < startMs || now > endMs) {
			continue;
		}

		float alpha = 1.0f;
		if (cue.fadeInMs > 0 && now < startMs + cue.fadeInMs) {
			alpha = std::clamp(
				static_cast<float>(now - startMs) / static_cast<float>(cue.fadeInMs),
				0.0f,
				1.0f
			);
		}

		if (alpha > 0.0f) {
			result.push_back({ cue.path, alpha });
		}
	}

	// Safety fallback: never leave the cutscene without an opaque image layer.
	if (result.empty() && !fallbackPath.empty()) {
		result.push_back({ fallbackPath, 1.0f });
	}

	// If the first active layer is still fading in, put an opaque fallback/base below it.
	// This prevents the world from becoming visible behind the cutscene.
	if (!result.empty() && result.front().alpha < 0.999f && !fallbackPath.empty() && result.front().path != fallbackPath) {
		result.insert(result.begin(), { fallbackPath, 1.0f });
	}

	return result;
}

void DialogueRuntime::refreshCutscenePresentation() {
	if (!activeCutscene) {
		return;
	}

	presentation.mode = PresentationMode::Cutscene;
	presentation.backgroundPath = activeCutscene->background;
	presentation.backgroundWidth = activeCutscene->backgroundWidth;
	presentation.backgroundHeight = activeCutscene->backgroundHeight;
	presentation.cutsceneCamera = evaluateCutsceneCameraBlend();
	presentation.cutsceneImages = evaluateCutsceneImages();
	presentation.cutsceneSkippable = activeCutscene->skippable;

	const int fadeOutMs    = activeCutscene->fadeOutMs;
	const int fadeInMs     = activeCutscene->fadeInMs;
	const int endFadeOutMs = activeCutscene->endFadeOutMs;
	const int endFadeInMs  = activeCutscene->endFadeInMs;
	const int endFadeOutStart = cutsceneContentDurationMs;
	const int endFadeInStart  = cutsceneContentDurationMs + endFadeOutMs;

	if (cutsceneElapsedMs < fadeOutMs) {
		// Start phase 1: game world fading to black
		presentation.cutsceneGlobalFadeAlpha = 0.0f;
		presentation.cutsceneBlackAlpha = std::clamp(
			static_cast<float>(cutsceneElapsedMs) / static_cast<float>(fadeOutMs),
			0.0f, 1.0f
		);
	} else if (cutsceneElapsedMs < endFadeOutStart) {
		// Content playing (also covers start phase 2 fade-in)
		presentation.cutsceneGlobalFadeAlpha = 1.0f;
		const int phase2elapsed = cutsceneElapsedMs - fadeOutMs;
		presentation.cutsceneBlackAlpha = (fadeInMs > 0)
			? std::clamp(1.0f - static_cast<float>(phase2elapsed) / static_cast<float>(fadeInMs), 0.0f, 1.0f)
			: 0.0f;
	} else if (cutsceneElapsedMs < endFadeInStart) {
		// End phase 1: cutscene fading to black
		presentation.cutsceneGlobalFadeAlpha = 1.0f;
		const int elapsed = cutsceneElapsedMs - endFadeOutStart;
		presentation.cutsceneBlackAlpha = (endFadeOutMs > 0)
			? std::clamp(static_cast<float>(elapsed) / static_cast<float>(endFadeOutMs), 0.0f, 1.0f)
			: 1.0f;
	} else {
		// End phase 2: game world appearing from black (image hidden, world shows through)
		presentation.cutsceneGlobalFadeAlpha = 0.0f;
		const int elapsed = cutsceneElapsedMs - endFadeInStart;
		presentation.cutsceneBlackAlpha = (endFadeInMs > 0)
			? std::clamp(1.0f - static_cast<float>(elapsed) / static_cast<float>(endFadeInMs), 0.0f, 1.0f)
			: 0.0f;
	}

	presentation.choices.clear();
	presentation.selectedChoice = -1;
	presentation.revealCompleted = true;

	const bool hasSubtitle = currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size());
	presentation.showCutsceneSubtitle = hasSubtitle;

	if (!hasSubtitle) {
		presentation.speaker.clear();
		presentation.fullText.clear();
		presentation.visibleText.clear();
		presentation.portraitPath.clear();
		return;
	}

	const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];


	if (!line.background.empty()) {
		currentCutsceneBackground = line.background;
		if (line.backgroundWidth > 0)  presentation.backgroundWidth  = line.backgroundWidth;
		if (line.backgroundHeight > 0) presentation.backgroundHeight = line.backgroundHeight;
	}

	presentation.mode = PresentationMode::Cutscene;
	presentation.speaker = line.speaker;
	presentation.fullText = line.text;
	presentation.visibleText = line.text;
	presentation.portraitPath = line.portrait;

	//presentation.backgroundPath = activeCutscene->background;
	presentation.backgroundPath = currentCutsceneBackground;
	presentation.choices.clear();
	presentation.selectedChoice = 0;
	presentation.revealCompleted = true;


	std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size()
		<< " current=" << currentCutsceneLine
		<< std::endl;
}

float DialogueRuntime::applyEasing(EasingType easing, float t) {
	t = std::clamp(t, 0.0f, 1.0f);
	constexpr float PI = 3.14159265358979323846f;

	switch (easing) {
	case EasingType::EaseInSine:
		return 1.0f - std::cos((t * PI) * 0.5f);
	case EasingType::EaseOutSine:
		return std::sin((t * PI) * 0.5f);
	case EasingType::EaseInOutSine:
		return -(std::cos(PI * t) - 1.0f) * 0.5f;
	case EasingType::EaseInQuad:
		return t * t;
	case EasingType::EaseOutQuad:
		return 1.0f - (1.0f - t) * (1.0f - t);
	case EasingType::EaseInOutQuad:
		return (t < 0.5f) ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) * 0.5f;
	case EasingType::EaseInCubic:
		return t * t * t;
	case EasingType::EaseOutCubic:
		return 1.0f - std::pow(1.0f - t, 3.0f);
	case EasingType::EaseInOutCubic:
		return (t < 0.5f) ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) * 0.5f;
	case EasingType::Linear:
	default:
		return t;
	}
}

int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
	const int cps = 17;
	const int minDuration = 1500;
	const int linger = 450;
	const int calculated = static_cast<int>((1000.0 * static_cast<double>(std::max<size_t>(text.size(), 1))) / cps);
	return std::max(minDuration, calculated + linger);
}

int DialogueRuntime::computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene) {
	int total = 0;
	for (const CutsceneCameraSegment& segment : cutscene.cameraTrack) {
		total += std::max(segment.durationMs, 0);
	}
	return total;
}

DialogueRuntime::json DialogueRuntime::buildSaveState() const {
	json result;
	result["active"] = isActive();
	result["dialogueId"] = activeDialogue ? activeDialogue->id : "";
	result["currentNodeId"] = currentNodeId;
	result["pendingNodeAfterCutscene"] = pendingNodeAfterCutscene;
	result["selectedChoice"] = selectedChoice;
	result["currentCutsceneLine"] = currentCutsceneLine;
	result["cutsceneTimerMs"] = cutsceneTimerMs;
	result["flags"] = flags;
	result["consumedChoices"] = consumedChoices;
	return result;
}

bool DialogueRuntime::restoreSaveState(const json& state) {
	if (!database) {
		return false;
	}

	flags.clear();
	consumedChoices.clear();

	if (state.contains("flags")) {
		flags = state["flags"].get<std::unordered_map<std::string, int>>();
	}
	if (state.contains("consumedChoices")) {
		consumedChoices = state["consumedChoices"].get<std::unordered_set<std::string>>();
	}

	const bool active = state.value("active", false);
	if (!active) {
		stop();
		return true;
	}

	const std::string dialogueId = state.value("dialogueId", "");
	if (!startDialogue(dialogueId)) {
		return false;
	}

	const std::string nodeId = state.value("currentNodeId", "");
	pendingNodeAfterCutscene = state.value("pendingNodeAfterCutscene", "");
	selectedChoice = state.value("selectedChoice", -1);
	currentCutsceneLine = state.value("currentCutsceneLine", -1);
	cutsceneTimerMs = state.value("cutsceneTimerMs", 0);

	const bool ok = nodeId.empty() ? true : enterNode(nodeId);
	if (mode == Mode::WaitingForChoice && !visibleChoices.empty()) {
		if (selectedChoice >= 0) {
			selectedChoice = std::clamp(selectedChoice, 0, static_cast<int>(visibleChoices.size()) - 1);
		}
		presentation.selectedChoice = selectedChoice;
	}
	return ok;
}

} // namespace ZL::Dialogue
