/* -LICENSE-START-
 ** Copyright (c) 2011 Blackmagic Design
 **
 ** Permission is hereby granted, free of charge, to any person or organization
 ** obtaining a copy of the software and accompanying documentation covered by
 ** this license (the "Software") to use, reproduce, display, distribute,
 ** 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, all subject to the following:
 ** 
 ** 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.
 ** 
 ** 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.
 ** -LICENSE-END-
 */

#include "DeckLinkDevice.h"

using namespace std;

#include <QDesktopServices>
#include <QTime>

DeckLinkDevice::DeckLinkDevice(ControllerImp* ui, IDeckLink* device) :
	m_deckLink(device),
	m_deckLinkEncoderInput(NULL),
	m_deckLinkEncoderConfiguration(NULL),
	m_videoWriter(NULL),
	m_currentlyCapturing(false),
	m_uiDelegate(ui),
	m_defaultMode(0),
	m_refCount(1)
{
}

DeckLinkDevice::~DeckLinkDevice()
{
	for(std::vector<IDeckLinkDisplayMode*>::iterator it = m_modeList.begin(); it != m_modeList.end(); ++it)
	{
		(*it)->Release();
	}

	if (m_deckLinkEncoderConfiguration)
	{
		m_deckLinkEncoderConfiguration->Release();
		m_deckLinkEncoderConfiguration = NULL;
	}

	if (m_deckLinkEncoderInput)
	{
		m_deckLinkEncoderInput->Release();
		m_deckLinkEncoderInput = NULL;
	}

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

HRESULT DeckLinkDevice::QueryInterface (REFIID iid, LPVOID *ppv)
{
	CFUUIDBytes		iunknown;
	HRESULT			result = E_NOINTERFACE;
	
	*ppv = NULL;
	
	iunknown = CFUUIDGetUUIDBytes(IUnknownUUID);
	if (memcmp(&iid, &iunknown, sizeof(REFIID)) == 0)
	{
		*ppv = this;
		AddRef();
		result = S_OK;
	}
	else if (memcmp(&iid, &IID_IDeckLinkNotificationCallback, sizeof(REFIID)) == 0)
	{
		*ppv = (IDeckLinkNotificationCallback*)this;
		AddRef();
		result = S_OK;
	}
	
	return result;
}

ULONG DeckLinkDevice::AddRef (void)
{
	int oldValue = m_refCount.fetchAndAddAcquire(1);
	return (ULONG)(oldValue + 1);
}

ULONG DeckLinkDevice::Release (void)
{
	int oldValue = m_refCount.fetchAndAddAcquire(-1);
	if (oldValue == 1)      // i.e. current value will be 0
		delete this;

	return (ULONG)(oldValue - 1);
}

bool DeckLinkDevice::init()
{
	const char* devName = NULL;
	if (m_deckLink->GetDisplayName(&devName) != S_OK)
		return false;

	m_deviceName = devName;

	IDeckLinkDisplayModeIterator*	displayModeIterator = NULL;
	IDeckLinkDisplayMode*			displayMode = NULL;

	if (m_deckLink->QueryInterface(IID_IDeckLinkEncoderInput, (void**) &m_deckLinkEncoderInput) != S_OK)
		return false;

	if (m_deckLinkEncoderInput->QueryInterface(IID_IDeckLinkEncoderConfiguration, (void**)&m_deckLinkEncoderConfiguration) != S_OK)
		return false;

	if (m_deckLinkEncoderInput->GetDisplayModeIterator(&displayModeIterator) == S_OK)
	{
		while (displayModeIterator->Next(&displayMode) == S_OK)
		{
			displayMode->AddRef();
			if (displayMode->GetDisplayMode() == bmdMode4K2160p24)
				m_defaultMode = (int)m_modeList.size();
			m_modeList.push_back(displayMode);
		}
		displayModeIterator->Release();
	}

	return true;
}

bool DeckLinkDevice::startCapture(int videoModeIndex, uint32_t bitrate)
{
	BMDVideoInputFlags videoInputFlags;
	
	videoInputFlags = bmdVideoInputEnableFormatDetection;
	
	if ((videoModeIndex < 0) || (videoModeIndex >= m_modeList.size()))
	{
		m_uiDelegate->showErrorMessage("Error starting the capture", "An invalid display mode was selected.");
		return false;
	}

	BMDTimeValue duration, timeScale;
	m_modeList[videoModeIndex]->GetFrameRate(&duration, &timeScale);

	QString filePath = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
	QString fileName = filePath + "/BlackmagicDesign_Recording " + QDateTime::currentDateTime().toString("yyyy-MM-dd HH.mm.ss") + ".hevc";

	m_videoWriter = new VideoWriter(fileName);
	if (!m_videoWriter)
	{
		m_uiDelegate->showErrorMessage("Error starting recording", "Failed to allocate video writer");
		return false;
	}

	if (!m_videoWriter->open())
	{
		m_uiDelegate->showErrorMessage("Error starting recording", "Failed to open output file");
		return false;
	}

	if (m_deckLinkEncoderConfiguration->SetInt(bmdDeckLinkEncoderConfigPreferredBitDepth, 10) != S_OK)
	{
		m_uiDelegate->showErrorMessage("Error starting recording", "Failed to set bit depth.");
		return false;
	}

	if (m_deckLinkEncoderConfiguration->SetInt(bmdDeckLinkEncoderConfigFrameCodingMode, bmdVideoEncoderFrameCodingModeInter) != S_OK)
	{
		m_uiDelegate->showErrorMessage("Failed to set frame coding mode.", "Error starting recording");
		return false;
	}

	if (m_deckLinkEncoderConfiguration->SetInt(bmdDeckLinkEncoderConfigH265TargetBitrate, bitrate) != S_OK)
	{
		m_uiDelegate->showErrorMessage("Error starting recording", "Failed to set target bit depth.");
		return false;
	}

	BMDDisplayModeSupport support;
	if (m_deckLinkEncoderInput->DoesSupportVideoMode(m_modeList[videoModeIndex]->GetDisplayMode(), bmdFormatH265, videoInputFlags, &support, NULL) != S_OK || support != bmdDisplayModeSupported)
	{
		m_uiDelegate->showErrorMessage("Error starting recording", "The encoder does not support the chosen video mode.");
		return false;
	}

	m_deckLinkEncoderInput->SetCallback(this);

	if (m_deckLinkEncoderInput->EnableVideoInput(m_modeList[videoModeIndex]->GetDisplayMode(), bmdFormatH265, videoInputFlags) != S_OK)
	{
		m_uiDelegate->showErrorMessage("Error starting recording", "This application was unable to select the chosen video mode. Perhaps, the selected device is currently in-use.");
		return false;
	}

	if (m_deckLinkEncoderInput->StartStreams() != S_OK)
	{
		m_uiDelegate->showErrorMessage("Error starting the capture", "This application was unable to start the capture. Perhaps, the selected device is currently in-use.");
		return false;
	}

	m_currentlyCapturing = true;

	return true;
}

void DeckLinkDevice::stopCapture(bool deleteFile)
{
	m_deckLinkEncoderInput->StopStreams();
	m_deckLinkEncoderInput->SetCallback(NULL);
	m_deckLinkEncoderInput->DisableVideoInput();

	if (m_videoWriter)
	{
		m_videoWriter->close(deleteFile);
		delete m_videoWriter;
		m_videoWriter = NULL;
	}
	
	m_currentlyCapturing = false;
}

HRESULT DeckLinkDevice::VideoInputSignalChanged (/* in */ BMDVideoInputFormatChangedEvents, /* in */ IDeckLinkDisplayMode *newMode, /* in */ BMDDetectedVideoInputFormatFlags)
{
	uint32_t modeIndex = 0;
	while (modeIndex < m_modeList.size()) 
	{
		if (m_modeList[modeIndex]->GetDisplayMode() == newMode->GetDisplayMode())
		{
			m_uiDelegate->selectDetectedVideoModeWithIndex(modeIndex);
			break;
		}
		modeIndex++;
	}

	return S_OK;
}

uint32_t DeckLinkDevice::getDisplayModeFrameRate(int displayModeIndex)
{
	if ((displayModeIndex < 0) || (displayModeIndex >= m_modeList.size()))
		return 0;

	BMDTimeScale frameDuration, frameScale;
	m_modeList[displayModeIndex]->GetFrameRate(&frameDuration, &frameScale);
	return (double)frameScale / (double)frameDuration;
}
QString DeckLinkDevice::getDisplayModeName(int displayModeIndex)
{
	if ((displayModeIndex < 0) || (displayModeIndex >= m_modeList.size()))
		return 0;

	const char* displayModeName = NULL;
	if(m_modeList[displayModeIndex]->GetName(&displayModeName) != S_OK)
		return QString();

	QString name;
	if(displayModeName)
	{
		name = displayModeName;
		free((void*)displayModeName);
	}

	return name;
}

HRESULT DeckLinkDevice::VideoPacketArrived(IDeckLinkEncoderVideoPacket* videoPacket)
{
	void* buffer = NULL;
	videoPacket->GetBytes(&buffer);

	if (buffer && m_videoWriter)
	{
		if (videoPacket->GetPacketType() == bmdPacketTypeStreamData)
			m_videoWriter->writeVideo((uint8_t*)buffer, videoPacket->GetSize());
	}

	return S_OK;
}

HRESULT DeckLinkDevice::AudioPacketArrived(IDeckLinkEncoderAudioPacket*)
{
	return S_OK;
}

DeckLinkDeviceDiscovery::DeckLinkDeviceDiscovery(ControllerImp* delegate)
: m_uiDelegate(delegate), deckLinkDiscovery(NULL), m_refCount(1)
{
	deckLinkDiscovery = CreateDeckLinkDiscoveryInstance();
}

DeckLinkDeviceDiscovery::~DeckLinkDeviceDiscovery()
{
	if (deckLinkDiscovery != NULL)
	{
		deckLinkDiscovery->UninstallDeviceNotifications();
		deckLinkDiscovery->Release();
		deckLinkDiscovery = NULL;
	}
}

bool DeckLinkDeviceDiscovery::Enable()
{
	HRESULT	 result = E_FAIL;	
	if (deckLinkDiscovery != NULL)
		result = deckLinkDiscovery->InstallDeviceNotifications(this);
	
	return result == S_OK;
}

void DeckLinkDeviceDiscovery::Disable()
{
	if (deckLinkDiscovery != NULL)
		deckLinkDiscovery->UninstallDeviceNotifications();
}

HRESULT DeckLinkDeviceDiscovery::DeckLinkDeviceArrived (/* in */ IDeckLink* deckLink)
{
	deckLink->AddRef();
	m_uiDelegate->addDevice(deckLink);

	return S_OK;
}

HRESULT DeckLinkDeviceDiscovery::DeckLinkDeviceRemoved (/* in */ IDeckLink* deckLink)
{	
	m_uiDelegate->removeDevice(deckLink);
	return S_OK;
}

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

	*ppv = NULL;

	iunknown = CFUUIDGetUUIDBytes(IUnknownUUID);
	if (memcmp(&iid, &iunknown, sizeof(REFIID)) == 0)
	{
		*ppv = this;
		AddRef();
		result = S_OK;
	}
	else if (memcmp(&iid, &IID_IDeckLinkDeviceNotificationCallback, sizeof(REFIID)) == 0)
	{
		*ppv = (IDeckLinkDeviceNotificationCallback*)this;
		AddRef();
		result = S_OK;
	}
	
	return result;
}

ULONG DeckLinkDeviceDiscovery::AddRef (void)
{
	int oldValue = m_refCount.fetchAndAddAcquire(1);
	return (ULONG)(oldValue + 1);
}

ULONG DeckLinkDeviceDiscovery::Release (void)
{
	int oldValue = m_refCount.fetchAndAddAcquire(-1);
	if (oldValue == 1)      // i.e. current value will be 0
		delete this;

	return (ULONG)(oldValue - 1);
}
