// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+

#include "DebugAnalysisSettingsWidget.h"

#include "SettingsWindow.h"
#include "SettingWidgetBinder.h"

#include "DebugTools/SymbolImporter.h"

#include <QtWidgets/QFileDialog>

DebugAnalysisSettingsWidget::DebugAnalysisSettingsWidget(QWidget* parent)
	: QWidget(parent)
{
	m_ui.setupUi(this);

	m_ui.automaticallyClearSymbols->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "AutomaticallySelectSymbolsToClear", true));

	setupSymbolSourceGrid();

	m_ui.importFromElf->setChecked(
		Host::GetBoolSettingValue("Debugger/Analysis", "ImportSymbolsFromELF", true));
	m_ui.importSymFileFromDefaultLocation->setChecked(
		Host::GetBoolSettingValue("Debugger/Analysis", "ImportSymFileFromDefaultLocation", true));
	m_ui.demangleSymbols->setChecked(
		Host::GetBoolSettingValue("Debugger/Analysis", "DemangleSymbols", true));
	m_ui.demangleParameters->setChecked(
		Host::GetBoolSettingValue("Debugger/Analysis", "DemangleParameters", true));

	setupSymbolFileList();

	std::string function_scan_mode = Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanMode");
	for (int i = 0;; i++)
	{
		if (Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames[i] == nullptr)
			break;

		if (function_scan_mode == Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames[i])
			m_ui.functionScanMode->setCurrentIndex(i);
	}

	m_ui.customAddressRange->setChecked(
		Host::GetBoolSettingValue("Debugger/Analysis", "CustomFunctionScanRange", false));
	m_ui.addressRangeStart->setText(QString::fromStdString(
		Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanStartAddress", "")));
	m_ui.addressRangeEnd->setText(QString::fromStdString(
		Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanEndAddress", "")));

	m_ui.grayOutOverwrittenFunctions->setChecked(
		Host::GetBoolSettingValue("Debugger/Analysis", "GenerateFunctionHashes", true));

	connect(m_ui.automaticallyClearSymbols, &QCheckBox::checkStateChanged,
		this, &DebugAnalysisSettingsWidget::updateEnabledStates);
	connect(m_ui.demangleSymbols, &QCheckBox::checkStateChanged,
		this, &DebugAnalysisSettingsWidget::updateEnabledStates);
	connect(m_ui.customAddressRange, &QCheckBox::checkStateChanged,
		this, &DebugAnalysisSettingsWidget::updateEnabledStates);

	updateEnabledStates();
}

DebugAnalysisSettingsWidget::DebugAnalysisSettingsWidget(SettingsWindow* dialog, QWidget* parent)
	: QWidget(parent)
	, m_dialog(dialog)
{
	SettingsInterface* sif = dialog->getSettingsInterface();

	m_ui.setupUi(this);

	// Make sure the user doesn't select symbol sources from both the global
	// settings and the per-game settings, as these settings will conflict with
	// each other. It only really makes sense to modify these settings on a
	// per-game basis anyway.
	if (dialog->isPerGameSettings())
	{
		SettingWidgetBinder::BindWidgetToBoolSetting(
			sif, m_ui.automaticallyClearSymbols, "Debugger/Analysis", "AutomaticallySelectSymbolsToClear", true);

		m_dialog->registerWidgetHelp(m_ui.automaticallyClearSymbols, tr("Automatically Select Symbols To Clear"), tr("Checked"),
			tr("Automatically delete symbols that were generated by any previous analysis runs."));

		setupSymbolSourceGrid();
	}
	else
	{
		m_ui.clearExistingSymbolsGroup->hide();
	}

	SettingWidgetBinder::BindWidgetToBoolSetting(
		sif, m_ui.importFromElf, "Debugger/Analysis", "ImportSymbolsFromELF", true);
	SettingWidgetBinder::BindWidgetToBoolSetting(
		sif, m_ui.importSymFileFromDefaultLocation, "Debugger/Analysis", "ImportSymFileFromDefaultLocation", true);
	SettingWidgetBinder::BindWidgetToBoolSetting(
		sif, m_ui.demangleSymbols, "Debugger/Analysis", "DemangleSymbols", true);
	SettingWidgetBinder::BindWidgetToBoolSetting(
		sif, m_ui.demangleParameters, "Debugger/Analysis", "DemangleParameters", true);

	m_dialog->registerWidgetHelp(m_ui.importFromElf, tr("Import From ELF"), tr("Checked"),
		tr("Import symbol tables stored in the game's boot ELF."));
	m_dialog->registerWidgetHelp(m_ui.importSymFileFromDefaultLocation, tr("Import Default .sym File"), tr("Checked"),
		tr("Import symbols from a .sym file with the same name as the loaded ISO file on disk if such a file exists."));
	m_dialog->registerWidgetHelp(m_ui.demangleSymbols, tr("Demangle Symbols"), tr("Checked"),
		tr("Demangle C++ symbols during the import process so that the function and global variable names shown in the "
		   "debugger are more readable."));
	m_dialog->registerWidgetHelp(m_ui.demangleParameters, tr("Demangle Parameters"), tr("Checked"),
		tr("Include parameter lists in demangled function names."));

	// Same as above. It only makes sense to load extra symbol files on a
	// per-game basis.
	if (dialog->isPerGameSettings())
	{
		setupSymbolFileList();
	}
	else
	{
		m_ui.symbolFileLabel->hide();
		m_ui.symbolFileTable->hide();
		m_ui.importSymbolFileButtons->hide();
	}

	SettingWidgetBinder::BindWidgetToEnumSetting(
		sif, m_ui.functionScanMode, "Debugger/Analysis", "FunctionScanMode",
		Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames, DebugFunctionScanMode::SCAN_ELF);

	m_dialog->registerWidgetHelp(m_ui.functionScanMode, tr("Scan Mode"), tr("Scan ELF"),
		tr("Choose where the function scanner looks to find functions. This option can be useful if the application "
		   "loads additional code at runtime."));

	// Same as above. It only makes sense to set a custom memory range on a
	// per-game basis.
	if (dialog->isPerGameSettings())
	{
		SettingWidgetBinder::BindWidgetToBoolSetting(
			sif, m_ui.customAddressRange, "Debugger/Analysis", "CustomFunctionScanRange", false);

		m_ui.addressRangeStart->setText(QString::fromStdString(
			getStringSettingValue("Debugger/Analysis", "FunctionScanStartAddress", "")));
		m_ui.addressRangeEnd->setText(QString::fromStdString(
			getStringSettingValue("Debugger/Analysis", "FunctionScanEndAddress", "")));

		connect(m_ui.addressRangeStart, &QLineEdit::textChanged,
			this, &DebugAnalysisSettingsWidget::saveFunctionScanRange);
		connect(m_ui.addressRangeEnd, &QLineEdit::textChanged,
			this, &DebugAnalysisSettingsWidget::saveFunctionScanRange);

		m_dialog->registerWidgetHelp(m_ui.customAddressRange, tr("Custom Address Range"), tr("Unchecked"),
			tr("Whether to look for functions from the address range specified (Checked), or from the ELF segment "
			   "containing the entry point (Unchecked)."));
	}
	else
	{
		m_ui.customAddressRange->hide();
		m_ui.customAddressRangeLineEdits->hide();
	}

	SettingWidgetBinder::BindWidgetToBoolSetting(
		sif, m_ui.grayOutOverwrittenFunctions, "Debugger/Analysis", "GenerateFunctionHashes", true);

	m_dialog->registerWidgetHelp(m_ui.grayOutOverwrittenFunctions, tr("Gray Out Symbols For Overwritten Functions"), tr("Checked"),
		tr("Generate hashes for all the detected functions, and gray out the symbols displayed in the debugger for "
		   "functions that no longer match."));

	connect(m_ui.automaticallyClearSymbols, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates);
	connect(m_ui.demangleSymbols, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates);
	connect(m_ui.customAddressRange, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates);

	updateEnabledStates();
}

void DebugAnalysisSettingsWidget::parseSettingsFromWidgets(Pcsx2Config::DebugAnalysisOptions& output)
{
	output.AutomaticallySelectSymbolsToClear = m_ui.automaticallyClearSymbols->isChecked();

	for (const auto& [name, temp] : m_symbol_sources)
	{
		DebugSymbolSource& source = output.SymbolSources.emplace_back();
		source.Name = name;
		source.ClearDuringAnalysis = temp.check_box->isChecked();
	}

	output.ImportSymbolsFromELF = m_ui.importFromElf->isChecked();
	output.ImportSymFileFromDefaultLocation = m_ui.importSymFileFromDefaultLocation->isChecked();
	output.DemangleSymbols = m_ui.demangleSymbols->isChecked();
	output.DemangleParameters = m_ui.demangleParameters->isChecked();

	for (int i = 0; i < m_symbol_file_model->rowCount(); i++)
	{
		DebugExtraSymbolFile& file = output.ExtraSymbolFiles.emplace_back();

		file.Path = m_symbol_file_model->item(i, PATH_COLUMN)->text().toStdString();
		file.BaseAddress = m_symbol_file_model->item(i, BASE_ADDRESS_COLUMN)->text().toStdString();
		file.Condition = m_symbol_file_model->item(i, CONDITION_COLUMN)->text().toStdString();
	}

	output.FunctionScanMode = static_cast<DebugFunctionScanMode>(m_ui.functionScanMode->currentIndex());
	output.CustomFunctionScanRange = m_ui.customAddressRange->isChecked();
	output.FunctionScanStartAddress = m_ui.addressRangeStart->text().toStdString();
	output.FunctionScanEndAddress = m_ui.addressRangeEnd->text().toStdString();
}

void DebugAnalysisSettingsWidget::setupSymbolSourceGrid()
{
	QGridLayout* layout = new QGridLayout(m_ui.symbolSourceGrid);

	if (!m_dialog || m_dialog->getSerial() == QtHost::GetCurrentGameSerial().toStdString())
	{
		// Add symbol sources for which the user has already selected whether or
		// not they should be cleared.
		int existing_symbol_source_count = getIntSettingValue("Debugger/Analysis/SymbolSources", "Count", 0);
		for (int i = 0; i < existing_symbol_source_count; i++)
		{
			std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i);
			std::string name = getStringSettingValue(section.c_str(), "Name", "");
			bool value = getBoolSettingValue(section.c_str(), "ClearDuringAnalysis", false);

			SymbolSourceTemp& source = m_symbol_sources[name];
			source.previous_value = value;
			source.modified_by_user = true;
		}

		// Add any more symbol sources for which the user hasn't made a
		// selection. These are separate since we don't want to have to store
		// configuration data for them.
		R5900SymbolGuardian.Read([&](const ccc::SymbolDatabase& database) {
			for (const ccc::SymbolSource& symbol_source : database.symbol_sources)
			{
				if (m_symbol_sources.find(symbol_source.name()) == m_symbol_sources.end() && symbol_source.name() != "Built-In")
				{
					SymbolSourceTemp& source = m_symbol_sources[symbol_source.name()];
					source.previous_value = SymbolImporter::ShouldClearSymbolsFromSourceByDefault(symbol_source.name());
					source.modified_by_user = false;
				}
			}
		});

		if (m_symbol_sources.empty())
		{
			m_ui.symbolSourceErrorMessage->setText(tr("<i>No symbol sources in database.</i>"));
			m_ui.symbolSourceScrollArea->hide();
			return;
		}

		// Create the check boxes.
		int i = 0;
		for (auto& [name, temp] : m_symbol_sources)
		{
			temp.check_box = new QCheckBox(QString::fromStdString(name));
			temp.check_box->setChecked(temp.previous_value);
			layout->addWidget(temp.check_box, i / 2, i % 2);

			connect(temp.check_box, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::symbolSourceCheckStateChanged);

			i++;
		}
	}
	else
	{
		m_ui.symbolSourceErrorMessage->setText(tr("<i>Start this game to modify the symbol sources list.</i>"));
		m_ui.symbolSourceScrollArea->hide();
		return;
	}

	m_ui.symbolSourceErrorMessage->hide();
}

void DebugAnalysisSettingsWidget::symbolSourceCheckStateChanged()
{
	QCheckBox* check_box = qobject_cast<QCheckBox*>(sender());
	if (!check_box)
		return;

	auto temp = m_symbol_sources.find(check_box->text().toStdString());
	if (temp == m_symbol_sources.end())
		return;

	temp->second.modified_by_user = true;

	saveSymbolSources();
}

void DebugAnalysisSettingsWidget::saveSymbolSources()
{
	if (!m_dialog)
		return;

	SettingsInterface* sif = m_dialog->getSettingsInterface();
	if (!sif)
		return;

	// Clean up old configuration entries.
	int old_count = sif->GetIntValue("Debugger/Analysis/SymbolSources", "Count");
	for (int i = 0; i < old_count; i++)
	{
		std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i);
		sif->RemoveSection(section.c_str());
	}

	sif->RemoveSection("Debugger/Analysis/SymbolSources");

	int symbol_sources_to_save = 0;
	for (auto& [name, temp] : m_symbol_sources)
		if (temp.modified_by_user)
			symbol_sources_to_save++;

	if (symbol_sources_to_save == 0)
		return;

	// Make new configuration entries.
	sif->SetIntValue("Debugger/Analysis/SymbolSources", "Count", symbol_sources_to_save);

	int i = 0;
	for (auto& [name, temp] : m_symbol_sources)
	{
		if (!temp.modified_by_user)
			continue;

		std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i);
		sif->SetStringValue(section.c_str(), "Name", name.c_str());
		sif->SetBoolValue(section.c_str(), "ClearDuringAnalysis", temp.check_box->isChecked());

		i++;
	}

	QtHost::SaveGameSettings(sif, true);
	g_emu_thread->reloadGameSettings();
}

