#include "logging.hpp"
#include <array>
#include <cerrno>
#include <cstdio>

#include <fcntl.h>
#include <qbytearrayview.h>
#include <qcoreapplication.h>
#include <qdatetime.h>
#include <qendian.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qhashfunctions.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qmutex.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h>
#include <qstring.h>
#include <qstringview.h>
#include <qsysinfo.h>
#include <qtenvironmentvariables.h>
#include <qtextstream.h>
#include <qthread.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <sys/mman.h>
#ifdef __linux__
#include <sys/sendfile.h>
#include <sys/types.h>
#endif
#ifdef __FreeBSD__
#include <unistd.h>
#endif

#include "instanceinfo.hpp"
#include "logcat.hpp"
#include "logging_p.hpp"
#include "logging_qtprivate.cpp" // NOLINT
#include "paths.hpp"
#include "ringbuf.hpp"

QS_LOGGING_CATEGORY(logBare, "quickshell.bare");

namespace qs::log {
using namespace qt_logging_registry;

QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);

namespace {
bool copyFileData(int sourceFd, int destFd, qint64 size) {
	auto usize = static_cast<size_t>(size);

#ifdef __linux__
	off_t offset = 0;
	auto remaining = usize;

	while (remaining > 0) {
		auto r = sendfile(destFd, sourceFd, &offset, remaining);
		if (r == -1) {
			if (errno == EINTR) continue;
			return false;
		}
		if (r == 0) break;
		remaining -= static_cast<size_t>(r);
	}

	return true;
#else
	std::array<char, 64 * 1024> buffer = {};
	auto remaining = usize;

	while (remaining > 0) {
		auto chunk = std::min(remaining, buffer.size());
		auto r = ::read(sourceFd, buffer.data(), chunk);
		if (r == -1) {
			if (errno == EINTR) continue;
			return false;
		}
		if (r == 0) break;

		auto readBytes = static_cast<size_t>(r);
		size_t written = 0;
		while (written < readBytes) {
			auto w = ::write(destFd, buffer.data() + written, readBytes - written);
			if (w == -1) {
				if (errno == EINTR) continue;
				return false;
			}
			written += static_cast<size_t>(w);
		}

		remaining -= readBytes;
	}

	return true;
#endif
}
} // namespace

bool LogMessage::operator==(const LogMessage& other) const {
	// note: not including time
	return this->type == other.type && this->category == other.category && this->body == other.body;
}

size_t qHash(const LogMessage& message) {
	return qHash(message.type) ^ qHash(message.category) ^ qHash(message.body);
}

void LogMessage::formatMessage(
    QTextStream& stream,
    const LogMessage& msg,
    bool color,
    bool timestamp,
    const QString& prefix
) {
	if (!prefix.isEmpty()) {
		if (color) stream << "\033[90m";
		stream << '[' << prefix << ']';
		if (timestamp) stream << ' ';
		if (color) stream << "\033[0m";
	}

	if (timestamp) {
		if (color) stream << "\033[90m";
		stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz");
	}

	if (msg.category == "quickshell.bare") {
		if (!prefix.isEmpty()) stream << ' ';
		stream << msg.body;
	} else {
		if (color) {
			switch (msg.type) {
			case QtDebugMsg: stream << "\033[34m DEBUG"; break;
			case QtInfoMsg: stream << "\033[32m  INFO"; break;
			case QtWarningMsg: stream << "\033[33m  WARN"; break;
			case QtCriticalMsg: stream << "\033[31m ERROR"; break;
			case QtFatalMsg: stream << "\033[31m FATAL"; break;
			}
		} else {
			switch (msg.type) {
			case QtDebugMsg: stream << " DEBUG"; break;
			case QtInfoMsg: stream << "  INFO"; break;
			case QtWarningMsg: stream << "  WARN"; break;
			case QtCriticalMsg: stream << " ERROR"; break;
			case QtFatalMsg: stream << " FATAL"; break;
			}
		}

		const auto isDefault = msg.category == "default";

		if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m";

		if (!isDefault) {
			stream << ' ' << msg.category;
		}

		if (color && msg.type != QtFatalMsg) stream << "\033[0m";

		stream << ": " << msg.body;

		if (color && msg.type == QtFatalMsg) stream << "\033[0m";
	}
}

