#include "emu_settings.h"

#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>

#include "stdafx.h"
#include "Emu/System.h"
#include "Utilities/Config.h"
#include "Utilities/Thread.h"
#include "Utilities/StrUtil.h"

#include <QMessageBox>

#if defined(_WIN32) || defined(HAVE_VULKAN)
#include "Emu/RSX/VK/VKHelpers.h"
#endif

#include "3rdparty/OpenAL/include/alext.h"

extern std::string g_cfg_defaults; //! Default settings grabbed from Utilities/Config.h

inline std::string sstr(const QString& _in) { return _in.toStdString(); }
inline std::string sstr(const QVariant& _in) { return sstr(_in.toString()); }

// Emit sorted YAML
namespace
{
	static NEVER_INLINE void emitData(YAML::Emitter& out, const YAML::Node& node)
	{
		// TODO
		out << node;
	}

	// Incrementally load YAML
	static NEVER_INLINE void operator +=(YAML::Node& left, const YAML::Node& node)
	{
		if (node && !node.IsNull())
		{
			if (node.IsMap())
			{
				for (const auto& pair : node)
				{
					if (pair.first.IsScalar())
					{
						auto&& lhs = left[pair.first.Scalar()];
						lhs += pair.second;
					}
					else
					{
						// Exotic case (TODO: probably doesn't work)
						auto&& lhs = left[YAML::Clone(pair.first)];
						lhs += pair.second;
					}
				}
			}
			else if (node.IsScalar() || node.IsSequence())
			{
				// Scalars and sequences are replaced completely, but this may change in future.
				// This logic may be overwritten by custom demands of every specific cfg:: node.
				left = node;
			}
		}
	}
}

// Helper methods to interact with YAML and the config settings.
namespace cfg_adapter
{
	static cfg::_base& get_cfg(cfg::_base& root, const std::string& name)
	{
		if (root.get_type() == cfg::type::node)
		{
			for (const auto& pair : static_cast<cfg::node&>(root).get_nodes())
			{
				if (pair.first == name)
				{
					return *pair.second;
				}
			}
		}

		fmt::throw_exception("Node not found: %s", name);
	}

	static cfg::_base& get_cfg(cfg::_base& root, cfg_location::const_iterator begin, cfg_location::const_iterator end)
	{
		return begin == end ? root : get_cfg(get_cfg(root, *begin), begin + 1, end);
	}

	static YAML::Node get_node(const YAML::Node& node, cfg_location::const_iterator begin, cfg_location::const_iterator end)
	{
		return begin == end ? node : get_node(node[*begin], begin + 1, end); // TODO
	}

	/** Syntactic sugar to get a setting at a given config location. */
	static YAML::Node get_node(const YAML::Node& node, cfg_location loc)
	{
		return get_node(node, loc.cbegin(), loc.cend());
	}
}

/** Returns possible options for values for some particular setting.*/
static QStringList getOptions(cfg_location location)
{
	QStringList values;
	auto begin = location.cbegin();
	auto end = location.cend();
	for (const auto& v : cfg_adapter::get_cfg(g_cfg, begin, end).to_list())
	{
		values.append(qstr(v));
	}
	return values;
}

emu_settings::Render_Creator::Render_Creator()
{
#if defined(WIN32) || defined(HAVE_VULKAN)
	// Some drivers can get stuck when checking for vulkan-compatible gpus, f.ex. if they're waiting for one to get
	// plugged in. This whole contraption is for showing an error message in case that happens, so that user has
	// some idea about why the emulator window isn't showing up.

	static std::atomic<bool> was_called = false;
	if (was_called.exchange(true))
		fmt::throw_exception("Render_Creator cannot be created more than once" HERE);

	static std::mutex mtx;
	static std::condition_variable cond;
	static bool thread_running = true;
	static bool device_found = false;

	static QStringList compatible_gpus;

	std::thread enum_thread = std::thread([&]
	{
		thread_ctrl::set_native_priority(-1);

		vk::context device_enum_context;
		if (device_enum_context.createInstance("RPCS3", true))
		{
			device_enum_context.makeCurrentInstance();
			std::vector<vk::physical_device> &gpus = device_enum_context.enumerateDevices();

			if (!gpus.empty())
			{
				device_found = true;

				for (auto &gpu : gpus)
				{
					compatible_gpus.append(qstr(gpu.get_name()));
				}
			}
		}

		std::scoped_lock{mtx}, thread_running = false;
		cond.notify_all();
	});

	{
		std::unique_lock lck(mtx);
		cond.wait_for(lck, std::chrono::seconds(10), [&]{ return !thread_running; });
	}

	if (thread_running)
	{
		LOG_ERROR(GENERAL, "Vulkan device enumeration timed out");
		auto button = QMessageBox::critical(nullptr, tr("Vulkan Check Timeout"),
			tr("Querying for Vulkan-compatible devices is taking too long. This is usually caused by malfunctioning "
			"graphics drivers, reinstalling them could fix the issue.\n\n"
			"Selecting ignore starts the emulator without Vulkan support."),
			QMessageBox::Ignore | QMessageBox::Abort, QMessageBox::Abort);

		enum_thread.detach();
		if (button != QMessageBox::Ignore)
			std::exit(1);

		supportsVulkan = false;
	}
	else
	{
		supportsVulkan = device_found;
		vulkanAdapters = std::move(compatible_gpus);
		enum_thread.join();
	}
#endif

	// Graphics Adapter
	Vulkan = Render_Info(name_Vulkan, vulkanAdapters, supportsVulkan, emu_settings::VulkanAdapter, true);
	OpenGL = Render_Info(name_OpenGL);
	NullRender = Render_Info(name_Null);

	renderers = { &Vulkan, &OpenGL, &NullRender };
}