void DebugAnalysisSettingsWidget::setupSymbolFileList()
{
	m_symbol_file_model = new QStandardItemModel(0, SYMBOL_FILE_COLUMN_COUNT, m_ui.symbolFileTable);

	QStringList headers;
	headers.emplace_back(tr("Path"));
	headers.emplace_back(tr("Base Address"));
	headers.emplace_back(tr("Condition"));
	m_symbol_file_model->setHorizontalHeaderLabels(headers);

	m_ui.symbolFileTable->setModel(m_symbol_file_model);

	m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(PATH_COLUMN, QHeaderView::Stretch);
	m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(BASE_ADDRESS_COLUMN, QHeaderView::Fixed);
	m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(CONDITION_COLUMN, QHeaderView::Fixed);

	m_ui.symbolFileTable->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);

	int extra_symbol_file_count = getIntSettingValue("Debugger/Analysis/ExtraSymbolFiles", "Count", 0);
	for (int i = 0; i < extra_symbol_file_count; i++)
	{
		std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i);

		int row = m_symbol_file_model->rowCount();
		if (!m_symbol_file_model->insertRow(row))
			continue;

		QStandardItem* path_item = new QStandardItem();
		path_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "Path", "")));
		m_symbol_file_model->setItem(row, PATH_COLUMN, path_item);

		QStandardItem* base_address_item = new QStandardItem();
		base_address_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "BaseAddress")));
		m_symbol_file_model->setItem(row, BASE_ADDRESS_COLUMN, base_address_item);

		QStandardItem* condition_item = new QStandardItem();
		condition_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "Condition")));
		m_symbol_file_model->setItem(row, CONDITION_COLUMN, condition_item);
	}

	connect(m_ui.addSymbolFile, &QPushButton::clicked, this, &DebugAnalysisSettingsWidget::addSymbolFile);
	connect(m_ui.removeSymbolFile, &QPushButton::clicked, this, &DebugAnalysisSettingsWidget::removeSymbolFile);

	connect(m_ui.symbolFileTable->selectionModel(), &QItemSelectionModel::selectionChanged,
		this, &DebugAnalysisSettingsWidget::updateEnabledStates);

	connect(m_symbol_file_model, &QStandardItemModel::dataChanged,
		this, &DebugAnalysisSettingsWidget::saveSymbolFiles);
	connect(m_symbol_file_model, &QStandardItemModel::dataChanged,
		this, &DebugAnalysisSettingsWidget::updateEnabledStates);
}