bool CategoryFilter::shouldDisplay(QtMsgType type) const {
	switch (type) {
	case QtDebugMsg: return this->debug;
	case QtInfoMsg: return this->info;
	case QtWarningMsg: return this->warn;
	case QtCriticalMsg: return this->critical;
	default: return true;
	}
}

void CategoryFilter::apply(QLoggingCategory* category) const {
	category->setEnabled(QtDebugMsg, this->debug);
	category->setEnabled(QtInfoMsg, this->info);
	category->setEnabled(QtWarningMsg, this->warn);
	category->setEnabled(QtCriticalMsg, this->critical);
}

void CategoryFilter::applyRule(
    QLatin1StringView category,
    const qt_logging_registry::QLoggingRule& rule
) {
	auto filterpass = rule.pass(category, QtDebugMsg);
	if (filterpass != 0) this->debug = filterpass > 0;

	filterpass = rule.pass(category, QtInfoMsg);
	if (filterpass != 0) this->info = filterpass > 0;

	filterpass = rule.pass(category, QtWarningMsg);
	if (filterpass != 0) this->warn = filterpass > 0;

	filterpass = rule.pass(category, QtCriticalMsg);
	if (filterpass != 0) this->critical = filterpass > 0;
}

LogManager::LogManager(): stdoutStream(stdout) {}

void LogManager::messageHandler(
    QtMsgType type,
    const QMessageLogContext& context,
    const QString& msg
) {
	auto message = LogMessage(type, QLatin1StringView(context.category), msg.toUtf8());

	auto* self = LogManager::instance();

	auto display = true;

	const auto* key = static_cast<const void*>(context.category);

	if (self->sparseFilters.contains(key)) {
		display = self->sparseFilters.value(key).shouldDisplay(type);
	}

	if (display) {
		auto locker = QMutexLocker(&self->stdoutMutex);
		LogMessage::formatMessage(
		    self->stdoutStream,
		    message,
		    self->colorLogs,
		    self->timestampLogs,
		    self->prefix
		);

		self->stdoutStream << Qt::endl;
	}

	emit self->logMessage(message, display);
}

void LogManager::filterCategory(QLoggingCategory* category) {
	auto* instance = LogManager::instance();

	auto categoryName = QLatin1StringView(category->categoryName());
	auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));

	CategoryFilter filter;

	// We don't respect log filters for qs logs because some distros like to ship
	// default configs that hide everything. QT_LOGGING_RULES is considered via the filter list.
	if (isQs) {
		// QtDebugMsg == 0, so default
		auto defaultLevel = instance->defaultLevels.value(categoryName);

		filter = CategoryFilter();
		// clang-format off
		filter.debug = instance->mDefaultLevel == QtDebugMsg || defaultLevel == QtDebugMsg;
		filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg || defaultLevel == QtInfoMsg;
		filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg || defaultLevel == QtWarningMsg;
		filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg || defaultLevel == QtCriticalMsg;
		// clang-format on
	} else if (instance->lastCategoryFilter) {
		instance->lastCategoryFilter(category);
		filter = CategoryFilter(category);
	}

	for (const auto& rule: *instance->rules) {
		filter.applyRule(categoryName, rule);
	}

	if (isQs && !instance->sparse) {
		// We assume the category name pointer will always be the same and be comparable in the message handler.
		instance->sparseFilters.insert(static_cast<const void*>(category->categoryName()), filter);

		// all enabled by default
		CategoryFilter().apply(category);
	} else {
		filter.apply(category);
	}

	instance->allFilters.insert(categoryName, filter);
}

LogManager* LogManager::instance() {
	static auto* instance = new LogManager(); // NOLINT
	return instance;
}

void LogManager::init(
    bool color,
    bool timestamp,
    bool sparseOnly,
    QtMsgType defaultLevel,
    const QString& rules,
    const QString& prefix
) {
	auto* instance = LogManager::instance();
	instance->colorLogs = color;
	instance->timestampLogs = timestamp;
	instance->sparse = sparseOnly;
	instance->prefix = prefix;
	instance->mDefaultLevel = defaultLevel;
	instance->mRulesString = rules;

	{
		QLoggingSettingsParser parser;
		// Load QT_LOGGING_RULES because we ignore the last category filter for QS messages
		// due to disk config files.
		parser.setContent(qEnvironmentVariable("QT_LOGGING_RULES"));
		instance->rules = new QList(parser.rules());
		parser.setContent(rules);
		instance->rules->append(parser.rules());
	}

	instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);

	if (instance->lastCategoryFilter == &LogManager::filterCategory) {
		qCFatal(logLogging) << "Quickshell's log filter has been installed twice. This is a bug.";
		instance->lastCategoryFilter = nullptr;
	}

	qInstallMessageHandler(&LogManager::messageHandler);

	qCDebug(logLogging) << "Creating offthread logger...";
	auto* thread = new QThread();
	instance->threadProxy.moveToThread(thread);
	thread->start();

	QMetaObject::invokeMethod(
	    &instance->threadProxy,
	    &LoggingThreadProxy::initInThread,
	    Qt::BlockingQueuedConnection
	);

	qCDebug(logLogging) << "Logger initialized.";
}

