/* -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 <chrono>
#include "platform.h"
#include "DeckLinkInputDevice.h"

static const std::chrono::seconds kValidFrameTimeout{5};

DeckLinkInputDevice::DeckLinkInputDevice(IDeckLink* device)
	: m_deckLink(device), m_deckLinkInput(NULL), m_cancelCapture(false), m_refCount(1)
{
	m_deckLink->AddRef();
}

DeckLinkInputDevice::~DeckLinkInputDevice()
{
	if (m_deckLinkInput != NULL)
	{
		m_deckLinkInput->Release();
		m_deckLinkInput = NULL;
	}

	while(!m_modeList.empty())
	{
		m_modeList.back()->Release();
		m_modeList.pop_back();
	}

	if (m_deckLink != NULL)
	{
		m_deckLink->Release();
		m_deckLink = NULL;
	}
}

HRESULT DeckLinkInputDevice::Init()
{
	HRESULT							result;
	IDeckLinkDisplayModeIterator*	displayModeIterator	= NULL;
	IDeckLinkDisplayMode*			displayMode			= NULL;
	dlstring_t						deviceNameStr;

	result = m_deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&m_deckLinkInput);
	if (result != S_OK)
	{
		fprintf(stderr, "Unable to get IDeckLinkInput interface\n");
		goto bail;
	}

	// Retrieve and cache mode list
	result = m_deckLinkInput->GetDisplayModeIterator(&displayModeIterator);
	if (result != S_OK)
	{
		fprintf(stderr, "Unable to get IDeckLinkDisplayModeIterator interface\n");
		goto bail;
	}

	while (displayModeIterator->Next(&displayMode) == S_OK)
	{
		m_modeList.push_back(displayMode);
	}

	// Get device name
	result = m_deckLink->GetDisplayName(&deviceNameStr);
	if (result == S_OK)
	{
		m_deviceName = DlToStdString(deviceNameStr);
		DeleteString(deviceNameStr);
	}
	else
	{
		m_deviceName = "DeckLink";
	}

bail:
	if (displayModeIterator != NULL)
	{
		displayModeIterator->Release();
		displayModeIterator = NULL;
	}

	return result;
}

HRESULT DeckLinkInputDevice::StartCapture(BMDDisplayMode displayMode, BMDPixelFormat pixelFormat, bool enableFormatDetection)
{
	HRESULT result;
	BMDVideoInputFlags inputFlags = bmdVideoInputFlagDefault;

	m_prevInputFrameValid = false;
	
	if (enableFormatDetection)
		inputFlags |= bmdVideoInputEnableFormatDetection;

	// Set capture callback
	m_deckLinkInput->SetCallback(this);

	// Set the video input mode
	result = m_deckLinkInput->EnableVideoInput(displayMode, pixelFormat, inputFlags);
	if (result != S_OK)
	{
		fprintf(stderr, "Unable to enable video input. Perhaps, the selected device is currently in-use.\n");
		goto bail;
	}

	// Start the capture
	result = m_deckLinkInput->StartStreams();
	if (result != S_OK)
	{
		fprintf(stderr, "Unable to start input streams\n");
		goto bail;
	}

bail:
	return result;
}


void DeckLinkInputDevice::CancelCapture()
{
	{
		// signal cancel flag to terminate wait condition
		std::lock_guard<std::mutex> lock(m_deckLinkInputMutex);
		m_cancelCapture = true;
	}
	m_deckLinkInputCondition.notify_one();
}

void DeckLinkInputDevice::StopCapture()
{
	if (m_deckLinkInput != NULL)
	{
		// Unregister capture callback
		m_deckLinkInput->SetCallback(NULL);
		
		{
			// Clear video frame queue
			std::lock_guard<std::mutex> lock(m_deckLinkInputMutex);
			while (!m_videoFrameQueue.empty())
			{
				m_videoFrameQueue.front()->Release();
				m_videoFrameQueue.pop();
			}
		}

		// Stop the capture
		m_deckLinkInput->StopStreams();

		// Disable video input
		m_deckLinkInput->DisableVideoInput();
	}
}

bool DeckLinkInputDevice::WaitForVideoFrameArrived(IDeckLinkVideoFrame** frame, bool& captureCancelled)
{
	std::unique_lock<std::mutex> lock(m_deckLinkInputMutex);
	if (!m_deckLinkInputCondition.wait_for(lock, kValidFrameTimeout, [&]{ return !m_videoFrameQueue.empty() || m_cancelCapture; }))
		// wait_for timeout
		return false;

	if (!m_videoFrameQueue.empty())
	{
		*frame = m_videoFrameQueue.front();
		m_videoFrameQueue.pop();
	}

	captureCancelled = m_cancelCapture;
	return true;
}

HRESULT DeckLinkInputDevice::VideoInputFormatChanged(/* in */ BMDVideoInputFormatChangedEvents notificationEvents, /* in */ IDeckLinkDisplayMode *newMode, /* in */ BMDDetectedVideoInputFormatFlags detectedSignalFlags)
{	
	HRESULT			result = S_OK;
	BMDPixelFormat	pixelFormat;
	dlstring_t		displayModeNameStr;

	if (detectedSignalFlags & bmdDetectedVideoInputRGB444)
	{
		if (detectedSignalFlags & bmdDetectedVideoInput8BitDepth)
			pixelFormat = bmdFormat8BitARGB;
		else if (detectedSignalFlags & bmdDetectedVideoInput10BitDepth)
			pixelFormat = bmdFormat10BitRGB;
		else if (detectedSignalFlags & bmdDetectedVideoInput12BitDepth)
			pixelFormat = bmdFormat12BitRGB;
		else
			// Invalid color depth for RGB
			return E_INVALIDARG;
	}
	else if (detectedSignalFlags & bmdDetectedVideoInputYCbCr422)
	{
		if (detectedSignalFlags & bmdDetectedVideoInput8BitDepth)
			pixelFormat = bmdFormat8BitYUV;
		else if (detectedSignalFlags & bmdDetectedVideoInput10BitDepth)
			pixelFormat = bmdFormat10BitYUV;
		else
			// Invalid color depth for YUV
			return E_INVALIDARG;
	}
	else
		// Unexpected detected video input format flags
		return E_INVALIDARG;

	// Restart streams if either display mode or colorspace has changed
	if (notificationEvents & (bmdVideoInputDisplayModeChanged | bmdVideoInputColorspaceChanged))
	{
		// Stop the capture
		m_deckLinkInput->StopStreams();

		// Set the detected video input mode
		result = m_deckLinkInput->EnableVideoInput(newMode->GetDisplayMode(), pixelFormat, bmdVideoInputEnableFormatDetection);
		if (result != S_OK)
		{
			fprintf(stderr, "Unable to re-enable video input on auto-format detection");
			return E_FAIL;
		}

		// Restart the capture
		result = m_deckLinkInput->StartStreams();
		if (result != S_OK)
		{
			fprintf(stderr, "Unable to restart streams on auto-format detection");
			return E_FAIL;
		}

		result = newMode->GetName(&displayModeNameStr);

		if (result == S_OK)
		{
			fprintf(stderr, "Video format changed to %s %s\n", DlToCString(displayModeNameStr), (detectedSignalFlags & bmdDetectedVideoInputRGB444) ? "RGB" : "YUV");
			DeleteString(displayModeNameStr);
		}
		else
			fprintf(stderr, "Unable to get new video format name\n");
	}

	return result;
}

