/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2023, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "system-precomp.h"	 // Precompiled headers
//
#include <mrpt/core/exceptions.h>
#include <mrpt/core/lock_helper.h>
#include <mrpt/system/COutputLogger.h>
#include <mrpt/system/datetime.h>
#include <mrpt/system/filesystem.h>

#include <cstdarg>	// for logFmt
#include <cstdio>
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>

#ifdef _MSC_VER
#define WIN32_LEAN_AND_MEAN
#include <windows.h>  // OutputDebugString() for MSVC
#endif

using namespace mrpt;
using namespace mrpt::system;
using namespace std;

/**
 * Implementation file for the COutputLogger header class
 */

// COutputLogger
// ////////////////////////////////////////////////////////////

static std::array<
	mrpt::system::ConsoleForegroundColor, NUMBER_OF_VERBOSITY_LEVELS>
	logging_levels_to_colors = {
		ConsoleForegroundColor::BLUE,  // LVL_DEBUG
		ConsoleForegroundColor::DEFAULT,  // LVL_INFO
		ConsoleForegroundColor::GREEN,	// LVL_WARN
		ConsoleForegroundColor::RED	 // LVL_ERROR
};

std::array<mrpt::system::ConsoleForegroundColor, NUMBER_OF_VERBOSITY_LEVELS>&
	COutputLogger::logging_levels_to_colors()
{
	return ::logging_levels_to_colors;
}

static std::array<std::string, NUMBER_OF_VERBOSITY_LEVELS>
	logging_levels_to_names = {
		"DEBUG",  // LVL_DEBUG
		"INFO ",  // LVL_INFO
		"WARN ",  // LVL_WARN
		"ERROR"	 // LVL_ERROR
};
std::array<std::string, NUMBER_OF_VERBOSITY_LEVELS>&
	COutputLogger::logging_levels_to_names()
{
	return ::logging_levels_to_names;
}

COutputLogger::~COutputLogger() = default;

void COutputLogger::logStr(
	const VerbosityLevel level, std::string_view msg_str) const
{
	// initialize a TMsg object
	TMsg msg(level, msg_str, *this);
	if (logging_enable_keep_record)
	{
		auto lck = mrpt::lockHelper(*m_historyMtx);
		m_history.push_back(msg);
	}

	if (level >= m_min_verbosity_level && logging_enable_console_output)
		msg.dumpToConsole();

	// User callbacks:
	if (level >= m_min_verbosity_level_callbacks)
	{
		for (const auto& c : m_listCallbacks)
			c(msg.body, msg.level, msg.name, msg.timestamp);
	}
}

void COutputLogger::logFmt(
	const VerbosityLevel level, const char* fmt, ...) const
{
	// check for nullptr pointer
	if (!fmt) return;

	// initialize the va_list and let generateStringFromFormat do the work
	// http://c-faq.com/varargs/handoff.html
	va_list argp;
	va_start(argp, fmt);
	std::string str = this->generateStringFromFormat(fmt, argp);
	va_end(argp);

	this->logStr(level, str);
}

std::string COutputLogger::generateStringFromFormat(
	std::string_view fmt, va_list argp) const
{
	int result = -1, length = 1024;
	std::vector<char> buffer;
	// make sure that the buffer is large enough to handle the string
	while (result == -1)
	{
		buffer.resize(length + 10);
		result = os::vsnprintf(&buffer[0], length, fmt.data(), argp);

		// http://www.cplusplus.com/reference/cstdio/vsnprintf/
		// only when this returned value is non-negative and less than n, the
		// string has been completely written
		if (result >= length) result = -1;
		length *= 2;
	}

	// return result to the caller
	return std::string(&buffer[0]);
}
void COutputLogger::logCond(
	const VerbosityLevel level, bool cond, const std::string& msg_str) const
{
	if (!cond) return;
	this->logStr(level, msg_str);
}

void COutputLogger::setLoggerName(const std::string& name)
{
	m_logger_name = name;
}

std::string COutputLogger::getLoggerName() const { return m_logger_name; }

void COutputLogger::setMinLoggingLevel(const VerbosityLevel level)
{
	m_min_verbosity_level = level;
	m_min_verbosity_level_callbacks = level;
}

void COutputLogger::setVerbosityLevelForCallbacks(const VerbosityLevel level)
{
	m_min_verbosity_level_callbacks = level;
}