void initLogCategoryLevel(const char* name, QtMsgType defaultLevel) {
	LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel);
}

void LogManager::initFs() {
	QMetaObject::invokeMethod(
	    &LogManager::instance()->threadProxy,
	    "initFs",
	    Qt::BlockingQueuedConnection
	);
}

QString LogManager::rulesString() const { return this->mRulesString; }
QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; }
bool LogManager::isSparse() const { return this->sparse; }

CategoryFilter LogManager::getFilter(QLatin1StringView category) {
	return this->allFilters.value(category);
}

void LoggingThreadProxy::initInThread() {
	this->logging = new ThreadLogging(this);
	this->logging->init();
}

void LoggingThreadProxy::initFs() { this->logging->initFs(); }

void ThreadLogging::init() {
	auto logMfd = memfd_create("quickshell:logs", 0);

	if (logMfd == -1) {
		qCCritical(logLogging) << "Failed to create memfd for initial log storage"
		                       << qt_error_string(-1);
	}

	auto dlogMfd = memfd_create("quickshell:detailedlogs", 0);

	if (dlogMfd == -1) {
		qCCritical(logLogging) << "Failed to create memfd for initial detailed log storage"
		                       << qt_error_string(-1);
	}

	if (logMfd != -1) {
		this->file = new QFile();

		if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) {
			this->fileStream.setDevice(this->file);
		} else {
			qCCritical(logLogging) << "Failed to open early logging memfd.";
		}
	}

	if (dlogMfd != -1) {
		crash::CrashInfo::INSTANCE.logFd = dlogMfd;

		this->detailedFile = new QFile();
		// buffered by WriteBuffer
		if (this->detailedFile
		        ->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle))
		{
			this->detailedWriter.setDevice(this->detailedFile);

			if (!this->detailedWriter.writeHeader()) {
				qCCritical(logLogging) << "Could not write header for detailed logs.";
				this->detailedWriter.setDevice(nullptr);
				delete this->detailedFile;
				this->detailedFile = nullptr;
			}
		} else {
			qCCritical(logLogging) << "Failed to open early detailed logging memfd.";
		}
	}

	// This connection is direct so it works while the event loop is destroyed between
	// QCoreApplication delete and Q(Gui)Application launch.
	QObject::connect(
	    LogManager::instance(),
	    &LogManager::logMessage,
	    this,
	    &ThreadLogging::onMessage,
	    Qt::DirectConnection
	);

	qCDebug(logLogging) << "Created memfd" << logMfd << "for early logs.";
	qCDebug(logLogging) << "Created memfd" << dlogMfd << "for early detailed logs.";
}

