#include "stdafx.h"

#include "log_viewer.h"
#include "gui_settings.h"
#include "syntax_highlighter.h"
#include "config_checker.h"
#include "Crypto/unzip.h"

#include <QActionGroup>
#include <QApplication>
#include <QClipboard>
#include <QMenu>
#include <QFile>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextStream>
#include <QHBoxLayout>
#include <QFontDatabase>
#include <QMimeData>
#include <QScrollBar>
#include <QMessageBox>

#include <deque>
#include <map>

LOG_CHANNEL(gui_log, "GUI");

log_viewer::log_viewer(std::shared_ptr<gui_settings> gui_settings)
    : m_gui_settings(std::move(gui_settings))
{
	update_title();

	setObjectName("log_viewer");
	setAttribute(Qt::WA_DeleteOnClose);
	setAttribute(Qt::WA_StyledBackground);
	setAcceptDrops(true);
	setMinimumSize(QSize(200, 150)); // seems fine on win 10
	resize(QSize(620, 395));

	m_path_last = m_gui_settings->GetValue(gui::fd_log_viewer).toString();
	m_show_timestamps = m_gui_settings->GetValue(gui::lv_show_timestamps).toBool();
	m_show_threads = m_gui_settings->GetValue(gui::lv_show_threads).toBool();
	m_log_levels = std::bitset<32>(m_gui_settings->GetValue(gui::lv_log_levels).toUInt());

	m_log_text = new QPlainTextEdit(this);
	m_log_text->setReadOnly(true);
	m_log_text->setContextMenuPolicy(Qt::CustomContextMenu);
	m_log_text->setWordWrapMode(QTextOption::NoWrap);
	m_log_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
	m_log_text->installEventFilter(this);

	// m_log_text syntax highlighter
	m_log_highlighter = new LogHighlighter(m_log_text->document());

	QHBoxLayout* layout = new QHBoxLayout();
	layout->addWidget(m_log_text);

	setLayout(layout);

	connect(m_log_text, &QWidget::customContextMenuRequested, this, &log_viewer::show_context_menu);
}

void log_viewer::update_title()
{
	QString suffix;

	if (!m_filter_term.isEmpty())
	{
		suffix = tr(" | Filter '%0'").arg(m_filter_term);
	}

	if (!m_exclude_term.isEmpty())
	{
		suffix += tr(" | Exclude '%0'").arg(m_exclude_term);
	}

	setWindowTitle(tr("Log Viewer%0").arg(suffix));
}

