#include "DeckLinkSession.h"

#include <atlbase.h>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <new>
#include <sstream>
#include <utility>
#include <vector>

namespace
{
constexpr int64_t kMinimumHealthyScheduleLeadFrames = 4;
constexpr int64_t kProactiveScheduleLeadFloorFrames = 1;

class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
{
public:
	SystemMemoryDeckLinkVideoBuffer(void* bytes, unsigned long long sizeBytes) :
		mBytes(bytes),
		mSizeBytes(sizeBytes),
		mRefCount(1)
	{
	}

	HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv) override
	{
		if (ppv == nullptr)
			return E_POINTER;
		if (iid == IID_IUnknown || iid == IID_IDeckLinkVideoBuffer)
		{
			*ppv = static_cast<IDeckLinkVideoBuffer*>(this);
			AddRef();
			return S_OK;
		}
		*ppv = nullptr;
		return E_NOINTERFACE;
	}

	ULONG STDMETHODCALLTYPE AddRef() override
	{
		return ++mRefCount;
	}

	ULONG STDMETHODCALLTYPE Release() override
	{
		const ULONG refCount = --mRefCount;
		if (refCount == 0)
			delete this;
		return refCount;
	}

	HRESULT STDMETHODCALLTYPE GetBytes(void** buffer) override
	{
		if (buffer == nullptr)
			return E_POINTER;
		*buffer = mBytes;
		return mBytes != nullptr ? S_OK : E_FAIL;
	}

	HRESULT STDMETHODCALLTYPE GetSize(unsigned long long* bufferSize) override
	{
		if (bufferSize == nullptr)
			return E_POINTER;
		*bufferSize = mSizeBytes;
		return S_OK;
	}

	HRESULT STDMETHODCALLTYPE StartAccess(BMDBufferAccessFlags) override
	{
		return S_OK;
	}

	HRESULT STDMETHODCALLTYPE EndAccess(BMDBufferAccessFlags) override
	{
		return S_OK;
	}

private:
	void* mBytes = nullptr;
	unsigned long long mSizeBytes = 0;
	std::atomic<ULONG> mRefCount;
};

std::string BstrToUtf8(BSTR value)
{
	if (value == nullptr)
		return std::string();

	const int requiredBytes = WideCharToMultiByte(CP_UTF8, 0, value, -1, NULL, 0, NULL, NULL);
	if (requiredBytes <= 1)
		return std::string();

	std::vector<char> utf8Name(static_cast<std::size_t>(requiredBytes), '\0');
	if (WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8Name.data(), requiredBytes, NULL, NULL) <= 0)
		return std::string();

	return std::string(utf8Name.data());
}

bool InputSupportsFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
	if (input == nullptr)
		return false;

	BOOL supported = FALSE;
	BMDDisplayMode actualMode = bmdModeUnknown;
	const HRESULT result = input->DoesSupportVideoMode(
		bmdVideoConnectionUnspecified,
		displayMode,
		pixelFormat,
		bmdNoVideoInputConversion,
		bmdSupportedVideoModeDefault,
		&actualMode,
		&supported);
	return result == S_OK && supported != FALSE;
}

bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
	if (output == nullptr)
		return false;

	BOOL supported = FALSE;
	BMDDisplayMode actualMode = bmdModeUnknown;
	const HRESULT result = output->DoesSupportVideoMode(
		bmdVideoConnectionUnspecified,
		displayMode,
		pixelFormat,
		bmdNoVideoOutputConversion,
		bmdSupportedVideoModeDefault,
		&actualMode,
		&supported);
	return result == S_OK && supported != FALSE;
}
}

DeckLinkSession::~DeckLinkSession()
{
	ReleaseResources();
}

void DeckLinkSession::ReleaseResources()
{
	if (input != nullptr)
		input->SetCallback(nullptr);
	captureDelegate.Release();
	input.Release();

	if (output != nullptr)
		output->SetScheduledFrameCompletionCallback(nullptr);

	if (keyer != nullptr)
	{
		keyer->Disable();
		mState.externalKeyingActive = false;
	}
	keyer.Release();

	playoutDelegate.Release();
	outputVideoFrameQueue.clear();
	output.Release();
}

bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
{
	CComPtr<IDeckLinkIterator> deckLinkIterator;
	CComPtr<IDeckLinkDisplayMode> inputMode;
	CComPtr<IDeckLinkDisplayMode> outputMode;
	BMDDisplayMode inputDisplayMode = bmdModeUnknown;
	BMDDisplayMode outputDisplayMode = bmdModeUnknown;

	if (!DeckLinkDisplayModeForVideoFormat(videoModes.input, inputDisplayMode))
	{
		error = "Cannot map configured input mode to DeckLink BMDDisplayMode: " + videoModes.input.displayName;
		return false;
	}
	if (!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode))
	{
		error = "Cannot map configured output mode to DeckLink BMDDisplayMode: " + videoModes.output.displayName;
		return false;
	}

	mState.inputDisplayModeName = videoModes.input.displayName;
	mState.outputDisplayModeName = videoModes.output.displayName;

	HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
	if (FAILED(result))
	{
		error = "Please install the Blackmagic DeckLink drivers to use the features of this application.";
		return false;
	}

	CComPtr<IDeckLink> deckLink;
	while (deckLinkIterator->Next(&deckLink) == S_OK)
	{
		int64_t duplexMode;
		bool deviceSupportsInternalKeying = false;
		bool deviceSupportsExternalKeying = false;
		std::string modelName;
		CComPtr<IDeckLinkProfileAttributes> deckLinkAttributes;

		if (deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes) != S_OK)
		{
			printf("Could not obtain the IDeckLinkProfileAttributes interface\n");
			deckLink.Release();
			continue;
		}

		result = deckLinkAttributes->GetInt(BMDDeckLinkDuplex, &duplexMode);
		BOOL attributeFlag = FALSE;
		if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &attributeFlag) == S_OK)
			deviceSupportsInternalKeying = (attributeFlag != FALSE);
		attributeFlag = FALSE;
		if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsExternalKeying, &attributeFlag) == S_OK)
			deviceSupportsExternalKeying = (attributeFlag != FALSE);
		CComBSTR modelNameBstr;
		if (deckLinkAttributes->GetString(BMDDeckLinkModelName, &modelNameBstr) == S_OK)
			modelName = BstrToUtf8(modelNameBstr);

		if (result != S_OK || duplexMode == bmdDuplexInactive)
		{
			deckLink.Release();
			continue;
		}

		bool inputUsed = false;
		if (!input && deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&input) == S_OK)
			inputUsed = true;

		if (!output && (!inputUsed || (duplexMode == bmdDuplexFull)))
		{
			if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
				output.Release();
			else
			{
				mState.outputModelName = modelName;
				mState.supportsInternalKeying = deviceSupportsInternalKeying;
				mState.supportsExternalKeying = deviceSupportsExternalKeying;
			}
		}

		deckLink.Release();

		if (output && input)
			break;
	}

	if (!output)
	{
		error = "Expected an Output DeckLink device";
		ReleaseResources();
		return false;
	}

	CComPtr<IDeckLinkDisplayModeIterator> inputDisplayModeIterator;
	if (input && input->GetDisplayModeIterator(&inputDisplayModeIterator) != S_OK)
	{
		error = "Cannot get input Display Mode Iterator.";
		ReleaseResources();
		return false;
	}

	if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, inputDisplayMode, &inputMode))
	{
		error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
		ReleaseResources();
		return false;
	}
	inputDisplayModeIterator.Release();

	CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
	if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
	{
		error = "Cannot get output Display Mode Iterator.";
		ReleaseResources();
		return false;
	}

	if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, outputDisplayMode, &outputMode))
	{
		error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName;
		ReleaseResources();
		return false;
	}

	mState.outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
	mState.inputFrameSize = inputMode
		? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
		: mState.outputFrameSize;
	if (!input)
		mState.inputDisplayModeName = "No input - black frame";
	BMDTimeValue frameDuration = 0;
	BMDTimeScale frameTimescale = 0;
	outputMode->GetFrameRate(&frameDuration, &frameTimescale);
	mScheduler.Configure(frameDuration, frameTimescale, mPlayoutPolicy);
	mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();

	mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
	mState.outputFrameRowBytes = mState.outputFrameSize.width * 4u;
	mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
	mState.outputPackTextureWidth = mState.outputFrameSize.width;
	mState.hasInputDevice = input != nullptr;
	mState.hasInputSource = false;

	return true;
}

bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
{
	if (!output)
	{
		error = "Expected an Output DeckLink device";
		return false;
	}

	mState.formatStatusMessage.clear();
	BMDDisplayMode inputDisplayMode = bmdModeUnknown;
	BMDDisplayMode outputDisplayMode = bmdModeUnknown;
	if (!DeckLinkDisplayModeForVideoFormat(videoModes.input, inputDisplayMode) ||
		!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode))
	{
		error = "DeckLink format selection failed while mapping configured video modes.";
		return false;
	}

	const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, inputDisplayMode, bmdFormat10BitYUV);
	mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
	if (input != nullptr && !inputTenBitSupported)
		mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";

	const bool outputTenBitSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUV);
	const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUVA);
	mState.outputPixelFormat = outputAlphaRequired
		? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
		: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
	if (outputAlphaRequired && outputTenBitYuvaSupported)
		mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
	else if (outputAlphaRequired)
		mState.formatStatusMessage += "External keying requires alpha, but DeckLink output does not report 10-bit YUVA support for the configured mode; using 8-bit BGRA output. ";
	else if (!outputTenBitSupported)
		mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";

	int deckLinkOutputRowBytes = 0;
	if (output->RowBytesForPixelFormat(DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat), mState.outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
	{
		error = "DeckLink output setup failed while calculating output row bytes.";
		return false;
	}
	mState.outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
	mState.outputPackTextureWidth = OutputIsTenBit()
		? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes)
		: mState.outputFrameSize.width;

	if (InputIsTenBit())
	{
		int deckLinkInputRowBytes = 0;
		if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
			mState.inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
		else
			mState.inputFrameRowBytes = MinimumV210RowBytes(mState.inputFrameSize.width);
	}
	else
	{
		mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
	}
	mState.captureTextureWidth = InputIsTenBit()
		? PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes)
		: mState.inputFrameSize.width / 2u;

	std::ostringstream status;
	status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(mState.inputPixelFormat) : "none")
		<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ".";
	if (!mState.formatStatusMessage.empty())
		status << " " << mState.formatStatusMessage;
	mState.formatStatusMessage = status.str();
	return true;
}

bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error)
{
	mInputFrameCallback = std::move(callback);

	if (!input)
	{
		mState.hasInputSource = false;
		mState.inputDisplayModeName = "No input - black frame";
		return true;
	}

	BMDDisplayMode inputDisplayMode = bmdModeUnknown;
	if (!DeckLinkDisplayModeForVideoFormat(inputVideoMode, inputDisplayMode))
	{
		error = "DeckLink input setup failed while mapping " + inputVideoMode.displayName + " to a DeckLink display mode.";
		return false;
	}
	const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
	if (input->EnableVideoInput(inputDisplayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
	{
		if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
		{
			OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
			mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
			mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
			mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
			if (input->EnableVideoInput(inputDisplayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
			{
				std::ostringstream status;
				status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
					<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat)
					<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
				mState.formatStatusMessage = status.str();
				goto input_enabled;
			}
		}

		OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
		input.Release();
		mState.hasInputDevice = false;
		mState.hasInputSource = false;
		mState.inputDisplayModeName = "No input - black frame";
		return true;
	}

input_enabled:
	captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this));
	if (captureDelegate == nullptr)
	{
		error = "DeckLink input setup failed while creating the capture callback.";
		return false;
	}
	if (input->SetCallback(captureDelegate) != S_OK)
	{
		error = "DeckLink input setup failed while installing the capture callback.";
		return false;
	}

	return true;
}

bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
{
	mOutputFrameCallback = std::move(callback);

	BMDDisplayMode outputDisplayMode = bmdModeUnknown;
	if (!DeckLinkDisplayModeForVideoFormat(outputVideoMode, outputDisplayMode))
	{
		error = "DeckLink output setup failed while mapping " + outputVideoMode.displayName + " to a DeckLink display mode.";
		return false;
	}
	if (output->EnableVideoOutput(outputDisplayMode, bmdVideoOutputFlagDefault) != S_OK)
	{
		error = "DeckLink output setup failed while enabling video output.";
		return false;
	}

	if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
		mState.keyerInterfaceAvailable = true;

	if (externalKeyingEnabled)
	{
		if (!mState.supportsExternalKeying)
		{
			mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
		}
		else if (!mState.keyerInterfaceAvailable)
		{
			mState.statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
		}
		else if (keyer->Enable(TRUE) != S_OK || keyer->SetLevel(255) != S_OK)
		{
			mState.statusMessage = "External keying was requested, but enabling the DeckLink keyer failed.";
		}
		else
		{
			mState.externalKeyingActive = true;
			mState.statusMessage = "External keying is active on the selected DeckLink output.";
		}
	}
	else if (mState.supportsExternalKeying)
	{
		mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
	}

	const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
	mPlayoutPolicy = policy;
	for (unsigned i = 0; i < policy.outputFramePoolSize; i++)
	{
		CComPtr<IDeckLinkMutableVideoFrame> outputFrame;

		const BMDPixelFormat deckLinkOutputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat);
		if (output->CreateVideoFrame(mState.outputFrameSize.width, mState.outputFrameSize.height, mState.outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
		{
			error = "DeckLink output setup failed while creating an output video frame.";
			return false;
		}

		outputVideoFrameQueue.push_back(outputFrame);
	}

	playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(this));
	if (playoutDelegate == nullptr)
	{
		error = "DeckLink output setup failed while creating the playout callback.";
		return false;
	}

	if (output->SetScheduledFrameCompletionCallback(playoutDelegate) != S_OK)
	{
		error = "DeckLink output setup failed while installing the scheduled-frame callback.";
		return false;
	}

	if (!mState.formatStatusMessage.empty())
		mState.statusMessage = mState.statusMessage.empty() ? mState.formatStatusMessage : mState.formatStatusMessage + " " + mState.statusMessage;

	return true;
}

double DeckLinkSession::FrameBudgetMilliseconds() const
{
	return mScheduler.FrameBudgetMilliseconds();
}

bool DeckLinkSession::AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame)
{
	if (outputVideoFrameQueue.empty())
		return false;

	outputVideoFrame = outputVideoFrameQueue.front();
	outputVideoFrameQueue.pop_front();
	return outputVideoFrame != nullptr;
}

bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame)
{
	if (outputVideoFrame == nullptr)
		return false;

	CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
	if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
		return false;

	if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
		return false;

	void* pFrame = nullptr;
	outputVideoFrameBuffer->GetBytes(&pFrame);

	frame.bytes = pFrame;
	frame.rowBytes = outputVideoFrame->GetRowBytes();
	frame.width = mState.outputFrameSize.width;
	frame.height = mState.outputFrameSize.height;
	frame.pixelFormat = mState.outputPixelFormat;
	outputVideoFrame->AddRef();
	frame.nativeFrame = outputVideoFrame;
	frame.nativeBuffer = outputVideoFrameBuffer.Detach();
	return true;
}

bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
{
	if (outputVideoFrame == nullptr || output == nullptr)
	{
		++mState.deckLinkScheduleFailureCount;
		return false;
	}

	if (mScheduleRealignmentPending)
	{
		RealignScheduleCursorToPlayback();
		mScheduleRealignmentPending = false;
	}

	UpdateScheduleLeadTelemetry();
	MaybeRealignScheduleCursorForLowLead();
	const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
	const auto scheduleStart = std::chrono::steady_clock::now();
	const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
	const auto scheduleEnd = std::chrono::steady_clock::now();
	mState.deckLinkScheduleCallMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(scheduleEnd - scheduleStart).count();
	if (result != S_OK)
		++mState.deckLinkScheduleFailureCount;
	RefreshBufferedVideoFrameCount();
	return result == S_OK;
}

void DeckLinkSession::UpdateScheduleLeadTelemetry()
{
	if (output == nullptr)
	{
		mState.deckLinkScheduleLeadAvailable = false;
		return;
	}

	BMDTimeValue streamTime = 0;
	double playbackSpeed = 0.0;
	if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
	{
		mState.deckLinkScheduleLeadAvailable = false;
		return;
	}

	const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0
		? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
		: 0;
	const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
	mState.deckLinkScheduleLeadAvailable = true;
	mState.deckLinkPlaybackStreamTime = streamTime;
	mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
	mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
	mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
}

