/* -LICENSE-START-
** Copyright (c) 2022 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 <QStandardItemModel>
#include <QStandardItem>
#include <QToolBox>
#include <QMessageBox>

#include "QuadPreview.h"
#include "DeckLinkInputPage.h"
#include "platform.h"

namespace
{
	const int kComboMinimumWidth = 185;

	const std::vector<std::pair<BMDVideoConnection, QString>> kVideoInputConnections =
	{
		std::make_pair(bmdVideoConnectionSDI,			QString("SDI")),
		std::make_pair(bmdVideoConnectionHDMI,			QString("HDMI")),
		std::make_pair(bmdVideoConnectionOpticalSDI,	QString("Optical SDI")),
		std::make_pair(bmdVideoConnectionComponent,		QString("Component")),
		std::make_pair(bmdVideoConnectionComposite,		QString("Composite")),
		std::make_pair(bmdVideoConnectionSVideo,		QString("S-Video")),
		std::make_pair(bmdVideoConnectionEthernet,		QString("Ethernet")),
		std::make_pair(bmdVideoConnectionOpticalEthernet,		QString("Optical Ethernet")),
	};

	template<class T>
	T* findParent(QWidget* widget)
	{
		T* result = nullptr;
		do
		{
			widget = widget->parentWidget();
			result = qobject_cast<T*>(widget);
		} while (widget && !result);
		return result;
	}
}

DeckLinkInputPage::DeckLinkInputPage() :
	m_selectedDevice(nullptr)
{
	m_formLayout = new QFormLayout(this);

	m_deviceListCombo = new QComboBox();
	m_deviceListCombo->setMinimumWidth(kComboMinimumWidth);
	m_formLayout->addRow("Input Device:", m_deviceListCombo);
	m_deviceListCombo->addItem("None");

	m_inputConnectionCombo = new QComboBox();
	m_inputConnectionCombo->setMinimumWidth(kComboMinimumWidth);
	m_inputConnectionCombo->setEnabled(false);
	m_formLayout->addRow("Input Connection:", m_inputConnectionCombo);

	m_videoFormatCombo = new QComboBox();
	m_videoFormatCombo->setMinimumWidth(kComboMinimumWidth);
	m_videoFormatCombo->setEnabled(false);
	m_formLayout->addRow("Video Format:", m_videoFormatCombo);

	m_autoDetectCheckBox = new QCheckBox();
	m_autoDetectCheckBox->setEnabled(false);
	m_formLayout->addRow("Auto-Detect Format:", m_autoDetectCheckBox);

	m_previewView = new DeckLinkOpenGLWidget(dynamic_cast<QWidget*>(this));
	m_previewView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

	connect(m_deviceListCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DeckLinkInputPage::inputDeviceChanged);
	connect(m_inputConnectionCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DeckLinkInputPage::inputConnectionChanged);
	connect(m_videoFormatCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DeckLinkInputPage::videoFormatChanged);
	connect(m_autoDetectCheckBox, &QCheckBox::stateChanged, this, &DeckLinkInputPage::autoDetectChanged);
}

DeckLinkInputPage::~DeckLinkInputPage()
{
	delete m_formLayout;
}

void DeckLinkInputPage::setPreviewSize(QSize previewSize)
{
	m_previewView->resize(previewSize);
	m_previewView->clear();
}

void DeckLinkInputPage::customEvent(QEvent *event)
{
	switch (event->type())
	{
		case kVideoFormatChangedEvent:
		{
			DeckLinkInputFormatChangedEvent* formatEvent = dynamic_cast<DeckLinkInputFormatChangedEvent*>(event);
			detectedVideoFormatChanged(formatEvent->displayMode());
		}
		break;

		case kErrorRestartingCaptureEvent:
		{
			event->ignore();
		}
		break;

		default:
			break;
	}
}

void DeckLinkInputPage::startCapture()
{
	if (!m_selectedDevice)
		return;

	BMDDisplayMode displayMode = bmdModeUnknown;
	bool applyDetectedInputMode = m_autoDetectCheckBox->isChecked();

	displayMode = (BMDDisplayMode)m_videoFormatCombo->currentData().value<unsigned int>();

	m_selectedDevice->startCapture(displayMode, m_previewView->delegate(), applyDetectedInputMode);
}

void DeckLinkInputPage::addDevice(com_ptr<IDeckLink>& deckLink, bool deviceIsActive)
{
	dlstring_t	deviceNameStr;
	QString		deviceName;

	if (deckLink->GetDisplayName(&deviceNameStr) == S_OK)
	{
		deviceName = DlToQString(deviceNameStr);
		DeleteString(deviceNameStr);
	}
	else
		return;

	int index = m_deviceListCombo->findData(QVariant::fromValue((void*)deckLink.get()));
	if (index == -1)
	{
		m_deviceListCombo->addItem(deviceName, QVariant::fromValue((void*)deckLink.get()));
		enableDevice(deckLink, deviceIsActive);
	}

	if (!m_selectedDevice && deviceIsActive)
	{
		// Request deckLink object from parent widget if it's not already in use by another page
		emit requestDeckLinkIfAvailable(deckLink);
	}
}

void DeckLinkInputPage::removeDevice(com_ptr<IDeckLink>& deckLink)
{
	// Find the combo box entry and remove entry
	int indexToRemove = m_deviceListCombo->findData(QVariant::fromValue((void*)deckLink.get()));
	bool removingCurrentDevice = false;

	if (indexToRemove > 0)
	{
		removingCurrentDevice = (indexToRemove == m_deviceListCombo->currentIndex());

		// Prevent signal on removeItem, so that we don't refresh connector/video mode for removed device
		bool blocked = m_deviceListCombo->blockSignals(true);
		m_deviceListCombo->removeItem(indexToRemove);
		m_deviceListCombo->blockSignals(blocked);
	}

	if (removingCurrentDevice)
		m_deviceListCombo->setCurrentIndex(0);
}

void DeckLinkInputPage::enableDevice(com_ptr<IDeckLink>& deckLink, bool enable)
{
	if (deckLink == nullptr)
		return;

	int index = m_deviceListCombo->findData(QVariant::fromValue((void*)deckLink.get()));
	if (index >= 0)
	{
		QStandardItemModel* model = qobject_cast<QStandardItemModel*>(m_deviceListCombo->model());
		QStandardItem* item = model->item(index);
		item->setFlags(enable ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled);
	}

	selectedDeviceChanged();
}

bool DeckLinkInputPage::releaseDeviceIfSelected(com_ptr<IDeckLink>& deckLink)
{
	if ((m_selectedDevice.get() != nullptr) && (m_selectedDevice->getDeckLinkInstance().get() == deckLink.get()))
	{
		// Device is selected, stop and release it
		m_selectedDevice->stopCapture();
		m_selectedDevice = nullptr;

		m_deviceListCombo->setCurrentIndex(0);
		return true;
	}
	return false;
}

void DeckLinkInputPage::inputDeviceChanged(int selectedDeviceIndex)
{
	if (selectedDeviceIndex == -1)
		return;

	if (m_selectedDevice)
	{
		com_ptr<IDeckLink> existingDevice = m_selectedDevice->getDeckLinkInstance();

		// Stop and release existing selected device
		m_selectedDevice->stopCapture();
		m_selectedDevice = nullptr;

		// Notity parent widget that device is available
		emit relinquishDeckLink(existingDevice);
	}

	QVariant selectedDeviceVariant = m_deviceListCombo->itemData(selectedDeviceIndex);
	com_ptr<IDeckLink> deckLink((IDeckLink*)selectedDeviceVariant.value<void*>());

	if (deckLink)
	{
		// Request deckLink object from parent widget
		emit requestDeckLink(deckLink);
	}
	else
	{
		// Update UI since "None" was selected
		selectedDeviceChanged();
	}
}

void DeckLinkInputPage::requestedDeviceGranted(com_ptr<IDeckLink>& device)
{
	m_selectedDevice = make_com_ptr<DeckLinkInputDevice>(this, device);

	// Register profile callback with newly selected device's profile manager
	if (m_selectedDevice)
		m_selectedDevice->initialize();

	selectedDeviceChanged();
	startCapture();
}

void DeckLinkInputPage::inputConnectionChanged(int selectedConnectionIndex)
{
	HRESULT result;

	if (selectedConnectionIndex == -1)
		return;

	QVariant selectedConnectionVariant = m_inputConnectionCombo->itemData(selectedConnectionIndex);
	result = m_selectedDevice->setInputVideoConnection((BMDVideoConnection)selectedConnectionVariant.value<int64_t>());
	if (result != S_OK)
	{
		QMessageBox::critical(this, "Input connection error", "Unable to set video input connector");
		return;
	}

	// Update the video mode popup menu
	refreshDisplayModeMenu();
}

void DeckLinkInputPage::videoFormatChanged(int)
{
	if (!m_autoDetectCheckBox->isChecked())
		restartCapture();
}

void DeckLinkInputPage::autoDetectChanged(int autoDetectState)
{
	m_videoFormatCombo->setEnabled(static_cast<Qt::CheckState>(autoDetectState) != Qt::Checked);
	restartCapture();
}

void DeckLinkInputPage::detectedVideoFormatChanged(BMDDisplayMode displayMode)
{
	// Update videoFormatPopup with auto-detected display mode
	int index = m_videoFormatCombo->findData(QVariant::fromValue((uint64_t)displayMode));
	if (index >= 0)
		m_videoFormatCombo->setCurrentIndex(index);
}

void DeckLinkInputPage::restartCapture()
{
	if (!m_selectedDevice)
		return;

	m_selectedDevice->stopCapture();
	startCapture();
}

void DeckLinkInputPage::selectedDeviceChanged(void)
{
	int indexToSelect = 0;
	bool active = true;
	if (m_selectedDevice)
	{
		indexToSelect = m_deviceListCombo->findData(QVariant::fromValue((void*)m_selectedDevice->getDeckLinkInstance().get()));
		m_autoDetectCheckBox->setEnabled(m_selectedDevice->supportsFormatDetection());
		m_autoDetectCheckBox->setChecked(m_selectedDevice->supportsFormatDetection());
		active = m_selectedDevice->isActive();
	}
	else
	{
		m_previewView->clear();
		m_autoDetectCheckBox->setEnabled(false);
	}

	// Select the item in the combo box, but we don't want to trigger any further processing
	bool blocked = m_deviceListCombo->blockSignals(true);
	m_deviceListCombo->setCurrentIndex(indexToSelect);
	m_deviceListCombo->blockSignals(blocked);

	// Update the input connector popup menu which will in turn update the video format popup menu
	refreshInputConnectionMenu();

	// Update the toolbox title and the overlay
	QToolBox* toolBox = findParent<QToolBox>(this);
	int pageIndex = toolBox->indexOf(this);
	QString title = QString("Input %1: %2%3").arg(pageIndex + 1).arg(m_deviceListCombo->itemText(indexToSelect)).arg(active ? "" : " [inactive]");
	toolBox->setItemText(pageIndex, title);

	m_previewView->overlay()->setDeviceLabel(title);
}

void DeckLinkInputPage::refreshInputConnectionMenu()
{
	BMDVideoConnection		supportedConnections;
	int64_t					currentInputConnection;

	if (!m_selectedDevice)
	{
		m_inputConnectionCombo->setEnabled(false);
		return;
	}

	// Get the available input video connections for the device
	supportedConnections = m_selectedDevice->getVideoConnections();

	// Get the current selected input connection
	if (m_selectedDevice->getDeckLinkConfiguration()->GetInt(bmdDeckLinkConfigVideoInputConnection, &currentInputConnection) != S_OK)
	{
		currentInputConnection = bmdVideoConnectionUnspecified;
	}

	m_inputConnectionCombo->clear();

	for (auto& inputConnection : kVideoInputConnections)
	{
		if (inputConnection.first & supportedConnections)
			m_inputConnectionCombo->addItem(inputConnection.second, QVariant::fromValue((int64_t)inputConnection.first));

		if (inputConnection.first == (BMDVideoConnection)currentInputConnection)
			m_inputConnectionCombo->setCurrentIndex(m_inputConnectionCombo->count() - 1);
	}

	m_inputConnectionCombo->setEnabled(true);
}

void DeckLinkInputPage::refreshDisplayModeMenu()
{
	m_videoFormatCombo->clear();

	if (!m_selectedDevice)
	{
		m_inputConnectionCombo->setEnabled(false);
		return;
	}

	// Populate the display mode menu with a list of display modes supported by the installed DeckLink card
	m_selectedDevice->querySupportedVideoModes([this](IDeckLinkDisplayMode* displayMode)
	{
		dlstring_t			modeName;
		BMDDisplayMode		mode = displayMode->GetDisplayMode();

		if (displayMode->GetName(&modeName) == S_OK)
		{
			m_videoFormatCombo->addItem(DlToQString(modeName), QVariant::fromValue((uint64_t)mode));
			DeleteString(modeName);
		}
	});

	m_videoFormatCombo->setCurrentIndex(0);
	m_inputConnectionCombo->setEnabled(true);
}