void log_viewer::show_context_menu(const QPoint& pos)
{
	QMenu menu;
	QAction* clear   = new QAction(tr("&Clear"));
	QAction* copy    = new QAction(tr("&Copy"));
	QAction* open    = new QAction(tr("&Open log file"));
	QAction* save    = new QAction(tr("&Save filtered log"));
	QAction* filter  = new QAction(tr("&Filter log%0").arg(m_filter_term.isEmpty() ? "" : QString(" (%0)").arg(m_filter_term)));
	QAction* exclude = new QAction(tr("&Exclude%0").arg(m_exclude_term.isEmpty() ? "" : QString(" (%0)").arg(m_exclude_term)));
	QAction* config  = new QAction(tr("&Check config"));

	QAction* timestamps = new QAction(tr("&Show Timestamps"));
	timestamps->setCheckable(true);
	timestamps->setChecked(m_show_timestamps);

	QAction* threads = new QAction(tr("&Show Threads"));
	threads->setCheckable(true);
	threads->setChecked(m_show_threads);

	QAction* last_actions_only = new QAction(tr("&Last actions only"));
	last_actions_only->setCheckable(true);
	last_actions_only->setChecked(m_last_actions_only);

	QActionGroup* log_level_acts = new QActionGroup(this);
	QAction* fatal_act = new QAction(tr("Fatal"), log_level_acts);
	QAction* error_act = new QAction(tr("Error"), log_level_acts);
	QAction* todo_act = new QAction(tr("Todo"), log_level_acts);
	QAction* success_act = new QAction(tr("Success"), log_level_acts);
	QAction* warning_act = new QAction(tr("Warning"), log_level_acts);
	QAction* notice_act = new QAction(tr("Notice"), log_level_acts);
	QAction* trace_act = new QAction(tr("Trace"), log_level_acts);
	log_level_acts->setExclusive(false);

	const auto init_action = [this](QAction* act, logs::level logLevel)
	{
		act->setCheckable(true);
		act->setChecked(m_log_levels.test(static_cast<u32>(logLevel)));

		// This sets the log level properly when the action is triggered.
		connect(act, &QAction::triggered, this, [this, logLevel](bool checked)
		{
			m_log_levels.set(static_cast<u32>(logLevel), checked);
			m_gui_settings->SetValue(gui::lv_log_levels, ::narrow<u32>(m_log_levels.to_ulong()));
			filter_log();
		});
	};

	init_action(fatal_act, logs::level::fatal);
	init_action(error_act, logs::level::error);
	init_action(todo_act, logs::level::todo);
	init_action(success_act, logs::level::success);
	init_action(warning_act, logs::level::warning);
	init_action(notice_act, logs::level::notice);
	init_action(trace_act, logs::level::trace);

	menu.addAction(copy);
	menu.addAction(clear);

	menu.addSeparator();
	menu.addAction(open);
	menu.addAction(config);
	menu.addAction(filter);
	menu.addAction(exclude);
	menu.addAction(save);
	menu.addSeparator();
	menu.addAction(timestamps);
	menu.addAction(threads);
	menu.addAction(last_actions_only);
	menu.addSeparator();
	menu.addActions(log_level_acts->actions());

	connect(copy, &QAction::triggered, this, [this]()
	{
		m_log_text->copy();
	});

	connect(clear, &QAction::triggered, this, [this]()
	{
		m_log_text->clear();
		m_full_log.clear();
	});

	connect(open, &QAction::triggered, this, [this]()
	{
		const QString file_path = QFileDialog::getOpenFileName(this, tr("Select log file"), m_path_last, tr("Log files (*.log *.gz);;All files (*.*)"));
		if (file_path.isEmpty())
			return;
		m_path_last = file_path;
		show_log();
	});

	connect(save, &QAction::triggered, this, [this]()
	{
		const QString file_path = QFileDialog::getSaveFileName(this, tr("Save to file"), m_path_last, tr("Log files (*.log *.gz);;All files (*.*)"));
		if (file_path.isEmpty())
			return;

		if (fs::file log_file; log_file.open(file_path.toStdString(), fs::rewrite))
		{
			const QByteArray bytes = m_log_text->toPlainText().toUtf8();

			if (file_path.endsWith(".gz"))
			{
				if (!zip(bytes.constData(), bytes.size(), log_file))
				{
					gui_log.error("Failed to zip filtered log to file '%s'", file_path);
					return;
				}
			}
			else
			{
				log_file.write(bytes.constData(), bytes.size());
			}

			gui_log.success("Exported filtered log to file '%s'", file_path);
		}
		else
		{
			gui_log.error("Failed to export filtered log to file '%s'", file_path);
		}
	});

	connect(config, &QAction::triggered, this, [this]()
	{
		config_checker* dlg = new config_checker(this, m_full_log, true);
		dlg->open();
	});

	connect(filter, &QAction::triggered, this, [this]()
	{
		bool ok = false;
		QString filter_term = QInputDialog::getText(this, tr("Filter log"), tr("Enter text"), QLineEdit::EchoMode::Normal, m_filter_term, &ok);
		if (!ok) return;
		m_filter_term = std::move(filter_term);
		update_title();
		filter_log();
	});

	connect(exclude, &QAction::triggered, this, [this]()
	{
		bool ok = false;
		QString exclude_term = QInputDialog::getText(this, tr("Exclude"), tr("Enter text (comma separated)"), QLineEdit::EchoMode::Normal, m_exclude_term, &ok);
		if (!ok) return;
		m_exclude_term = std::move(exclude_term);
		m_exclude_terms = m_exclude_term.split(',', Qt::SkipEmptyParts);
		update_title();
		filter_log();
	});

	connect(threads, &QAction::toggled, this, [this](bool checked)
	{
		m_show_threads = checked;
		m_gui_settings->SetValue(gui::lv_show_threads, m_show_threads);
		filter_log();
	});

	connect(timestamps, &QAction::toggled, this, [this](bool checked)
	{
		m_show_timestamps = checked;
		m_gui_settings->SetValue(gui::lv_show_timestamps, m_show_timestamps);
		filter_log();
	});

	connect(last_actions_only, &QAction::toggled, this, [this](bool checked)
	{
		m_last_actions_only = checked;
		filter_log();
	});

	const auto obj = qobject_cast<QPlainTextEdit*>(sender());

	QPoint origin;

	if (obj == m_log_text)
	{
		origin = m_log_text->viewport()->mapToGlobal(pos);
	}
	else
	{
		origin = mapToGlobal(pos);
	}

	menu.exec(origin);
}