void ThreadLogging::initFs() {
	qCDebug(logLogging) << "Starting filesystem logging...";
	auto* runDir = QsPaths::instance()->instanceRunDir();

	if (!runDir) {
		qCCritical(
		    logLogging
		) << "Could not start filesystem logging as the runtime directory could not be created.";
		return;
	}

	auto path = runDir->filePath("log.log");
	auto detailedPath = runDir->filePath("log.qslog");
	auto* file = new QFile(path);
	auto* detailedFile = new QFile(detailedPath);

	if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
		qCCritical(
		    logLogging
		) << "Could not start filesystem logger as the log file could not be created:"
		  << path;
		delete file;
		file = nullptr;
	} else {
		qInfo() << "Saving logs to" << detailedPath;
	}

	// buffered by WriteBuffer
	if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
		qCCritical(
		    logLogging
		) << "Could not start detailed filesystem logger as the log file could not be created:"
		  << detailedPath;
		delete detailedFile;
		detailedFile = nullptr;
	} else {
		struct flock lock = {
		    .l_type = F_WRLCK,
		    .l_whence = SEEK_SET,
		    .l_start = 0,
		    .l_len = 0,
		    .l_pid = 0,
		};

		if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT
			qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from "
			                         "other instances will not work.";
		}

		qCInfo(logLogging) << "Saving detailed logs to" << path;
	}

	qCDebug(logLogging) << "Copying memfd logs to log file...";

	if (file) {
		auto* oldFile = this->file;
		if (oldFile) {
			oldFile->seek(0);

			if (!copyFileData(oldFile->handle(), file->handle(), oldFile->size())) {
				qCritical(logLogging) << "Failed to copy log from memfd with error code " << errno
				                      << qt_error_string(errno);
			}
		}

		this->file = file;
		this->fileStream.setDevice(file);
		delete oldFile;
	}

	if (detailedFile) {
		auto* oldFile = this->detailedFile;
		if (oldFile) {
			oldFile->seek(0);
			if (!copyFileData(oldFile->handle(), detailedFile->handle(), oldFile->size())) {
				qCritical(logLogging) << "Failed to copy detailed log from memfd with error code " << errno
				                      << qt_error_string(errno);
			}
		}

		crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();

		this->detailedFile = detailedFile;
		this->detailedWriter.setDevice(detailedFile);

		if (!oldFile) {
			if (!this->detailedWriter.writeHeader()) {
				qCCritical(logLogging) << "Could not write header for detailed logs.";
				this->detailedWriter.setDevice(nullptr);
				delete this->detailedFile;
				this->detailedFile = nullptr;
			}
		}

		delete oldFile;
	}

	qCDebug(logLogging) << "Switched logging to disk logs.";

	auto* logManager = LogManager::instance();
	QObject::disconnect(logManager, &LogManager::logMessage, this, &ThreadLogging::onMessage);

	QObject::connect(
	    logManager,
	    &LogManager::logMessage,
	    this,
	    &ThreadLogging::onMessage,
	    Qt::QueuedConnection
	);

	qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection.";
}

void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
	if (showInSparse) {
		if (this->fileStream.device() == nullptr) return;
		LogMessage::formatMessage(this->fileStream, msg, false, true);
		this->fileStream << Qt::endl;
	}

	if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) {
		this->detailedWriter.setDevice(nullptr);

		if (this->detailedFile) {
			this->detailedFile->close();
			this->detailedFile = nullptr;
			qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
		}
	}
}

CompressedLogType compressedTypeOf(QtMsgType type) {
	switch (type) {
	case QtDebugMsg: return CompressedLogType::Debug;
	case QtInfoMsg: return CompressedLogType::Info;
	case QtWarningMsg: return CompressedLogType::Warn;
	case QtCriticalMsg:
	case QtFatalMsg: return CompressedLogType::Critical;
	}

	return CompressedLogType::Info; // unreachable under normal conditions
}

QtMsgType typeOfCompressed(CompressedLogType type) {
	switch (type) {
	case CompressedLogType::Debug: return QtDebugMsg;
	case CompressedLogType::Info: return QtInfoMsg;
	case CompressedLogType::Warn: return QtWarningMsg;
	case CompressedLogType::Critical: return QtCriticalMsg;
	}

	return QtInfoMsg; // unreachable under normal conditions
}

void WriteBuffer::setDevice(QIODevice* device) { this->device = device; }
bool WriteBuffer::hasDevice() const { return this->device; }

bool WriteBuffer::flush() {
	auto written = this->device->write(this->buffer);
	auto success = written == this->buffer.length();
	this->buffer.clear();
	return success;
}

void WriteBuffer::writeBytes(const char* data, qsizetype length) {
	this->buffer.append(data, length);
}

void WriteBuffer::writeU8(quint8 data) { this->writeBytes(reinterpret_cast<char*>(&data), 1); }

void WriteBuffer::writeU16(quint16 data) {
	data = qToLittleEndian(data);
	this->writeBytes(reinterpret_cast<char*>(&data), 2);
}

void WriteBuffer::writeU32(quint32 data) {
	data = qToLittleEndian(data);
	this->writeBytes(reinterpret_cast<char*>(&data), 4);
}

void WriteBuffer::writeU64(quint64 data) {
	data = qToLittleEndian(data);
	this->writeBytes(reinterpret_cast<char*>(&data), 8);
}

void DeviceReader::setDevice(QIODevice* device) { this->device = device; }
bool DeviceReader::hasDevice() const { return this->device; }

