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

#include "LogWindow.h"
#include "MainWindow.h"
#include "QtHost.h"
#include "SettingWidgetBinder.h"
#include "VMManager.h"

#include <QtCore/QLatin1StringView>
#include <QtCore/QUtf8StringView>
#include <QtGui/QIcon>
#include <QtWidgets/QCheckBox>
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QMenuBar>
#include <QtWidgets/QScrollBar>

#include <mutex>

// Need a lock so the other threads don't try to write to a deleting window.
LogWindow* g_log_window;
static std::mutex s_log_mutex;

static LOGLEVEL GetWindowLogLevel()
{
#ifdef _DEBUG
	return LOGLEVEL_DEBUG;
#else
	return (IsDevBuild || Host::GetBaseBoolSettingValue("Logging", "EnableVerbose", false)) ? LOGLEVEL_DEV : LOGLEVEL_INFO;
#endif
}

LogWindow::LogWindow(bool attach_to_main)
	: QMainWindow()
	, m_attached_to_main_window(attach_to_main)
{
	restoreSize();
	createUi();

	Log::SetHostOutputLevel(GetWindowLogLevel(), &LogWindow::logCallback);
}

LogWindow::~LogWindow() = default;

void LogWindow::updateSettings()
{
	std::unique_lock lock(s_log_mutex);

	const bool new_enabled = Host::GetBaseBoolSettingValue("Logging", "EnableLogWindow", false) && !Host::InNoGUIMode();
	const bool attach_to_main = Host::GetBaseBoolSettingValue("Logging", "AttachLogWindowToMainWindow", true);
	const bool curr_enabled = Log::IsHostOutputEnabled();
	const bool input_enabled = Host::GetBaseBoolSettingValue("Logging", "ShowEESIOInput");

	if(g_log_window && g_log_window->m_line_input)
	{
		g_log_window->m_input_widget->setVisible(input_enabled);
	}

	if (new_enabled == curr_enabled)
	{
		if (g_log_window && g_log_window->m_attached_to_main_window != attach_to_main)
		{
			g_log_window->m_attached_to_main_window = attach_to_main;
			if (attach_to_main)
				g_log_window->reattachToMainWindow();
		}

		// Update level.
		if (new_enabled)
			Log::SetHostOutputLevel(GetWindowLogLevel(), &LogWindow::logCallback);

		return;
	}

	if (new_enabled)
	{
		g_log_window = new LogWindow(attach_to_main);
		if (attach_to_main && g_main_window && g_main_window->isVisible())
			g_log_window->reattachToMainWindow();

		g_log_window->show();
	}
	else if (g_log_window)
	{
		g_log_window->m_destroying = true;
		g_log_window->close();
		g_log_window->deleteLater();
		g_log_window = nullptr;
	}
}

void LogWindow::destroy()
{
	std::unique_lock lock(s_log_mutex);
	if (!g_log_window)
		return;

	g_log_window->m_destroying = true;
	g_log_window->close();
	g_log_window->deleteLater();
	g_log_window = nullptr;
}

void LogWindow::reattachToMainWindow()
{
	// Skip when maximized.
	if (g_main_window->windowState() & (Qt::WindowMaximized | Qt::WindowFullScreen))
		return;

	const QPoint new_pos = g_main_window->pos() + QPoint(g_main_window->width() + 10, 0);
	if (pos() != new_pos)
		move(new_pos);
}

void LogWindow::updateWindowTitle()
{
	QString title;

	const QString& serial = QtHost::GetCurrentGameSerial();

	if (QtHost::IsVMValid() && !serial.isEmpty())
	{
		const QFileInfo fi(QtHost::GetCurrentGamePath());
		title = tr("Log Window - %1 [%2]").arg(serial).arg(fi.fileName());
	}
	else
	{
		title = tr("Log Window");
	}

	setWindowTitle(title);
}