void log_viewer::show_log()
{
	if (m_path_last.isEmpty())
	{
		return;
	}

	m_full_log.clear();
	m_log_text->clear();
	m_log_text->setPlainText(tr("Loading file..."));
	QApplication::processEvents();

	bool failed = false;

	if (m_path_last.endsWith(".gz"))
	{
		if (fs::file file{m_path_last.toStdString()})
		{
			const std::vector<u8> decompressed = unzip(file.to_vector<u8>());
			m_full_log = QString::fromUtf8(reinterpret_cast<const char*>(decompressed.data()), decompressed.size());
		}
		else
		{
			failed = true;
		}
	}
	else if (QFile file(m_path_last); file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text))
	{
		// TODO: Due to a bug in Qt 6.5.2 QTextStream::readAll is ridiculously slow to the point where it gets stuck on large files.
		//       In Qt 5.15.2 this was much faster than QFile::readAll. Use QTextStream again once this bug is fixed upstream.
		//QTextStream stream(&file);
		//m_full_log = stream.readAll();
		m_full_log = file.readAll();
	}
	else
	{
		failed = true;
	}

	if (failed)
	{
		gui_log.error("log_viewer: Failed to open %s", m_path_last);
		m_log_text->setPlainText(tr("Failed to open '%0'").arg(m_path_last));
	}
	else
	{
		m_gui_settings->SetValue(gui::fd_log_viewer, m_path_last);
		m_full_log.replace('\0', '0');
	}

	filter_log();
}

void log_viewer::set_text_and_keep_position(const QString& text)
{
	m_log_text->setPlainText(tr("Pasting..."));
	QApplication::processEvents();

	const int pos = m_log_text->verticalScrollBar()->value();
	m_log_text->setPlainText(text);
	m_log_text->verticalScrollBar()->setValue(pos);
}

