/* -LICENSE-START-
 ** Copyright (c) 2014 Blackmagic Design
 **  
 ** Permission is hereby granted, free of charge, to any person or organization 
 ** obtaining a copy of the software and accompanying documentation (the 
 ** "Software") to use, reproduce, display, distribute, sub-license, execute, 
 ** and transmit the Software, and to prepare derivative works of the Software, 
 ** and to permit third-parties to whom the Software is furnished to do so, in 
 ** accordance with:
 ** 
 ** (1) if the Software is obtained from Blackmagic Design, the End User License 
 ** Agreement for the Software Development Kit (“EULA”) available at 
 ** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
 ** 
 ** (2) if the Software is obtained from any third party, such licensing terms 
 ** as notified by that third party,
 ** 
 ** and all subject to the following:
 ** 
 ** (3) the copyright notices in the Software and this entire statement, 
 ** including the above license grant, this restriction and the following 
 ** disclaimer, must be included in all copies of the Software, in whole or in 
 ** part, and all derivative works of the Software, unless such copies or 
 ** derivative works are solely in the form of machine-executable object code 
 ** generated by a source language processor.
 ** 
 ** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 ** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 ** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 
 ** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 
 ** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 
 ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 ** DEALINGS IN THE SOFTWARE.
 ** 
 ** A copy of the Software is available free of charge at 
 ** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
 ** 
 ** -LICENSE-END-
 */

#include <atomic>
#include "platform.h"

#if defined(_WIN32)
typedef BOOL BMDbool;
#else
typedef bool BMDbool;
#endif

// Video mode parameters
const BMDDisplayMode      kDisplayMode = bmdModeHD1080i50;
const BMDVideoOutputFlags kOutputFlags = bmdVideoOutputRP188;
const BMDPixelFormat      kPixelFormat = bmdFormat10BitYUV;

// Frame parameters
const INT32_UNSIGNED kRowBytes = 5120;
BMDTimeValue         gFrameDuration = 0;
BMDTimeScale         gTimeScale = 0;
INT32_UNSIGNED       gDropFrames = 0;

// Timecode options
const bool kIsDropFrame = false;

// 10-bit YUV blue pixels
const INT32_UNSIGNED kBlueData[4] = { 0x40aa298, 0x2a8a62a8, 0x298aa040, 0x2a8102a8 };

// The display mode object corresponding to kDisplayMode
IDeckLinkDisplayMode* gDisplayMode = NULL;

// Keep track of the number of scheduled frames
INT32_UNSIGNED gTotalFramesScheduled = 0;

static void convertFrameCountToTimecode(INT32_UNSIGNED frameCount, INT8_UNSIGNED* hours, INT8_UNSIGNED* minutes, INT8_UNSIGNED* seconds, INT8_UNSIGNED* frames)
{
	INT32_UNSIGNED maxFPS = (INT32_UNSIGNED)(gTimeScale / 1000);

	if (gDropFrames)
	{
		INT32_UNSIGNED deciMins, deciMinsRemainder;

		INT32_UNSIGNED framesIn10mins = (INT32_UNSIGNED)((60 * 10 * maxFPS) - (9 * gDropFrames));
		deciMins = frameCount / framesIn10mins;
		deciMinsRemainder = frameCount - (deciMins * framesIn10mins);

		// Add gDropFrames for 9 minutes of every 10 minutes that have elapsed
		// AND gDropFrames for every minute (over the first minute) in this 10-minute block.
		frameCount += gDropFrames * 9 * deciMins;
		if (deciMinsRemainder >= gDropFrames)
			frameCount += gDropFrames * ((deciMinsRemainder - gDropFrames) / (framesIn10mins / 10));
	}

	*frames = (INT8_UNSIGNED)(frameCount % maxFPS);
	frameCount /= maxFPS;
	*seconds = (INT8_UNSIGNED)(frameCount % 60);
	frameCount /= 60;
	*minutes = (INT8_UNSIGNED)(frameCount % 60);
	frameCount /= 60;
	*hours = (INT8_UNSIGNED)frameCount;
}

