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

using System;
using DeckLinkAPI;
using System.Collections.Generic;

namespace CapturePreviewCSharp
{
	class DeckLinkInputInvalidException : Exception { }
	class DeckLinkStartCaptureException : Exception { }

	public class DeckLinkDeviceInputVideoFrameEventArgs : EventArgs
	{
		public readonly IDeckLinkVideoInputFrame videoFrame;

		public DeckLinkDeviceInputVideoFrameEventArgs(IDeckLinkVideoInputFrame videoFrame)
		{
			this.videoFrame = videoFrame;
		}
	}

	public class DeckLinkDeviceInputFormatEventArgs : EventArgs
	{
		public readonly _BMDDisplayMode displayMode;
		public readonly bool dualStream3D;

		public DeckLinkDeviceInputFormatEventArgs(_BMDDisplayMode displayMode, bool dualStream3D)
		{
			this.displayMode = displayMode;
			this.dualStream3D = dualStream3D;
		}
	}

	class DeckLinkDevice : IDeckLinkInputCallback
	{
		private readonly IDeckLink m_deckLink;
		private readonly IDeckLinkInput m_deckLinkInput;
		private readonly IDeckLinkConfiguration m_deckLinkConfiguration;
		private readonly IDeckLinkProfileManager m_deckLinkProfileManager;

		private bool m_applyDetectedInputMode = true;
		private bool m_currentlyCapturing = false;

		private readonly bool m_supportsInputFormatDetection;
		private readonly long m_availableInputConnections;
		private readonly string m_displayName;

		public event EventHandler<DeckLinkDeviceInputVideoFrameEventArgs> VideoFrameArrived;
		public event EventHandler<DeckLinkDeviceInputFormatEventArgs> InputFormatChanged;

		public DeckLinkDevice(IDeckLink deckLink, IDeckLinkProfileCallback profileCallback)
		{
			m_deckLink = deckLink;

			// Ensure card has a capture interface (not a DeckLink Mini Monitor, for instance)
			var deckLinkAttributes = m_deckLink as IDeckLinkProfileAttributes;
			deckLinkAttributes.GetInt(_BMDDeckLinkAttributeID.BMDDeckLinkVideoIOSupport, out long ioSupportAttribute);
			if (!((_BMDVideoIOSupport)ioSupportAttribute).HasFlag(_BMDVideoIOSupport.bmdDeviceSupportsCapture))
				throw new DeckLinkInputInvalidException();

			deckLinkAttributes.GetInt(_BMDDeckLinkAttributeID.BMDDeckLinkVideoInputConnections, out m_availableInputConnections);

			deckLinkAttributes.GetFlag(_BMDDeckLinkAttributeID.BMDDeckLinkSupportsInputFormatDetection, out int inputFormatDetectionAttribute);
			m_supportsInputFormatDetection = Convert.ToBoolean(inputFormatDetectionAttribute);

			// Get device input interface
			m_deckLinkInput = m_deckLink as IDeckLinkInput;

			// Get device configuration interface.
			// We hold onto interface for object lifecycle in order to temporarily configure input connection 
			m_deckLinkConfiguration = m_deckLink as IDeckLinkConfiguration;

			// Get device display name
			m_displayName = DeckLinkDeviceTools.GetDisplayLabel(m_deckLink);

			// Get profile manager for the device
			m_deckLinkProfileManager = m_deckLink as IDeckLinkProfileManager;
			m_deckLinkProfileManager?.SetCallback(profileCallback);
		}

		~DeckLinkDevice()
		{
			m_deckLinkProfileManager?.SetCallback(null);
		}