void LogWindow::createUi()
{
	setWindowIcon(QtHost::GetAppIcon());
	setWindowFlag(Qt::WindowCloseButtonHint, false);
	updateWindowTitle();

	QAction* action;

	QMenuBar* menu = new QMenuBar(this);
	setMenuBar(menu);

	QMenu* log_menu = menu->addMenu("&Log");
	action = log_menu->addAction(tr("&Clear"));
	connect(action, &QAction::triggered, this, &LogWindow::onClearTriggered);
	action = log_menu->addAction(tr("&Save..."));
	connect(action, &QAction::triggered, this, &LogWindow::onSaveTriggered);

	log_menu->addSeparator();

	action = log_menu->addAction(tr("Cl&ose"));
	connect(action, &QAction::triggered, this, &LogWindow::close);

	QMenu* settings_menu = menu->addMenu(tr("&Settings"));

#if 0
	// TODO: These are duplicated with the main window...
	action = settings_menu->addAction(tr("Log To &System Console"));
	action->setCheckable(true);
	SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "EnableSystemConsole", false);

	action = settings_menu->addAction(tr("Log To &Debug Console"));
	action->setCheckable(true);
	SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "EnableDebugConsole", false);

	action = settings_menu->addAction(tr("Log To &File"));
	action->setCheckable(true);
	SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "EnableFileLogging", false);

	settings_menu->addSeparator();
#endif

	action = settings_menu->addAction(tr("Attach To &Main Window"));
	action->setCheckable(true);
	SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "AttachLogWindowToMainWindow", true);

	action = settings_menu->addAction(tr("Show &Timestamps"));
	action->setCheckable(true);
	SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "EnableTimestamps", true);

	action = settings_menu->addAction(tr("Show EE SIO &Input"));
	action->setCheckable(true);
	SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, action, "Logging", "ShowEESIOInput", false);

	settings_menu->addSeparator();

	// TODO: Log Level

	m_text = new QPlainTextEdit(this);
	m_text->setReadOnly(true);
	m_text->setUndoRedoEnabled(false);
	m_text->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse);
	m_text->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
	m_text->setWordWrapMode(QTextOption::WrapAnywhere);
	m_text->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

	m_line_input = new QLineEdit(this);
	connect(m_line_input, &QLineEdit::returnPressed, this, &LogWindow::onInputEntered);
	
	m_local_echo_checkbox = new QCheckBox(tr("Local Echo"), this);
	m_local_echo_checkbox->setChecked(m_local_echo);
	connect(m_local_echo_checkbox, &QCheckBox::checkStateChanged, this, [&](Qt::CheckState state)
	{
		m_local_echo = state == Qt::CheckState::Checked;
	});
	
	m_newline_on_enter_checkbox = new QCheckBox(tr("Newline on send"), this);
	m_newline_on_enter_checkbox->setChecked(m_newline_on_enter);
	connect(m_newline_on_enter_checkbox, &QCheckBox::checkStateChanged, this, [&](Qt::CheckState state)
	{
		m_newline_on_enter = state == Qt::CheckState::Checked;
	});

	m_input_hbox = new QHBoxLayout(this);
	m_input_hbox->addWidget(m_line_input, 1);
	m_input_hbox->addWidget(m_local_echo_checkbox);
	m_input_hbox->addWidget(m_newline_on_enter_checkbox);
	m_input_hbox->setSpacing(8);

#if defined(_WIN32)
	QFont font("Consolas");
	font.setPointSize(10);
#elif defined(__APPLE__)
	QFont font("Monaco");
	font.setPointSize(11);
#else
	QFont font("Monospace");
	font.setStyleHint(QFont::TypeWriter);
#endif
	m_text->setFont(font);

	QWidget* central_widget = new QWidget(this);
	m_input_widget = new QWidget(this);
	m_input_widget->setVisible(Host::GetBaseBoolSettingValue("Logging", "ShowEESIOInput"));
	m_input_widget->setLayout(m_input_hbox);

	QVBoxLayout* vlayout = new QVBoxLayout(central_widget);
	vlayout->setContentsMargins(0, 0, 0, 0);
	vlayout->setSpacing(0);
	vlayout->addWidget(m_text);
	vlayout->addWidget(m_input_widget);

	central_widget->setLayout(vlayout);
	setCentralWidget(central_widget);
}