void DebugAnalysisSettingsWidget::addSymbolFile()
{
	std::string path = Path::ToNativePath(QFileDialog::getOpenFileName(this, tr("Add Symbol File")).toStdString());
	if (path.empty())
		return;

	std::string relative_path = Path::MakeRelative(path, EmuFolders::GameSettings);
	if (!relative_path.starts_with(".."))
		path = std::move(relative_path);

	int row = m_symbol_file_model->rowCount();
	if (!m_symbol_file_model->insertRow(row))
		return;

	QStandardItem* path_item = new QStandardItem();
	path_item->setText(QString::fromStdString(path));
	m_symbol_file_model->setItem(row, PATH_COLUMN, path_item);

	QStandardItem* base_address_item = new QStandardItem();
	base_address_item->setText("");
	m_symbol_file_model->setItem(row, BASE_ADDRESS_COLUMN, base_address_item);

	QStandardItem* condition_item = new QStandardItem();
	condition_item->setText("");
	m_symbol_file_model->setItem(row, CONDITION_COLUMN, condition_item);

	saveSymbolFiles();
	updateEnabledStates();
}

void DebugAnalysisSettingsWidget::removeSymbolFile()
{
	QItemSelectionModel* selection_model = m_ui.symbolFileTable->selectionModel();
	if (!selection_model)
		return;

	while (!selection_model->selectedIndexes().isEmpty())
	{
		QModelIndex index = selection_model->selectedIndexes().first();
		m_symbol_file_model->removeRow(index.row(), index.parent());
	}

	saveSymbolFiles();
	updateEnabledStates();
}