		public IDeckLink DeckLink => m_deckLink;
		public IDeckLinkInput DeckLinkInput => m_deckLinkInput;
		public IDeckLinkConfiguration DeckLinkConfiguration => m_deckLinkConfiguration;
		public string DisplayName => m_displayName;
		public _BMDVideoConnection AvailableInputConnections => (_BMDVideoConnection)m_availableInputConnections;
		public bool SupportsFormatDetection => m_supportsInputFormatDetection;
		public bool IsCapturing => m_currentlyCapturing;
		public bool IsActive => DeckLinkDeviceTools.IsDeviceActive(m_deckLink);

		public _BMDVideoConnection CurrentVideoInputConnection
		{
			get
			{
				m_deckLinkConfiguration.GetInt(_BMDDeckLinkConfigurationID.bmdDeckLinkConfigVideoInputConnection, out long currentInputConnection);
				return (_BMDVideoConnection)currentInputConnection;
			}
			set
			{
				m_deckLinkConfiguration.SetInt(_BMDDeckLinkConfigurationID.bmdDeckLinkConfigVideoInputConnection, (long)value);
			}
		}

		public IEnumerable<IDeckLinkDisplayMode> DisplayModes
		{
			get
			{
				// Create a display mode iterator
				m_deckLinkInput.GetDisplayModeIterator(out IDeckLinkDisplayModeIterator displayModeIterator);
				if (displayModeIterator == null)
					yield break;

				// Scan through all display modes
				while (true)
				{
					displayModeIterator.Next(out IDeckLinkDisplayMode displayMode);

					if (displayMode != null)
					{
						// Check display mode is supported for selected connection
						m_deckLinkInput.DoesSupportVideoMode(CurrentVideoInputConnection, displayMode.GetDisplayMode(), _BMDPixelFormat.bmdFormatUnspecified, 
							_BMDVideoInputConversionMode.bmdNoVideoInputConversion, _BMDSupportedVideoModeFlags.bmdSupportedVideoModeDefault, out _, out int supported);
						if (supported == 0)
							continue;

						yield return displayMode;
					}
					else
						yield break;
				}
			}
		}

		void IDeckLinkInputCallback.VideoInputFormatChanged(_BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode newDisplayMode, _BMDDetectedVideoInputFormatFlags detectedSignalFlags)
		{
			_BMDPixelFormat pixelFormat;
			
			// Restart capture with the new video mode if told to
			if (!m_applyDetectedInputMode)
				return;

			var videoInputFlags = _BMDVideoInputFlags.bmdVideoInputEnableFormatDetection;

			var bmdDisplayMode = newDisplayMode.GetDisplayMode();

			if (detectedSignalFlags.HasFlag(_BMDDetectedVideoInputFormatFlags.bmdDetectedVideoInputRGB444))
			{
				if (detectedSignalFlags.HasFlag(_BMDDetectedVideoInputFormatFlags.bmdDetectedVideoInput8BitDepth))
					pixelFormat = _BMDPixelFormat.bmdFormat8BitARGB;
				else if (detectedSignalFlags.HasFlag(_BMDDetectedVideoInputFormatFlags.bmdDetectedVideoInput10BitDepth))
					pixelFormat = _BMDPixelFormat.bmdFormat10BitRGB;
				else if (detectedSignalFlags.HasFlag(_BMDDetectedVideoInputFormatFlags.bmdDetectedVideoInput12BitDepth))
					pixelFormat = _BMDPixelFormat.bmdFormat12BitRGB;
				else
					// Invalid color depth for RGB
					return;
			}
			else if (detectedSignalFlags.HasFlag(_BMDDetectedVideoInputFormatFlags.bmdDetectedVideoInputYCbCr422))
			{
				if (detectedSignalFlags.HasFlag(_BMDDetectedVideoInputFormatFlags.bmdDetectedVideoInput8BitDepth))
					pixelFormat = _BMDPixelFormat.bmdFormat8BitYUV;
				else if (detectedSignalFlags.HasFlag(_BMDDetectedVideoInputFormatFlags.bmdDetectedVideoInput10BitDepth))
					pixelFormat = _BMDPixelFormat.bmdFormat10BitYUV;
				else
					// Invalid color depth for YUV
					return;
			}
			else
				// Unexpected detected video input format flags
				return;

			var dualStream3D = detectedSignalFlags.HasFlag(_BMDDetectedVideoInputFormatFlags.bmdDetectedVideoInputDualStream3D);
			if (dualStream3D)
				videoInputFlags |= _BMDVideoInputFlags.bmdVideoInputDualStream3D;

			if (notificationEvents.HasFlag(_BMDVideoInputFormatChangedEvents.bmdVideoInputDisplayModeChanged) ||
				notificationEvents.HasFlag(_BMDVideoInputFormatChangedEvents.bmdVideoInputColorspaceChanged))
			{
				// Stop the capture
				m_deckLinkInput.StopStreams();

				// Set the video input mode
				m_deckLinkInput.EnableVideoInput(bmdDisplayMode, pixelFormat, videoInputFlags);

				// Start the capture
				m_deckLinkInput.StartStreams();

				InputFormatChanged?.Invoke(this, new DeckLinkDeviceInputFormatEventArgs(bmdDisplayMode, dualStream3D));
			}
		}

