/******************************************************************************
    Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
    Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/

#include "OBSApp.hpp"
#include "OBSBasicSourceSelect.hpp"

#include <utility/ResizeSignaler.hpp>
#include <utility/ThumbnailManager.hpp>
#include <utility/ThumbnailView.hpp>

#include "qt-wrappers.hpp"

#include <QList>
#include <QMessageBox>

#include "moc_OBSBasicSourceSelect.cpp"

constexpr int kUnversionedIdRole = Qt::UserRole + 1;
constexpr int kDeprecatedRole = Qt::UserRole + 2;

constexpr QStringView kRecentTypeId = u"_recent";
constexpr int kRecentListLimit = 16;

struct AddSourceData {
	// Input data
	obs_source_t *source;
	bool visible;
	obs_transform_info *transform = nullptr;
	obs_sceneitem_crop *crop = nullptr;
	obs_blending_method *blend_method = nullptr;
	obs_blending_type *blend_mode = nullptr;
	obs_scale_type *scale_type = nullptr;
	const char *show_transition_id = nullptr;
	const char *hide_transition_id = nullptr;
	OBSData show_transition_settings;
	OBSData hide_transition_settings;
	uint32_t show_transition_duration = 300;
	uint32_t hide_transition_duration = 300;
	OBSData private_settings;

	// Return data
	obs_sceneitem_t *scene_item = nullptr;
};

namespace {
QString getDisplayNameForSourceType(QString type)
{
	if (type == "scene") {
		return QTStr("Basic.Scene");
	}

	const char *inputChar = obs_get_latest_input_type_id(type.toUtf8().constData());
	const char *displayName = obs_source_get_display_name(inputChar);

	if (!displayName) {
		return QString();
	}

	return QString::fromUtf8(displayName);
}

std::string getNewSourceName(const std::string name)
{
	std::string newName{name};
	int suffix = 1;

	for (;;) {
		OBSSourceAutoRelease existing_source = obs_get_source_by_name(newName.c_str());
		if (!existing_source) {
			break;
		}

		char nextName[256];
		std::snprintf(nextName, sizeof(nextName), "%s %d", name.data(), ++suffix);
		newName = nextName;
	}

	return newName;
}

void setupSceneItem(void *_data, obs_scene_t *scene)
{
	AddSourceData *data = (AddSourceData *)_data;
	obs_sceneitem_t *sceneitem;

	sceneitem = obs_scene_add(scene, data->source);

	if (data->transform != nullptr) {
		obs_sceneitem_set_info2(sceneitem, data->transform);
	}
	if (data->crop != nullptr) {
		obs_sceneitem_set_crop(sceneitem, data->crop);
	}
	if (data->blend_method != nullptr) {
		obs_sceneitem_set_blending_method(sceneitem, *data->blend_method);
	}
	if (data->blend_mode != nullptr) {
		obs_sceneitem_set_blending_mode(sceneitem, *data->blend_mode);
	}
	if (data->scale_type != nullptr) {
		obs_sceneitem_set_scale_filter(sceneitem, *data->scale_type);
	}

	if (data->show_transition_id && *data->show_transition_id) {
		OBSSourceAutoRelease source = obs_source_create(data->show_transition_id, data->show_transition_id,
								data->show_transition_settings, nullptr);

		if (source) {
			obs_sceneitem_set_transition(sceneitem, true, source);
		}
	}

	if (data->hide_transition_id && *data->hide_transition_id) {
		OBSSourceAutoRelease source = obs_source_create(data->hide_transition_id, data->hide_transition_id,
								data->hide_transition_settings, nullptr);

		if (source) {
			obs_sceneitem_set_transition(sceneitem, false, source);
		}
	}

	obs_sceneitem_set_transition_duration(sceneitem, true, data->show_transition_duration);
	obs_sceneitem_set_transition_duration(sceneitem, false, data->hide_transition_duration);

	obs_sceneitem_set_visible(sceneitem, data->visible);

	if (data->private_settings) {
		OBSDataAutoRelease newPrivateSettings = obs_sceneitem_get_private_settings(sceneitem);
		obs_data_apply(newPrivateSettings, data->private_settings);
	}

	data->scene_item = sceneitem;
}

std::optional<OBSSceneItem> setupExistingSource(std::string_view uuid, bool visible, bool duplicate,
						SourceCopyInfo *info = nullptr, OBSScene scene = nullptr)
{
	OBSSourceAutoRelease temp = obs_get_source_by_uuid(uuid.data());
	if (!temp) {
		return std::nullopt;
	}

	if (!scene) {
		OBSBasic *main = OBSBasic::Get();
		scene = main->GetCurrentScene();
	}
	if (!scene) {
		return std::nullopt;
	}

	if (duplicate) {
		OBSSource source = temp.Get();
		std::string new_name = getNewSourceName(obs_source_get_name(source));
		temp = obs_source_duplicate(source, new_name.c_str(), false);

		if (!source) {
			return std::nullopt;
		}
	}

	AddSourceData data;
	data.source = temp;
	data.visible = visible;

	if (info) {
		data.transform = &info->transform;
		data.crop = &info->crop;
		data.blend_method = &info->blend_method;
		data.blend_mode = &info->blend_mode;
		data.scale_type = &info->scale_type;
		data.show_transition_id = info->show_transition_id;
		data.hide_transition_id = info->hide_transition_id;
		data.show_transition_settings = std::move(info->show_transition_settings);
		data.hide_transition_settings = std::move(info->hide_transition_settings);
		data.show_transition_duration = info->show_transition_duration;
		data.hide_transition_duration = info->hide_transition_duration;
		data.private_settings = std::move(info->private_settings);
	}

	obs_enter_graphics();
	obs_scene_atomic_update(scene, setupSceneItem, &data);
	obs_leave_graphics();

	return OBSSceneItem(data.scene_item);
}

std::optional<OBSSource> setupNewSource(QWidget *parent, const char *id, const char *name)
{
	OBSBasic *main = OBSBasic::Get();
	OBSScene scene = main->GetCurrentScene();

	if (!scene) {
		return std::nullopt;
	}

	OBSSourceAutoRelease source = obs_get_source_by_name(name);
	if (source && parent) {
		OBSMessageBox::information(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text"));
		return std::nullopt;
	}

	const char *v_id = obs_get_latest_input_type_id(id);

	source = obs_source_create(v_id, name, NULL, nullptr);
	if (!source) {
		return std::nullopt;
	}

	return OBSSource(source.Get());
}
} // namespace

OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, undo_stack &undo_s)
	: QDialog(parent),
	  ui(new Ui::OBSBasicSourceSelect),
	  undo_s(undo_s),
	  selectedTypeId(kRecentTypeId.toString()),
	  sourceButtons(new QButtonGroup(this))
{
	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);

	ui->setupUi(this);

	existingFlowLayout = ui->existingListFrame->flowLayout();
	existingFlowLayout->setContentsMargins(0, 0, 0, 0);
	existingFlowLayout->setSpacing(0);

	// The scroll viewport is not accessible via Designer, so we have to disable autoFillBackground here.
	// Additionally when Qt calls setWidget on a scrollArea to set the contents widget, it force sets
	// autoFillBackground to true overriding whatever is set in Designer so we have to do that here too.
	ui->existingScrollArea->viewport()->setAutoFillBackground(false);
	ui->existingScrollContents->setAutoFillBackground(false);

	ResizeSignaler *resizeSignaler = new ResizeSignaler(ui->existingScrollArea);
	ui->existingScrollArea->installEventFilter(resizeSignaler);

	connect(resizeSignaler, &ResizeSignaler::resized, this, &OBSBasicSourceSelect::updateButtonVisibility);
	connect(ui->existingScrollArea->verticalScrollBar(), &QScrollBar::valueChanged, this,
		&OBSBasicSourceSelect::updateButtonVisibility);
	connect(ui->existingScrollArea->horizontalScrollBar(), &QScrollBar::valueChanged, this,
		&OBSBasicSourceSelect::updateButtonVisibility);

	ui->createNewSource->setVisible(false);
	ui->noExistingLabel->setVisible(false);
	ui->deprecatedFrame->setVisible(false);

	rebuildSourceTypeList();
	refreshSources();

	updateExistingSources(kRecentListLimit);

	signalHandlers.reserve(2);
	signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", &OBSBasicSourceSelect::obsSourceCreated,
				    this);
	signalHandlers.emplace_back(obs_get_signal_handler(), "source_destroy", &OBSBasicSourceSelect::obsSourceRemoved,
				    this);

	connect(ui->sourceTypeList, &QListWidget::itemDoubleClicked, this, &OBSBasicSourceSelect::createNew);
	connect(ui->sourceTypeList, &QListWidget::currentItemChanged, this, &OBSBasicSourceSelect::sourceTypeSelected);
	connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);

	connect(ui->addExistingButton, &QAbstractButton::clicked, this, &OBSBasicSourceSelect::addSelectedSources);
	connect(this, &OBSBasicSourceSelect::selectedItemsChanged, this, [this]() {
		ui->addExistingButton->setEnabled(selectedItems.size() > 0);
		if (selectedItems.size() > 0) {
			ui->addExistingButton->setText(
				QTStr("Basic.SourceSelect.AddExisting").arg(selectedItems.size()));
		} else {
			ui->addExistingButton->setText(QTStr("Basic.SourceSelect.NoSelection"));
		}
	});

	App()->DisableHotkeys();
}

OBSBasicSourceSelect::~OBSBasicSourceSelect()
{
	App()->UpdateHotkeyFocusSetting();
}

void OBSBasicSourceSelect::obsSourceCreated(void *data, calldata_t *)
{
	QMetaObject::invokeMethod(static_cast<OBSBasicSourceSelect *>(data), "handleSourceCreated",
				  Qt::QueuedConnection);
}

void OBSBasicSourceSelect::obsSourceRemoved(void *data, calldata_t *params)
{
	obs_source_t *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
	const char *uuidPointer = obs_source_get_uuid(source);

	if (!uuidPointer) {
		return;
	}

	QMetaObject::invokeMethod(static_cast<OBSBasicSourceSelect *>(data), "handleSourceRemoved",
				  Qt::QueuedConnection, Q_ARG(QString, QString::fromUtf8(uuidPointer)));
}

void OBSBasicSourceSelect::sourcePaste(SourceCopyInfo &info, bool duplicate, OBSScene scene)
{
	OBSSource source = OBSGetStrongRef(info.weak_source);
	if (!source) {
		return;
	}

	std::string uuid = obs_source_get_uuid(source);

	setupExistingSource(uuid, info.visible, duplicate, &info, scene);
}

void OBSBasicSourceSelect::showEvent(QShowEvent *)
{
	QTimer::singleShot(0, this, [this] { updateButtonVisibility(); });
}

void OBSBasicSourceSelect::updateButtonVisibility()
{
	QList<QAbstractButton *> buttons = sourceButtons->buttons();

	if (buttons.size() <= 0) {
		return;
	}

	QAbstractButton *firstButton = buttons.first();

	// Allow some room for previous/next rows to make scrolling a bit more seamless
	QRect scrollAreaRect(QPoint(0, 0), ui->existingScrollArea->size());
	scrollAreaRect.setTop(scrollAreaRect.top() - firstButton->rect().height());
	scrollAreaRect.setBottom(scrollAreaRect.bottom() + firstButton->rect().height());

	for (const auto &button : buttons) {
		SourceSelectButton *sourceButton = qobject_cast<SourceSelectButton *>(button);
		if (sourceButton) {
			QRect buttonRect = button->rect();
			buttonRect.moveTo(button->mapTo(ui->existingScrollArea, buttonRect.topLeft()));

			if (scrollAreaRect.intersects(buttonRect)) {
				sourceButton->setThumbnailEnabled(true);
			} else {
				sourceButton->setThumbnailEnabled(false);
			}
		}
	}
}

void OBSBasicSourceSelect::refreshSources()
{
	weakSources.clear();

	obs_enum_sources(enumSourcesCallback, this);

	struct obs_frontend_source_list list = {};
	obs_frontend_get_scenes(&list);

	for (size_t i = 0; i < list.sources.num; ++i) {
		OBSSource source = list.sources.array[i];

		OBSWeakSourceAutoRelease weakSource = obs_source_get_weak_source(source);
		weakSources.emplace_back(weakSource);
	}
	obs_frontend_source_list_free(&list);

	emit sourcesUpdated();
}

void OBSBasicSourceSelect::updateExistingSources(int limit)
{
	QLayout *layout = ui->existingListFrame->flowLayout();

	// Clear existing buttons when switching types
	QLayoutItem *child = nullptr;
	while ((child = layout->takeAt(0)) != nullptr) {
		if (child->widget()) {
			child->widget()->deleteLater();
		}
		delete child;
	}

	if (sourceButtons) {
		sourceButtons->deleteLater();
	}

	sourceButtons = new QButtonGroup(this);
	sourceButtons->setExclusive(false);

	std::vector<obs_weak_source_t *> matchingSources{};
	std::copy_if(weakSources.begin(), weakSources.end(), std::back_inserter(matchingSources),
		     [this](obs_weak_source_t *weak) {
			     OBSSource source = OBSGetStrongRef(weak);

			     if (!source || obs_source_removed(source)) {
				     return false;
			     }

			     const char *id = obs_source_get_unversioned_id(source);
			     QString sourceTypeId = QString(id);

			     if (sourceTypeId.compare("group") == 0) {
				     return false;
			     }

			     if (selectedTypeId.compare(kRecentTypeId) == 0) {
				     // Skip listing scenes in recent sources list
				     if (sourceTypeId.compare("scene") == 0) {
					     return false;
				     }

				     return true;
			     }

			     if (selectedTypeId.compare(sourceTypeId) == 0) {
				     return true;
			     }

			     return false;
		     });

	QWidget *prevTabWidget = ui->sourceTypeList;

	auto createSourceButton = [this, &prevTabWidget](obs_weak_source_t *weak) {
		OBSSource source{OBSGetStrongRef(weak)};

		if (!source) {
			return;
		}

		SourceSelectButton *newButton = new SourceSelectButton(weak, ui->existingListFrame);
		std::string uuid = obs_source_get_uuid(source);

		existingFlowLayout->addWidget(newButton);
		sourceButtons->addButton(newButton);

		bool isSelected = false;
		if (selectedItems.size() > 0) {
			if (std::find(selectedItems.begin(), selectedItems.end(), uuid) != selectedItems.end()) {
				isSelected = true;
			}
		}

		newButton->setChecked(isSelected);

		if (!prevTabWidget) {
			setTabOrder(ui->existingListFrame, newButton);
		} else {
			setTabOrder(prevTabWidget, newButton);
		}

		prevTabWidget = newButton;
	};

	bool isReverseListOrder = selectedTypeId.compare(kRecentTypeId) == 0;
	size_t iterationLimit = limit > 0 ? std::min(static_cast<size_t>(limit), matchingSources.size())
					  : matchingSources.size();
	if (isReverseListOrder) {
		std::for_each(matchingSources.rbegin(), matchingSources.rbegin() + iterationLimit, createSourceButton);
	} else {
		std::for_each(matchingSources.begin(), matchingSources.begin() + iterationLimit, createSourceButton);
	}

	setTabOrder(prevTabWidget, ui->addExistingContainer);

	connect(sourceButtons, &QButtonGroup::buttonToggled, this, &OBSBasicSourceSelect::sourceButtonToggled);

	ui->existingListFrame->adjustSize();
}

bool OBSBasicSourceSelect::enumSourcesCallback(void *data, obs_source_t *source)
{
	if (obs_source_is_hidden(source)) {
		return true;
	}

	OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);

	OBSWeakSourceAutoRelease weakSource = obs_source_get_weak_source(source);
	window->weakSources.emplace_back(weakSource);

	return true;
}

void OBSBasicSourceSelect::rebuildSourceTypeList()
{
	ui->sourceTypeList->clear();

	OBSBasic *main = qobject_cast<OBSBasic *>(App()->GetMainWindow());

	const char *unversioned_type;
	const char *type;

	size_t idx = 0;

	while (obs_enum_input_types2(++idx, &type, &unversioned_type)) {
		const char *name = obs_source_get_display_name(type);
		uint32_t caps = obs_get_source_output_flags(type);

		if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) {
			continue;
		}

		QListWidgetItem *newItem = new QListWidgetItem(ui->sourceTypeList);
		newItem->setData(Qt::DisplayRole, name);
		newItem->setData(kUnversionedIdRole, unversioned_type);

		if ((caps & OBS_SOURCE_DEPRECATED) != 0) {
			newItem->setData(kDeprecatedRole, true);
		} else {
			newItem->setData(kDeprecatedRole, false);

			QIcon icon;
			icon = main->GetSourceIcon(type);
			newItem->setIcon(icon);
		}
	}

	QListWidgetItem *newItem = new QListWidgetItem(ui->sourceTypeList);
	newItem->setData(Qt::DisplayRole, Str("Basic.Scene"));
	newItem->setData(kUnversionedIdRole, "scene");

	QIcon icon = main->GetSceneIcon();
	newItem->setIcon(icon);

	ui->sourceTypeList->sortItems();

	auto *deprecatedTitle = new QListWidgetItem(ui->sourceTypeList);
	deprecatedTitle->setText(QTStr("Deprecated"));
	deprecatedTitle->setFlags(deprecatedTitle->flags() & ~Qt::ItemIsEnabled);
	deprecatedTitle->setFlags(deprecatedTitle->flags() & ~Qt::ItemIsSelectable);

	ui->sourceTypeList->addItem(deprecatedTitle);

	// Shift Deprecated sources to the bottom
	QList<QListWidgetItem *> deprecatedItems;
	for (int i = ui->sourceTypeList->count() - 1; i >= 0; --i) {
		QListWidgetItem *item = ui->sourceTypeList->item(i);
		if (!item) {
			continue;
		}

		bool isDeprecated = item->data(kDeprecatedRole).toBool();
		if (isDeprecated) {
			ui->sourceTypeList->takeItem(i);

			QVariant unversionedIdData = item->data(kUnversionedIdRole);

			deprecatedItems.append(item);
		}
	}

	std::reverse(deprecatedItems.begin(), deprecatedItems.end());
	for (const auto &item : deprecatedItems) {
		ui->sourceTypeList->addItem(item);
	}

	QListWidgetItem *allSources = new QListWidgetItem();
	allSources->setData(Qt::DisplayRole, Str("Basic.SourceSelect.Recent"));
	allSources->setData(kUnversionedIdRole, QVariant(kRecentTypeId.toString()));
	ui->sourceTypeList->insertItem(0, allSources);

	ui->sourceTypeList->setCurrentItem(allSources);
	ui->sourceTypeList->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
}

void OBSBasicSourceSelect::sourceButtonToggled(QAbstractButton *button, bool checked)
{
	SourceSelectButton *sourceButton = dynamic_cast<SourceSelectButton *>(button);

	Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
	bool ctrlDown = (modifiers & Qt::ControlModifier);
	bool shiftDown = (modifiers & Qt::ShiftModifier);

	if (!sourceButton) {
		clearSelectedItems();
		return;
	}

	int toggledIndex = existingFlowLayout->indexOf(sourceButton);

	std::string_view toggledUuid = sourceButton->uuid();

	if (shiftDown) {
		if (!ctrlDown) {
			clearSelectedItems();
		}

		QSignalBlocker block(sourceButtons);

		int originalSelectedIndex = lastSelectedIndex;

		int start = std::min(toggledIndex, lastSelectedIndex);
		int end = std::max(toggledIndex, lastSelectedIndex);

		for (int i = start; i <= end; ++i) {
			QLayoutItem *item = existingFlowLayout->itemAt(i);
			if (!item) {
				continue;
			}

			QWidget *widget = item->widget();
			if (!widget) {
				continue;
			}

			SourceSelectButton *entry = dynamic_cast<SourceSelectButton *>(widget);
			if (entry) {
				QSignalBlocker blocker(entry);
				entry->setChecked(true);

				std::string_view uuid = entry->uuid();
				addSelectedItem(uuid.data());
			}
		}

		lastSelectedIndex = originalSelectedIndex;

	} else if (ctrlDown) {
		lastSelectedIndex = toggledIndex;

		if (checked) {
			addSelectedItem(toggledUuid.data());
		} else {
			removeSelectedItem(toggledUuid.data());
		}
	} else {
		lastSelectedIndex = toggledIndex;

		bool reselectItem = selectedItems.size() > 1;
		clearSelectedItems();
		if (checked) {
			addSelectedItem(toggledUuid.data());
		} else if (reselectItem) {
			QSignalBlocker blocker(button);
			button->setChecked(true);
			addSelectedItem(toggledUuid.data());
		}
	}
}

void OBSBasicSourceSelect::sourceDropped(QString uuid)
{
	OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.toStdString().c_str());
	if (source) {
		bool visible = ui->sourceVisible->isChecked();

		addExisting(uuid.toStdString(), visible);
	}
}

void OBSBasicSourceSelect::addSelectedItem(const std::string &uuid)
{
	auto it = std::find(selectedItems.begin(), selectedItems.end(), uuid);

	if (it == selectedItems.end()) {
		selectedItems.emplace_back(uuid);
		emit selectedItemsChanged();
	}

	SourceSelectButton *button = findButtonForUuid(uuid);
	if (!button) {
		return;
	}

	lastSelectedIndex = existingFlowLayout->indexOf(button);
}

void OBSBasicSourceSelect::removeSelectedItem(const std::string &uuid)
{
	auto it = std::find(selectedItems.begin(), selectedItems.end(), uuid);
	if (it != selectedItems.end()) {
		selectedItems.erase(it);
		emit selectedItemsChanged();
	}

	SourceSelectButton *button = findButtonForUuid(uuid);
	if (!button) {
		return;
	}

	lastSelectedIndex = existingFlowLayout->indexOf(button);
}

void OBSBasicSourceSelect::clearSelectedItems()
{
	if (selectedItems.size() == 0) {
		return;
	}

	sourceButtons->blockSignals(true);
	for (const auto &uuid : selectedItems) {
		auto sourceButton = findButtonForUuid(uuid);
		if (sourceButton) {
			QSignalBlocker block(sourceButton);
			sourceButton->setChecked(false);
		}
	}
	sourceButtons->blockSignals(false);

	selectedItems.clear();
	emit selectedItemsChanged();
}

SourceSelectButton *OBSBasicSourceSelect::findButtonForUuid(const std::string &uuid)
{
	for (int i = 0; i <= existingFlowLayout->count(); ++i) {
		QLayoutItem *layoutItem = existingFlowLayout->itemAt(i);
		if (!layoutItem || !layoutItem->widget()) {
			continue;
		}

		QWidget *widget = layoutItem->widget();
		if (!widget) {
			continue;
		}

		SourceSelectButton *entry = dynamic_cast<SourceSelectButton *>(widget);
		if (entry && entry->uuid() == uuid) {
			return entry;
		}
	}

	return nullptr;
}

void OBSBasicSourceSelect::createNew()
{
	bool visible = ui->sourceVisible->isChecked();

	if (selectedTypeId.compare(kRecentTypeId) == 0) {
		return;
	}

	if (selectedTypeId.compare("scene") == 0) {
		return;
	}

	std::string sourceType = selectedTypeId.toStdString();
	const char *id = sourceType.c_str();
	QString placeholder = getDisplayNameForSourceType(selectedTypeId);
	std::string newName = getNewSourceName(placeholder.toStdString().c_str());

	std::optional<OBSSource> createResult = setupNewSource(this, id, newName.c_str());
	if (!createResult.has_value()) {
		return;
	}

	OBSSource newSource = createResult.value();
	if (strcmp(obs_source_get_id(newSource), "group") == 0) {
		return;
	}

	std::optional<OBSSceneItem> addResult = setupExistingSource(obs_source_get_uuid(newSource), visible, false);
	if (!addResult.has_value()) {
		return;
	}

	OBSSceneItem item = addResult.value();

	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
	std::string sceneUuid = obs_source_get_uuid(main->GetCurrentSceneSource());
	auto undo = [sceneUuid](const std::string &data) {
		OBSBasic *main = OBSBasic::Get();

		OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str());
		obs_source_remove(source);

		OBSSourceAutoRelease sceneSource = obs_get_source_by_uuid(sceneUuid.c_str());
		main->SetCurrentScene(sceneSource.Get(), true);
	};
	OBSDataAutoRelease wrapper = obs_data_create();
	obs_data_set_string(wrapper, "id", id);
	obs_data_set_int(wrapper, "item_id", obs_sceneitem_get_id(item));
	obs_data_set_string(wrapper, "name", newName.c_str());
	obs_data_set_bool(wrapper, "visible", visible);

	auto redo = [sceneUuid](const std::string &data) {
		OBSBasic *main = OBSBasic::Get();

		OBSSourceAutoRelease sceneSource = obs_get_source_by_uuid(sceneUuid.c_str());
		main->SetCurrentScene(sceneSource.Get(), true);

		OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str());

		std::optional<OBSSource> createResult =
			setupNewSource(NULL, obs_data_get_string(dat, "id"), obs_data_get_string(dat, "name"));
		if (!createResult.has_value()) {
			return;
		}

		OBSSource source = createResult.value();

		std::optional<OBSSceneItem> addResult =
			setupExistingSource(obs_source_get_uuid(source), obs_data_get_bool(dat, "visible"), false);
		if (!addResult.has_value()) {
			return;
		}

		OBSSceneItem item = addResult.value();

		obs_sceneitem_set_id(item, static_cast<int64_t>(obs_data_get_int(dat, "item_id")));
	};
	undo_s.add_action(QTStr("Undo.Add").arg(QString::fromStdString(newName)), undo, redo,
			  std::string(obs_source_get_uuid(newSource)), std::string(obs_data_get_json(wrapper)));

	main->CreatePropertiesWindow(newSource);

	close();
}

void OBSBasicSourceSelect::addExisting(const std::string &uuid, bool visible)
{
	OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str());
	if (!source) {
		return;
	}

	QString name = obs_source_get_name(source);
	setupExistingSource(uuid, visible, false);

	OBSBasic *main = OBSBasic::Get();
	const char *sceneUuidPtr = obs_source_get_uuid(main->GetCurrentSceneSource());
	if (!sceneUuidPtr) {
		return;
	}

	std::string sceneUuid{sceneUuidPtr};

	auto undo = [sceneUuid, main](const std::string &) {
		OBSSourceAutoRelease sceneSource = obs_get_source_by_uuid(sceneUuid.c_str());
		main->SetCurrentScene(sceneSource.Get(), true);

		OBSScene scene = obs_scene_from_source(sceneSource);
		OBSSceneItem item;
		auto cb = [](obs_scene_t *, obs_sceneitem_t *sceneitem, void *data) {
			OBSSceneItem &last = *static_cast<OBSSceneItem *>(data);
			last = sceneitem;
			return true;
		};
		obs_scene_enum_items(scene, cb, &item);

		obs_sceneitem_remove(item);
	};

	auto redo = [sceneUuid, main, uuid, visible](const std::string &) {
		OBSSourceAutoRelease sceneSource = obs_get_source_by_uuid(sceneUuid.c_str());
		main->SetCurrentScene(sceneSource.Get(), true);

		setupExistingSource(uuid, visible, false);
	};

	undo_s.add_action(QTStr("Undo.Add").arg(name), undo, redo, "", "");
}

void OBSBasicSourceSelect::on_createNewSource_clicked(bool)
{
	createNew();
}

void OBSBasicSourceSelect::addSelectedSources()
{
	if (selectedItems.size() == 0) {
		return;
	}

	bool visible = ui->sourceVisible->isChecked();

	for (const auto &uuid : selectedItems) {
		addExisting(uuid, visible);
	}
	close();
}

void OBSBasicSourceSelect::handleSourceCreated()
{
	refreshSources();

	if (selectedTypeId.compare(kRecentTypeId) == 0) {
		updateExistingSources(kRecentListLimit);
	} else {
		updateExistingSources();
	}
}

void OBSBasicSourceSelect::handleSourceRemoved(QString uuid)
{
	refreshSources();

	removeSelectedItem(uuid.toStdString());

	if (selectedTypeId.compare(kRecentTypeId) == 0) {
		updateExistingSources(kRecentListLimit);
	} else {
		updateExistingSources();
	}
}

void OBSBasicSourceSelect::sourceTypeSelected(QListWidgetItem *current, QListWidgetItem *)
{
	clearSelectedItems();

	QVariant unversionedIdData = current->data(kUnversionedIdRole);
	QVariant deprecatedData = current->data(kDeprecatedRole);

	bool isDeprecatedType = deprecatedData.toBool();
	ui->deprecatedFrame->setVisible(isDeprecatedType);

	if (unversionedIdData.toString().compare(kRecentTypeId) == 0) {
		ui->sourceSelectTitle->setText(QTStr("Basic.SourceSelect.Recent"));
		selectedTypeId = kRecentTypeId.toString();
		ui->noExistingLabel->setVisible(false);
		ui->existingScrollArea->setVisible(true);
		ui->createNewSource->setVisible(false);
		updateExistingSources(kRecentListLimit);
		return;
	}

	QString type = unversionedIdData.toString();
	if (type.compare(selectedTypeId) == 0) {
		return;
	}

	ui->createNewSource->setVisible(type != "scene");

	selectedTypeId = type;

	QString placeHolderText{getDisplayNameForSourceType(selectedTypeId)};

	ui->createNewSource->setText(
		QTStr("Basic.SourceSelect.NewSource").arg(getDisplayNameForSourceType(selectedTypeId)));
	ui->sourceSelectTitle->setText(QString("%1").arg(getDisplayNameForSourceType(selectedTypeId)));

	updateExistingSources();

	ui->noExistingLabel->setText(
		QTStr("Basic.SourceSelect.NoExisting").arg(getDisplayNameForSourceType(selectedTypeId)));

	ui->existingScrollArea->setVisible(existingFlowLayout->count() != 0);
	ui->noExistingLabel->setVisible(existingFlowLayout->count() == 0);
}
