/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "Math/ByteSize.hpp"
#include "Logger.hpp"
#include "IO/AppFolders.hpp"
#include "Utils.hpp"
#include <iostream>
#include <filesystem>
#include <csignal>
#include <inttypes.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/null_sink.h>
#ifndef NO_CONSOLE_LOG
#include <spdlog/sinks/stdout_color_sinks.h>
#endif
#ifdef _MSC_VER
#include <spdlog/sinks/msvc_sink.h>
#endif
#ifdef _WIN32
# include <windows.h>
# include <dbghelp.h>
#endif


namespace OpenVulkano
{

	constexpr size_t MAX_LOG_FILE_SIZE = 512_MiB;
	constexpr size_t MAX_LOG_FILE_COUNT = 3;

	std::vector<spdlog::sink_ptr> Logger::SINKS;
	std::vector<std::weak_ptr<spdlog::logger>> Logger::LOGGERS;
	Logger::Ptr Logger::WINDOW = nullptr;
	Logger::Ptr Logger::MANAGER = nullptr;
	Logger::Ptr Logger::RENDER = nullptr;
	Logger::Ptr Logger::PHYSIC = nullptr;
	Logger::Ptr Logger::AUDIO = nullptr;
	Logger::Ptr Logger::DATA = nullptr;
	Logger::Ptr Logger::SCENE = nullptr;
	Logger::Ptr Logger::INPUT = nullptr;
	Logger::Ptr Logger::FILESYS = nullptr;
	Logger::Ptr Logger::AR = nullptr;
	Logger::Ptr Logger::PERF = nullptr;
	Logger::Ptr Logger::APP = nullptr;

	namespace
	{
		bool didLoggerShutdown = false;

		void signalHandler(int sigCode)
		{
			if(!didLoggerShutdown)
				spdlog::error("Received signal {}", sigCode);
			spdlog::shutdown();
			didLoggerShutdown = true;
			std::raise(sigCode); // calls default handler
		}

	#ifdef _WIN32
		LONG WINAPI win32ExceptionHandler(EXCEPTION_POINTERS *exceptionPointers)
		{
			// NOTE(vb): We can report some of the exceptionPointers fields to the user... But this is for windows only 
			if(!didLoggerShutdown)
				spdlog::error("An exception occured");
			spdlog::shutdown();
			didLoggerShutdown = true;
			return EXCEPTION_CONTINUE_SEARCH;
		}
	#endif

		void setupSignalHandlers()
		{
			std::signal(SIGSEGV, signalHandler);
			std::signal(SIGABRT, signalHandler);
			std::signal(SIGFPE, signalHandler);
			std::signal(SIGILL, signalHandler);
			// std::signal(SIGINT, signalHandler);
			std::signal(SIGTERM, signalHandler);
	#ifdef _WIN32
			SetUnhandledExceptionFilter(win32ExceptionHandler);
	#endif
		}
	}

	void Logger::SetupLogger(std::string logFolder, const std::string& logFile)
	{
		if (logFolder.empty()) logFolder = (AppFolders::GetAppDataHomeDir() / "logs").string();
		if (!SINKS.empty()) return;
		try
		{
			try
			{
				SINKS.push_back(std::make_shared<spdlog::sinks::rotating_file_sink_mt>(logFolder + "/" + logFile, MAX_LOG_FILE_SIZE, MAX_LOG_FILE_COUNT, true));
			}
			catch (const spdlog::spdlog_ex& e)
			{
				std::cerr << "Failed to create file log sink: " << e.what() << std::endl;
			}
#ifndef NO_CONSOLE_LOG
			SINKS.push_back(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
#endif
#ifdef _MSC_VER // If it was build with msvc in debug we can use the msvc sink
			SINKS.push_back(std::make_shared<spdlog::sinks::msvc_sink_mt>());
#endif
			// Make sure that there is always a sink for the loggers
			if (SINKS.empty()) SINKS.push_back(std::make_shared<spdlog::sinks::null_sink_mt>());

			MANAGER = CreateLogger("manager");
			WINDOW = CreateLogger("window");
			RENDER = CreateLogger("render");
			PHYSIC = CreateLogger("physic");
			AUDIO = CreateLogger("audio");
			DATA = CreateLogger("data");
			SCENE = CreateLogger("scene");
			INPUT = CreateLogger("input");
			FILESYS = CreateLogger("filesys");
			AR = CreateLogger("ar");
			PERF = CreateLogger("perf");
			APP = CreateLogger("app");

			spdlog::flush_every(std::chrono::seconds(5));

			setupSignalHandlers();

			MANAGER->info("Logger initialized");
		}
		catch (const spdlog::spdlog_ex& e)
		{
			std::cerr << "Log initialization failed: " << e.what() << std::endl;
		}
	}

	Logger::Ptr Logger::CreateLogger(const std::string& name, const bool reg)
	{
		if (reg) { auto logger = spdlog::get(name); if (logger) return logger; }
		auto logger = std::make_shared<spdlog::logger>(name, SINKS.begin(), SINKS.end());
		if (reg) spdlog::register_logger(logger);
		auto formatter = std::make_unique<spdlog::pattern_formatter>();
		formatter->add_flag<CustomThreadNameFormatterFlag>('t'); // overloading t

#ifdef LOG_DATE
		formatter->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [T:%t] [%^%l%$] [%n]: %v");
#else
		formatter->set_pattern("[%H:%M:%S.%e] [T:%t] [%^%l%$] [%n]: %v");
#endif
#ifdef DEBUG
		logger->set_level(spdlog::level::debug);
#else
		logger->set_level(spdlog::level::info);
#endif
		logger->set_formatter(std::move(formatter));
		logger->flush_on(spdlog::level::err); // Flush on error

		LOGGERS.push_back(logger);

		return logger;
	}

	Logger::Ptr Logger::GetLogger(const std::string& name)
	{
		return spdlog::get(name);
	}

	void Logger::RegisterSink(const spdlog::sink_ptr& newSink)
	{
		auto iter = LOGGERS.begin();
		while(iter != LOGGERS.end())
		{
			if (const auto logger = (*iter).lock())
			{
				logger->sinks().push_back(newSink);
			}
			else
			{
				iter = LOGGERS.erase(iter);
			}
		}
		SINKS.push_back(newSink);
	}

	void CustomThreadNameFormatterFlag::format(const spdlog::details::log_msg &msg, const std::tm &tmTime, spdlog::memory_buf_t &dest)
	{
		std::string threadName = OpenVulkano::Utils::GetThreadName();
		if(threadName.length())
		{
			dest.append(threadName.c_str(), threadName.c_str() + threadName.length());
		}
		else
		{
			uint64_t threadId = OpenVulkano::Utils::GetThreadId();
			char buffer[64];
			snprintf(buffer, sizeof(buffer), "%" PRIu64, threadId);
			dest.append(buffer, buffer + strlen(buffer));
		}
	}

	std::unique_ptr<spdlog::custom_flag_formatter> CustomThreadNameFormatterFlag::clone() const
	{
		return spdlog::details::make_unique<CustomThreadNameFormatterFlag>();
	}
}