static HRESULT setRP188VitcTimecodeOnFrame(IDeckLinkMutableVideoFrame* videoFrame, INT8_UNSIGNED hours, INT8_UNSIGNED minutes, INT8_UNSIGNED seconds, INT8_UNSIGNED frames, BOOL setHFRTCTimecode)
{
	HRESULT	result = S_OK;

	BMDTimecodeFlags flags = bmdTimecodeFlagDefault;
	bool setVITC1Timecode = false;
	bool setVITC2Timecode = false;

	if (kIsDropFrame)
		flags |= bmdTimecodeIsDropFrame;

	// Clear old timecode from frame
	videoFrame->SetTimecode(bmdTimecodeRP188HighFrameRate, NULL);
	videoFrame->SetTimecode(bmdTimecodeRP188VITC1, NULL);
	videoFrame->SetTimecode(bmdTimecodeRP188VITC2, NULL);

	if (setHFRTCTimecode)
	{
		result = videoFrame->SetTimecodeFromComponents(bmdTimecodeRP188HighFrameRate, hours, minutes, seconds, frames, flags);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not set HFRTC timecode on frame - result = %08x\n", result);
			goto bail;
		}
	}
	
	if (gDisplayMode->GetFieldDominance() != bmdProgressiveFrame)
	{
		// An interlaced or PsF frame has both VITC1 and VITC2 set with the same timecode value (SMPTE ST 12-2:2014 7.2)
		setVITC1Timecode = true;
		setVITC2Timecode = true;
	}
	else if (gTimeScale / gFrameDuration <= 30)
	{
		// If this isn't a High-P mode, then just use VITC1 (SMPTE ST 12-2:2014 7.2)
		setVITC1Timecode = true;
	}
	else if (gTimeScale / gFrameDuration <= 60)
	{
		// If this is a High-P mode then use VITC1 on even frames and VITC2 on odd frames. This is done because the 
		// frames field of the RP188 VITC timecode cannot hold values greater than 30 (SMPTE ST 12-2:2014 7.2, 9.2)
		if ((frames & 1) == 0)
			setVITC1Timecode = true;
		else
			setVITC2Timecode = true;

		// Shift the frame count so it's maximum is within the valid range
		frames >>= 1;
	}

	if (setVITC1Timecode)
	{
		result = videoFrame->SetTimecodeFromComponents(bmdTimecodeRP188VITC1, hours, minutes, seconds, frames, flags);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not set VITC1 timecode on interlaced frame - result = %08x\n", result);
			goto bail;
		}
	}

	if (setVITC2Timecode)
	{
		// The VITC2 timecode also has the field mark flag set
		result = videoFrame->SetTimecodeFromComponents(bmdTimecodeRP188VITC2, hours, minutes, seconds, frames, flags | bmdTimecodeFieldMark);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not set VITC1 timecode on interlaced frame - result = %08x\n", result);
			goto bail;
		}
	}

bail:
	return result;
}

class OutputCallback: public IDeckLinkVideoOutputCallback
{
public:
	OutputCallback(IDeckLinkOutput* deckLinkOutput, BOOL deckLinkSupportsHFRTC) : m_refCount(1)
	{
		m_deckLinkOutput = deckLinkOutput;
		m_deckLinkOutput->AddRef();
		
		m_deckLinkSupportsHFRTC = deckLinkSupportsHFRTC;
	}

	virtual HRESULT	STDMETHODCALLTYPE ScheduledFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
	{
		return scheduleNextFrame(completedFrame);
	}

	virtual HRESULT	STDMETHODCALLTYPE ScheduledPlaybackHasStopped(void)
	{
		return S_OK;
	}