void LogWindow::onClearTriggered()
{
	m_text->clear();
}

void LogWindow::onSaveTriggered()
{
	const QString path = QFileDialog::getSaveFileName(this, tr("Select Log File"), QString(), tr("Log Files (*.txt)"));
	if (path.isEmpty())
		return;

	QFile file(path);
	if (!file.open(QFile::WriteOnly | QFile::Text))
	{
		QMessageBox::critical(this, tr("Error"), tr("Failed to open file for writing."));
		return;
	}

	file.write(m_text->toPlainText().toUtf8());
	file.close();

	appendMessage(LOGLEVEL_INFO, Color_Default, tr("Log was written to %1.\n").arg(path));
}

void LogWindow::logCallback(LOGLEVEL level, ConsoleColors color, std::string_view message)
{
	std::unique_lock lock(s_log_mutex);
	if (!g_log_window)
		return;

	// I don't like the memory allocations here either...
	QString qmessage;
	qmessage.reserve(message.length() + 1);
	qmessage.append(QUtf8StringView(message.data(), message.length()));
	qmessage.append(QChar('\n'));

	if (g_emu_thread->isOnUIThread())
	{
		g_log_window->appendMessage(static_cast<u32>(level), static_cast<u32>(color), qmessage);
	}
	else
	{
		QMetaObject::invokeMethod(g_log_window, "appendMessage", Qt::QueuedConnection,
			Q_ARG(quint32, static_cast<u32>(level)), Q_ARG(quint32, static_cast<u32>(color)),
			Q_ARG(const QString&, qmessage));
	}
}

void LogWindow::closeEvent(QCloseEvent* event)
{
	if (!m_destroying)
	{
		event->ignore();
		return;
	}
	Log::SetHostOutputLevel(LOGLEVEL_NONE, nullptr);

	saveSize();

	QMainWindow::closeEvent(event);
}