bool DeviceReader::readBytes(char* data, qsizetype length) {
	return this->device->read(data, length) == length;
}

qsizetype DeviceReader::peekBytes(char* data, qsizetype length) {
	return this->device->peek(data, length);
}

bool DeviceReader::skip(qsizetype length) { return this->device->skip(length) == length; }

bool DeviceReader::readU8(quint8* data) {
	return this->readBytes(reinterpret_cast<char*>(data), 1);
}

bool DeviceReader::readU16(quint16* data) {
	return this->readBytes(reinterpret_cast<char*>(data), 2);
}

bool DeviceReader::readU32(quint32* data) {
	return this->readBytes(reinterpret_cast<char*>(data), 4);
}

bool DeviceReader::readU64(quint64* data) {
	return this->readBytes(reinterpret_cast<char*>(data), 8);
}

void EncodedLogWriter::setDevice(QIODevice* target) { this->buffer.setDevice(target); }
void EncodedLogReader::setDevice(QIODevice* source) { this->reader.setDevice(source); }

constexpr quint8 LOG_VERSION = 2;

bool EncodedLogWriter::writeHeader() {
	this->buffer.writeU8(LOG_VERSION);
	return this->buffer.flush();
}

bool EncodedLogReader::readHeader(bool* success, quint8* version, quint8* readerVersion) {
	if (!this->reader.readU8(version)) return false;
	*success = *version == LOG_VERSION;
	*readerVersion = LOG_VERSION;
	return true;
}

bool EncodedLogWriter::write(const LogMessage& message) {
	if (!this->buffer.hasDevice()) return false;

	LogMessage* prevMessage = nullptr;
	auto index = this->recentMessages.indexOf(message, &prevMessage);

	// If its a dupe, save memory by reusing the buffer of the first message and letting
	// the new one be deallocated.
	auto body = prevMessage ? prevMessage->body : message.body;
	this->recentMessages.emplace(message.type, message.category, body, message.time);

	if (index != -1) {
		auto secondDelta = this->lastMessageTime.secsTo(message.time);

		if (secondDelta < 16 && index < 16) {
			this->writeOp(EncodedLogOpcode::RecentMessageShort);
			this->buffer.writeU8(index | (secondDelta << 4));
		} else {
			this->writeOp(EncodedLogOpcode::RecentMessageLong);
			this->buffer.writeU8(index);
			this->writeVarInt(secondDelta);
		}

		goto finish;
	} else {
		auto categoryId = this->getOrCreateCategory(message.category);
		this->writeVarInt(categoryId);

		auto writeFullTimestamp = [this, &message]() {
			this->buffer.writeU64(message.time.toSecsSinceEpoch());
		};

		if (message.type == QtFatalMsg) {
			this->buffer.writeU8(0xff);
			writeFullTimestamp();
		} else {
			quint8 field = compressedTypeOf(message.type);

			auto secondDelta = this->lastMessageTime.secsTo(message.time);
			if (secondDelta >= 0x1d) {
				// 0x1d = followed by delta int
				// 0x1e = followed by epoch delta int
				field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3;
			} else {
				field |= secondDelta << 3;
			}

			this->buffer.writeU8(field);

			if (secondDelta >= 0x1d) {
				if (secondDelta > 0xffff) {
					writeFullTimestamp();
				} else {
					this->writeVarInt(secondDelta);
				}
			}
		}

		this->writeString(message.body);
	}

finish:
	// copy with second precision
	this->lastMessageTime = QDateTime::fromSecsSinceEpoch(message.time.toSecsSinceEpoch());
	return this->buffer.flush();
}

