/* -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"

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

static const BMDTimeScale kDefaultPreviewTimeScale = 100000;

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("Playback Initialization Error"),	CFSTR("Failed to enable video output in the required display mode.") },
	{ CFSTR("Playback Initialization Error"),	CFSTR("Failed to enable audio output in the required display mode.") },
	{ CFSTR("Video Output Error"),				CFSTR("Failed to output video frame.") },
	{ CFSTR("Media Read Error"),				CFSTR("Failed to read media file.") },
	{ CFSTR("Frame Displayed Late"),			CFSTR("The system may not be fast enough to play this file.") },
};

CocoaViewController::CocoaViewController(AppDelegate* appDelegate) :
m_appDelegate(appDelegate), m_streamDuration(0), m_timeScale(0)
{
}

CocoaViewController::~CocoaViewController()
{
}

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

void CocoaViewController::deviceManagerUpdated(com_ptr<DeckLinkPlaybackDevice> 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<DeckLinkPlaybackDevice> device, DeviceStatus status)
{
	dispatch_async(dispatch_get_main_queue(), ^{
		switch (status)
		{
			case kDeviceStateChanged:
			{
				DeviceIOState deviceState = device->getDeviceIOState();
				[m_appDelegate setDeviceState:deviceState];
				updateDeviceState(device);
				break;
			}
			case kDeviceProfileChanged:
			{
				updateDevice(device);
				break;
			}
			case kPlaybackStreamTimeUpdated:
			{
				updateStreamTime(device);
				break;
			}
			default:
				break;
		}
	});
}

void CocoaViewController::updateDeviceState(com_ptr<DeckLinkPlaybackDevice> device)
{
	[m_appDelegate setFilePath:device->filePath()];
	[m_appDelegate clearDisplayModeName];

	device->queryDisplayModes([this](com_ptr<IDeckLinkDisplayMode>& displayMode) {
		CFStringRef displayModeName;
		displayMode->GetName(&displayModeName);
		[m_appDelegate setDisplayModeName:(NSInteger)displayMode->GetDisplayMode() displayModeName:displayModeName];
		CFRelease(displayModeName);
	}, true);
}

void CocoaViewController::updateStreamTime(com_ptr<DeckLinkPlaybackDevice> device)
{
	BMDTimeValue streamTime = 0;
	device->streamTime(streamTime, m_streamDuration, m_timeScale);

	int64_t elapsedSeconds = streamTime / m_timeScale;
	int64_t totalSeconds = m_streamDuration / m_timeScale;
	[m_appDelegate setPlayPosition:((double)streamTime / (double)m_streamDuration) elapsedSeconds:elapsedSeconds totalSeconds:totalSeconds];
}

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

	dispatch_async(dispatch_get_main_queue(), ^{
		[m_appDelegate showError:kDeviceErrorMessage[error].title detail:kDeviceErrorMessage[error].detail];
	});
}

bool CocoaViewController::selectDevice(intptr_t deviceID)
{
	com_ptr<DeckLinkPlaybackDevice> currentDevice = m_deviceManager->device();
	
	if (currentDevice)
	{
		// If the current device is selected or is unavailable, return false (nothing changed)
		if (deviceID == currentDevice->getDeviceID())
			return false;
		
		currentDevice->disable();
		currentDevice->setErrorListener(nullptr);
	}

	com_ptr<DeckLinkPlaybackDevice> 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<DeckLinkPlaybackDevice> device)
{
	if (!device)
		return false;
	
	com_ptr<DeckLinkPlaybackDevice> currentDevice = m_deviceManager->device();
	[m_appDelegate updateDevice:(intptr_t)device->getDeviceID() enabled:(BOOL)device->isAvailable()];
	
	return true;
}

void CocoaViewController::previewFile(const std::string& filePath, double position)
{
	auto previewPosition = (BMDTimeValue)0;
	auto previewTimeScale = kDefaultPreviewTimeScale;
	com_ptr<DeckLinkPlaybackDevice> device = m_deviceManager->device();

	if (!device)
		return;

	if (device->getDeviceIOState() == kFileIORunning)
		device->stop(0, kDefaultPreviewTimeScale);

	// If a time scale is available, a file is already open
	if (m_timeScale)
	{
		previewPosition = (BMDTimeValue)(position * (double)m_streamDuration);
		previewTimeScale = m_timeScale;
	}

	device->preview([m_appDelegate screenPreviewCallback], filePath, previewPosition, previewTimeScale);
}

void CocoaViewController::play()
{
	com_ptr<DeckLinkPlaybackDevice> device = m_deviceManager->device();

	if (!device)
		return;

	DeviceIOState state = device->getDeviceIOState();

	if (state == kDeviceIOEnabled)
	{
		BMDTimeValue streamTime = 0;
		BMDTimeValue streamDuration = 0;
		BMDTimeScale timeScale = 0;

		device->streamTime(streamTime, streamDuration, timeScale);
		device->play(streamTime, timeScale);
	}
}

void CocoaViewController::stop()
{
	com_ptr<DeckLinkPlaybackDevice> device = m_deviceManager->device();

	if (!device)
		return;

	DeviceIOState state = device->getDeviceIOState();

	if (state == kFileIORunning)
		device->stop(0, kDefaultPreviewTimeScale);
}

void CocoaViewController::setPosition(double position)
{
	com_ptr<DeckLinkPlaybackDevice> device = m_deviceManager->device();

	if (!device)
		return;

	DeviceIOState state = device->getDeviceIOState();

	if (state != kDeviceIOEnabled && state != kFileIORunning)
		return;

	if (state == kFileIORunning)
		device->stop(0, kDefaultPreviewTimeScale);

	device->scrub((BMDTimeValue)(position * (double)m_streamDuration), m_timeScale);
}