emu_settings::Microphone_Creator::Microphone_Creator()
{
	RefreshList();
}

void emu_settings::Microphone_Creator::RefreshList()
{
	microphones_list.clear();
	microphones_list.append(mic_none);

	if (alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT") == AL_TRUE)
	{
		const char *devices = alcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER);

		while (*devices != 0)
		{
			microphones_list.append(qstr(devices));
			devices += strlen(devices) + 1;
		}
	}
	else
	{
		// Without enumeration we can only use one device
		microphones_list.append(qstr(alcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER)));
	}
}

std::string emu_settings::Microphone_Creator::SetDevice(u32 num, QString& text)
{
	if (text == mic_none)
		sel_list[num-1] = "";
	else
		sel_list[num-1] = text.toStdString();

	const std::string final_list = sel_list[0] + "@@@" + sel_list[1] + "@@@" + sel_list[2] + "@@@" + sel_list[3] + "@@@";
	return final_list;
}

void emu_settings::Microphone_Creator::ParseDevices(std::string list)
{
	for (u32 index = 0; index < 4; index++)
	{
		sel_list[index] = "";
	}

	const auto devices_list = fmt::split(list, { "@@@" });
	for (u32 index = 0; index < std::min<u32>(4, ::size32(devices_list)); index++)
	{
		sel_list[index] = devices_list[index];
	}
}

emu_settings::emu_settings() : QObject()
{
}

emu_settings::~emu_settings()
{
}

void emu_settings::LoadSettings(const std::string& title_id)
{
	m_title_id = title_id;

	// Create config path if necessary
	fs::create_path(title_id.empty() ? fs::get_config_dir() : Emulator::GetCustomConfigDir());

	// Load default config
	m_defaultSettings = YAML::Load(g_cfg_defaults);
	m_currentSettings = YAML::Load(g_cfg_defaults);

	// Add global config
	fs::file config(fs::get_config_dir() + "/config.yml", fs::read + fs::write + fs::create);
	m_currentSettings += YAML::Load(config.to_string());
	config.close();

	// Add game config
	if (!title_id.empty())
	{
		const std::string config_path_new = Emulator::GetCustomConfigPath(m_title_id);
		const std::string config_path_old = Emulator::GetCustomConfigPath(m_title_id, true);

		if (fs::is_file(config_path_new))
		{
			config = fs::file(config_path_new, fs::read + fs::write);
			m_currentSettings += YAML::Load(config.to_string());
			config.close();
		}
		else if (fs::is_file(config_path_old))
		{
			config = fs::file(config_path_old, fs::read + fs::write);
			m_currentSettings += YAML::Load(config.to_string());
			config.close();
		}
	}
}

void emu_settings::SaveSettings()
{
	fs::file config;
	YAML::Emitter out;
	emitData(out, m_currentSettings);

	std::string config_name;

	if (m_title_id.empty())
	{
		config_name = fs::get_config_dir() + "/config.yml";
	}
	else
	{
		config_name = Emulator::GetCustomConfigPath(m_title_id);
	}

	config = fs::file(config_name, fs::read + fs::write + fs::create);

	// Save config
	config.seek(0);
	config.trunc(0);
	config.write(out.c_str(), out.size());

	// Check if the running config/title is the same as the edited config/title.
	if (config_name == g_cfg.name || m_title_id == Emu.GetTitleID())
	{
		// Update current config
		g_cfg.from_string(config.to_string(), true);

		if (!Emu.IsStopped()) // Don't spam the log while emulation is stopped. The config will be logged on boot anyway.
		{
			LOG_NOTICE(LOADER, "Updated configuration:\n%s\n", g_cfg.to_string());
		}
	}

	config.close();
}

