/* -LICENSE-START-
 ** Copyright (c) 2018 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 "platform.h"
#include <array>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>

#define kDeviceCount 2

// Video mode parameters
const BMDDisplayMode      kDisplayMode = bmdModeHD1080p25;
const BMDVideoInputFlags  kInputFlag  = bmdVideoInputSynchronizeToCaptureGroup;
const BMDPixelFormat      kPixelFormat = bmdFormat10BitYUV;

// Frame parameters
const INT32_UNSIGNED kFrameDuration = 1000;
const INT32_UNSIGNED kTimeScale = 25000;
const INT32_UNSIGNED kSynchronizedCaptureGroup = 2;

static const BMDTimeScale kMicroSecondsTimeScale = 1000000;

class DeckLinkDevice;

class InputCallback: public IDeckLinkInputCallback
{
public:
	InputCallback(DeckLinkDevice* deckLinkDevice) :
		m_deckLinkDevice(deckLinkDevice),
		m_refCount(1)
	{
	}

	HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode *newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags) override;

	HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket) override;

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

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

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

		if (newRefValue == 0)
			delete this;

		return newRefValue;
	}

private:
	DeckLinkDevice*  m_deckLinkDevice;
	std::atomic<INT32_SIGNED> m_refCount;
};

class NotificationCallback : public IDeckLinkNotificationCallback
{
public:
	NotificationCallback(DeckLinkDevice* deckLinkDevice) :
		m_deckLinkDevice(deckLinkDevice),
		m_refCount(1)
	{
	}

	HRESULT STDMETHODCALLTYPE Notify(BMDNotifications topic, uint64_t param1, uint64_t param2) override;

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

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

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

		if (newRefValue == 0)
			delete this;

		return newRefValue;
	}
private:
	DeckLinkDevice*		m_deckLinkDevice;
	std::atomic<ULONG>	m_refCount;
};

class DeckLinkDevice
{
public:
	DeckLinkDevice() :
		m_index(0),
		m_deckLink(nullptr),
		m_deckLinkConfig(nullptr),
		m_deckLinkStatus(nullptr),
		m_deckLinkNotification(nullptr),
		m_notificationCallback(nullptr),
		m_deckLinkInput(nullptr),
		m_inputCallback(nullptr)
	{
	}

	HRESULT setup(IDeckLink* deckLink, unsigned index)
	{
		m_deckLink = deckLink;
		m_index = index;

		// Obtain the configuration interface for the DeckLink device
		HRESULT result = m_deckLink->QueryInterface(IID_IDeckLinkConfiguration, (void**)&m_deckLinkConfig);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not obtain the IDeckLinkConfiguration interface - result = %08x\n", result);
			goto bail;
		}

		// Set the synchronized capture group number. This can be any 32-bit number
		// All devices enabled for synchronized capture with the same group number are started together
		result = m_deckLinkConfig->SetInt(bmdDeckLinkConfigCaptureGroup, kSynchronizedCaptureGroup);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not set capture group - result = %08x\n", result);
			goto bail;
		}

		// Obtain the status interface for the DeckLink device
		result = m_deckLink->QueryInterface(IID_IDeckLinkStatus, (void**)&m_deckLinkStatus);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not obtain the IDeckLinkStatus interface - result = %08x\n", result);
			goto bail;
		}

		// Obtain the notification interface for the DeckLink device
		result = m_deckLink->QueryInterface(IID_IDeckLinkNotification, (void**)&m_deckLinkNotification);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not obtain the IDeckLinkNotification interface - result = %08x\n", result);
			goto bail;
		}

		m_notificationCallback = new NotificationCallback(this);
		if (m_notificationCallback == nullptr)
		{
			fprintf(stderr, "Could not create notification callback object\n");
			goto bail;
		}

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

		// Obtain the input interface for the DeckLink device
		result = m_deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&m_deckLinkInput);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not obtain the IDeckLinkInput interface - result = %08x\n", result);
			goto bail;
		}

		// Create an instance of output callback
		m_inputCallback = new InputCallback(this);
		if (m_inputCallback == nullptr)
		{
			fprintf(stderr, "Could not create input callback object\n");
			goto bail;
		}

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

	bail:
		return result;
	}

	HRESULT waitForSignalLock()
	{
		// When performing synchronized capture, all participating devices need to have their signal locked
		std::unique_lock<std::mutex> guard(m_mutex);

		HRESULT result = S_OK;

		m_signalCondition.wait(guard, [this, &result]()
		{
			INT64_SIGNED displayMode;
			result = m_deckLinkStatus->GetInt(bmdDeckLinkStatusDetectedVideoInputMode, &displayMode);
			if (result != S_OK && result != S_FALSE)
			{
				fprintf(stderr, "Could not query input status - result = %08x\n", result);
				return true;
			}

			return (BMDDisplayMode)displayMode == kDisplayMode;
		});

		return result;
	}

	void notifyVideoInputChanged()
	{
		m_signalCondition.notify_all();
	}

	HRESULT prepareForCapture()
	{
		// Enable video output
		HRESULT result = m_deckLinkInput->EnableVideoInput(kDisplayMode, kPixelFormat, kInputFlag);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not enable video input - result = %08x\n", result);
			goto bail;
		}

	bail:
		return result;
	}

	HRESULT startCapture()
	{
		HRESULT result = m_deckLinkInput->StartStreams();
		if (result != S_OK)
		{
			fprintf(stderr, "Could not start - result = %08x\n", result);
			goto bail;
		}

	bail:
		return result;
	}

	HRESULT stopCapture()
	{
		HRESULT result = m_deckLinkInput->StopStreams();
		if (result != S_OK)
		{
			fprintf(stderr, "Could not stop - result = %08x\n", result);
			goto bail;
		}

	bail:
		return result;
	}

	HRESULT cleanUpFromCapture()
	{
		HRESULT result = m_deckLinkInput->DisableVideoInput();
		if (result != S_OK)
		{
			fprintf(stderr, "Could not disable - result = %08x\n", result);
			goto bail;
		}

	bail:
		return result;
	}

	HRESULT frameArrived(IDeckLinkVideoInputFrame* videoFrame)
	{
		BMDTimeValue time;
		HRESULT result = videoFrame->GetStreamTime(&time, nullptr, kTimeScale);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not get stream time from frame - result = %08x\n", result);
			return S_OK;
		}

		unsigned frames = (unsigned)((time % kTimeScale) / kFrameDuration);
		unsigned seconds = (unsigned)((time / kTimeScale) % 60);
		unsigned minutes = (unsigned)((time / kTimeScale / 60) % 60);
		unsigned hours = (unsigned)(time / kTimeScale / 60 / 60);

		BMDTimeValue hwTime;
		result = videoFrame->GetHardwareReferenceTimestamp(kMicroSecondsTimeScale, &hwTime, NULL);
		if (result != S_OK)
		{
			fprintf(stderr, "Could not get hardwar reference time from frame - result = %08x\n", result);
			return S_OK;
		}


		printf("[%llu.%06llu] Device #%u: Frame %02u:%02u:%02u:%03u arrived\n", hwTime / kMicroSecondsTimeScale, hwTime % kMicroSecondsTimeScale, m_index, hours, minutes, seconds, frames);

		return S_OK;
	}

	~DeckLinkDevice()
	{
		if (m_inputCallback)
		{
			m_deckLinkInput->SetCallback(nullptr);
			m_inputCallback->Release();
		}

		if (m_notificationCallback)
		{
			m_deckLinkNotification->Unsubscribe(bmdStatusChanged, m_notificationCallback);
			m_notificationCallback->Release();
		}

		if (m_deckLink)
			m_deckLink->Release();

		if (m_deckLinkConfig)
			m_deckLinkConfig->Release();

		if (m_deckLinkStatus)
			m_deckLinkStatus->Release();

		if (m_deckLinkInput)
			m_deckLinkInput->Release();

		if (m_deckLinkNotification)
			m_deckLinkNotification->Release();
	}

private:
	unsigned										m_index;
	IDeckLink*										m_deckLink;
	IDeckLinkConfiguration*							m_deckLinkConfig;
	IDeckLinkStatus*								m_deckLinkStatus;
	IDeckLinkNotification*							m_deckLinkNotification;
	NotificationCallback*							m_notificationCallback;
	IDeckLinkInput*									m_deckLinkInput;
	InputCallback*									m_inputCallback;
	std::mutex										m_mutex;
	std::condition_variable							m_signalCondition;
};

HRESULT InputCallback::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode *newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags)
{
	return S_OK;
}

HRESULT InputCallback::VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket)
{
	if (!videoFrame || (videoFrame->GetFlags() & bmdFrameHasNoInputSource))
	{
		printf("No valid frame\n");
		return S_OK;
	}

	return m_deckLinkDevice->frameArrived(videoFrame);
}

HRESULT NotificationCallback::Notify(BMDNotifications topic, uint64_t param1, uint64_t param2)
{
	if (topic != bmdStatusChanged)
		return S_OK;

	if ((BMDDeckLinkStatusID)param1 != bmdDeckLinkStatusDetectedVideoInputMode)
		return S_OK;

	m_deckLinkDevice->notifyVideoInputChanged();
	return S_OK;
}

static BOOL supportsSynchronizedCapture(IDeckLink* deckLink)
{
	IDeckLinkProfileAttributes* attributes = nullptr;
	HRESULT result = E_FAIL;

	result = deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&attributes);
	if (result != S_OK)
		return false;

	BOOL supported = false;
	result = attributes->GetFlag(BMDDeckLinkSupportsSynchronizeToCaptureGroup, &supported);
	if (result != S_OK)
		supported = false;

	attributes->Release();

	return supported;
}

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

	IDeckLinkIterator*      deckLinkIterator = nullptr;
	IDeckLink*				deckLink = nullptr;
	std::array<DeckLinkDevice, kDeviceCount> deckLinkDevices;
	HRESULT                 result;
	unsigned				index = 0;

	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;
	}

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

			if (supportsSynchronizedCapture(deckLink))
				break;

			deckLink->Release();
		}

		result = device.setup(deckLink, index++);
		if (result != S_OK)
			goto bail;

		deckLink = nullptr;
	}

	for (auto& device : deckLinkDevices)
	{
		result = device.prepareForCapture();
		if (result != S_OK)
			goto bail;
	}

	// Wait for devices to lock to the signal
	printf("Waiting for signal lock...\n");

	for (auto& device : deckLinkDevices)
	{
		result = device.waitForSignalLock();
		if (result != S_OK)
			goto bail;
	}

	// Start capture - This only needs to be performed on one device in the group
	result = deckLinkDevices[0].startCapture();
	if (result != S_OK)
		goto bail;

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

	getchar();

	printf("Exiting.\n");

	// Stop capture - This only needs to be performed on one device in the group
	result = deckLinkDevices[0].stopCapture();

	// Disable the video input interface
	for (auto& device : deckLinkDevices)
		result = device.cleanUpFromCapture();

	// Release resources
bail:

	// Release the Decklink object
	if (deckLink != nullptr)
		deckLink->Release();

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

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