		void IDeckLinkInputCallback.VideoInputFrameArrived(IDeckLinkVideoInputFrame videoFrame, IDeckLinkAudioInputPacket audioPacket)
		{
			if (videoFrame != null)
			{
				VideoFrameArrived?.Invoke(this, new DeckLinkDeviceInputVideoFrameEventArgs(videoFrame));

				// Add GC memory pressure to regularly cleanup incoming frame objects
				GC.AddMemoryPressure(videoFrame.GetRowBytes() * videoFrame.GetHeight());
			}
		}

		public void StartCapture(_BMDDisplayMode displayMode, IDeckLinkScreenPreviewCallback screenPreviewCallback, bool applyDetectedInputMode)
		{
			if (m_currentlyCapturing)
				return;

			var videoInputFlags = _BMDVideoInputFlags.bmdVideoInputFlagDefault;

			m_applyDetectedInputMode = applyDetectedInputMode;

			// Enable input video mode detection if the device supports it
			if (m_supportsInputFormatDetection && m_applyDetectedInputMode)
				videoInputFlags |= _BMDVideoInputFlags.bmdVideoInputEnableFormatDetection;

			try
			{
				// Set the screen preview
				m_deckLinkInput.SetScreenPreviewCallback(screenPreviewCallback);

				// Set capture callback
				m_deckLinkInput.SetCallback(this);

				// Set the video input mode
				m_deckLinkInput.EnableVideoInput(displayMode, _BMDPixelFormat.bmdFormat8BitYUV, videoInputFlags);

				// Start the capture
				m_deckLinkInput.StartStreams();

				m_currentlyCapturing = true;
			}
			catch (Exception)
			{
				throw new DeckLinkStartCaptureException();
			}
		}

		public void StopCapture()
		{
			if (!m_currentlyCapturing)
				return;

			// Stop the capture
			m_deckLinkInput.StopStreams();

			// Disable video input
			m_deckLinkInput.DisableVideoInput();

			// disable callbacks
			m_deckLinkInput.SetScreenPreviewCallback(null);
			m_deckLinkInput.SetCallback(null);

			m_currentlyCapturing = false;
		}
	}

	public static class DeckLinkDeviceTools
	{
		public static string GetDisplayLabel(IDeckLink device)
		{
			device.GetDisplayName(out string displayName);
			return displayName;
		}

		public static bool IsDeviceActive(IDeckLink device)
		{
			var deckLinkAttributes = device as IDeckLinkProfileAttributes;
			deckLinkAttributes.GetInt(_BMDDeckLinkAttributeID.BMDDeckLinkDuplex, out long duplexMode);
			return ((_BMDDuplexMode)duplexMode != _BMDDuplexMode.bmdDuplexInactive);
		}
	}
}