void LogWindow::appendMessage(quint32 level, quint32 color, const QString& message)
{
	QTextCursor temp_cursor = m_text->textCursor();
	QScrollBar* scrollbar = m_text->verticalScrollBar();
	const bool cursor_at_end = temp_cursor.atEnd();
	const bool scroll_at_end = scrollbar->sliderPosition() == scrollbar->maximum();

	temp_cursor.movePosition(QTextCursor::End);

	{
		static constexpr const QColor qcolors[2][ConsoleColors_Count] = {
			// Light theme
			{
				QColor(0, 0, 0), // Color_Default
				QColor(0, 0, 0), // Color_Black
				QColor(128, 0, 0), // Color_Red
				QColor(0, 128, 0), // Color_Green
				QColor(0, 0, 128), // Color_Blue
				QColor(160, 0, 160), // Color_Magenta
				QColor(160, 120, 0), // Color_Orange
				QColor(108, 108, 108), // Color_Gray

				QColor(128, 180, 180), // Color_Cyan
				QColor(180, 180, 128), // Color_Yellow
				QColor(160, 160, 160), // Color_White

				QColor(0, 0, 0), // Color_StrongBlack
				QColor(128, 0, 0), // Color_StrongRed
				QColor(0, 128, 0), // Color_StrongGreen
				QColor(0, 0, 128), // Color_StrongBlue
				QColor(160, 0, 160), // Color_StrongMagenta
				QColor(160, 120, 0), // Color_StrongOrange
				QColor(108, 108, 108), // Color_StrongGray

				QColor(128, 180, 180), // Color_StrongCyan
				QColor(180, 180, 128), // Color_StrongYellow
				QColor(160, 160, 160), // Color_StrongWhite
			},
			// Dark theme
			{
				QColor(208, 208, 208), // Color_Default
				QColor(255, 255, 255), // Color_Black
				QColor(180, 0, 0), // Color_Red
				QColor(0, 160, 0), // Color_Green
				QColor(32, 32, 204), // Color_Blue
				QColor(160, 0, 160), // Color_Magenta
				QColor(160, 120, 0), // Color_Orange
				QColor(128, 128, 128), // Color_Gray
				QColor(128, 180, 180), // Color_Cyan
				QColor(180, 180, 128), // Color_Yellow
				QColor(160, 160, 160), // Color_White
				QColor(255, 255, 255), // Color_StrongBlack
				QColor(180, 0, 0), // Color_StrongRed
				QColor(0, 160, 0), // Color_StrongGreen
				QColor(32, 32, 204), // Color_StrongBlue
				QColor(160, 0, 160), // Color_StrongMagenta
				QColor(160, 120, 0), // Color_StrongOrange
				QColor(128, 128, 128), // Color_StrongGray
				QColor(128, 180, 180), // Color_StrongCyan
				QColor(180, 180, 128), // Color_StrongYellow
				QColor(160, 160, 160), // Color_StrongWhite
			},
		};

		static constexpr const QColor timestamp_color = QColor(0xcc, 0xcc, 0xcc);

		QTextCharFormat format = temp_cursor.charFormat();

		if (Log::AreTimestampsEnabled())
		{
			const float message_time = Log::GetCurrentMessageTime();
			const QString qtimestamp = QStringLiteral("[%1] ").arg(message_time, 10, 'f', 4);
			format.setForeground(QBrush(timestamp_color));
			temp_cursor.setCharFormat(format);
			temp_cursor.insertText(qtimestamp);
		}

		const bool dark = static_cast<u32>(QtHost::IsDarkApplicationTheme());

		// message has \n already
		format.setForeground(QBrush(qcolors[static_cast<u32>(dark)][color]));
		temp_cursor.setCharFormat(format);
		temp_cursor.insertText(message);
	}

	if (cursor_at_end)
	{
		if (scroll_at_end)
		{
			m_text->setTextCursor(temp_cursor);
			scrollbar->setSliderPosition(scrollbar->maximum());
		}
		else
		{
			// Can't let changing the cursor affect the scroll bar...
			const int pos = scrollbar->sliderPosition();
			m_text->setTextCursor(temp_cursor);
			scrollbar->setSliderPosition(pos);
		}
	}
}

void LogWindow::onInputEntered()
{
	QString text = m_line_input->text();
	if (text.isEmpty() && !m_newline_on_enter)
		return;

	if(m_newline_on_enter)
		text.append('\n');

	std::string str = text.toUtf8().toStdString();

	if(VMManager::WriteBytesToEESIORXFIFO({reinterpret_cast<const u8*>(str.data()), str.size()}))
	{
		m_line_input->clear();

		if(m_local_echo)
			// appendMessage expects a newline to be at the end of the string
			appendMessage(0, 0, m_newline_on_enter ? text : (text + '\n') );

		QTextCursor cursor(m_text->textCursor());
		cursor.movePosition(QTextCursor::End);
		m_text->setTextCursor(cursor);
	}
}

void LogWindow::saveSize()
{
	const int current_width = Host::GetBaseIntSettingValue("UI", "LogWindowWidth", DEFAULT_WIDTH);
	const int current_height = Host::GetBaseIntSettingValue("UI", "LogWindowHeight", DEFAULT_HEIGHT);
	const QSize wsize = size();

	bool changed = false;
	if (current_width != wsize.width())
	{
		Host::SetBaseIntSettingValue("UI", "LogWindowWidth", wsize.width());
		changed = true;
	}
	if (current_height != wsize.height())
	{
		Host::SetBaseIntSettingValue("UI", "LogWindowHeight", wsize.height());
		changed = true;
	}

	if (changed)
		Host::CommitBaseSettingChanges();
}

void LogWindow::restoreSize()
{
	const int width = Host::GetBaseIntSettingValue("UI", "LogWindowWidth", DEFAULT_WIDTH);
	const int height = Host::GetBaseIntSettingValue("UI", "LogWindowHeight", DEFAULT_HEIGHT);
	resize(width, height);
}
