#include "RenderCadenceController.h"

#include <algorithm>
#include <cmath>

void RenderCadenceController::Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy)
{
	mTargetFrameDuration = IsPositive(targetFrameDuration) ? targetFrameDuration : std::chrono::milliseconds(1);
	mPolicy = policy;
	if (mPolicy.skipThresholdFrames < 1.0)
		mPolicy.skipThresholdFrames = 1.0;
	Reset(firstRenderTime);
}

void RenderCadenceController::Reset(TimePoint firstRenderTime)
{
	mNextRenderTime = firstRenderTime;
	mNextFrameIndex = 0;
	mMetrics = RenderCadenceMetrics();
}

RenderCadenceDecision RenderCadenceController::Tick(TimePoint now)
{
	RenderCadenceDecision decision;
	decision.frameIndex = mNextFrameIndex;
	decision.renderTargetTime = mNextRenderTime;
	decision.nextRenderTime = mNextRenderTime;

	if (now < mNextRenderTime)
	{
		decision.action = RenderCadenceAction::Wait;
		decision.waitDuration = mNextRenderTime - now;
		decision.reason = "waiting-for-next-render-tick";
		return decision;
	}

	const Duration lateness = now - mNextRenderTime;
	const uint64_t skippedTicks = SkippedTicksForLateness(lateness);
	if (skippedTicks > 0)
	{
		decision.skippedTicks = skippedTicks;
		decision.frameIndex = mNextFrameIndex + skippedTicks;
		decision.renderTargetTime = mNextRenderTime + (mTargetFrameDuration * skippedTicks);
		decision.reason = "late-skip-render-ticks";
		mMetrics.skippedTickCount += skippedTicks;
	}
	else
	{
		decision.reason = IsPositive(lateness) ? "late-render-now" : "on-time-render";
	}

	decision.action = RenderCadenceAction::Render;
	decision.lateness = now > decision.renderTargetTime
		? now - decision.renderTargetTime
		: Duration::zero();
	mNextFrameIndex = decision.frameIndex + 1;
	mNextRenderTime = decision.renderTargetTime + mTargetFrameDuration;
	decision.nextRenderTime = mNextRenderTime;

	++mMetrics.renderedFrameCount;
	mMetrics.nextFrameIndex = mNextFrameIndex;
	mMetrics.lastLateness = decision.lateness;
	if (IsPositive(decision.lateness))
	{
		++mMetrics.lateFrameCount;
		mMetrics.maxLateness = (std::max)(mMetrics.maxLateness, decision.lateness);
	}

	return decision;
}

uint64_t RenderCadenceController::SkippedTicksForLateness(Duration lateness) const
{
	if (!mPolicy.skipLateTicks || !IsPositive(lateness) || !IsPositive(mTargetFrameDuration))
		return 0;

	const double lateFrames = static_cast<double>(lateness.count()) / static_cast<double>(mTargetFrameDuration.count());
	if (lateFrames < mPolicy.skipThresholdFrames)
		return 0;

	const uint64_t elapsedTicks = static_cast<uint64_t>(std::floor(lateFrames));
	if (elapsedTicks == 0)
		return 0;
	return (std::min)(elapsedTicks, mPolicy.maxSkippedTicksPerDecision);
}

bool RenderCadenceController::IsPositive(Duration duration)
{
	return duration > Duration::zero();
}

const char* RenderCadenceActionName(RenderCadenceAction action)
{
	switch (action)
	{
	case RenderCadenceAction::Render:
		return "Render";
	case RenderCadenceAction::Wait:
	default:
		return "Wait";
	}
}