HRESULT DeckLinkInputDevice::VideoInputFrameArrived(/* in */ IDeckLinkVideoInputFrame* videoFrame, /* in */ IDeckLinkAudioInputPacket* audioPacket)
{
	if (videoFrame)
	{
		bool inputFrameValid = ((videoFrame->GetFlags() & bmdFrameHasNoInputSource) == 0);

		// Detect change in input signal, restart stream when valid stream detected 
		if (inputFrameValid && !m_prevInputFrameValid)
		{
			m_deckLinkInput->StopStreams();
			m_deckLinkInput->FlushStreams();
			m_deckLinkInput->StartStreams();
		}

		if (inputFrameValid && m_prevInputFrameValid)
		{
			// If valid frame, add to queue for processing and notify
			videoFrame->AddRef();
			{
				std::lock_guard<std::mutex> lock(m_deckLinkInputMutex);
				m_videoFrameQueue.push(videoFrame);
			}
			m_deckLinkInputCondition.notify_one();
		}

		m_prevInputFrameValid = inputFrameValid;
	}

	return S_OK;
}

HRESULT	STDMETHODCALLTYPE DeckLinkInputDevice::QueryInterface(REFIID iid, LPVOID *ppv)
{
	CFUUIDBytes		iunknown;
	HRESULT			result = E_NOINTERFACE;

	if (ppv == NULL)
		return E_INVALIDARG;

	// Initialise the return result
	*ppv = NULL;

	// Obtain the IUnknown interface and compare it the provided REFIID
	iunknown = CFUUIDGetUUIDBytes(IUnknownUUID);
	if (memcmp(&iid, &iunknown, sizeof(REFIID)) == 0)
	{
		*ppv = this;
		AddRef();
		result = S_OK;
	}
	else if (memcmp(&iid, &IID_IDeckLinkInputCallback, sizeof(REFIID)) == 0)
	{
		*ppv = (IDeckLinkInputCallback*)this;
		AddRef();
		result = S_OK;
	}

	return result;
}

ULONG STDMETHODCALLTYPE DeckLinkInputDevice::AddRef(void)
{
	return ++m_refCount;
}

ULONG STDMETHODCALLTYPE DeckLinkInputDevice::Release(void)
{

	ULONG newRefValue = --m_refCount;
	if (newRefValue == 0)
		delete this;

	return newRefValue;
}