void log_viewer::filter_log()
{
	if (m_full_log.isEmpty())
	{
		set_text_and_keep_position(m_full_log);
		return;
	}

	m_log_text->setPlainText(tr("Filtering..."));

	std::vector<QString> excluded_log_levels;
	if (!m_log_levels.test(static_cast<u32>(logs::level::fatal)))   excluded_log_levels.push_back("·F ");
	if (!m_log_levels.test(static_cast<u32>(logs::level::error)))   excluded_log_levels.push_back("·E ");
	if (!m_log_levels.test(static_cast<u32>(logs::level::todo)))    excluded_log_levels.push_back("·U ");
	if (!m_log_levels.test(static_cast<u32>(logs::level::success))) excluded_log_levels.push_back("·S ");
	if (!m_log_levels.test(static_cast<u32>(logs::level::warning))) excluded_log_levels.push_back("·W ");
	if (!m_log_levels.test(static_cast<u32>(logs::level::notice)))  excluded_log_levels.push_back("·! ");
	if (!m_log_levels.test(static_cast<u32>(logs::level::trace)))   excluded_log_levels.push_back("·T ");

	if (m_filter_term.isEmpty() && m_exclude_terms.isEmpty() && excluded_log_levels.empty() && m_show_timestamps && m_show_threads && !m_last_actions_only)
	{
		set_text_and_keep_position(m_full_log);
		return;
	}

	QString result;
	QTextStream stream(&m_full_log);
	const QRegularExpression thread_regexp("\\{.*\\} ");
	const QRegularExpression timestamp_regexp("\\d?\\d:\\d\\d:\\d\\d\\.\\d\\d\\d\\d\\d\\d ");

	const auto add_line = [this, &result, &excluded_log_levels, &timestamp_regexp, &thread_regexp](QString& line)
	{
		if (!line.isEmpty())
		{
			for (QStringView log_level_prefix : excluded_log_levels)
			{
				if (line.startsWith(log_level_prefix))
				{
					return;
				}
			}

			for (QStringView term : m_exclude_terms)
			{
				if (line.contains(term))
				{
					return;
				}
			}
		}

		if (!m_filter_term.isEmpty() && !line.contains(m_filter_term))
		{
			return;
		}

		if (line.isEmpty())
		{
			result += "\n";
			return;
		}

		if (!m_show_timestamps)
		{
			line.remove(timestamp_regexp);
		}

		if (!m_show_threads)
		{
			line.remove(thread_regexp);
		}

		if (!line.isEmpty())
		{
			result += line + "\n";
		}
	};

	if (m_last_actions_only)
	{
		if (const int start_pos = m_full_log.lastIndexOf("LDR: Used configuration:"); start_pos >= 0)
		{
			if (!stream.seek(start_pos)) // TODO: is this correct ?
			{
				gui_log.error("Log viewer failed to seek to pos %d of log.", start_pos);
			}

			std::map<QString, std::deque<QString>> all_thread_actions;

			for (QString line = stream.readLine(); !line.isNull(); line = stream.readLine())
			{
				if (const QRegularExpressionMatch match = thread_regexp.match(line); match.hasMatch())
				{
					if (const QString thread_name = match.captured(); !thread_name.isEmpty())
					{
						std::deque<QString>& actions = all_thread_actions[thread_name];
						actions.push_back(line);

						if (actions.size() > 10)
						{
							actions.pop_front();
						}
					}
				}
			}

			for (auto& [thread_name, actions] : all_thread_actions)
			{
				for (QString& line : actions)
				{
					add_line(line);
				}
			}

			set_text_and_keep_position(result);
			return;
		}

		QMessageBox::information(this, tr("Ooops!"), tr("Cannot find any game boot!"));
		// Pass through to regular log filter
	}

	if (!stream.seek(0))
	{
		gui_log.error("Log viewer failed to seek to beginning of log.");
	}

	for (QString line = stream.readLine(); !line.isNull(); line = stream.readLine())
	{
		add_line(line);
	};

	set_text_and_keep_position(result);
}

bool log_viewer::is_valid_file(const QMimeData& md, bool save)
{
	const QList<QUrl> urls = md.urls();

	if (urls.count() > 1)
	{
		return false;
	}

	const QString suffix = QFileInfo(urls[0].fileName()).suffix().toLower();

	if (suffix == "log" || suffix == "gz")
	{
		if (save)
		{
			m_path_last = urls[0].toLocalFile();
		}
		return true;
	}
	return false;
}

void log_viewer::dropEvent(QDropEvent* ev)
{
	if (is_valid_file(*ev->mimeData(), true))
	{
		ev->acceptProposedAction();
		show_log();
	}
}

void log_viewer::dragEnterEvent(QDragEnterEvent* ev)
{
	if (is_valid_file(*ev->mimeData()))
	{
		ev->acceptProposedAction();
	}
}

void log_viewer::dragMoveEvent(QDragMoveEvent* ev)
{
	if (is_valid_file(*ev->mimeData()))
	{
		ev->acceptProposedAction();
	}
}

bool log_viewer::eventFilter(QObject* object, QEvent* event)
{
	if (object != m_log_text)
	{
		return QWidget::eventFilter(object, event);
	}

	if (event->type() == QEvent::KeyPress)
	{
		QKeyEvent* e = static_cast<QKeyEvent*>(event);
		if (e && !e->isAutoRepeat() && e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_F)
		{
			if (m_find_dialog && m_find_dialog->isVisible())
				m_find_dialog->close();

			m_find_dialog.reset(new find_dialog(static_cast<QPlainTextEdit*>(object), this));
		}
	}

	return QWidget::eventFilter(object, event);
}
