/* -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 <stdio.h>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include "platform.h"
#include "ImageLoader.h"
#include "DeckLinkAPI.h"

static const BMDPixelFormat kConvertedPixelFormat = bmdFormat10BitYUV;

std::mutex					g_playbackMutex;
std::condition_variable		g_playbackStopCondition;
bool						g_keyPressed = false;

void PlaybackStills(IDeckLinkOutput* deckLinkOutput, IDeckLinkVideoFrame* playbackFrame, std::vector<std::string>& pngFiles, long updateIntervalms, bool loopPlayback, bool convertOutput)
{
	std::chrono::milliseconds	timerPeriod(updateIntervalms);
	int							playbackStillsCount	= 0;
	bool						playbackRunning		= true;
	HRESULT						result				= S_OK;
	IDeckLinkVideoConversion*	frameConverter		= NULL;
	IDeckLinkMutableVideoFrame*	convertedVideoFrame	= NULL;
	
	if (convertOutput)
	{
		int outputBytesPerRow;

		result = GetDeckLinkFrameConverter(&frameConverter);
		if (result != S_OK)
		{
			fprintf(stderr, "Unable to get Video Conversion interface\n");
			goto bail;
		}

		// Refer to DeckLink SDK Manual - 2.7.4 Pixel Formats
		switch (kConvertedPixelFormat)
		{
			case bmdFormat8BitYUV:
				outputBytesPerRow = playbackFrame->GetWidth() * 2;
				break;

			case bmdFormat10BitYUV:
				outputBytesPerRow = ((playbackFrame->GetWidth() + 47) / 48) * 128;
				break;

			default:
				fprintf(stderr, "Unexpected output pixel format\n");
				goto bail;
		}

		if (deckLinkOutput->CreateVideoFrame(playbackFrame->GetWidth(), playbackFrame->GetHeight(), outputBytesPerRow,
			kConvertedPixelFormat, playbackFrame->GetFlags(), &convertedVideoFrame) != S_OK)
		{
			fprintf(stderr, "Could not create video frame to convert into\n");
			playbackRunning = false;
		}
	}
	
	while (playbackRunning)
	{
		result = ImageLoader::ConvertPNGToDeckLinkVideoFrame(pngFiles[playbackStillsCount], playbackFrame);
		if (result != S_OK)
		{
			fprintf(stderr, "Error reading PNG file: %s\n", pngFiles[playbackStillsCount].c_str());
			playbackRunning = false;
		}
		
		if (convertOutput)
		{
			// Pixel format conversion required to output frame
			if (frameConverter->ConvertFrame(playbackFrame, convertedVideoFrame) != S_OK)
			{
				playbackRunning = false;
				continue;
			}

			result = deckLinkOutput->DisplayVideoFrameSync(convertedVideoFrame);
			if (result != S_OK)
			{
				fprintf(stderr, "Unable to display video output\n");
				playbackRunning = false;
			}
		}
		else
		{
			// Output frame without format conversion
			result = deckLinkOutput->DisplayVideoFrameSync(playbackFrame);
			if (result != S_OK)
			{
				fprintf(stderr, "Unable to display video output\n");
				playbackRunning = false;
			}
		}
		
		std::unique_lock<std::mutex> lock(g_playbackMutex);
		if (g_playbackStopCondition.wait_for(lock, timerPeriod, [&]{ return g_keyPressed; }))
		{
			// Key press.
			playbackRunning = false;
		}
		else
		{
			// Timeout
			if (++playbackStillsCount >= (int)pngFiles.size())
			{
				if (loopPlayback)
					playbackStillsCount = 0;
				else
					playbackRunning = false;
			}
		}
	}

bail:
	if (convertedVideoFrame != NULL)
		convertedVideoFrame->Release();

	if (frameConverter != NULL)
		frameConverter->Release();

}

void DisplayUsage(const IDeckLinkOutput* selectedDeckLinkOutput, const std::vector<std::string>& deviceNames,
					const std::vector<IDeckLinkDisplayMode*>& displayModes, const int selectedDeviceIndex)
{
	HRESULT result;

	fprintf(stderr,
		"\n"
		"Usage: ./PlaybackStills -d <device id> -m <mode id> [OPTIONS]\n"
		"\n"
		"    -d <device id>:\n"
		);

	if (deviceNames.empty())
	{
		fprintf(stderr, "        No DeckLink devices found. Please check Desktop Video installation\n");
	}
	else
	{
		// Loop through all available devices
		for (size_t i = 0; i < deviceNames.size(); i++)
		{
			fprintf(stderr,
				"       %c%2d:  %s\n",
				((int)i == selectedDeviceIndex) ? '*' : ' ',
				(int)i,
				deviceNames[i].c_str()
				);
		}
	}

	fprintf(stderr,
		"    -m <mode id>: (%s)\n", (selectedDeviceIndex >= 0) ? deviceNames[selectedDeviceIndex].c_str() : ""
		);

	// Loop through all available display modes on the selected DeckLink device
	if (selectedDeckLinkOutput == NULL)
	{
		fprintf(stderr, "        No DeckLink device selected\n");
	}
	else
	{
		for (size_t i = 0; i < displayModes.size(); i++)
		{
			dlstring_t displayModeName;

			result = displayModes[i]->GetName(&displayModeName);
			if (result == S_OK)
			{
				BMDTimeValue frameRateDuration;
				BMDTimeValue frameRateScale;

				displayModes[i]->GetFrameRate(&frameRateDuration, &frameRateScale);

				fprintf(stderr,
					"        %2d:  %-20s \t %4li x %4li \t %.2f FPS\n",
					(int)i,
					DlToCString(displayModeName),
					displayModes[i]->GetWidth(),
					displayModes[i]->GetHeight(),
					(double)frameRateScale / (double)frameRateDuration
				);

				DeleteString(displayModeName);
			}
		}
	}

	fprintf(stderr,
		"    -i <interval>\n        Playback frame interval rate (default is 1 - every frame)\n"
		"    -l\n        Loop playback\n"
		"    <imagedirectory>\n"
		"\n"
		"Playback PNG image stills from a specified directory. eg:\n"
		"\n"
		"    ./PlaybackStills -d 0 -m 2 -i 60 -l ~/Pictures/\n"
		);
}


int main(int argc, char* argv[])
{
	// Configuration flags
	bool						displayHelp			= false;
	int							deckLinkIndex		= -1;
	int							displayModeIndex	= -1;
	bool						loopPlayback		= false;
	int							updateInterval		= 1;
	bool						convertOutputFormat = false;
	std::string					playbackDirectory;

	HRESULT						result;
	int							exitStatus = 1;
	int							idx;

	std::thread					playbackStillsThread;

	IDeckLinkIterator*			deckLinkIterator		= NULL;
	IDeckLink*					deckLink				= NULL;
	IDeckLinkOutput*			selectedDeckLinkOutput	= NULL;
	IDeckLinkMutableVideoFrame*	playbackFrame			= NULL;

	BMDDisplayMode				selectedDisplayMode		= bmdModeNTSC;
	std::string					selectedDisplayModeName;
	BMDTimeValue				frameDuration			= 1001;
	BMDTimeValue				frameTimescale			= 30000;

	std::vector<IDeckLinkDisplayMode*>	displayModes;
	std::vector<std::string>			deckLinkDeviceNames;
	std::vector<std::string>			pngFiles;


	result = GetDeckLinkIterator(&deckLinkIterator);
	if (result != S_OK)
		goto bail;

	// Process the command line arguments 
	for (int i = 1; i < argc; i++)
	{
		if (strcmp(argv[i], "-d") == 0)
			deckLinkIndex = atoi(argv[++i]);

		else if (strcmp(argv[i], "-m") == 0)
			displayModeIndex = atoi(argv[++i]);

		else if (strcmp(argv[i], "-i") == 0)
			updateInterval = atoi(argv[++i]);

		else if (strcmp(argv[i], "-l") == 0)
			loopPlayback = true;

		else if ((strcmp(argv[i], "?") == 0) || (strcmp(argv[i], "-h") == 0))
			displayHelp = true;

		else if (i == argc - 1)
		{
			playbackDirectory = argv[i];
		}
	}

	if (playbackDirectory.empty())
	{
		fprintf(stderr, "You must set a playback directory\n");
		displayHelp = true;
	}
	else if (!IsPathDirectory(playbackDirectory))
	{
		fprintf(stderr, "Invalid directory specified\n");
		displayHelp = true;
	}
	else
	{
		// Find all images of extension in specified directory
		result = ImageLoader::GetPNGFilesFromDir(playbackDirectory, pngFiles);
		if ((result != S_OK) || (pngFiles.empty()))
		{
			fprintf(stderr, "No images found in playback directory\n");
			displayHelp = true;
		}
	}
	
	if (deckLinkIndex < 0)
	{
		fprintf(stderr, "You must select a device\n");
		displayHelp = true;
	}

	// Obtain the required DeckLink device
	idx = 0;

	while ((result = deckLinkIterator->Next(&deckLink)) == S_OK)
	{
		dlstring_t deckLinkName;

		result = deckLink->GetDisplayName(&deckLinkName);
		if (result == S_OK)
		{
			deckLinkDeviceNames.push_back(DlToStdString(deckLinkName));
			DeleteString(deckLinkName);
		}

		if (idx++ == deckLinkIndex)
		{
			// Check that selected device supports playback
			IDeckLinkProfileAttributes*	deckLinkAttributes = NULL;
			int64_t						ioSupportAttribute = 0;

			result = deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes);

			if (result != S_OK)
			{
				fprintf(stderr, "Unable to get IDeckLinkAttributes interface\n");
				goto bail;
			}

			if (deckLinkAttributes->GetInt(BMDDeckLinkVideoIOSupport, &ioSupportAttribute) != S_OK)
				ioSupportAttribute = 0;

			deckLinkAttributes->Release();

			if ((ioSupportAttribute & bmdDeviceSupportsPlayback) != 0)
			{
				result = deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&selectedDeckLinkOutput);
				if (result != S_OK)
				{
					fprintf(stderr, "Unable to get IDeckLinkOutput interface\n");
					goto bail;
				}
			}
			else
			{
				fprintf(stderr, "Selected device does not support playback\n");
				displayHelp = true;
			}
		}

		deckLink->Release();
		deckLink = NULL;
	}

	// Get display modes from the selected decklink output 
	if (selectedDeckLinkOutput != NULL)
	{
		IDeckLinkDisplayModeIterator* displayModeIterator;
		IDeckLinkDisplayMode* displayMode;

		result = selectedDeckLinkOutput->GetDisplayModeIterator(&displayModeIterator);
		if (result != S_OK)
		{
			fprintf(stderr, "Unable to get IDeckLinkDisplayModeIterator interface\n");
			goto bail;
		}

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

		displayModeIterator->Release();

		if ((displayModeIndex < 0) || (displayModeIndex > (int)displayModes.size()))
		{
			fprintf(stderr, "You must select a valid display mode\n");
			displayHelp = true;
		}
		else
		{
			dlbool_t				displayModeSupported;
			dlstring_t				displayModeName;

			result = displayModes[displayModeIndex]->GetName(&displayModeName);
			if (result != S_OK)
				goto bail;

			selectedDisplayModeName = DlToStdString(displayModeName);
			DeleteString(displayModeName);

			selectedDisplayMode = displayModes[displayModeIndex]->GetDisplayMode();

			result = displayModes[displayModeIndex]->GetFrameRate(&frameDuration, &frameTimescale);
			if (result != S_OK)
				goto bail;

			// Check display mode is supported with given options
			result = selectedDeckLinkOutput->DoesSupportVideoMode(bmdVideoConnectionUnspecified, selectedDisplayMode, ImageLoader::kImageLoaderPixelFormat, bmdNoVideoOutputConversion, bmdSupportedVideoModeDefault, NULL, &displayModeSupported);
			if ((result != S_OK) || (!displayModeSupported))
			{
				// Video mode is unsupported, check whether we can support with format conversion
				result = selectedDeckLinkOutput->DoesSupportVideoMode(bmdVideoConnectionUnspecified, selectedDisplayMode, kConvertedPixelFormat, bmdNoVideoOutputConversion, bmdSupportedVideoModeDefault, nullptr, &displayModeSupported);
				if ((result != S_OK) || (!displayModeSupported))
				{
					fprintf(stderr, "The display mode %s is not supported by device\n", selectedDisplayModeName.c_str());
					displayHelp = true;
				}
				else
					convertOutputFormat = true;
			}
			else
				convertOutputFormat = false;
		}
	}

	if (displayHelp)
	{
		DisplayUsage(selectedDeckLinkOutput, deckLinkDeviceNames, displayModes, deckLinkIndex);
		goto bail;
	}

	// Set the video output mode
	result = selectedDeckLinkOutput->EnableVideoOutput(selectedDisplayMode, bmdVideoOutputFlagDefault);
	if (result != S_OK)
	{
		fprintf(stderr, "Unable to enable video output\n");
		goto bail;
	}
	
	// Create video frame for playback, as we are outputting frame synchronously, 
	// then we can reuse without waiting on callback 
	result = selectedDeckLinkOutput->CreateVideoFrame((int32_t)displayModes[displayModeIndex]->GetWidth(),
													  (int32_t)displayModes[displayModeIndex]->GetHeight(),
													  (int32_t)displayModes[displayModeIndex]->GetWidth() * 4,
													  ImageLoader::kImageLoaderPixelFormat,
													  bmdFrameFlagDefault,
													  &playbackFrame);
	if (result != S_OK)
	{
		fprintf(stderr, "Unable to create video frame\n");
		goto bail;
	}
	
	// OK to start playback - print configuration
	fprintf(stderr, "Output with the following configuration:\n"
		" - Playback device: %s\n"
		" - Video mode: %s\n"
		" - Playback update interval: %d\n"
		" - Loop Playback: %s\n"
		" - Playback directory: %s\n"
		" - Number of images to playback: %d\n",
		deckLinkDeviceNames[deckLinkIndex].c_str(),
		selectedDisplayModeName.c_str(),
		updateInterval,
		loopPlayback ? "YES" : "NO",
		playbackDirectory.c_str(),
		(int)pngFiles.size()
		);
	fprintf(stderr, "Starting Playback, press <RETURN> to exit\n");

	// Start thread for message processing
	playbackStillsThread = std::thread([&]{
		PlaybackStills(selectedDeckLinkOutput, (IDeckLinkVideoFrame*)playbackFrame, pngFiles,
						updateInterval * 1000 * (long)frameDuration / (long)frameTimescale, loopPlayback, convertOutputFormat);
	});
	
	// Wait on return press, then notify playback thread to finalize
	getchar();
	{
		std::lock_guard<std::mutex> lock(g_playbackMutex);
		g_keyPressed = true;
	}
	g_playbackStopCondition.notify_one();
	playbackStillsThread.join();

	fprintf(stderr, "Stopping Playback\n");
	result = selectedDeckLinkOutput->DisableVideoOutput();
	if (result != S_OK)
		goto bail;

	exitStatus = 0;

bail:
	while (!displayModes.empty())
	{
		displayModes.back()->Release();
		displayModes.pop_back();
	}
	
	if (playbackFrame != NULL)
	{
		playbackFrame->Release();
		playbackFrame = NULL;
	}

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

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

	return exitStatus;
}

