/* -LICENSE-START-
** Copyright (c) 2019 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 <stdexcept>

#include "platform.h"
#include "DeckLinkInputDevice.h"
#include "ReferenceTime.h"

DeckLinkInputDevice::DeckLinkInputDevice(com_ptr<IDeckLink>& device) :
	m_refCount(1),
	m_deckLink(device),
	m_deckLinkInput(IID_IDeckLinkInput, device),
	m_frameTimescale(1001),
	m_seenValidSignal(false),
	m_readyForCapture(false),
	m_videoFormatChangedCallback(nullptr),
	m_videoInputArrivedCallback(nullptr),
	m_audioInputArrivedCallback(nullptr),
	m_videoInputFrameDroppedCallback(nullptr)
{
	// Check that device has an input interface, this will throw an error if using a playback-only device such as DeckLink Mini Monitor
	if (!m_deckLinkInput)
		throw std::runtime_error("DeckLink device does not have an input interface");
}

// IUnknown methods

HRESULT	DeckLinkInputDevice::QueryInterface(REFIID iid, LPVOID *ppv)
{
	HRESULT result = S_OK;

	if (ppv == nullptr)
		return E_INVALIDARG;

	// Obtain the IUnknown interface and compare it the provided REFIID
	if (iid == IID_IUnknown)
	{
		*ppv = this;
		AddRef();
	}
	else if (iid == IID_IDeckLinkInputCallback)
	{
		*ppv = (IDeckLinkInputCallback*)this;
		AddRef();
	}
	else
	{
		*ppv = nullptr;
		result = E_NOINTERFACE;
	}

	return result;
}

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

ULONG DeckLinkInputDevice::Release(void)
{
	ULONG newRefValue = --m_refCount;
	
	if (newRefValue == 0)
		delete this;

	return newRefValue;
}

// IDeckLinkInputCallback methods

HRESULT DeckLinkInputDevice::VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket)
{
	// Get the current timestamp for the entry to callback for latency measurements.
	BMDTimeValue referenceCount = ReferenceTime::getSteadyClockUptimeCount();
	
	if (videoFrame)
	{
		BMDTimeValue					streamTime;
		BMDTimeValue					frameDuration;

		bool inputFrameValid = ((videoFrame->GetFlags() & bmdFrameHasNoInputSource) == 0);
		if (inputFrameValid)
			m_seenValidSignal = true;

		// Get stream time for input frame
		if (videoFrame->GetStreamTime(&streamTime, &frameDuration, m_frameTimescale) != S_OK)
			return E_FAIL;

		if (m_seenValidSignal && m_readyForCapture && m_videoInputFrameDroppedCallback)
		{
			// If there are any gaps in the stream time, then report the missing frames as dropped
			while (streamTime >= m_lastStreamTime + 2 * frameDuration)
			{
				m_lastStreamTime += frameDuration;
				m_videoInputFrameDroppedCallback(m_lastStreamTime, frameDuration, m_frameTimescale);
			}
		}

		m_lastStreamTime = streamTime;

		if (m_readyForCapture)
		{
			if (m_videoInputFrameDroppedCallback && m_seenValidSignal && !inputFrameValid)
			{
				// Report any dropped frames
				m_videoInputFrameDroppedCallback(streamTime, frameDuration, m_frameTimescale);
			}
			else if (m_videoInputArrivedCallback)
			{
				BMDTimeValue	referenceFrameTime;
				BMDTimeValue	referenceFrameDuration;

				auto loopThroughVideoFrame = std::make_shared<LoopThroughVideoFrame>(com_ptr<IDeckLinkVideoFrame>(videoFrame));
				loopThroughVideoFrame->setInputFrameArrivedReferenceTime(referenceCount);

				// Get the captured timestamp for the incoming frame
				if (videoFrame->GetHardwareReferenceTimestamp(ReferenceTime::kTimescale, &referenceFrameTime, &referenceFrameDuration) != S_OK)
					return E_FAIL;

				// The time for start of frame on the wire is the timestamp attached to the frame at completion minus the frame duration
				loopThroughVideoFrame->setInputFrameStartReferenceTime(referenceFrameTime - referenceFrameDuration);

				loopThroughVideoFrame->setVideoStreamTime(streamTime);
				loopThroughVideoFrame->setVideoFrameDuration(frameDuration);

				m_videoInputArrivedCallback(std::move(loopThroughVideoFrame));
			}
		}
	}

	if (m_readyForCapture && audioPacket && m_audioInputArrivedCallback)
	{
		BMDTimeValue	packetTime;
		void*			audioBuffer;
		
		// Get audio buffer for loop through
		if (audioPacket->GetBytes(&audioBuffer) != S_OK)
			return E_FAIL;
		
		// Add reference to input audio packet to maintain IDeckLinkAudioInputPacket object after returning from callback,
		// object will be released in shared_ptr custom deleter
		audioPacket->AddRef();
		
		auto loopThroughAudioPacket = std::make_shared<LoopThroughAudioPacket>(audioBuffer, audioPacket->GetSampleFrameCount(), [=]() { audioPacket->Release(); });
		
		loopThroughAudioPacket->setInputPacketArrivedReferenceTime(referenceCount);

		// Get stream time from input audio packet
		if (audioPacket->GetPacketTime(&packetTime, m_frameTimescale) != S_OK)
			return E_FAIL;
		loopThroughAudioPacket->setAudioStreamTime(packetTime);

		m_audioInputArrivedCallback(std::move(loopThroughAudioPacket));
	}

	return S_OK;
}

HRESULT DeckLinkInputDevice::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents /*notificationEvents*/, IDeckLinkDisplayMode *newMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags)
{
	BMDPixelFormat		pixelFormat = (detectedSignalFlags & bmdDetectedVideoInputRGB444) ? bmdFormat10BitRGB : bmdFormat10BitYUV;
	bool				detected3DMode = (detectedSignalFlags & bmdDetectedVideoInputDualStream3D) != 0;

	// Notify that display mode, pixel format or 3D mode has changed
	if (m_videoFormatChangedCallback)
		m_videoFormatChangedCallback(newMode->GetDisplayMode(), detected3DMode, pixelFormat);

	return S_OK;
}