void DebugAnalysisSettingsWidget::saveSymbolFiles()
{
	if (!m_dialog)
		return;

	SettingsInterface* sif = m_dialog->getSettingsInterface();
	if (!sif)
		return;

	// Clean up old configuration entries.
	int old_count = sif->GetIntValue("Debugger/Analysis/ExtraSymbolFiles", "Count");
	for (int i = 0; i < old_count; i++)
	{
		std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i);
		sif->RemoveSection(section.c_str());
	}

	sif->RemoveSection("Debugger/Analysis/ExtraSymbolFiles");

	if (m_symbol_file_model->rowCount() == 0)
		return;

	// Make new configuration entries.
	sif->SetIntValue("Debugger/Analysis/ExtraSymbolFiles", "Count", m_symbol_file_model->rowCount());

	for (int i = 0; i < m_symbol_file_model->rowCount(); i++)
	{
		std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i);

		if (QStandardItem* path_item = m_symbol_file_model->item(i, PATH_COLUMN))
			sif->SetStringValue(section.c_str(), "Path", path_item->text().toStdString().c_str());

		if (QStandardItem* base_address_item = m_symbol_file_model->item(i, BASE_ADDRESS_COLUMN))
			sif->SetStringValue(section.c_str(), "BaseAddress", base_address_item->text().toStdString().c_str());

		if (QStandardItem* condition_item = m_symbol_file_model->item(i, CONDITION_COLUMN))
			sif->SetStringValue(section.c_str(), "Condition", condition_item->text().toStdString().c_str());
	}

	QtHost::SaveGameSettings(sif, true);
	g_emu_thread->reloadGameSettings();
}