	// IUnknown needs only a dummy implementation
	virtual HRESULT	STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv)
	{
		return E_NOINTERFACE;
	}

	virtual ULONG STDMETHODCALLTYPE AddRef()
	{
		return ++m_refCount;
	}

	virtual ULONG STDMETHODCALLTYPE Release()
	{
		ULONG newRefValue = --m_refCount;

		if (newRefValue == 0)
			delete this;

		return newRefValue;
	}

	virtual HRESULT scheduleNextFrame(IDeckLinkVideoFrame* videoFrame)
	{
		HRESULT                     result;
		IDeckLinkMutableVideoFrame* mutableFrame = NULL;
		INT8_UNSIGNED               hours;
		INT8_UNSIGNED               minutes;
		INT8_UNSIGNED               seconds;
		INT8_UNSIGNED               frames;

		convertFrameCountToTimecode(gTotalFramesScheduled, &hours, &minutes, &seconds, &frames);

		result = videoFrame->QueryInterface(IID_IDeckLinkMutableVideoFrame, (void**)&mutableFrame);
		if (result != S_OK)
			goto bail;

		result = setRP188VitcTimecodeOnFrame(mutableFrame, hours, minutes, seconds, frames, m_deckLinkSupportsHFRTC);
		if (result != S_OK)
			goto bail;

		// When a video frame completes,reschedule another frame
		result = m_deckLinkOutput->ScheduleVideoFrame(videoFrame, gTotalFramesScheduled * gFrameDuration, gFrameDuration, gTimeScale);
		if (result != S_OK)
			goto bail;

		gTotalFramesScheduled++;

	bail:
		if (mutableFrame)
			mutableFrame->Release();

		return result;
	}

private:
	IDeckLinkOutput*  	m_deckLinkOutput;
	BOOL              	m_deckLinkSupportsHFRTC;
	std::atomic<ULONG>	m_refCount;
	
	virtual ~OutputCallback(void)
	{
		m_deckLinkOutput->Release();
	}
};

static void FillBlue(IDeckLinkMutableVideoFrame* theFrame)
{
	INT32_UNSIGNED* nextWord;
	INT32_UNSIGNED  wordsRemaining;
	INT32_UNSIGNED  height = (INT32_UNSIGNED)gDisplayMode->GetHeight();

	theFrame->GetBytes((void**)&nextWord);
	wordsRemaining = (kRowBytes * height) / 4;

	while (wordsRemaining > 0)
	{
		*(nextWord++) = kBlueData[0];
		*(nextWord++) = kBlueData[1];
		*(nextWord++) = kBlueData[2];
		*(nextWord++) = kBlueData[3];
		wordsRemaining = wordsRemaining - 4;
	}
}

static IDeckLinkMutableVideoFrame* CreateFrame(IDeckLinkOutput* deckLinkOutput)
{
	HRESULT                         result;
	IDeckLinkMutableVideoFrame*     frame = NULL;

	result = deckLinkOutput->CreateVideoFrame((int)gDisplayMode->GetWidth(), (int)gDisplayMode->GetHeight(), kRowBytes, kPixelFormat, bmdFrameFlagDefault, &frame);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not create a video frame - result = %08x\n", result);
		goto bail;
	}

	FillBlue(frame);

bail:
	return frame;
}


