#pragma once
// used: [win] winapi
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>

#include "../common.h"

// using: stringcopy, stringcat, timetostring
#include "crt.h"

// used: sdk datatypes
#include "../sdk/datatypes/color.h"
#include "../sdk/datatypes/vector.h"
#include "../sdk/datatypes/qangle.h"

// @todo: poorly designed in case we don't need logging at all, xor continues wasteful compilation without any references | add smth like dummystream_t and unreference macro params?

#pragma region log_definitions
#ifdef _DEBUG
#if defined(CS_COMPILER_CLANG)
#define L_PRINT(LEVEL) L::stream(LEVEL, "[" __FILE_NAME__ ":" CS_STRINGIFY(__LINE__) "] ")
#else
#define L_PRINT(LEVEL) L::stream(LEVEL, L::DETAIL::MakeFileBlock<CRT::StringLength(L::DETAIL::GetFileName(__FILE__)), CRT::StringLength(CS_STRINGIFY(__LINE__))>(L::DETAIL::GetFileName(__FILE__), CS_STRINGIFY(__LINE__)).Get())
#endif
#else
#define L_PRINT(LEVEL) L::stream(LEVEL)
#endif
#pragma endregion

#pragma region log_enumerations

enum ELogLevel : std::uint8_t
{
	LOG_NONE = 0,
	LOG_INFO,
	LOG_WARNING,
	LOG_ERROR
};

using LogModeFlags_t = std::uint16_t;

enum ELogModeFlags : LogModeFlags_t
{
	LOG_MODE_NONE = 0U,

	// boolean formatting
	LOG_MODE_BOOL_ALPHA = (1U << 0U), // switches between textual and numeric representation of booleans

	// integer formatting
	LOG_MODE_INT_SHOWBASE = (1U << 1U), // switches display of number base prefixes used by C++ literal constants
	LOG_MODE_INT_FORMAT_HEX = (1U << 2U), // switches integer numbers hexadecimal format
	LOG_MODE_INT_FORMAT_DEC = (1U << 3U), // switches integer numbers decimal format
	LOG_MODE_INT_FORMAT_OCT = (1U << 4U), // switches integer numbers octal format
	LOG_MODE_INT_FORMAT_BIN = (1U << 5U), // switches integer numbers binary format
	LOG_MODE_INT_FORMAT_MASK = (LOG_MODE_INT_FORMAT_HEX | LOG_MODE_INT_FORMAT_DEC | LOG_MODE_INT_FORMAT_OCT | LOG_MODE_INT_FORMAT_BIN),

	// floating-point formatting
	LOG_MODE_FLOAT_SHOWPOINT = (1U << 6U), // switches decimal point for those numbers whose decimal part is zero
	LOG_MODE_FLOAT_FORMAT_HEX = (1U << 7U), // switches floating-point numbers hexadecimal format
	LOG_MODE_FLOAT_FORMAT_FIXED = (1U << 8U), // switches floating-point numbers formatting in fixed-point notation
	LOG_MODE_FLOAT_FORMAT_SCIENTIFIC = (1U << 9U), // switches floating-point numbers formatting in scientific notation
	LOG_MODE_FLOAT_FORMAT_MASK = (LOG_MODE_FLOAT_FORMAT_HEX | LOG_MODE_FLOAT_FORMAT_FIXED | LOG_MODE_FLOAT_FORMAT_SCIENTIFIC),

	// numerical formatting
	LOG_MODE_NUM_SHOWPOSITIVE = (1U << 10U), // switches display of plus sign '+' in non-negative numbers
	LOG_MODE_NUM_UPPERCASE = (1U << 11U), // switches uppercase characters in numbers

	/* [internal] */
	LOG_MODE_REMOVE = (1U << 15U)
};

using LogColorFlags_t = std::uint16_t;

enum ELogColorFlags : LogColorFlags_t
{
	LOG_COLOR_FORE_BLUE = FOREGROUND_BLUE,
	LOG_COLOR_FORE_GREEN = FOREGROUND_GREEN,
	LOG_COLOR_FORE_RED = FOREGROUND_RED,
	LOG_COLOR_FORE_INTENSITY = FOREGROUND_INTENSITY,
	LOG_COLOR_FORE_GRAY = FOREGROUND_INTENSITY,
	LOG_COLOR_FORE_CYAN = FOREGROUND_BLUE | FOREGROUND_GREEN,
	LOG_COLOR_FORE_MAGENTA = FOREGROUND_BLUE | FOREGROUND_RED,
	LOG_COLOR_FORE_YELLOW = FOREGROUND_GREEN | FOREGROUND_RED,
	LOG_COLOR_FORE_BLACK = 0U,
	LOG_COLOR_FORE_WHITE = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE,