void COutputLogger::getLogAsString(std::string& fname) const
{
	fname.clear();
	auto lck = mrpt::lockHelper(*m_historyMtx);
	for (const auto& h : m_history)
		fname += h.getAsString();
}
std::string COutputLogger::getLogAsString() const
{
	std::string str;
	this->getLogAsString(str);
	return str;
}
void COutputLogger::writeLogToFile(const std::optional<string> fname_in) const
{
	using namespace std::string_literals;

	// determine the filename - open it
	std::string fname = fname_in.has_value()
		? fname_in.value()
		: mrpt::system::fileNameStripInvalidChars(m_logger_name) + ".log"s;

	std::ofstream f(fname);
	ASSERTMSG_(
		f.is_open(),
		mrpt::format(
			"[%s] Could not open external file: %s", m_logger_name.c_str(),
			fname.c_str()));

	std::string hist_str;
	this->getLogAsString(hist_str);
	f << hist_str;
	f.close();
}

void COutputLogger::dumpLogToConsole() const
{
	auto lck = mrpt::lockHelper(*m_historyMtx);
	for (const auto& h : m_history)
		h.dumpToConsole();
}

std::string COutputLogger::getLoggerLastMsg() const
{
	auto lck = mrpt::lockHelper(*m_historyMtx);
	TMsg last_msg = m_history.back();
	return last_msg.getAsString();
}

void COutputLogger::getLoggerLastMsg(std::string& msg_str) const
{
	msg_str = this->getLoggerLastMsg();
}

void COutputLogger::loggerReset() { *this = COutputLogger(); }

// TMsg Struct
// ////////////////////////////////////////////////////////////

COutputLogger::TMsg::TMsg(
	const mrpt::system::VerbosityLevel in_level, std::string_view msg_str,
	const COutputLogger& logger)
	: timestamp(mrpt::Clock::now()),  // fill with the current time
	  level(in_level),
	  name(logger.getLoggerName()),
	  body(msg_str)
{
}

COutputLogger::TMsg::~TMsg() = default;

std::string COutputLogger::TMsg::getAsString() const
{
	stringstream out;
	out << "[" << mrpt::system::timeLocalToString(timestamp, 4) << "|"
		<< COutputLogger::logging_levels_to_names()[level] << "|" << name
		<< "] " << body;
	if (!body.empty() && *body.rbegin() != '\n') out << std::endl;

	return out.str();
}
void COutputLogger::TMsg::getAsString(std::string* contents) const
{
	*contents = this->getAsString();
}
void COutputLogger::TMsg::writeToStream(std::ostream& out) const
{
	const std::string str = getAsString();
	out << str;
#ifdef _MSC_VER
	OutputDebugStringA(str.c_str());
#endif
}
void COutputLogger::TMsg::dumpToConsole() const
{
	const std::string str = getAsString();

	const bool dump_to_cerr = (level == LVL_ERROR);	 // LVL_ERROR alternatively
	// dumped to stderr instead
	// of stdout

	// Set console color:
	const ConsoleForegroundColor concol =
		COutputLogger::logging_levels_to_colors()[level];
	mrpt::system::consoleColorAndStyle(
		concol, ConsoleBackgroundColor::DEFAULT, ConsoleTextStyle::REGULAR,
		dump_to_cerr);
	// Output msg:
	(dump_to_cerr ? std::cerr : std::cout) << str;
	// Switch back to normal color:
	mrpt::system::consoleColorAndStyle(
		ConsoleForegroundColor::DEFAULT, ConsoleBackgroundColor::DEFAULT,
		ConsoleTextStyle::REGULAR, dump_to_cerr);
#ifdef _MSC_VER
	OutputDebugStringA(
		str.c_str());  // call benchmarked: avrg 90 us (50-200 us)
#endif
}

void COutputLogger::logRegisterCallback(output_logger_callback_t userFunc)
{
	m_listCallbacks.emplace_back(userFunc);
}

template <typename T, typename... U>
size_t getAddress(std::function<T(U...)> f)
{
	using fnType = T (*)(U...);
	auto** fnPointer = f.template target<fnType*>();
	return (size_t)*fnPointer;
}

bool COutputLogger::logDeregisterCallback(output_logger_callback_t userFunc)
{
	for (auto it = m_listCallbacks.begin(); it != m_listCallbacks.end(); ++it)
	{
		if (getAddress(*it) == getAddress(userFunc))
		{
			m_listCallbacks.erase(it);
			return true;
		}
	}
	return false;
}