void DeckLinkSession::MaybeRealignScheduleCursorForLowLead()
{
	if (!mState.deckLinkScheduleLeadAvailable)
		return;

	if (mState.deckLinkScheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
	{
		mProactiveScheduleRealignmentArmed = true;
		return;
	}

	if (!mProactiveScheduleRealignmentArmed || mState.deckLinkScheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
		return;

	RealignScheduleCursorToPlayback();
	mProactiveScheduleRealignmentArmed = false;
}

void DeckLinkSession::RealignScheduleCursorToPlayback()
{
	if (output == nullptr)
		return;

	BMDTimeValue streamTime = 0;
	double playbackSpeed = 0.0;
	if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
		return;

	const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
	mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames);
	++mState.deckLinkScheduleRealignmentCount;
	UpdateScheduleLeadTelemetry();
}

bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
{
	if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
		return false;

	CComPtr<IDeckLinkVideoBuffer> videoBuffer;
	videoBuffer.Attach(new (std::nothrow) SystemMemoryDeckLinkVideoBuffer(
		frame.bytes,
		static_cast<unsigned long long>(frame.rowBytes) * static_cast<unsigned long long>(frame.height)));
	if (videoBuffer == nullptr)
		return false;

	CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
	const BMDPixelFormat pixelFormat = DeckLinkPixelFormatForVideoIO(frame.pixelFormat);
	if (output->CreateVideoFrameWithBuffer(
		frame.width,
		frame.height,
		frame.rowBytes,
		pixelFormat,
		bmdFrameFlagFlipVertical,
		videoBuffer,
		&outputVideoFrame) != S_OK)
	{
		return false;
	}

	IDeckLinkVideoFrame* scheduledFrame = outputVideoFrame;
	{
		std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
		mScheduledSystemFrameBuffers[scheduledFrame] = frame.bytes;
	}

	if (ScheduleFrame(outputVideoFrame))
		return true;

	{
		std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
		mScheduledSystemFrameBuffers.erase(scheduledFrame);
	}
	return false;
}

bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
{
	if (outputVideoFrame == nullptr)
		return false;

	CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
	if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
		return false;

	if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
		return false;

	void* pFrame = nullptr;
	outputVideoFrameBuffer->GetBytes((void**)&pFrame);
	memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);

	outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
	return ScheduleFrame(outputVideoFrame);
}

void DeckLinkSession::RefreshBufferedVideoFrameCount()
{
	if (output == nullptr)
	{
		mState.actualDeckLinkBufferedFramesAvailable = false;
		return;
	}

	unsigned int bufferedFrameCount = 0;
	if (output->GetBufferedVideoFrameCount(&bufferedFrameCount) == S_OK)
	{
		mState.actualDeckLinkBufferedFrames = bufferedFrameCount;
		mState.actualDeckLinkBufferedFramesAvailable = true;
	}
	else
	{
		mState.actualDeckLinkBufferedFramesAvailable = false;
	}
}

bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
{
	CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
	return AcquireNextOutputVideoFrame(outputVideoFrame) && PopulateOutputFrame(outputVideoFrame, frame);
}

void DeckLinkSession::EndOutputFrame(VideoIOOutputFrame& frame)
{
	IDeckLinkVideoBuffer* outputVideoFrameBuffer = static_cast<IDeckLinkVideoBuffer*>(frame.nativeBuffer);
	if (outputVideoFrameBuffer != nullptr)
	{
		outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
		outputVideoFrameBuffer->Release();
	}
	frame.nativeBuffer = nullptr;
	frame.bytes = nullptr;
}

VideoPlayoutRecoveryDecision DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth)
{
	return mScheduler.AccountForCompletionResult(completionResult, readyQueueDepth);
}

bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
{
	if (frame.nativeFrame == nullptr)
		return ScheduleSystemMemoryFrame(frame);

	IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
	const bool scheduled = ScheduleFrame(outputVideoFrame);
	if (outputVideoFrame != nullptr)
		outputVideoFrame->Release();
	return scheduled;
}

bool DeckLinkSession::PrepareOutputSchedule()
{
	mScheduler.Reset();
	RefreshBufferedVideoFrameCount();
	return output != nullptr;
}

bool DeckLinkSession::StartInputStreams()
{
	if (!input)
		return true;

	if (input->StartStreams() != S_OK)
	{
		MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
		return false;
	}
	return true;
}

bool DeckLinkSession::StartScheduledPlayback()
{
	if (!output)
	{
		MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
		return false;
	}

	if (output->StartScheduledPlayback(0, mScheduler.TimeScale(), 1.0) != S_OK)
	{
		MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
		return false;
	}
	RefreshBufferedVideoFrameCount();
	return true;
}

