/* -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 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 "AppDelegate.h"
#include "CocoaViewController.h"
#include "DeckLinkDeviceState.h"
#include <list>

typedef struct
{
	CFStringRef title;
	CFStringRef detail;
} DeviceErrorMessage;

static const DeviceErrorMessage kDeviceErrorMessage[] =
{
	{ CFSTR("No Error"),						CFSTR("No error.") },
	{ CFSTR("Device Configuration Error"),		CFSTR("This device does not support the required display mode.") },
	{ CFSTR("Capture Initialization Error"),	CFSTR("Failed to enable video input in the required display mode.") },
	{ CFSTR("Capture Initialization Error"),	CFSTR("Failed to enable audio input in the required format.") },
	{ CFSTR("Capture Initialization Error"),	CFSTR("Failed to enable capture.") },
	{ CFSTR("Media Write Error"),				CFSTR("Video frames were dropped.") },
	{ CFSTR("Media Write Error"),				CFSTR("Failed to write media file.") }
};

static const std::list<std::pair<BMDVideoConnection, CFStringRef> > kInputConnections =
{
	{ bmdVideoConnectionSDI,		CFSTR("SDI") },
	{ bmdVideoConnectionHDMI,		CFSTR("HDMI") },
	{ bmdVideoConnectionOpticalSDI,	CFSTR("Optical SDI") },
	{ bmdVideoConnectionComponent,	CFSTR("Component") },
	{ bmdVideoConnectionComposite,	CFSTR("Composite") },
	{ bmdVideoConnectionSVideo,		CFSTR("S-Video") },
};

CocoaViewController::CocoaViewController(AppDelegate* appDelegate) :
m_appDelegate(appDelegate)
{
}

CocoaViewController::~CocoaViewController()
{
}

void CocoaViewController::setDeviceManager(std::shared_ptr<DeviceManager> deviceManager)
{
	m_deviceManager = deviceManager;
}

void CocoaViewController::deviceManagerUpdated(com_ptr<DeckLinkCaptureDevice> device, DeviceManagerStatus status)
{
	using namespace std::placeholders;
	device->setStatusListener(std::bind(&CocoaViewController::deviceStateChanged, status == kDeviceAdded ? this : nullptr, _1, _2));
	
	dispatch_async(dispatch_get_main_queue(), ^{
		switch (status)
		{
			case kDeviceAdded:
				[m_appDelegate addDevice:device->getDeviceID() displayName:device->displayName() enabled:device->isAvailable()];
				break;
			case kDeviceRemoved:
				[m_appDelegate removeDevice:device->getDeviceID()];
				break;
			default:
				;
		}
	});
}

void CocoaViewController::deviceStateChanged(com_ptr<DeckLinkCaptureDevice> device, DeviceStatus status)
{
	dispatch_async(dispatch_get_main_queue(), ^{
		switch (status)
		{
			case kDeviceStateChanged:
			{
				[m_appDelegate setDeviceState:device->getDeviceIOState()];
				break;
			}
			case kDeviceVideoInputChanged:
			{
				[m_appDelegate setVideoInputConnection:device->activeVideoInputConnection()];
				break;
			}
			case kDeviceProfileChanged:
			{
				updateDevice(device);
				break;
			}
			case kCaptureSignalValid:
			{
				[m_appDelegate setSignalPresence:TRUE];
				break;
			}
			case kCaptureSignalInvalid:
			{
				[m_appDelegate setSignalPresence:FALSE];
				break;
			}
			case kCaptureFormatChanged:
			{
				[m_appDelegate setDisplayMode:device->activeDisplayMode()];
				break;
			}
			case kRecordingComplete:
			{
				std::string filePath = device->filePath();
				[m_appDelegate saveRecodedFile:filePath];
				break;
			}
			default:
				break;
		}
	});
}

void CocoaViewController::deviceErrorOccurred(com_ptr<DeckLinkCaptureDevice> device, DeviceError error)
{
	if (error >= kErrorCount)
		return;

	__block com_ptr<DeckLinkCaptureDevice> errorDevice(device);
	
	dispatch_async(dispatch_get_main_queue(), ^{
		switch (error)
		{
			case kMediaFileWriteFailed:
				errorDevice->stopRecording();
			case kVideoDisplayModeNotSupported:
			case kEnableVideoInputFailed:
			case kEnableAudioInputFailed:
			case kStartCaptureFailed:
				[m_appDelegate showError:kDeviceErrorMessage[error].title detail:kDeviceErrorMessage[error].detail];
				break;
			case kMediaFileFrameDropped:
				[m_appDelegate showInfo:kDeviceErrorMessage[error].title detail:kDeviceErrorMessage[error].detail];
				break;
			default:
				break;
		}
	});
}

bool CocoaViewController::selectDevice(intptr_t deviceID)
{
	com_ptr<DeckLinkCaptureDevice> currentDevice = m_deviceManager->device();
	
	if (currentDevice)
	{
		// If the current device is selected, return false (nothing changed)
		if (deviceID == currentDevice->getDeviceID())
			return false;

		currentDevice->disable();
		currentDevice->setErrorListener(nullptr);
	}
	
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->findDevice(deviceID);
	if (!device)
		goto bail;
	
	if (!device->isAvailable())
	{
		[m_appDelegate setDeviceState:kDeviceIOIdle];
		return false;
	}
	
	m_deviceManager->setDevice(device);

	using namespace std::placeholders;
	device->setErrorListener(std::bind(&CocoaViewController::deviceErrorOccurred, this, _1, _2));
	updateDevice(device);
	
	if (device)
	{
		[m_appDelegate setDeviceState:kDeviceIOIdle];
		return true;
	}
	
bail:
	[m_appDelegate setDeviceState:kDeviceIOUnknown];
	return false;
}

bool CocoaViewController::updateDevice(com_ptr<DeckLinkCaptureDevice> device)
{
	if (!device)
		return false;
	
	com_ptr<DeckLinkCaptureDevice> currentDevice = m_deviceManager->device();

	// Update the availablity of all devices
	[m_appDelegate updateDevice:(intptr_t)device->getDeviceID() enabled:(BOOL)device->isAvailable()];
	
	// Only update video connections and modes for the currently selected device
	if (!currentDevice || device->getDeviceID() != currentDevice->getDeviceID())
		return false;
	
	// Load active video input connections and set the active select the input
	int64_t inputConnections = device->videoInputConnections();
	[m_appDelegate clearVideoInputConnectionList];
	
	for (auto connection : kInputConnections)
	{
		if ((connection.first & inputConnections) != 0)
			[m_appDelegate addVideoInputConnection:(NSInteger)connection.first connectionName:connection.second];
	}
	[m_appDelegate setVideoInputConnection:(NSInteger)device->activeVideoInputConnection()];
	
	// Load supported video modes
	[m_appDelegate clearDisplayModeList];
	device->queryDisplayModes([this](com_ptr<IDeckLinkDisplayMode> displayMode) {
		CFStringRef		displayModeName;
		displayMode->GetName(&displayModeName);
		[m_appDelegate addDisplayMode:(NSInteger)displayMode->GetDisplayMode() displayModeName:displayModeName];
		CFRelease(displayModeName);
	});
	
	return true;
}

bool CocoaViewController::selectInput(BMDVideoConnection connection)
{
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->device();
	
	if (!device)
		return false;
	
	device->setVideoInputConnection(connection);
	return true;
}

bool CocoaViewController::selectFormat(BMDDisplayMode displayMode)
{
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->device();
	
	if (!device || device->activeDisplayMode() == displayMode)
		return false;
	
	if (device->getDeviceIOState() != kDeviceIOIdle)
		device->disable();
	return true;
}

bool CocoaViewController::setFormatAutoDetect(bool enabled)
{
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->device();
	
	if (!device || device->formatAutoDetect() == enabled)
		return false;
	
	device->setFormatAutoDetect(enabled);
	return true;
}

bool CocoaViewController::formatAutoDetectState(bool& supported, bool& enabled)
{
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->device();
	
	if (!device)
		return false;
	
	supported	= device->supportsFormatAutoDetect();
	enabled		= device->formatAutoDetect();
	
	return true;
}

void CocoaViewController::capture(BMDDisplayMode displayMode)
{
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->device();

	if (device)
		device->capture([m_appDelegate screenPreviewCallback], displayMode);
}

void CocoaViewController::stopCapturing()
{
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->device();

	if (device)
		device->disable();
}

void CocoaViewController::record(const std::string& filePath)
{
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->device();

	if (device)
		device->record(filePath);
}

void CocoaViewController::stopRecording()
{
	com_ptr<DeckLinkCaptureDevice> device = m_deviceManager->device();

	if (device)
		device->stopRecording();
}