int main(int argc, const char * argv[])
{

	IDeckLinkIterator*      deckLinkIterator = NULL;
	IDeckLink*              deckLink         = NULL;
	IDeckLinkOutput*        deckLinkOutput   = NULL;
	IDeckLinkProfileAttributes*	deckLinkAttributes = NULL;
	OutputCallback*         outputCallback   = NULL;
	IDeckLinkVideoFrame*    videoFrameBlue   = NULL;
	BMDbool					supported;
	BMDbool					deckLinkSupportsHFRTC;
	HRESULT                 result;

	Initialize();

	// Create an IDeckLinkIterator object to enumerate all DeckLink cards in the system
	result = GetDeckLinkIterator(&deckLinkIterator);
	if (result != S_OK)
	{
		fprintf(stderr, "A DeckLink iterator could not be created.  The DeckLink drivers may not be installed.\n");
		goto bail;
	}

	// Obtain the first DeckLink device
	result = deckLinkIterator->Next(&deckLink);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not find DeckLink device - result = %08x\n", result);
		goto bail;
	}

	// Obtain the output interface for the DeckLink device
	result = deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&deckLinkOutput);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not obtain the IDeckLinkOutput interface - result = %08x\n", result);
		goto bail;
	}
	
	// Obtain attributes interface for the DeckLink device
	result = deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not obtain the IDeckLinkProfileAttributes interface - result = %08x\n", result);
		goto bail;
	}
	
	// Check whether HFRTC is supported by this device
	result = deckLinkAttributes->GetFlag(BMDDeckLinkSupportsHighFrameRateTimecode, &deckLinkSupportsHFRTC);
	if (result != S_OK)
	{
		deckLinkSupportsHFRTC = false;
	}

	// Create an instance of output callback
	outputCallback = new OutputCallback(deckLinkOutput, deckLinkSupportsHFRTC);
	if (outputCallback == NULL)
	{
		fprintf(stderr, "Could not create output callback object\n");
		goto bail;
	}

	// Set the callback object to the DeckLink device's output interface
	result = deckLinkOutput->SetScheduledFrameCompletionCallback(outputCallback);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not set callback - result = %08x\n", result);
		goto bail;
	}

	// Check the selected display mode/pixelformat can be output 
	result = deckLinkOutput->DoesSupportVideoMode(bmdVideoConnectionUnspecified, kDisplayMode, kPixelFormat, bmdNoVideoOutputConversion, bmdSupportedVideoModeDefault, NULL, &supported);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not determine whether video mode is supported - result = %08x\n", result);
		goto bail;
	}

	if (! supported)
	{
		fprintf(stderr, "Video mode is not supported\n");
		goto bail;
	}

	result = deckLinkOutput->GetDisplayMode(kDisplayMode, &gDisplayMode);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not get display mode - result = %08x\n", result);
		goto bail;
	}

	// Store the frame duration and timesale for later use
	gDisplayMode->GetFrameRate(&gFrameDuration, &gTimeScale);

	// Frame rates with multiple 30-frame counting can implement Drop Frames compensation, refer to SMPTE 12-3
	if (kIsDropFrame && gTimeScale % 30000 == 0)
	{
		gDropFrames = 2 * (gTimeScale / 30000);
	}
	else
		gDropFrames = 0;
	
	// Enable video output
	result = deckLinkOutput->EnableVideoOutput(kDisplayMode, kOutputFlags);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not enable video output - result = %08x\n", result);
		goto bail;
	}

	// Create a frame with defined format
	videoFrameBlue = CreateFrame(deckLinkOutput);

	// Schedule a blue frame 3 times
	for (int i = 0; i < 3; i++)
	{
		result = outputCallback->scheduleNextFrame(videoFrameBlue);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not schedule video frame - result = %08x\n", result);
			goto bail;
		}
	}

	// Start
	result = deckLinkOutput->StartScheduledPlayback(0, gTimeScale, 1.0);
	if (result != S_OK)
	{
		fprintf(stderr, "Could not start - result = %08x\n", result);
		goto bail;
	}

	// Wait until user presses Enter
	printf("Monitoring... Press <RETURN> to exit\n");

	getchar();

	printf("Exiting.\n");

	// Stop capture
	result = deckLinkOutput->StopScheduledPlayback(0, NULL, 0);

	// Disable the video input interface
	result = deckLinkOutput->DisableVideoOutput();

	// Release resources
bail:

	// Release the video input interface
	if (deckLinkOutput != NULL)
		deckLinkOutput->Release();

	// Release the attributes interface
	if (deckLinkAttributes != NULL)
		deckLinkAttributes->Release();
	
	// Release the Decklink object
	if (deckLink != NULL)
		deckLink->Release();

	// Release the DeckLink iterator
	if (deckLinkIterator != NULL)
		deckLinkIterator->Release();

	// Release the videoframe object
	if (videoFrameBlue != NULL)
		videoFrameBlue->Release();

	// Release the outputCallback callback object
	if (outputCallback != NULL)
		outputCallback->Release();

	return (result == S_OK) ? 0 : 1;
}