bool DeckLinkSession::Start()
{
	if (!output)
	{
		MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
		return false;
	}
	if (outputVideoFrameQueue.empty())
	{
		MessageBoxA(NULL, "Cannot start playout because the output frame queue is empty.", "DeckLink start failed", MB_OK | MB_ICONERROR);
		return false;
	}

	const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
	mPlayoutPolicy = policy;
	if (!PrepareOutputSchedule())
		return false;

	for (unsigned i = 0; i < policy.targetPrerollFrames; i++)
	{
		CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
		if (!AcquireNextOutputVideoFrame(outputVideoFrame))
		{
			MessageBoxA(NULL, "Could not acquire a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
			return false;
		}
		if (!ScheduleBlackFrame(outputVideoFrame))
		{
			MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
			return false;
		}
	}

	return StartInputStreams() && StartScheduledPlayback();
}

bool DeckLinkSession::Stop()
{
	if (keyer != nullptr)
	{
		keyer->Disable();
		mState.externalKeyingActive = false;
	}

	if (input)
	{
		input->StopStreams();
		input->DisableVideoInput();
	}

	if (output)
	{
		output->StopScheduledPlayback(0, NULL, 0);
		output->DisableVideoOutput();
	}

	return true;
}

void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
{
	mState.hasInputSource = !hasNoInputSource;
	if (hasNoInputSource || mInputFrameCallback == nullptr)
	{
		VideoIOFrame frame;
		frame.width = mState.inputFrameSize.width;
		frame.height = mState.inputFrameSize.height;
		frame.pixelFormat = mState.inputPixelFormat;
		frame.hasNoInputSource = hasNoInputSource;
		if (mInputFrameCallback)
			mInputFrameCallback(frame);
		return;
	}

	CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
	void* videoPixels = nullptr;
	if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
		return;
	if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
		return;

	inputFrameBuffer->GetBytes(&videoPixels);

	VideoIOFrame frame;
	frame.bytes = videoPixels;
	frame.rowBytes = inputFrame->GetRowBytes();
	frame.width = static_cast<unsigned>(inputFrame->GetWidth());
	frame.height = static_cast<unsigned>(inputFrame->GetHeight());
	frame.pixelFormat = mState.inputPixelFormat;
	frame.hasNoInputSource = hasNoInputSource;
	mInputFrameCallback(frame);

	inputFrameBuffer->EndAccess(bmdBufferAccessRead);
}

void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
{
	RefreshBufferedVideoFrameCount();

	void* completedSystemBuffer = nullptr;
	if (completedFrame != nullptr)
	{
		bool externalSystemFrame = false;
		{
			std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
			auto externalFrame = mScheduledSystemFrameBuffers.find(completedFrame);
			if (externalFrame != mScheduledSystemFrameBuffers.end())
			{
				completedSystemBuffer = externalFrame->second;
				mScheduledSystemFrameBuffers.erase(externalFrame);
				externalSystemFrame = true;
			}
		}

		if (!externalSystemFrame)
		{
			CComPtr<IDeckLinkMutableVideoFrame> reusableFrame;
			if (completedFrame->QueryInterface(IID_IDeckLinkMutableVideoFrame, reinterpret_cast<void**>(&reusableFrame)) == S_OK &&
				reusableFrame != nullptr)
			{
				outputVideoFrameQueue.push_back(reusableFrame);
			}
		}
	}

	if (!mOutputFrameCallback)
		return;

	VideoIOCompletion completion;
	completion.result = TranslateCompletionResult(completionResult);
	if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped)
	{
		if (mScheduleRealignmentArmed)
		{
			mScheduleRealignmentPending = true;
			mScheduleRealignmentArmed = false;
		}
	}
	else if (completion.result == VideoIOCompletionResult::Completed)
	{
		mScheduleRealignmentArmed = true;
	}
	completion.outputFrameBuffer = completedSystemBuffer;
	mOutputFrameCallback(completion);
}

VideoIOCompletionResult DeckLinkSession::TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult)
{
	switch (completionResult)
	{
	case bmdOutputFrameDisplayedLate:
		return VideoIOCompletionResult::DisplayedLate;
	case bmdOutputFrameDropped:
		return VideoIOCompletionResult::Dropped;
	case bmdOutputFrameFlushed:
		return VideoIOCompletionResult::Flushed;
	case bmdOutputFrameCompleted:
		return VideoIOCompletionResult::Completed;
	default:
		return VideoIOCompletionResult::Unknown;
	}
}
