#include "VideoPlayoutScheduler.h"

void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
{
	Configure(frameDuration, timeScale, VideoPlayoutPolicy());
}

void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy)
{
	mFrameDuration = frameDuration;
	mTimeScale = timeScale;
	mPolicy = NormalizeVideoPlayoutPolicy(policy);
	Reset();
}

void VideoPlayoutScheduler::Reset()
{
	mScheduledFrameIndex = 0;
	mCompletedFrameIndex = 0;
	mLateStreak = 0;
	mDropStreak = 0;
}

VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
{
	VideoIOScheduleTime time;
	time.streamTime = static_cast<int64_t>(mScheduledFrameIndex) * mFrameDuration;
	time.duration = mFrameDuration;
	time.timeScale = mTimeScale;
	time.frameIndex = mScheduledFrameIndex;
	++mScheduledFrameIndex;
	return time;
}

void VideoPlayoutScheduler::AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames)
{
	if (mFrameDuration <= 0 || streamTime < 0)
		return;

	const uint64_t playbackFrameIndex = static_cast<uint64_t>(streamTime / mFrameDuration);
	const uint64_t minimumScheduleIndex = playbackFrameIndex + leadFrames;
	if (minimumScheduleIndex > mScheduledFrameIndex)
		mScheduledFrameIndex = minimumScheduleIndex;
}

VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
{
	++mCompletedFrameIndex;
	if (result == VideoIOCompletionResult::DisplayedLate)
		++mLateStreak;
	else
		mLateStreak = 0;
	if (result == VideoIOCompletionResult::Dropped)
		++mDropStreak;
	else
		mDropStreak = 0;

	const uint64_t measuredLagFrames = MeasureLag(result, readyQueueDepth);
	const uint64_t catchUpFrames = measuredLagFrames < mPolicy.lateOrDropCatchUpFrames
		? measuredLagFrames
		: mPolicy.lateOrDropCatchUpFrames;
	if (catchUpFrames > 0)
		mScheduledFrameIndex += catchUpFrames;

	VideoPlayoutRecoveryDecision decision;
	decision.result = result;
	decision.completedFrameIndex = mCompletedFrameIndex;
	decision.scheduledFrameIndex = mScheduledFrameIndex;
	decision.readyQueueDepth = readyQueueDepth;
	decision.scheduledLeadFrames = mScheduledFrameIndex > mCompletedFrameIndex
		? mScheduledFrameIndex - mCompletedFrameIndex
		: 0;
	decision.measuredLagFrames = measuredLagFrames;
	decision.catchUpFrames = catchUpFrames;
	decision.lateStreak = mLateStreak;
	decision.dropStreak = mDropStreak;
	return decision;
}

double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
{
	return mTimeScale != 0
		? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
		: 0.0;
}

uint64_t VideoPlayoutScheduler::MeasureLag(VideoIOCompletionResult result, uint64_t readyQueueDepth) const
{
	if (result != VideoIOCompletionResult::DisplayedLate && result != VideoIOCompletionResult::Dropped)
		return 0;

	uint64_t lagFrames = 1;
	if (result == VideoIOCompletionResult::DisplayedLate && mLateStreak > lagFrames)
		lagFrames = mLateStreak;
	if (result == VideoIOCompletionResult::Dropped && mDropStreak * 2 > lagFrames)
		lagFrames = mDropStreak * 2;

	if (mCompletedFrameIndex >= mScheduledFrameIndex)
	{
		const uint64_t scheduleLagFrames = mCompletedFrameIndex - mScheduledFrameIndex + 1;
		if (scheduleLagFrames > lagFrames)
			lagFrames = scheduleLagFrames;
	}
	if (readyQueueDepth < mPolicy.targetReadyFrames && mPolicy.targetReadyFrames - readyQueueDepth > lagFrames)
		lagFrames = mPolicy.targetReadyFrames - readyQueueDepth;

	return lagFrames;
}