	LOG_COLOR_BACK_BLUE = BACKGROUND_BLUE,
	LOG_COLOR_BACK_GREEN = BACKGROUND_GREEN,
	LOG_COLOR_BACK_RED = BACKGROUND_RED,
	LOG_COLOR_BACK_INTENSITY = BACKGROUND_INTENSITY,
	LOG_COLOR_BACK_GRAY = BACKGROUND_INTENSITY,
	LOG_COLOR_BACK_CYAN = BACKGROUND_BLUE | BACKGROUND_GREEN,
	LOG_COLOR_BACK_MAGENTA = BACKGROUND_BLUE | BACKGROUND_RED,
	LOG_COLOR_BACK_YELLOW = BACKGROUND_GREEN | BACKGROUND_RED,
	LOG_COLOR_BACK_BLACK = 0U,
	LOG_COLOR_BACK_WHITE = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE,

	/* [internal] */
	LOG_COLOR_DEFAULT = LOG_COLOR_FORE_WHITE | LOG_COLOR_BACK_BLACK
};

#pragma endregion

/*
 * LOGGING
 * - simple logging system with file and console output
 *   used for debugging and fetching values/errors at run-time
 * @todo: currently not thread safe and can mess messages when used from different threads
 */
namespace L
{
	namespace DETAIL
	{
		// @todo: constructs string per-byte in the stack, how do we can optimize this?
		template <std::size_t N>
		struct FileBlockStorage_t
		{
			template <std::size_t... I1, std::size_t... I2>
			consteval explicit FileBlockStorage_t(const char* szFileName, const char* szLineNumber, std::index_sequence<I1...>, std::index_sequence<I2...>) :
				szStorage{ '[', szFileName[I1]..., ':', szLineNumber[I2]..., ']', ' ', '\0' } { }

			[[nodiscard]] constexpr const char* Get() const
			{
				return szStorage;
			}

			const char szStorage[N + 5U];
		};

		// fail-free version of 'StringCharR'
		consteval const char* GetFileName(const char* szFilePath)
		{
			const char* szLastPath = szFilePath;

			do
			{
				if (*szFilePath == '\\')
					szLastPath = szFilePath + 1U;
			} while (*szFilePath++ != '\0');

			return szLastPath;
		}

		// helper to generate file info block for logging message at compile-time
		template <std::size_t N1, std::size_t N2>
		consteval auto MakeFileBlock(const char* szFileName, const char* szFileNumber) noexcept
		{
			return FileBlockStorage_t<N1 + N2>(szFileName, szFileNumber, std::make_index_sequence<N1>{}, std::make_index_sequence<N2>{});
		}
	}

	/* @section: main */
	// attach console to current window with write permission and given title
	bool AttachConsole(const wchar_t* wszWindowTitle);
	// close write streams and detach console from current window
	void DetachConsole();
	// open logging output file
	bool OpenFile(const wchar_t* wszFileName);
	// close logging output file
	void CloseFile();
	// write message to the file or/and console
	void WriteMessage(const char* szMessage, const std::size_t nMessageLength);

	// alternative of C++ 'std::cout' and other STL-like streams logging scheme
	// @todo: is it faster to constantly call 'WriteMessage' instead of concatenating all of the output and print once? i dont think so, due to additional allocations, thread-safe requirements, conditions when we still should call print due to color/etc changes | but generally this should lead to better inlining and less complicated compiled code
	struct Stream_t
	{
		// special unique return type markers to determine and handle change of those flags, just a snap for compile-time, inlined as underlying types at run-time
		struct ColorMarker_t
		{
			LogColorFlags_t nColorFlags;
		};

		struct PrecisionMarker_t
		{
			int iPrecision;
		};

		struct ModeMarker_t
		{
			LogModeFlags_t nModeFlags;
		};

		// begin of each log message, puts time, file & line, level blocks
		Stream_t& operator()(const ELogLevel nLevel, const char* szFileBlock = nullptr);

		// manipulators
		Stream_t& operator<<(ColorMarker_t);
		Stream_t& operator<<(PrecisionMarker_t);
		Stream_t& operator<<(ModeMarker_t);
		// message
		Stream_t& operator<<(const char* szMessage);
		Stream_t& operator<<(const wchar_t* wszMessage);
		// conversion
		Stream_t& operator<<(const bool bValue);

		template <typename T> requires std::is_integral_v<T>
		Stream_t& operator<<(const T value)
		{
#if defined(CS_LOG_CONSOLE) || defined(CS_LOG_FILE)
			int iBase = 10;
			const char* szPrefix = nullptr;

			if (nModeFlags & LOG_MODE_INT_FORMAT_HEX)
			{
				iBase = 16;
				szPrefix = "0x";
			}
			else if (nModeFlags & LOG_MODE_INT_FORMAT_OCT)
				iBase = 8;
			else if (nModeFlags & LOG_MODE_INT_FORMAT_BIN)
			{
				iBase = 2;
				szPrefix = "0b";
			}

			// @todo: LOG_MODE_NUM_UPPERCASE not handled
			char szIntegerBuffer[CRT::IntegerToString_t<std::int64_t, 2U>::MaxCount() + 2U];
			char* szInteger = CRT::IntegerToString(value, szIntegerBuffer + 2U, sizeof(szIntegerBuffer) - 2U, iBase);

			// @todo: after int2str rework could be simplified | or completely replaced with strformat
			if (szPrefix != nullptr && (nModeFlags & LOG_MODE_INT_SHOWBASE))
			{
				*--szInteger = szPrefix[1];
				*--szInteger = szPrefix[0];
			}

			if constexpr (std::is_signed_v<T>)
			{
				if (value >= 0 && (nModeFlags & LOG_MODE_NUM_SHOWPOSITIVE))
					*--szInteger = '+';
			}

			const std::size_t nIntegerLength = szIntegerBuffer + sizeof(szIntegerBuffer) - szInteger - 1;
			WriteMessage(szInteger, nIntegerLength);
#endif
			return *this;
		}

