#include "Logger.h"

#include <windows.h>

#include <iomanip>
#include <iostream>
#include <sstream>
#include <ctime>
#include <filesystem>

namespace RenderCadenceCompositor
{
namespace
{
int Rank(LogLevel level)
{
	switch (level)
	{
	case LogLevel::Log:
		return 0;
	case LogLevel::Warning:
		return 1;
	case LogLevel::Error:
		return 2;
	default:
		return 0;
	}
}

std::string FormatTimestamp(std::chrono::system_clock::time_point timestamp)
{
	const std::time_t time = std::chrono::system_clock::to_time_t(timestamp);
	std::tm localTime = {};
	localtime_s(&localTime, &time);

	std::ostringstream stream;
	stream << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S");
	return stream.str();
}

std::string FormatRecord(const Logger::Record& record)
{
	std::ostringstream stream;
	stream << FormatTimestamp(record.timestamp)
		<< " [" << LogLevelName(record.level) << "]"
		<< " [" << record.subsystem << "] "
		<< record.message;
	return stream.str();
}
}

const char* LogLevelName(LogLevel level)
{
	switch (level)
	{
	case LogLevel::Log:
		return "log";
	case LogLevel::Warning:
		return "warning";
	case LogLevel::Error:
		return "error";
	default:
		return "log";
	}
}

Logger& Logger::Instance()
{
	static Logger logger;
	return logger;
}

Logger::~Logger()
{
	Stop();
}

void Logger::Start(LoggerConfig config)
{
	std::lock_guard<std::mutex> lock(mMutex);
	if (mRunning)
		return;

	mConfig = config;
	mStopping = false;
	mRunning = true;
	OpenFileSink();
	{
		std::lock_guard<std::mutex> countersLock(mCountersMutex);
		mCounters = LoggerCounters();
	}
	mThread = std::thread([this]() { ThreadMain(); });
}

void Logger::Stop()
{
	{
		std::lock_guard<std::mutex> lock(mMutex);
		if (!mRunning && !mThread.joinable())
			return;
		mStopping = true;
	}
	mCondition.notify_all();

	if (mThread.joinable())
		mThread.join();

	std::lock_guard<std::mutex> lock(mMutex);
	CloseFileSink();
	mRunning = false;
}

void Logger::Write(LogLevel level, const std::string& subsystem, const std::string& message)
{
	Enqueue(Record{ std::chrono::system_clock::now(), std::this_thread::get_id(), level, subsystem, message }, true);
}

bool Logger::TryWrite(LogLevel level, const std::string& subsystem, const std::string& message)
{
	return Enqueue(Record{ std::chrono::system_clock::now(), std::this_thread::get_id(), level, subsystem, message }, false);
}

void Logger::Log(const std::string& subsystem, const std::string& message)
{
	Write(LogLevel::Log, subsystem, message);
}

void Logger::Warning(const std::string& subsystem, const std::string& message)
{
	Write(LogLevel::Warning, subsystem, message);
}

void Logger::Error(const std::string& subsystem, const std::string& message)
{
	Write(LogLevel::Error, subsystem, message);
}

LoggerCounters Logger::Counters() const
{
	std::lock_guard<std::mutex> lock(mCountersMutex);
	return mCounters;
}

bool Logger::IsRunning() const
{
	std::lock_guard<std::mutex> lock(mMutex);
	return mRunning;
}

bool Logger::ShouldWrite(LogLevel level) const
{
	return Rank(level) >= Rank(mConfig.minimumLevel);
}

bool Logger::Enqueue(Record record, bool block)
{
	if (!ShouldWrite(record.level))
		return true;

	std::unique_lock<std::mutex> lock(mMutex, std::defer_lock);
	if (block)
	{
		lock.lock();
	}
	else if (!lock.try_lock())
	{
		std::lock_guard<std::mutex> countersLock(mCountersMutex);
		++mCounters.dropped;
		return false;
	}

	if (!mRunning)
	{
		lock.unlock();
		WriteRecord(record);
		return true;
	}

	if (mQueue.size() >= mConfig.maxQueuedMessages)
	{
		std::lock_guard<std::mutex> countersLock(mCountersMutex);
		++mCounters.dropped;
		return false;
	}

	mQueue.push_back(std::move(record));
	{
		std::lock_guard<std::mutex> countersLock(mCountersMutex);
		++mCounters.queued;
	}
	lock.unlock();
	mCondition.notify_one();
	return true;
}

void Logger::ThreadMain()
{
	for (;;)
	{
		Record record;
		{
			std::unique_lock<std::mutex> lock(mMutex);
			mCondition.wait(lock, [this]() {
				return mStopping || !mQueue.empty();
			});

			if (mQueue.empty())
			{
				if (mStopping)
					return;
				continue;
			}

			record = std::move(mQueue.front());
			mQueue.pop_front();
		}
		WriteRecord(record);
	}
}

void Logger::WriteRecord(const Record& record)
{
	const std::string line = FormatRecord(record);

	if (mConfig.writeToDebugOutput)
		OutputDebugStringA((line + "\n").c_str());

	if (mConfig.writeToConsole)
	{
		if (record.level == LogLevel::Error)
			std::cerr << line << "\n";
		else
			std::cout << line << "\n";
	}
	if (mFile.is_open())
	{
		mFile << line << "\n";
		mFile.flush();
	}

	std::lock_guard<std::mutex> lock(mCountersMutex);
	++mCounters.written;
}

void Logger::OpenFileSink()
{
	CloseFileSink();
	if (!mConfig.writeToFile || mConfig.filePath.empty())
		return;

	std::error_code error;
	const std::filesystem::path logPath(mConfig.filePath);
	if (logPath.has_parent_path())
		std::filesystem::create_directories(logPath.parent_path(), error);
	mFile.open(logPath, std::ios::out | std::ios::app);
}

void Logger::CloseFileSink()
{
	if (mFile.is_open())
		mFile.close();
}

bool TryLog(LogLevel level, const std::string& subsystem, const std::string& message)
{
	return Logger::Instance().TryWrite(level, subsystem, message);
}

void Log(const std::string& subsystem, const std::string& message)
{
	Logger::Instance().Log(subsystem, message);
}

void LogWarning(const std::string& subsystem, const std::string& message)
{
	Logger::Instance().Warning(subsystem, message);
}

void LogError(const std::string& subsystem, const std::string& message)
{
	Logger::Instance().Error(subsystem, message);
}
}