bool EncodedLogReader::read(LogMessage* slot) {
start:
	quint32 next = 0;
	if (!this->readVarInt(&next)) return false;

	if (next < EncodedLogOpcode::BeginCategories) {
		if (next == EncodedLogOpcode::RegisterCategory) {
			if (!this->registerCategory()) return false;
			goto start;
		} else if (next == EncodedLogOpcode::RecentMessageShort
		           || next == EncodedLogOpcode::RecentMessageLong)
		{
			quint8 index = 0;
			quint32 secondDelta = 0;

			if (next == EncodedLogOpcode::RecentMessageShort) {
				quint8 field = 0;
				if (!this->reader.readU8(&field)) return false;
				index = field & 0xf;
				secondDelta = field >> 4;
			} else {
				if (!this->reader.readU8(&index)) return false;
				if (!this->readVarInt(&secondDelta)) return false;
			}

			if (index >= this->recentMessages.size()) return false;
			*slot = this->recentMessages.at(index);
			this->lastMessageTime = this->lastMessageTime.addSecs(static_cast<qint64>(secondDelta));
			slot->time = this->lastMessageTime;
		}
	} else {
		auto categoryId = next - EncodedLogOpcode::BeginCategories;
		auto category = this->categories.value(categoryId);

		quint8 field = 0;
		if (!this->reader.readU8(&field)) return false;

		auto msgType = QtDebugMsg;
		quint64 secondDelta = 0;
		auto needsTimeRead = false;

		if (field == 0xff) {
			msgType = QtFatalMsg;
			needsTimeRead = true;
		} else {
			msgType = typeOfCompressed(static_cast<CompressedLogType>(field & 0x07));
			secondDelta = field >> 3;

			if (secondDelta == 0x1d) {
				quint32 slot = 0;
				if (!this->readVarInt(&slot)) return false;
				secondDelta = slot;
			} else if (secondDelta == 0x1e) {
				needsTimeRead = true;
			}
		}

		if (needsTimeRead) {
			if (!this->reader.readU64(&secondDelta)) return false;
		}

		this->lastMessageTime = this->lastMessageTime.addSecs(static_cast<qint64>(secondDelta));

		QByteArray body;
		if (!this->readString(&body)) return false;

		*slot = LogMessage(msgType, QLatin1StringView(category.first), body, this->lastMessageTime);
		slot->readCategoryId = categoryId;
	}

	this->recentMessages.emplace(*slot);
	return true;
}

CategoryFilter EncodedLogReader::categoryFilterById(quint16 id) {
	return this->categories.value(id).second;
}

void EncodedLogWriter::writeOp(EncodedLogOpcode opcode) { this->buffer.writeU8(opcode); }

void EncodedLogWriter::writeVarInt(quint32 n) {
	if (n < 0xff) {
		this->buffer.writeU8(n);
	} else if (n < 0xffff) {
		this->buffer.writeU8(0xff);
		this->buffer.writeU16(n);
	} else {
		this->buffer.writeU8(0xff);
		this->buffer.writeU16(0xffff);
		this->buffer.writeU32(n);
	}
}

bool EncodedLogReader::readVarInt(quint32* slot) {
	auto bytes = std::array<quint8, 7>();
	auto readLength = this->reader.peekBytes(reinterpret_cast<char*>(bytes.data()), 7);

	if (bytes[0] != 0xff && readLength >= 1) {
		auto n = *reinterpret_cast<quint8*>(bytes.data());
		if (!this->reader.skip(1)) return false;
		*slot = qFromLittleEndian(n);
	} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
		auto n = *reinterpret_cast<quint16*>(bytes.data() + 1); // NOLINT
		if (!this->reader.skip(3)) return false;
		*slot = qFromLittleEndian(n);
	} else if (readLength == 7) {
		auto n = *reinterpret_cast<quint32*>(bytes.data() + 3); // NOLINT
		if (!this->reader.skip(7)) return false;
		*slot = qFromLittleEndian(n);
	} else return false;

	return true;
}

void EncodedLogWriter::writeString(QByteArrayView bytes) {
	this->writeVarInt(bytes.length());
	this->buffer.writeBytes(bytes.constData(), bytes.length());
}

bool EncodedLogReader::readString(QByteArray* slot) {
	quint32 length = 0;
	if (!this->readVarInt(&length)) return false;

	*slot = QByteArray(length, Qt::Uninitialized);
	auto r = this->reader.readBytes(slot->data(), slot->size());
	return r;
}

quint16 EncodedLogWriter::getOrCreateCategory(QLatin1StringView category) {
	if (this->categories.contains(category)) {
		return this->categories.value(category);
	} else {
		this->writeOp(EncodedLogOpcode::RegisterCategory);
		// id is implicitly the next available id
		this->writeString(category);

		auto id = this->nextCategory++;
		this->categories.insert(category, id);

		auto filter = LogManager::instance()->getFilter(category);
		quint8 flags = 0;
		flags |= filter.debug << 0;
		flags |= filter.info << 1;
		flags |= filter.warn << 2;
		flags |= filter.critical << 3;

		this->buffer.writeU8(flags);
		return id;
	}
}