void DebugAnalysisSettingsWidget::saveFunctionScanRange()
{
	if (!m_dialog)
		return;

	SettingsInterface* sif = m_dialog->getSettingsInterface();
	if (!sif)
		return;

	QString start_address = m_ui.addressRangeStart->text();
	QString end_address = m_ui.addressRangeEnd->text();

	sif->SetStringValue("Debugger/Analysis", "FunctionScanStartAddress", start_address.toStdString().c_str());
	sif->SetStringValue("Debugger/Analysis", "FunctionScanEndAddress", end_address.toStdString().c_str());

	QtHost::SaveGameSettings(sif, true);
	g_emu_thread->reloadGameSettings();
}

void DebugAnalysisSettingsWidget::updateEnabledStates()
{
	m_ui.symbolSourceScrollArea->setEnabled(!m_ui.automaticallyClearSymbols->isChecked());
	m_ui.symbolSourceErrorMessage->setEnabled(!m_ui.automaticallyClearSymbols->isChecked());
	m_ui.demangleParameters->setEnabled(m_ui.demangleSymbols->isChecked());
	m_ui.removeSymbolFile->setEnabled(
		m_ui.symbolFileTable->selectionModel() && m_ui.symbolFileTable->selectionModel()->hasSelection());
	m_ui.customAddressRangeLineEdits->setEnabled(m_ui.customAddressRange->isChecked());
}

std::string DebugAnalysisSettingsWidget::getStringSettingValue(
	const char* section, const char* key, const char* default_value)
{
	if (m_dialog)
		return m_dialog->getEffectiveStringValue(section, key, default_value);

	return Host::GetStringSettingValue(section, key, default_value);
}

bool DebugAnalysisSettingsWidget::getBoolSettingValue(
	const char* section, const char* key, bool default_value)
{
	if (m_dialog)
		return m_dialog->getEffectiveBoolValue(section, key, default_value);

	return Host::GetBoolSettingValue(section, key, default_value);
}

int DebugAnalysisSettingsWidget::getIntSettingValue(
	const char* section, const char* key, int default_value)
{
	if (m_dialog)
		return m_dialog->getEffectiveIntValue(section, key, default_value);

	return Host::GetIntSettingValue(section, key, default_value);
}