// Other methods

bool DeckLinkInputDevice::startCapture(BMDDisplayMode displayMode, bool enable3D, BMDPixelFormat pixelFormat, BMDAudioSampleType audioSampleType, uint32_t audioChannelCount)
{
	BMDVideoInputFlags				videoInputFlags = bmdVideoInputEnableFormatDetection;
	com_ptr<IDeckLinkDisplayMode>	deckLinkDisplayMode;

	if (enable3D)
		videoInputFlags |= bmdVideoInputDualStream3D;

	m_seenValidSignal = false;
	m_readyForCapture = false;
	
	// Get timescale for requested display mode
	if (m_deckLinkInput->GetDisplayMode(displayMode, deckLinkDisplayMode.releaseAndGetAddressOf()) != S_OK)
		return false;

	if (deckLinkDisplayMode->GetFrameRate(&m_frameDuration, &m_frameTimescale) != S_OK)
		return false;

	// Register input callback
	if (m_deckLinkInput->SetCallback(this) != S_OK)
		return false;
	
	// Set the video input mode
	if (m_deckLinkInput->EnableVideoInput(displayMode, pixelFormat, videoInputFlags) != S_OK)
		return false;

	// Set the audio input mode
	if (m_deckLinkInput->EnableAudioInput(bmdAudioSampleRate48kHz, audioSampleType, audioChannelCount) != S_OK)
		return false;

	// Start the capture
	if (m_deckLinkInput->StartStreams() != S_OK)
		return false;

	return true;
}

void DeckLinkInputDevice::stopCapture()
{
	// Stop the capture
	m_deckLinkInput->StopStreams();

	// Disable audio and video inputs
	m_deckLinkInput->DisableVideoInput();
	m_deckLinkInput->DisableAudioInput();
	
	// Unregister input callback
	m_deckLinkInput->SetCallback(nullptr);
}

void DeckLinkInputDevice::setReadyForCapture()
{
	m_readyForCapture = true;
}