		template <typename T> requires std::is_floating_point_v<T>
		Stream_t& operator<<(const T value)
		{
#if defined(CS_LOG_CONSOLE) || defined(CS_LOG_FILE)
			//static_assert((nModeFlags & (LOG_MODE_FLOAT_FORMAT_FIXED | LOG_MODE_FLOAT_FORMAT_SCIENTIFIC)) && std::is_same_v<T, float>); // expected 'double' or 'long double'
			int iDesiredPrecision = /*((nModeFlags & (LOG_MODE_FLOAT_FORMAT_FIXED | LOG_MODE_FLOAT_FORMAT_SCIENTIFIC)) ? -1 : (*/ iPrecision > 0 ? iPrecision : FLT_DIG; //));

			char szFormatBuffer[8];
			char* szFormat = szFormatBuffer;
			*szFormat++ = '%';

			if (nModeFlags & LOG_MODE_NUM_SHOWPOSITIVE)
				*szFormat++ = '+';

			if (nModeFlags & LOG_MODE_FLOAT_SHOWPOINT)
				*szFormat++ = '#';

			*szFormat++ = '.';
			*szFormat++ = '*';
			if constexpr (std::is_same_v<T, long double>)
				*szFormat++ = 'L';

			if (nModeFlags & LOG_MODE_FLOAT_FORMAT_FIXED)
				*szFormat++ = 'f';
			else
			{
				const bool bIsUpperCase = (nModeFlags & LOG_MODE_NUM_UPPERCASE);

				if (nModeFlags & LOG_MODE_FLOAT_FORMAT_HEX)
					*szFormat++ = bIsUpperCase ? 'A' : 'a';
				else if (nModeFlags & LOG_MODE_FLOAT_FORMAT_SCIENTIFIC)
					*szFormat++ = bIsUpperCase ? 'E' : 'e';
				else
					*szFormat++ = bIsUpperCase ? 'G' : 'g';
			}
			*szFormat = '\0';

			char szFloatBuffer[96];
			const int nFloatLength = CRT::StringPrintN(szFloatBuffer, sizeof(szFloatBuffer), szFormatBuffer, iDesiredPrecision, value);

			WriteMessage(szFloatBuffer, nFloatLength);
#endif
			return *this;
		}

		Stream_t& operator<<(const Vector_t& vecValue)
		{
			this->nModeFlags |= LOG_MODE_FLOAT_FORMAT_FIXED;
			this->iPrecision = 3;
			*this << CS_XOR("vector3d: (") << vecValue.x << CS_XOR(" | ") << vecValue.y << CS_XOR(" | ") << vecValue.z << CS_XOR(")");
			return *this;
		}

		Stream_t& operator<<(const QAngle_t& angValue)
		{
			this->nModeFlags |= LOG_MODE_FLOAT_FORMAT_FIXED;
			this->iPrecision = 3;
			*this << CS_XOR("qangle: (") << angValue.x << CS_XOR(" | ") << angValue.y << CS_XOR(" | ") << angValue.z << CS_XOR(")");
			return *this;
		}

		Stream_t& operator<<(const Color_t& colValue)
		{
			*this << CS_XOR("color: (") << static_cast<int>(colValue.r) << CS_XOR(" | ") << static_cast<int>(colValue.g) << CS_XOR(" | ") << static_cast<int>(colValue.b) << CS_XOR(" | ") << static_cast<int>(colValue.a) << CS_XOR(")");
			return *this;
		}

		bool bFirstPrint = true;
		int iPrecision = 0;
		LogModeFlags_t nModeFlags = LOG_MODE_NONE;
	};

	/* @section: stream control */
	// set console color flags for current stream, will reset on next message
	Stream_t::ColorMarker_t SetColor(const LogColorFlags_t nColorFlags);
	// set the decimal precision to be used to format floating-point values for current stream, will reset on next message
	Stream_t::PrecisionMarker_t SetPrecision(const int m_iPrecision);
	// add logging mode flags for current stream, will reset on next message
	Stream_t::ModeMarker_t AddFlags(const LogModeFlags_t m_nModeFlags);
	// remove logging mode flags for current stream
	Stream_t::ModeMarker_t RemoveFlags(const LogModeFlags_t m_nModeFlags);

	/* @section: values */
	// primary logging stream
	inline Stream_t stream;
}