void emu_settings::EnhanceComboBox(QComboBox* combobox, SettingsType type, bool is_ranged, bool use_max, int max, bool sorted)
{
	if (!combobox)
	{
		LOG_FATAL(GENERAL, "EnhanceComboBox '%s' was used with an invalid object", GetSettingName(type));
		return;
	}

	if (is_ranged)
	{
		if (sorted)
		{
			LOG_WARNING(GENERAL, "EnhanceCombobox '%s': ignoring sorting request on ranged combo box", GetSettingName(type));
		}

		QStringList range = GetSettingOptions(type);

		int max_item = use_max ? max : range.last().toInt();

		for (int i = range.first().toInt(); i <= max_item; i++)
		{
			combobox->addItem(QString::number(i), QVariant(QString::number(i)));
		}
	}
	else
	{
		QStringList settings = GetSettingOptions(type);

		if (sorted)
		{
			settings.sort();
		}

		for (QString setting : settings)
		{
			combobox->addItem(tr(setting.toStdString().c_str()), QVariant(setting));
		}
	}

	std::string selected = GetSetting(type);
	int index = combobox->findData(qstr(selected));

	if (index == -1)
	{
		std::string def = GetSettingDefault(type);
		LOG_ERROR(GENERAL, "EnhanceComboBox '%s' tried to set an invalid value: %s. Setting to default: %s", GetSettingName(type), selected, def);
		combobox->setCurrentIndex(combobox->findData(qstr(def)));
		m_broken_types.insert(type);
	}
	else
	{
		combobox->setCurrentIndex(index);
	}

	connect(combobox, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [=](int index)
	{
		SetSetting(type, sstr(combobox->itemData(index)));
	});
}

void emu_settings::EnhanceCheckBox(QCheckBox* checkbox, SettingsType type)
{
	if (!checkbox)
	{
		LOG_FATAL(GENERAL, "EnhanceCheckBox '%s' was used with an invalid object", GetSettingName(type));
		return;
	}

	std::string def = GetSettingDefault(type);
	std::transform(def.begin(), def.end(), def.begin(), ::tolower);

	if (def != "true" && def != "false")
	{
		LOG_FATAL(GENERAL, "EnhanceCheckBox '%s' was used with an invalid SettingsType", GetSettingName(type));
		return;
	}

	std::string selected = GetSetting(type);
	std::transform(selected.begin(), selected.end(), selected.begin(), ::tolower);

	if (selected == "true")
	{
		checkbox->setChecked(true);
	}
	else if (selected != "false")
	{
		LOG_ERROR(GENERAL, "EnhanceCheckBox '%s' tried to set an invalid value: %s. Setting to default: %s", GetSettingName(type), selected, def);
		checkbox->setChecked(def == "true");
		m_broken_types.insert(type);
	}

	connect(checkbox, &QCheckBox::stateChanged, [=](int val)
	{
		std::string str = val != 0 ? "true" : "false";
		SetSetting(type, str);
	});
}

void emu_settings::EnhanceSlider(QSlider* slider, SettingsType type)
{
	if (!slider)
	{
		LOG_FATAL(GENERAL, "EnhanceSlider '%s' was used with an invalid object", GetSettingName(type));
		return;
	}

	QStringList range = GetSettingOptions(type);
	bool ok_def, ok_sel, ok_min, ok_max;

	int def = qstr(GetSettingDefault(type)).toInt(&ok_def);
	int min = range.first().toInt(&ok_min);
	int max = range.last().toInt(&ok_max);

	if (!ok_def || !ok_min || !ok_max)
	{
		LOG_FATAL(GENERAL, "EnhanceSlider '%s' was used with an invalid SettingsType", GetSettingName(type));
		return;
	}

	QString selected = qstr(GetSetting(type));
	int val = selected.toInt(&ok_sel);

	if (!ok_sel || val < min || val > max)
	{
		LOG_ERROR(GENERAL, "EnhanceSlider '%s' tried to set an invalid value: %d. Setting to default: %d. Allowed range: [%d, %d]", GetSettingName(type), val, def, min, max);
		val = def;
		m_broken_types.insert(type);
	}

	slider->setRange(min, max);
	slider->setValue(val);

	connect(slider, &QSlider::valueChanged, [=](int value)
	{
		SetSetting(type, sstr(value));
	});
}