bool EncodedLogReader::registerCategory() {
	QByteArray name;
	quint8 flags = 0;
	if (!this->readString(&name)) return false;
	if (!this->reader.readU8(&flags)) return false;

	CategoryFilter filter;
	filter.debug = (flags >> 0) & 1;
	filter.info = (flags >> 1) & 1;
	filter.warn = (flags >> 2) & 1;
	filter.critical = (flags >> 3) & 1;

	this->categories.append(qMakePair(name, filter));
	return true;
}

bool LogReader::initialize() {
	this->reader.setDevice(this->file);

	bool readable = false;
	quint8 logVersion = 0;
	quint8 readerVersion = 0;
	if (!this->reader.readHeader(&readable, &logVersion, &readerVersion)) {
		qCritical() << "Failed to read log header.";
		return false;
	}

	if (!readable) {
		qCritical() << "This log was encoded with version" << logVersion
		            << "of the quickshell log encoder, which cannot be decoded by the current "
		               "version of quickshell, with log version"
		            << readerVersion;
		return false;
	}

	return true;
}

bool LogReader::continueReading() {
	auto color = LogManager::instance()->colorLogs;
	auto tailRing = RingBuffer<LogMessage>(this->remainingTail);

	LogMessage message;
	auto stream = QTextStream(stdout);
	auto readCursor = this->file->pos();
	while (this->reader.read(&message)) {
		readCursor = this->file->pos();

		CategoryFilter filter;
		if (this->filters.contains(message.readCategoryId)) {
			filter = this->filters.value(message.readCategoryId);
		} else {
			filter = this->reader.categoryFilterById(message.readCategoryId);

			for (const auto& rule: this->rules) {
				filter.applyRule(message.category, rule);
			}

			this->filters.insert(message.readCategoryId, filter);
		}

		if (filter.shouldDisplay(message.type)) {
			if (this->remainingTail == 0) {
				LogMessage::formatMessage(stream, message, color, this->timestamps);
				stream << '\n';
			} else {
				tailRing.emplace(message);
			}
		}
	}

	if (this->remainingTail != 0) {
		for (auto i = tailRing.size() - 1; i != -1; i--) {
			auto& message = tailRing.at(i);
			LogMessage::formatMessage(stream, message, color, this->timestamps);
			stream << '\n';
		}
	}

	stream << Qt::flush;

	if (this->file->pos() != readCursor) {
		qCritical() << "An error occurred parsing the end of this log file.";
		qCritical() << "Remaining data:" << this->file->readAll();
		return false;
	}

	return true;
}

void LogFollower::FcntlWaitThread::run() {
	struct flock lock = {
	    .l_type = F_RDLCK, // won't block other read locks when we take it
	    .l_whence = SEEK_SET,
	    .l_start = 0,
	    .l_len = 0,
	    .l_pid = 0,
	};

	auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT

	if (r != 0) {
		qCWarning(logLogging).nospace()
		    << "Failed to wait for write locks to be removed from log file with error code " << errno
		    << ": " << qt_error_string();
	}
}

bool LogFollower::follow() {
	QObject::connect(&this->waitThread, &QThread::finished, this, &LogFollower::onFileLocked);

	QObject::connect(
	    &this->fileWatcher,
	    &QFileSystemWatcher::fileChanged,
	    this,
	    &LogFollower::onFileChanged
	);

	this->fileWatcher.addPath(this->path);
	this->waitThread.start();

	auto r = QCoreApplication::exec();
	return r == 0;
}

void LogFollower::onFileChanged() {
	if (!this->reader->continueReading()) {
		QCoreApplication::exit(1);
	}
}

void LogFollower::onFileLocked() {
	if (!this->reader->continueReading()) {
		QCoreApplication::exit(1);
	} else {
		QCoreApplication::exit(0);
	}
}

bool readEncodedLogs(
    QFile* file,
    const QString& path,
    bool timestamps,
    int tail,
    bool follow,
    const QString& rulespec
) {
	QList<QLoggingRule> rules;

	{
		QLoggingSettingsParser parser;
		parser.setContent(rulespec);
		rules = parser.rules();
	}

	auto reader = LogReader(file, timestamps, tail, rules);

	if (!reader.initialize()) return false;
	if (!reader.continueReading()) return false;

	if (follow) {
		auto follower = LogFollower(&reader, path);
		return follower.follow();
	}

	return true;
}

} // namespace qs::log
