/* -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 System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using DeckLinkAPI;

namespace SignalGenCSharp
{
	/// <summary>
	/// Interaction logic for MainWindow.xaml
	/// </summary>
	public partial class MainWindow : Window
	{
		enum OutputSignal
		{
			kOutputSignalPip = 0,
			kOutputSignalDrop = 1
		};

		const _BMD3DPreviewFormat kDefault3DPreviewFormat = _BMD3DPreviewFormat.bmd3DPreviewFormatTopBottom;

		private IReadOnlyList<int> kAudioChannels = new List<int> { 2, 8, 16, 64 };

		private readonly Dictionary<string, _BMDLinkConfiguration> kLinkWidthDict = new Dictionary<string, _BMDLinkConfiguration>
		{
			{ "Single-Link",   _BMDLinkConfiguration.bmdLinkConfigurationSingleLink },
			{ "Dual-Link",     _BMDLinkConfiguration.bmdLinkConfigurationDualLink },
			{ "Quad-Link",     _BMDLinkConfiguration.bmdLinkConfigurationQuadLink },
		};

		private readonly Dictionary<string, _BMDPixelFormat> kPixelFormatDict = new Dictionary<string, _BMDPixelFormat>
		{
			{ "8-Bit YUV",   _BMDPixelFormat.bmdFormat8BitYUV },
			{ "10-Bit YUV",  _BMDPixelFormat.bmdFormat10BitYUV },
			{ "8-Bit RGB",   _BMDPixelFormat.bmdFormat8BitARGB },
			{ "10-Bit RGB",  _BMDPixelFormat.bmdFormat10BitRGB },
		};

		private readonly Dictionary<string, _BMD3DPreviewFormat> k3DPreviewFormatDict = new Dictionary<string, _BMD3DPreviewFormat>
		{
			{ "Left-Eye Frame",   _BMD3DPreviewFormat.bmd3DPreviewFormatLeftOnly },
			{ "Right-Eye Frame",  _BMD3DPreviewFormat.bmd3DPreviewFormatRightOnly },
			{ "Side by Side",     _BMD3DPreviewFormat.bmd3DPreviewFormatSideBySide },
			{ "Top-Bottom",       _BMD3DPreviewFormat.bmd3DPreviewFormatTopBottom },
		};

		private readonly Dictionary<string, _BMDTimecodeFormat> kTimecodeFormatList = new Dictionary<string, _BMDTimecodeFormat>
		{
			{ "VITC Field 1",   _BMDTimecodeFormat.bmdTimecodeVITC },
			{ "VITC Field 2",   _BMDTimecodeFormat.bmdTimecodeVITCField2 },
			{ "RP188 VITC1",    _BMDTimecodeFormat.bmdTimecodeRP188VITC1 },
			{ "RP188 VITC2",    _BMDTimecodeFormat.bmdTimecodeRP188VITC2 },
			{ "RP188 LTC",      _BMDTimecodeFormat.bmdTimecodeRP188LTC },
			{ "RP188 HFRTC",    _BMDTimecodeFormat.bmdTimecodeRP188HighFrameRate },
		};

		private readonly Dictionary<string, _BMDColorspace> kColorspaceList = new Dictionary<string, _BMDColorspace>
		{
			{ "Rec.601",    _BMDColorspace.bmdColorspaceRec601 },
			{ "Rec.709",    _BMDColorspace.bmdColorspaceRec709 },
			{ "Rec.2020",   _BMDColorspace.bmdColorspaceRec2020 },
		};

		private readonly Dictionary<string, OutputSignal> kOutputSignalList = new Dictionary<string, OutputSignal>
		{
			{ "Pip",        OutputSignal.kOutputSignalPip },
			{ "Dropout",    OutputSignal.kOutputSignalDrop },
		};

		const double kReferenceSoundPressure = 20.0; // dB
		const double kReferenceAudioToneLevel = -20.0; // dB
		const int kReferenceAudioToneFreq = 1000; // Hz

		public class TimecodeData
		{
			public string Format { get; set; }
			public string Value { get; set; }
			public string UserBits { get; set; }
		}

		private delegate void FillFrameDelegate(IDeckLinkVideoFrame frame);

		private Thread m_deckLinkMainThread;
		private readonly EventWaitHandle m_applicationCloseWaitHandle;
		private CancellationTokenSource m_playbackCancel;
		private CancellationTokenSource m_deviceRemovedCancel;
		private CancellationTokenSource m_profileChangingCancel;
		private CancellationTokenSource m_windowClosingCancel;
		private SemaphoreSlim m_scheduleFrameSemaphore;

		private DeckLinkOutputDevice m_selectedDevice;
		private DeckLinkDeviceDiscovery m_deckLinkDeviceDiscovery;
		private ProfileCallback m_profileCallback;
		private PreviewCallback m_previewCallback;

		private int m_frameWidth;
		private int m_frameHeight;
		private long m_frameDuration;
		private long m_frameTimescale;
		private uint m_framesPerSecond;
		private _BMDFieldDominance m_fieldDominance;
		private IDeckLinkMutableVideoFrame m_videoFrameBlack;
		private IDeckLinkMutableVideoFrame m_videoFrameBars;
		private uint m_totalFramesScheduled;

		private OutputSignal m_outputSignal;
		private IntPtr m_audioBuffer;
		private uint m_audioBufferOffset;
		private uint m_audioBufferSampleLength;
		private uint m_audioChannelCount;
		private long m_audioStreamTime;
		private _BMDAudioSampleRate m_audioSampleRate;
		private _BMDAudioSampleType m_audioSampleDepth;
		private _BMDPixelFormat m_selectedPixelFormat;
		private _BMDColorspace m_selectedColorspace;

		private _BMDTimecodeFormat m_timecodeFormat;
		private bool m_hfrtcSupported;
		private uint m_dropFrames;
		private Timecode m_timecode;

		private static void UpdateUIElement(DispatcherObject element, Action action)
		{
			if (element == null)
				return;

			if (!element.Dispatcher.CheckAccess())
			{
				element.Dispatcher.BeginInvoke(DispatcherPriority.Normal, action);
			}
			else
			{
				action();
			}
		}

		private void ComboBoxSelectFirstEnabledItem(ComboBox comboBox)
		{
			foreach (ComboBoxItem item in comboBox.Items)
			{
				if (item.IsEnabled)
				{
					comboBoxDevice.SelectedItem = item;
					break;
				}
			}
		}
		private void SetChildrenControlsEnabled(DependencyObject parent, bool enable)
		{
			// Recursively find child elements and enable/disable
			for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
			{
				var child = VisualTreeHelper.GetChild(parent, i);

				if (child is UIElement)
					((UIElement)child).IsEnabled = enable;

				SetChildrenControlsEnabled(child, enable);
			}
		}

		public MainWindow()
		{
			InitializeComponent();

			SetChildrenControlsEnabled(groupBoxOutputProperties, false);
			SetChildrenControlsEnabled(groupBoxSignalProperties, false);

			comboBoxSignal.ItemsSource = kOutputSignalList;
			comboBoxSignal.DisplayMemberPath = "Key";
			comboBoxSignal.SelectedValuePath = "Value";
			comboBoxSignal.SelectedIndex = 0;

			m_applicationCloseWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
		}

		#region dl_events
		// All events occur in MTA threading context
		public void AddDevice(object sender, DeckLinkDiscoveryEventArgs e)
		{
			try
			{
				DeckLinkOutputDevice device = new DeckLinkOutputDevice(e.deckLink, m_profileCallback);
				var deviceActive = device.IsActive;

				// Update combo box with new device
				UpdateUIElement(comboBoxDevice, new Action(() => UpdateComboNewDevice(device, deviceActive)));
			}
			catch (DeckLinkOutputInvalidException)
			{
				// Device does not support output interface
			}
		}

		public void RemoveDevice(object sender, DeckLinkDiscoveryEventArgs e)
		{
			// Stop playback thread if the selected playback device was removed
			if (m_selectedDevice?.DeckLink == e.deckLink)
				m_deviceRemovedCancel?.Cancel();

			// Remove device from combo box
			UpdateUIElement(comboBoxDevice, new Action(() => UpdateComboRemoveDevice(e.deckLink)));
		}

		public void RenderD3DImage(object sender, DeckLinkRenderFrameEventArgs e)
		{
			ExtractTimecodeFromFrame(e.previewFrame);

			m_previewCallback.PreviewHelper.SetFrame(e.previewFrame);

			UpdateUIElement(d3dPreview, new Action(() =>
			{
				var actualWidth = gridPreview.RenderSize.Width;
				var actualHeight = gridPreview.RenderSize.Height;

				if (d3dPreview.IsFrontBufferAvailable)
				{
					IntPtr surface = IntPtr.Zero;
					if (actualWidth > 0 && actualHeight > 0)
					{
						new MTAAction(() =>
						{
							m_previewCallback.PreviewHelper.SetSurfaceSize((uint)actualWidth, (uint)actualHeight);
							m_previewCallback.PreviewHelper.GetBackBuffer(out surface);
						});
					}
					
					d3dPreview.Lock();

					// If the D3D device becomes unavailable, SetBackBuffer will be called with back buffer set to null and the device will be reset
					// The subsequent call to Render will clean up and reset the IDeckLinkWPFDX9ScreenPreviewHelper 
					d3dPreview.SetBackBuffer(System.Windows.Interop.D3DResourceType.IDirect3DSurface9, surface);
					new MTAAction(() => m_previewCallback.PreviewHelper.Render());

					if (surface != IntPtr.Zero)
						d3dPreview.AddDirtyRect(new Int32Rect(0, 0, d3dPreview.PixelWidth, d3dPreview.PixelHeight));

					d3dPreview.Unlock();
				}
			}));
		}

		public void ProfileChanging(object sender, DeckLinkProfileEventArgs e)
		{
			e.deckLinkProfile.GetDevice(out IDeckLink deckLink);

			if ((m_selectedDevice != null) && (deckLink == m_selectedDevice.DeckLink))
			{
				// Profile is change for selected device. Stop existing capture gracefully.
				// This must be performed in callback thread context
				m_selectedDevice.StopPlayback();

				if (m_profileChangingCancel != null)
					m_profileChangingCancel.Cancel();
			}
		}

		public void ProfileActivated(object sender, DeckLinkProfileEventArgs e)
		{
			e.deckLinkProfile.GetDevice(out IDeckLink deckLink);
			bool deviceActive = DeckLinkDeviceTools.IsDeviceActive(deckLink);

			// Update device state in combo box
			UpdateUIElement(comboBoxDevice, new Action(() => UpdateComboActiveDevice(deckLink, deviceActive)));
		}
		#endregion

		private void ExtractTimecodeFromFrame(IDeckLinkVideoFrame inputFrame)
		{
			List<TimecodeData> timecodeDataList = new List<TimecodeData>();

			foreach (var timecodeFormat in kTimecodeFormatList)
			{
				inputFrame.GetTimecode(timecodeFormat.Value, out IDeckLinkTimecode timecode);
				if (timecode != null)
				{
					timecode.GetString(out string timecodeString);
					timecode.GetTimecodeUserBits(out uint userBits);
					timecodeDataList.Add(new TimecodeData()
					{
						Format = timecodeFormat.Key,
						Value = timecodeString,
						UserBits = userBits.ToString()
					});
				}
			}
			UpdateUIElement(dataGridTimecode, new Action(() => dataGridTimecode.ItemsSource = timecodeDataList));
		}

		private void DeckLinkMainThread()
		{
			m_previewCallback = new PreviewCallback();
			m_previewCallback.RenderFrame += RenderD3DImage;
			m_previewCallback.PreviewHelper.Set3DPreviewFormat(kDefault3DPreviewFormat);

			m_profileCallback = new ProfileCallback();
			m_profileCallback.ProfileChanging += ProfileChanging;
			m_profileCallback.ProfileActivated += ProfileActivated;

			m_deckLinkDeviceDiscovery = new DeckLinkDeviceDiscovery();
			m_deckLinkDeviceDiscovery.DeviceArrived += AddDevice;
			m_deckLinkDeviceDiscovery.DeviceRemoved += RemoveDevice;
			m_deckLinkDeviceDiscovery.Enable();

			m_applicationCloseWaitHandle.WaitOne();

			m_selectedDevice?.StopPlayback();
			m_selectedDevice = null;

			m_previewCallback.RenderFrame -= RenderD3DImage;

			m_profileCallback.ProfileChanging -= ProfileChanging;
			m_profileCallback.ProfileActivated -= ProfileActivated;

			m_deckLinkDeviceDiscovery.Disable();
			m_deckLinkDeviceDiscovery.DeviceArrived -= AddDevice;
			m_deckLinkDeviceDiscovery.DeviceRemoved -= RemoveDevice;
		}

		private void UpdateComboNewDevice(DeckLinkOutputDevice device, bool deviceActive)
		{
			ComboBoxItem newItem = new ComboBoxItem
			{
				Content = new StringObjectPair<DeckLinkOutputDevice>(device.DisplayName, device),
				IsEnabled = deviceActive
			};
			comboBoxDevice.Items.Add(newItem);

			// If first device, then enable capture interface
			if (comboBoxDevice.Items.Count == 1)
			{
				comboBoxDevice.SelectedIndex = 0;
				SetChildrenControlsEnabled(groupBoxOutputProperties, true);
				SetChildrenControlsEnabled(groupBoxSignalProperties, true);
			}
		}

		private void UpdateComboRemoveDevice(IDeckLink deckLink)
		{
			bool selectedDeviceRemoved = false;

			if (m_selectedDevice?.DeckLink == deckLink)
				selectedDeviceRemoved = true;

			// Remove the device from the device dropdown
			foreach (ComboBoxItem item in comboBoxDevice.Items)
			{
				if (((StringObjectPair<DeckLinkOutputDevice>)item.Content).Value.DeckLink == deckLink)
				{
					comboBoxDevice.Items.Remove(item);
					break;
				}
			}

			if (comboBoxDevice.Items.Count == 0)
			{
				m_selectedDevice = null;
				SetChildrenControlsEnabled(groupBoxOutputProperties, false);
				SetChildrenControlsEnabled(groupBoxSignalProperties, false);
			}
			else if (selectedDeviceRemoved)
			{
				ComboBoxSelectFirstEnabledItem(comboBoxDevice);
			}
		}

		private void UpdateComboActiveDevice(IDeckLink deckLink, bool active)
		{
			foreach (ComboBoxItem item in comboBoxDevice.Items)
			{
				if (((StringObjectPair<DeckLinkOutputDevice>)item.Content).Value.DeckLink == deckLink)
				{
					item.IsEnabled = active;

					if (m_selectedDevice.DeckLink == deckLink)
					{
						if (active)
						{
							// Trigger event to restart capture
							comboBoxDevice_SelectionChanged(comboBoxDevice, null);
						}
						else
						{
							ComboBoxSelectFirstEnabledItem(comboBoxDevice);
						}
					}
					break;
				}
			}
		}

		private void UpdateComboLinkConfigurations()
		{
			if (m_selectedDevice == null)
				return;

			// Disable selection event handler, as the selection takes the current link setting for the selected device
			comboBoxLinkWidth.SelectionChanged -= comboBoxLinkWidth_SelectionChanged;

			// Bind available input connections of selected device to connections combo-box
			comboBoxLinkWidth.ItemsSource = kLinkWidthDict.Where((width, ret) =>
			{
				var isLinkWidthSupportedFunc = new MTAFunc<bool>(() => m_selectedDevice.IsLinkConfigurationSupported(width.Value));
				return isLinkWidthSupportedFunc.Value;
			});
			comboBoxLinkWidth.DisplayMemberPath = "Key";
			comboBoxLinkWidth.SelectedValuePath = "Value";

			var currentLinkConfiguration = new MTAFunc<_BMDLinkConfiguration>(() => m_selectedDevice.CurrentLinkConfiguration);
			comboBoxLinkWidth.SelectedValue = currentLinkConfiguration.Value;

			// Restore selection event handler
			comboBoxLinkWidth.SelectionChanged += comboBoxLinkWidth_SelectionChanged;

			comboBoxLinkWidth.Visibility = comboBoxLinkWidth.Items.Count > 1 ? Visibility.Visible : Visibility.Collapsed;
			labelLinkWidth.Visibility = comboBoxLinkWidth.Items.Count > 1 ? Visibility.Visible : Visibility.Collapsed;
		}

		private void UpdateComboVideoModes()
		{
			if (m_selectedDevice == null)
				return;

			List<DisplayModeEntry> displayModeEntries = new List<DisplayModeEntry>();

			new MTAAction(() =>
			{
				foreach (IDeckLinkDisplayMode displayMode in m_selectedDevice.DisplayModes)
					displayModeEntries.Add(new DisplayModeEntry(displayMode));
			});

			// Bind display mode list to combo
			comboBoxVideoFormat.ItemsSource = displayModeEntries;
			comboBoxVideoFormat.DisplayMemberPath = "DisplayString";
			comboBoxVideoFormat.SelectedValuePath = "Value";

			if (comboBoxVideoFormat.Items.Count > 0)
				comboBoxVideoFormat.SelectedIndex = 0;
		}

		private void UpdateComboPixelFormats()
		{
			comboBoxPixelFormat.ItemsSource = kPixelFormatDict.Where((pf, ret) =>
			{
				_BMDDisplayMode displayMode = ((DisplayModeEntry)comboBoxVideoFormat.SelectedItem).Value;
				var isPixelFormatSupportedFunc = new MTAFunc<bool>(() => m_selectedDevice.IsDisplayModeSupported(displayMode, pf.Value));
				return isPixelFormatSupportedFunc.Value;
			});
			comboBoxPixelFormat.DisplayMemberPath = "Key";
			comboBoxPixelFormat.SelectedValuePath = "Value";

			comboBoxPixelFormat.SelectedIndex = 0;
		}

		private void UpdateComboColorspace()
		{
			comboBoxColorspace.ItemsSource = kColorspaceList.Where((cs, ret) =>
			{
				_BMDDisplayMode displayMode = ((DisplayModeEntry)comboBoxVideoFormat.SelectedItem).Value;
				var isColorspaceSupportedFunc = new MTAFunc<bool>(() => m_selectedDevice.IsColorspaceSupported(displayMode, cs.Value));
				return isColorspaceSupportedFunc.Value;
			});
			comboBoxColorspace.DisplayMemberPath = "Key";
			comboBoxColorspace.SelectedValuePath = "Value";

			comboBoxColorspace.SelectedIndex = 0;

			labelColorspace.Visibility = m_selectedDevice.SupportsColorspaceMetadata ? Visibility.Visible : Visibility.Collapsed;
			comboBoxColorspace.Visibility = m_selectedDevice.SupportsColorspaceMetadata ? Visibility.Visible : Visibility.Collapsed;
		}

		private void Update3DCheckboxVisibility()
		{
			_BMDDisplayMode displayMode = ((DisplayModeEntry)comboBoxVideoFormat.SelectedItem).Value;

			var is3DSupportedFunc = new MTAFunc<bool>(() => m_selectedDevice.DoesDisplayModeSupport3D(displayMode));
			bool output3DSupported = is3DSupportedFunc.Value;

			labelOutput3D.Visibility = output3DSupported ? Visibility.Visible : Visibility.Collapsed;
			checkBoxOutput3D.Visibility = output3DSupported ? Visibility.Visible : Visibility.Collapsed;
			label3DPreview.Visibility = output3DSupported ? Visibility.Visible : Visibility.Collapsed;
			comboBox3DPreviewFormat.Visibility = output3DSupported ? Visibility.Visible : Visibility.Collapsed;

			checkBoxOutput3D.IsChecked = false;
			comboBox3DPreviewFormat.IsEnabled = false;
		}

		private void UpdateComboAudioChannels()
		{
			if (m_selectedDevice == null)
				return;

			var maximumAudioChannelFunc = new MTAFunc<uint>(() => m_selectedDevice.MaxAudioChannels);
			var maxAudioChannels = maximumAudioChannelFunc.Value;

			comboBoxAudioChannels.Items.Clear();
			foreach (int channels in kAudioChannels.Where(ch => (ch <= maxAudioChannels)))
				comboBoxAudioChannels.Items.Add(channels);

			comboBoxAudioChannels.SelectedIndex = comboBoxAudioChannels.Items.Count - 1;
		}

		private async void TogglePlayback()
		{
			// If currently playing, cancel it.
			if (m_playbackCancel != null)
			{
				m_playbackCancel.Cancel();
			}
			else 
			{
				if (m_selectedDevice == null)
					return;

				try
				{
					// Update UI
					buttonStartStop.Content = "Stop Playback";
					SetChildrenControlsEnabled(groupBoxOutputProperties, false);
					SetChildrenControlsEnabled(groupBoxSignalProperties, false);

					// Register callbacks for selected device
					m_selectedDevice.ScheduledFrameCompleted += ScheduledFrameCompleted;
					m_selectedDevice.RenderAudioSamples += WriteNextAudioSamples;

					// Enable output and generate output signals
					StartRunning();

					// Generate semaphore for scheduling frame, initialize to preroll size
					var videoWaterLevelFunc = new MTAFunc<uint>(() => m_selectedDevice.VideoWaterLevel);
					int videoWaterLevel = (int)videoWaterLevelFunc.Value;
					m_scheduleFrameSemaphore = new SemaphoreSlim(videoWaterLevel, videoWaterLevel + 1);

					// Generate playback cancellation tokens
					m_playbackCancel = new CancellationTokenSource();
					m_deviceRemovedCancel = new CancellationTokenSource();
					m_profileChangingCancel = new CancellationTokenSource();
					m_windowClosingCancel = new CancellationTokenSource();

					// Run playback as a task.
					await TaskPlayback(m_playbackCancel.Token, m_deviceRemovedCancel.Token, m_profileChangingCancel.Token, m_windowClosingCancel.Token);
				}
				catch (OperationCanceledException)
				{
				}
				finally
				{
					// Dispose of synchronization items 
					m_playbackCancel.Dispose();
					m_playbackCancel = null;

					m_deviceRemovedCancel.Dispose();
					m_deviceRemovedCancel = null;

					m_profileChangingCancel.Dispose();
					m_profileChangingCancel = null;

					m_windowClosingCancel.Dispose();
					m_windowClosingCancel = null;

					m_scheduleFrameSemaphore.Dispose();
					m_scheduleFrameSemaphore = null;

					if (m_selectedDevice != null)
					{
						// Unregister callbacks
						m_selectedDevice.ScheduledFrameCompleted -= ScheduledFrameCompleted;
						m_selectedDevice.RenderAudioSamples -= WriteNextAudioSamples;

						// Playback stopped
						new MTAAction(() => m_selectedDevice.StopPlayback());
					}

					// Free audio buffer
					Marshal.FreeCoTaskMem(m_audioBuffer);

					// Release video frames
					m_videoFrameBars = null;
					m_videoFrameBlack = null;

					buttonStartStop.Content = "Start Playback";
					if (comboBoxDevice.Items.Count > 0)
					{
						SetChildrenControlsEnabled(groupBoxOutputProperties, true);
						SetChildrenControlsEnabled(groupBoxSignalProperties, true);
					}
				}
			}
		}

		private async Task TaskPlayback(CancellationToken cancelToken, CancellationToken deviceRemovedToken, CancellationToken profileChangingToken, CancellationToken windowClosingToken)
		{
			using (CancellationTokenSource linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, deviceRemovedToken, profileChangingToken, windowClosingToken))
			{
				while (true)
				{
					await m_scheduleFrameSemaphore.WaitAsync(linkedCancellation.Token);
					if (linkedCancellation.IsCancellationRequested)
						break;
					var mtaThread = new MTAAction(() => ScheduleNextFrame());
				}
			}
		}

		private void StartRunning()
		{
			m_outputSignal = (OutputSignal)comboBoxSignal.SelectedValue;
			m_audioChannelCount = (uint)((int)comboBoxAudioChannels.SelectedItem);
			m_audioSampleDepth = radioButtonAudioDepth32Bit.IsChecked.GetValueOrDefault(false) ? _BMDAudioSampleType.bmdAudioSampleType32bitInteger : _BMDAudioSampleType.bmdAudioSampleType16bitInteger;
			m_audioSampleRate = _BMDAudioSampleRate.bmdAudioSampleRate48kHz;
			//
			//- Extract the IDeckLinkDisplayMode from the display mode popup menu
			_BMDDisplayMode selectedDisplayMode = (_BMDDisplayMode)comboBoxVideoFormat.SelectedValue;
			bool output3DSelected = checkBoxOutput3D.IsChecked.GetValueOrDefault(false);
			m_selectedPixelFormat = (_BMDPixelFormat)comboBoxPixelFormat.SelectedValue;
			m_selectedColorspace = (_BMDColorspace)comboBoxColorspace.SelectedValue;

			new MTAAction(() =>
			{
				_BMDVideoOutputFlags videoOutputFlags = _BMDVideoOutputFlags.bmdVideoOutputFlagDefault;

				if (output3DSelected)
					videoOutputFlags |= _BMDVideoOutputFlags.bmdVideoOutputDualStream3D;

				m_selectedDevice.DeckLinkOutput.GetDisplayMode(selectedDisplayMode, out IDeckLinkDisplayMode deckLinkDisplayMode);
				m_frameWidth = deckLinkDisplayMode.GetWidth();
				m_frameHeight = deckLinkDisplayMode.GetHeight();
				m_fieldDominance = deckLinkDisplayMode.GetFieldDominance();
				deckLinkDisplayMode.GetFrameRate(out m_frameDuration, out m_frameTimescale);

				// Calculate the number of frames per second, rounded up to the nearest integer.  For example, for NTSC (29.97 FPS), framesPerSecond == 30.
				m_framesPerSecond = (uint)((m_frameTimescale + (m_frameDuration - 1)) / m_frameDuration);

				// m-rate frame rates with multiple 30-frame counting should implement Drop Frames compensation, refer to SMPTE 12-1
				if (m_frameDuration == 1001 && m_frameTimescale % 30000 == 0)
					m_dropFrames = (uint)(2 * (m_frameTimescale / 30000));
				else
					m_dropFrames = 0;

				m_hfrtcSupported = m_selectedDevice.SupportsHFRTimecode;

				if ((selectedDisplayMode == _BMDDisplayMode.bmdModeNTSC) ||
					(selectedDisplayMode == _BMDDisplayMode.bmdModeNTSC2398) ||
					(selectedDisplayMode == _BMDDisplayMode.bmdModePAL))
				{
					m_timecodeFormat = _BMDTimecodeFormat.bmdTimecodeVITC;
					videoOutputFlags |= _BMDVideoOutputFlags.bmdVideoOutputVITC;
				}
				else
				{
					m_timecodeFormat = _BMDTimecodeFormat.bmdTimecodeRP188Any;
					videoOutputFlags |= _BMDVideoOutputFlags.bmdVideoOutputRP188;
				}

				m_timecode = new Timecode(0, m_framesPerSecond, m_dropFrames);

				// Generate one second of audio
				m_audioBufferSampleLength = (uint)((m_framesPerSecond * (uint)m_audioSampleRate * m_frameDuration) / m_frameTimescale);
				m_audioBuffer = Marshal.AllocCoTaskMem((int)(m_audioBufferSampleLength * m_audioChannelCount * ((uint)m_audioSampleDepth / 8)));
				m_audioBufferOffset = 0;
				m_audioStreamTime = 0;

				// Zero the buffer (interpreted as audio silence)
				for (int i = 0; i < (m_audioBufferSampleLength * m_audioChannelCount * (uint)m_audioSampleDepth / 8); i += 4)
					Marshal.WriteInt32(m_audioBuffer, i, 0);
				uint audioSamplesPerFrame = (uint)(((uint)m_audioSampleRate * m_frameDuration) / m_frameTimescale);

				if (m_outputSignal == OutputSignal.kOutputSignalPip)
					FillSine(m_audioBuffer, audioSamplesPerFrame, m_audioChannelCount, m_audioSampleDepth);
				else
					FillSine(new IntPtr(m_audioBuffer.ToInt64() + (audioSamplesPerFrame * m_audioChannelCount * (uint)m_audioSampleDepth / 8)), 
						(m_audioBufferSampleLength - audioSamplesPerFrame), m_audioChannelCount, m_audioSampleDepth);

				// Generate a frame of black
				m_videoFrameBlack = CreateOutputVideoFrame(ColorBars.FillBlack);
				if (output3DSelected)
				{
					var videoFrame3DExtensionProvider = new VideoFrame3DExtension.Provider(owner: m_videoFrameBlack, right: CreateOutputVideoFrame(ColorBars.FillBlack));
					m_videoFrameBlack.SetInterfaceProvider(typeof(IDeckLinkVideoFrame3DExtensions).GUID, videoFrame3DExtensionProvider);
				}

				// Generate a frame of colour bars
				m_videoFrameBars = CreateOutputVideoFrame((frame) => ColorBars.FillColorBars(frame, colorspace: m_selectedColorspace, reverse: false));
				if (output3DSelected)
				{
					var videoFrame3DExtensionProvider = new VideoFrame3DExtension.Provider(owner: m_videoFrameBars, right: CreateOutputVideoFrame((frame) => ColorBars.FillColorBars(frame, colorspace: m_selectedColorspace, reverse: true)));
					m_videoFrameBars.SetInterfaceProvider(typeof(IDeckLinkVideoFrame3DExtensions).GUID, videoFrame3DExtensionProvider);
				}

				try
				{
					m_selectedDevice.StartPlayback(selectedDisplayMode, m_selectedPixelFormat, videoOutputFlags, m_audioSampleDepth, m_audioChannelCount, m_previewCallback);
				}
				catch (DeckLinkStartPlaybackException)
				{
					MessageBox.Show("Unable to start playback, perhaps the output device is already in use",
									"Start playback error", MessageBoxButton.OK, MessageBoxImage.Error);
				}
			});

			m_totalFramesScheduled = 0;
		}

		private IDeckLinkMutableVideoFrame CreateOutputVideoFrame(FillFrameDelegate fillFrame)
		{
			m_selectedDevice.DeckLinkOutput.RowBytesForPixelFormat(m_selectedPixelFormat, m_frameWidth, out int scheduleFrameRowBytes);

			m_selectedDevice.DeckLinkOutput.CreateVideoFrame(m_frameWidth, m_frameHeight, scheduleFrameRowBytes,
				m_selectedPixelFormat, _BMDFrameFlags.bmdFrameFlagDefault, out IDeckLinkMutableVideoFrame scheduleFrame);
			var scheduleFrameMeta = scheduleFrame as IDeckLinkVideoFrameMutableMetadataExtensions;
			scheduleFrameMeta.SetInt(_BMDDeckLinkFrameMetadataID.bmdDeckLinkFrameMetadataColorspace, (long)m_selectedColorspace);

			if (m_selectedPixelFormat == _BMDPixelFormat.bmdFormat10BitYUV)
			{
				// Fill 8-bit YUV directly without conversion
				fillFrame(scheduleFrame);
			}
			else
			{
				const _BMDPixelFormat kReferenceFramePixelFormat = _BMDPixelFormat.bmdFormat10BitYUV;

				// Pixel formats are different, first generate 10-bit YUV bars frame and convert to required format
				IDeckLinkVideoConversion frameConverter = new CDeckLinkVideoConversion();

				m_selectedDevice.DeckLinkOutput.RowBytesForPixelFormat(kReferenceFramePixelFormat, m_frameWidth, out int referenceFrameRowBytes);

				m_selectedDevice.DeckLinkOutput.CreateVideoFrame(m_frameWidth, m_frameHeight, referenceFrameRowBytes,
					kReferenceFramePixelFormat, _BMDFrameFlags.bmdFrameFlagDefault, out IDeckLinkMutableVideoFrame referenceFrame);
				var refFrameMeta = scheduleFrame as IDeckLinkVideoFrameMutableMetadataExtensions;
				refFrameMeta.SetInt(_BMDDeckLinkFrameMetadataID.bmdDeckLinkFrameMetadataColorspace, (long)m_selectedColorspace);
				fillFrame(referenceFrame);
                frameConverter.ConvertFrame(referenceFrame, scheduleFrame);
			}

			return scheduleFrame;
		}

		private void ScheduleNextFrame()
		{
			if (!m_selectedDevice.IsRunning)
				return;

			IDeckLinkMutableVideoFrame currentFrame;
			bool pipOrDropoutFrame = (m_totalFramesScheduled % m_framesPerSecond) == 0;

			if (m_outputSignal == OutputSignal.kOutputSignalPip)
			{
				// On each second, schedule a frame of bars, otherwise schedule frames of black
				currentFrame = (pipOrDropoutFrame ? m_videoFrameBars : m_videoFrameBlack) as IDeckLinkMutableVideoFrame;
			}
			else
			{
				// On each second, schedule a frame of black, otherwise schedule frames of bars
				currentFrame = (pipOrDropoutFrame ? m_videoFrameBlack : m_videoFrameBars) as IDeckLinkMutableVideoFrame;
			}

			if (m_timecodeFormat == _BMDTimecodeFormat.bmdTimecodeVITC)
			{
				currentFrame.SetTimecodeFromComponents(_BMDTimecodeFormat.bmdTimecodeVITC, m_timecode.Hours, m_timecode.Minutes,
														m_timecode.Seconds, m_timecode.Frames, _BMDTimecodeFlags.bmdTimecodeFlagDefault);
			}
			else
			{
				bool setVITC1Timecode = false;
				bool setVITC2Timecode = false;
				byte frames = m_timecode.Frames;

				if (m_hfrtcSupported)
				{
					currentFrame.SetTimecodeFromComponents(_BMDTimecodeFormat.bmdTimecodeRP188HighFrameRate, m_timecode.Hours, m_timecode.Minutes,
															m_timecode.Seconds, frames, _BMDTimecodeFlags.bmdTimecodeFlagDefault);
				}

				if (m_fieldDominance != _BMDFieldDominance.bmdProgressiveFrame)
				{
					// An interlaced or PsF frame has both VITC1 and VITC2 set with the same timecode value (SMPTE ST 12-2:2014 7.2)
					setVITC1Timecode = true;
					setVITC2Timecode = true;
				}
				else if (m_framesPerSecond <= 30)
				{
					// If this isn't a High-P mode, then just use VITC1 (SMPTE ST 12-2:2014 7.2)
					setVITC1Timecode = true;
				}
				else if (m_framesPerSecond <= 60)
				{
					// If this is a High-P mode then use VITC1 on even frames and VITC2 on odd frames. This is done because the
					// frames field of the RP188 VITC timecode cannot hold values greater than 30 (SMPTE ST 12-2:2014 7.2, 9.2)
					if ((frames & 1) == 0)
						setVITC1Timecode = true;
					else
						setVITC2Timecode = true;

					frames >>= 1;
				}

				if (setVITC1Timecode)
				{
					currentFrame.SetTimecodeFromComponents(_BMDTimecodeFormat.bmdTimecodeRP188VITC1, m_timecode.Hours, m_timecode.Minutes,
															m_timecode.Seconds, frames, _BMDTimecodeFlags.bmdTimecodeFlagDefault);
				}

				if (setVITC2Timecode)
				{
						// The VITC2 timecode also has the field mark flag set
					currentFrame.SetTimecodeFromComponents(_BMDTimecodeFormat.bmdTimecodeRP188VITC2, m_timecode.Hours, m_timecode.Minutes,
															m_timecode.Seconds, frames, _BMDTimecodeFlags.bmdTimecodeFieldMark);
				}
			}

			m_selectedDevice.DeckLinkOutput.ScheduleVideoFrame(currentFrame, (m_totalFramesScheduled * m_frameDuration), m_frameDuration, m_frameTimescale);

			m_timecode++;
			m_totalFramesScheduled++;
		}

		void ScheduledFrameCompleted(object sender, DeckLinkScheduledFrameCompletedEventArgs e)
		{
			m_scheduleFrameSemaphore?.Release();
		}

		void WriteNextAudioSamples(object sender, DeckLinkRenderAudioSamplesEventArgs e)
		{
			// Try to maintain the number of audio samples buffered in the API at a specified waterlevel
			if (e.bufferedAudioSamples < m_selectedDevice.AudioWaterLevel)
			{
				uint samplesToEndOfBuffer = (m_audioBufferSampleLength - m_audioBufferOffset);
				uint samplesToWrite = (m_selectedDevice.AudioWaterLevel - e.bufferedAudioSamples);
				if (samplesToWrite > samplesToEndOfBuffer)
					samplesToWrite = samplesToEndOfBuffer;

				m_selectedDevice.DeckLinkOutput.ScheduleAudioSamples(new IntPtr(m_audioBuffer.ToInt64() + (m_audioBufferOffset * m_audioChannelCount * (uint)m_audioSampleDepth / 8)),
					samplesToWrite, m_audioStreamTime, (long)m_audioSampleRate, out uint samplesWritten);
				m_audioBufferOffset = ((m_audioBufferOffset + samplesWritten) % m_audioBufferSampleLength);
				m_audioStreamTime += samplesWritten;
			}
		}

		#region uievents
		// All UI events are in STA apartment thread context, calls to DeckLinkAPI must be performed in MTA thread context
		private void Window_Loaded(object sender, RoutedEventArgs e)
		{
			// Bind 3D preview formats to combo box
			comboBox3DPreviewFormat.ItemsSource = k3DPreviewFormatDict;
			comboBox3DPreviewFormat.DisplayMemberPath = "Key";
			comboBox3DPreviewFormat.SelectedValuePath = "Value";
			comboBox3DPreviewFormat.SelectedValue = kDefault3DPreviewFormat;

			m_deckLinkMainThread = new Thread(() => DeckLinkMainThread());
			m_deckLinkMainThread.SetApartmentState(ApartmentState.MTA);
			m_deckLinkMainThread.Start();
		}

		private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
		{
			if (m_windowClosingCancel != null)
				m_windowClosingCancel.Cancel();

			m_applicationCloseWaitHandle.Set();
			m_deckLinkMainThread.Join();
		}

		private void comboBoxDevice_SelectionChanged(object sender, SelectionChangedEventArgs e)
		{
			m_selectedDevice = null;

			// Reset connection and video format combo box source
			comboBoxLinkWidth.ItemsSource = null;

			if (comboBoxDevice.SelectedIndex < 0)
				return;

			m_selectedDevice = ((StringObjectPair<DeckLinkOutputDevice>)((ComboBoxItem)comboBoxDevice.SelectedItem).Content).Value;

			UpdateComboLinkConfigurations();

			// Update the supported audio and video modes for the link width configuration
			UpdateComboAudioChannels();
			UpdateComboVideoModes();
		}

		private void comboBoxLinkWidth_SelectionChanged(object sender, RoutedEventArgs e)
		{
			if (comboBoxLinkWidth.SelectedIndex < 0)
				return;

			comboBoxVideoFormat.ItemsSource = null;

			// Link width combo box selection changed, set link configuration value
			var linkWidthConfiguration = (_BMDLinkConfiguration)comboBoxLinkWidth.SelectedValue;
			new MTAAction(() => m_selectedDevice.CurrentLinkConfiguration = linkWidthConfiguration);

			UpdateComboAudioChannels();
			UpdateComboVideoModes();
		}

		private void comboBoxVideoFormat_SelectionChanged(object sender, RoutedEventArgs e)
		{
			if (comboBoxVideoFormat.SelectedIndex < 0)
				return;

			comboBoxPixelFormat.ItemsSource = null;

			UpdateComboPixelFormats();
			UpdateComboColorspace();
			Update3DCheckboxVisibility();
		}

		private void checkBoxOutput3D_CheckedChanged(object sender, RoutedEventArgs e)
		{
			comboBox3DPreviewFormat.IsEnabled = checkBoxOutput3D.IsChecked.GetValueOrDefault(false);
		}

		private void comboBox3DPreviewFormat_SelectionChanged(object sender, RoutedEventArgs e)
		{
			if (m_previewCallback != null)
			{
				var previewFormat = (_BMD3DPreviewFormat)comboBox3DPreviewFormat.SelectedValue;
				new MTAAction(() => m_previewCallback.PreviewHelper.Set3DPreviewFormat(previewFormat));
			}
		}

		private void buttonStartStop_Click(object sender, RoutedEventArgs e)
		{
			TogglePlayback();
		}
		#endregion

		#region buffer filling
		void FillSine(IntPtr audioBuffer, uint samplesToWrite, uint channels, _BMDAudioSampleType sampleDepth)
		{
			int audioMaxLevel = (1 << ((int)sampleDepth - 1)) - 1;
			double toneMaxLevel = (double)audioMaxLevel * Math.Pow(10.0, kReferenceAudioToneLevel / kReferenceSoundPressure);

			if (sampleDepth == _BMDAudioSampleType.bmdAudioSampleType16bitInteger)
			{
				Int16[] buffer = new Int16[channels * samplesToWrite];

				for (uint i = 0; i < samplesToWrite; i++)
				{
					Int16 sample = (Int16)(toneMaxLevel * Math.Sin((i * 2.0 * Math.PI * kReferenceAudioToneFreq) / (int)_BMDAudioSampleRate.bmdAudioSampleRate48kHz));
					for (uint ch = 0; ch < channels; ch++)
					{
						buffer[i * channels + ch] = sample;
					}
				}
				// Copy it into unmanaged buffer
				Marshal.Copy(buffer, 0, audioBuffer, (int)(channels * samplesToWrite));
			}
			else if (sampleDepth == _BMDAudioSampleType.bmdAudioSampleType32bitInteger)
			{
				Int32[] buffer = new Int32[channels * samplesToWrite];

				for (uint i = 0; i < samplesToWrite; i++)
				{
					Int32 sample = (Int32)(toneMaxLevel * Math.Sin((i * 2.0 * Math.PI * kReferenceAudioToneFreq) / (int)_BMDAudioSampleRate.bmdAudioSampleRate48kHz));
					for (uint ch = 0; ch < channels; ch++)
					{
						buffer[i * channels + ch] = sample;
					}
				}
				// Copy it into unmanaged buffer
				Marshal.Copy(buffer, 0, audioBuffer, (int)(channels * samplesToWrite));
			}
		}
		#endregion

		#region combotypes
		struct DisplayModeEntry
		/// Used for putting the BMDDisplayMode value into the video format combo
		{
			private readonly string m_displayString;

			public DisplayModeEntry(IDeckLinkDisplayMode displayMode)
			{
				Value = displayMode.GetDisplayMode();
				displayMode.GetName(out m_displayString);
			}

			public _BMDDisplayMode Value { get; set; }
			public string DisplayString { get => m_displayString; }
		}

		/// Used for putting other object types into combo boxes.
		struct StringObjectPair<T>
		{
			public StringObjectPair(string name, T value)
			{
				Name = name;
				Value = value;
			}
			public string Name { get; }
			public T Value { get; set; }
			public override string ToString() => Name;
		}
		#endregion
	}
}