void emu_settings::EnhanceSpinBox(QSpinBox* spinbox, SettingsType type, const QString& prefix, const QString& suffix)
{
	if (!spinbox)
	{
		LOG_FATAL(GENERAL, "EnhanceSpinBox '%s' was used with an invalid object", GetSettingName(type));
		return;
	}

	QStringList range = GetSettingOptions(type);
	bool ok_def, ok_sel, ok_min, ok_max;

	int def = qstr(GetSettingDefault(type)).toInt(&ok_def);
	int min = range.first().toInt(&ok_min);
	int max = range.last().toInt(&ok_max);

	if (!ok_def || !ok_min || !ok_max)
	{
		LOG_FATAL(GENERAL, "EnhanceSpinBox '%s' was used with an invalid type", GetSettingName(type));
		return;
	}

	std::string selected = GetSetting(type);
	int val = qstr(selected).toInt(&ok_sel);

	if (!ok_sel || val < min || val > max)
	{
		LOG_ERROR(GENERAL, "EnhanceSpinBox '%s' tried to set an invalid value: %d. Setting to default: %d. Allowed range: [%d, %d]", GetSettingName(type), selected, def, min, max);
		val = def;
		m_broken_types.insert(type);
	}

	spinbox->setPrefix(prefix);
	spinbox->setSuffix(suffix);
	spinbox->setRange(min, max);
	spinbox->setValue(val);

	connect(spinbox, QOverload<const QString &>::of(&QSpinBox::valueChanged), [=](const QString&/* value*/)
	{
		SetSetting(type, sstr(spinbox->cleanText()));
	});
}

void emu_settings::EnhanceDoubleSpinBox(QDoubleSpinBox* spinbox, SettingsType type, const QString& prefix, const QString& suffix)
{
	if (!spinbox)
	{
		LOG_FATAL(GENERAL, "EnhanceDoubleSpinBox '%s' was used with an invalid object", GetSettingName(type));
		return;
	}

	QStringList range = GetSettingOptions(type);
	bool ok_def, ok_sel, ok_min, ok_max;

	double def = qstr(GetSettingDefault(type)).toDouble(&ok_def);
	double min = range.first().toDouble(&ok_min);
	double max = range.last().toDouble(&ok_max);

	if (!ok_def || !ok_min || !ok_max)
	{
		LOG_FATAL(GENERAL, "EnhanceDoubleSpinBox '%s' was used with an invalid type", GetSettingName(type));
		return;
	}

	std::string selected = GetSetting(type);
	double val = qstr(selected).toDouble(&ok_sel);

	if (!ok_sel || val < min || val > max)
	{
		LOG_ERROR(GENERAL, "EnhanceDoubleSpinBox '%s' tried to set an invalid value: %f. Setting to default: %f. Allowed range: [%f, %f]", GetSettingName(type), val, def, min, max);
		val = def;
		m_broken_types.insert(type);
	}

	spinbox->setPrefix(prefix);
	spinbox->setSuffix(suffix);
	spinbox->setRange(min, max);
	spinbox->setValue(val);

	connect(spinbox, QOverload<const QString &>::of(&QDoubleSpinBox::valueChanged), [=](const QString&/* value*/)
	{
		SetSetting(type, sstr(spinbox->cleanText()));
	});
}

std::vector<std::string> emu_settings::GetLoadedLibraries()
{
	return m_currentSettings["Core"]["Load libraries"].as<std::vector<std::string>, std::initializer_list<std::string>>({});
}

void emu_settings::SaveSelectedLibraries(const std::vector<std::string>& libs)
{
	m_currentSettings["Core"]["Load libraries"] = libs;
}

QStringList emu_settings::GetSettingOptions(SettingsType type) const
{
	return getOptions(const_cast<cfg_location&&>(SettingsLoc[type]));
}

std::string emu_settings::GetSettingName(SettingsType type) const
{
	cfg_location loc = SettingsLoc[type];
	return loc[loc.size() - 1];
}

std::string emu_settings::GetSettingDefault(SettingsType type) const
{
	return cfg_adapter::get_node(m_defaultSettings, SettingsLoc[type]).Scalar();
}

std::string emu_settings::GetSetting(SettingsType type) const
{
	return cfg_adapter::get_node(m_currentSettings, SettingsLoc[type]).Scalar();
}

void emu_settings::SetSetting(SettingsType type, const std::string& val)
{
	cfg_adapter::get_node(m_currentSettings, SettingsLoc[type]) = val;
}

void emu_settings::OpenCorrectionDialog(QWidget* parent)
{
	if (m_broken_types.size() && QMessageBox::question(parent, tr("Fix invalid settings?"),
		tr(
			"Your config file contained one or more unrecognized values for settings.\n"
			"Their default value will be used until they are corrected.\n"
			"Consider that a correction might render them invalid for other versions of RPCS3.\n"
			"\n"
			"Do you wish to let the program correct them for you?\n"
			"This change will only be final when you save the config."
		),
		QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)
	{
		for (const auto& type : m_broken_types)
		{
			std::string def = GetSettingDefault(type);
			std::string old = GetSetting(type);
			SetSetting(type, def);
			LOG_SUCCESS(GENERAL, "The config entry '%s' was corrected from '%s' to '%s'", GetSettingName(type), old, def);
		}

		m_broken_types.clear();
		LOG_SUCCESS(GENERAL, "You need to save the settings in order to make these changes permanent!");
	}
}
