// Taken from https://github.com/agauniyal/rang
// Provided under Unlicense
// Modified by carstene1ns for EasyRPG inclusion

#ifndef RANG_DOT_HPP
#define RANG_DOT_HPP

#if defined(__unix__) || defined(__unix) || defined(__linux__)
#define OS_LINUX
#elif defined(WIN32) || defined(_WIN32) || defined(_WIN64)
#define OS_WIN
#elif defined(__APPLE__) || defined(__MACH__)
#define OS_MAC
#endif

#if defined(OS_LINUX) || defined(OS_MAC)
#include <unistd.h>

#elif defined(OS_WIN)

#if defined(_WIN32_WINNT) && (_WIN32_WINNT < 0x0600)
#error                                                                         \
  "Please include rang.hpp before any windows system headers or set _WIN32_WINNT at least to _WIN32_WINNT_VISTA"
#elif !defined(_WIN32_WINNT)
#define _WIN32_WINNT _WIN32_WINNT_VISTA
#endif

#include <windows.h>
#include <io.h>
#include <memory>

// Only defined in windows 10 onwards, redefining in lower windows since it
// doesn't gets used in lower versions
// https://docs.microsoft.com/en-us/windows/console/getconsolemode
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif

#endif

#include <algorithm>
#include <atomic>
#include <cstdlib>
#include <cstring>
#include <iostream>

namespace rang {

/* For better compability with most of terminals do not use any style settings
 * except of reset, bold and reversed.
 * Note that on Windows terminals bold style is same as fgB color.
 */
enum class style {
	reset     = 0,
	bold      = 1,
	dim       = 2,
	italic    = 3,
	underline = 4,
	blink     = 5,
	rblink    = 6,
	reversed  = 7,
	conceal   = 8,
	crossed   = 9
};

enum class fg {
	black   = 30,
	red     = 31,
	green   = 32,
	yellow  = 33,
	blue    = 34,
	magenta = 35,
	cyan    = 36,
	gray    = 37,
	reset   = 39
};

enum class bg {
	black   = 40,
	red     = 41,
	green   = 42,
	yellow  = 43,
	blue    = 44,
	magenta = 45,
	cyan    = 46,
	gray    = 47,
	reset   = 49
};

enum class fgB {
	black   = 90,
	red     = 91,
	green   = 92,
	yellow  = 93,
	blue    = 94,
	magenta = 95,
	cyan    = 96,
	gray    = 97
};

enum class bgB {
	black   = 100,
	red     = 101,
	green   = 102,
	yellow  = 103,
	blue    = 104,
	magenta = 105,
	cyan    = 106,
	gray    = 107
};

enum class control {  // Behaviour of rang function calls
	Off   = 0,  // toggle off rang style/color calls
	Auto  = 1,  // (Default) autodect terminal and colorize if needed
	Force = 2  // force ansi color output to non terminal streams
};
// Use rang::setControlMode to set rang control mode

enum class winTerm {  // Windows Terminal Mode
	Auto   = 0,  // (Default) automatically detects wheter Ansi or Native API
	Ansi   = 1,  // Force use Ansi API
	Native = 2  // Force use Native API
};
// Use rang::setWinTermMode to explicitly set terminal API for Windows
// Calling rang::setWinTermMode have no effect on other OS

namespace rang_implementation {

	inline std::atomic<control> &controlMode() noexcept
	{
		static std::atomic<control> value(control::Auto);
		return value;
	}

	inline std::atomic<winTerm> &winTermMode() noexcept
	{
		static std::atomic<winTerm> termMode(winTerm::Auto);
		return termMode;
	}

	inline bool supportsColor() noexcept
	{
#if defined(OS_LINUX) || defined(OS_MAC)
		static const bool result = [] {
			const char *Terms[]
			  = { "ansi",    "color",  "console", "cygwin", "gnome",
				  "konsole", "kterm",  "linux",   "msys",   "putty",
				  "rxvt",    "screen", "vt100",   "xterm" };

			const char *env_p = std::getenv("TERM");
			if (env_p == nullptr) {
				return false;
			}
			return std::any_of(std::begin(Terms), std::end(Terms),
							   [&](const char *term) {
								   return std::strstr(env_p, term) != nullptr;
							   });
		}();
#elif defined(OS_WIN)
		// All windows versions support colors through native console methods
		static constexpr bool result = true;
#else
		// No color for unknown platforms
		static constexpr bool result = false;

#endif
		return result;
	}

#ifdef OS_WIN
	inline bool isMsysPty(int fd) noexcept
	{
		// Dynamic load for binary compability with old Windows
		const auto ptrGetFileInformationByHandleEx
		  = reinterpret_cast<decltype(&GetFileInformationByHandleEx)>(
			GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")),
						   "GetFileInformationByHandleEx"));
		if (!ptrGetFileInformationByHandleEx) {
			return false;
		}

		HANDLE h = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
		if (h == INVALID_HANDLE_VALUE) {
			return false;
		}

		// Check that it's a pipe:
		if (GetFileType(h) != FILE_TYPE_PIPE) {
			return false;
		}

		// POD type is binary compatible with FILE_NAME_INFO from WinBase.h
		// It have the same alignment and used to avoid UB in caller code
		struct MY_FILE_NAME_INFO {
			DWORD FileNameLength;
			WCHAR FileName[MAX_PATH];
		};

		auto pNameInfo = std::unique_ptr<MY_FILE_NAME_INFO>(
		  new (std::nothrow) MY_FILE_NAME_INFO());
		if (!pNameInfo) {
			return false;
		}

		// Check pipe name is template of
		// {"cygwin-","msys-"}XXXXXXXXXXXXXXX-ptyX-XX
		if (!ptrGetFileInformationByHandleEx(h, FileNameInfo, pNameInfo.get(),
											 sizeof(MY_FILE_NAME_INFO))) {
			return false;
		}
		std::wstring name(pNameInfo->FileName, pNameInfo->FileNameLength / sizeof(WCHAR));
		if ((name.find(L"msys-") == std::wstring::npos
			 && name.find(L"cygwin-") == std::wstring::npos)
			|| name.find(L"-pty") == std::wstring::npos) {
			return false;
		}

		return true;
	}

#endif

	inline bool isTerminal(const std::streambuf *osbuf) noexcept
	{
		using std::cerr;
		using std::clog;
		using std::cout;
#if defined(OS_LINUX) || defined(OS_MAC)
		if (osbuf == cout.rdbuf()) {
			static const bool cout_term = isatty(fileno(stdout)) != 0;
			return cout_term;
		} else if (osbuf == cerr.rdbuf() || osbuf == clog.rdbuf()) {
			static const bool cerr_term = isatty(fileno(stderr)) != 0;
			return cerr_term;
		}
#elif defined(OS_WIN)
		if (osbuf == cout.rdbuf()) {
			static const bool cout_term
			  = (_isatty(_fileno(stdout)) || isMsysPty(_fileno(stdout)));
			return cout_term;
		} else if (osbuf == cerr.rdbuf() || osbuf == clog.rdbuf()) {
			static const bool cerr_term
			  = (_isatty(_fileno(stderr)) || isMsysPty(_fileno(stderr)));
			return cerr_term;
		}
#endif
		return false;
	}

	template <typename T>
	using enableStd = typename std::enable_if<
	  std::is_same<T, rang::style>::value || std::is_same<T, rang::fg>::value
		|| std::is_same<T, rang::bg>::value || std::is_same<T, rang::fgB>::value
		|| std::is_same<T, rang::bgB>::value,
	  std::ostream &>::type;


#ifdef OS_WIN
	struct SGR {  // Select Graphic Rendition parameters for Windows console
		BYTE fgColor;  // foreground color (0-15) lower 3 rgb bits + intense bit
		BYTE bgColor;  // background color (0-15) lower 3 rgb bits + intense bit
		BYTE bold;  // emulated as FOREGROUND_INTENSITY bit
		BYTE underline;  // emulated as BACKGROUND_INTENSITY bit
		BOOLEAN inverse;  // swap foreground/bold & background/underline
		BOOLEAN conceal;  // set foreground/bold to background/underline
	};

	enum class AttrColor : BYTE {  // Color attributes for console screen buffer
		black   = 0,
		red     = 4,
		green   = 2,
		yellow  = 6,
		blue    = 1,
		magenta = 5,
		cyan    = 3,
		gray    = 7
	};

	inline HANDLE getConsoleHandle(const std::streambuf *osbuf) noexcept
	{
		if (osbuf == std::cout.rdbuf()) {
			static const HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
			return hStdout;
		} else if (osbuf == std::cerr.rdbuf() || osbuf == std::clog.rdbuf()) {
			static const HANDLE hStderr = GetStdHandle(STD_ERROR_HANDLE);
			return hStderr;
		}
		return INVALID_HANDLE_VALUE;
	}

	inline bool setWinTermAnsiColors(const std::streambuf *osbuf) noexcept
	{
		HANDLE h = getConsoleHandle(osbuf);
		if (h == INVALID_HANDLE_VALUE) {
			return false;
		}
		DWORD dwMode = 0;
		if (!GetConsoleMode(h, &dwMode)) {
			return false;
		}
		dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
		if (!SetConsoleMode(h, dwMode)) {
			return false;
		}
		return true;
	}

	inline bool supportsAnsi(const std::streambuf *osbuf) noexcept
	{
		using std::cerr;
		using std::clog;
		using std::cout;
		if (osbuf == cout.rdbuf()) {
			static const bool cout_ansi
			  = (isMsysPty(_fileno(stdout)) || setWinTermAnsiColors(osbuf));
			return cout_ansi;
		} else if (osbuf == cerr.rdbuf() || osbuf == clog.rdbuf()) {
			static const bool cerr_ansi
			  = (isMsysPty(_fileno(stderr)) || setWinTermAnsiColors(osbuf));
			return cerr_ansi;
		}
		return false;
	}

	inline const SGR &defaultState() noexcept
	{
		static const SGR defaultSgr = []() -> SGR {
			CONSOLE_SCREEN_BUFFER_INFO info;
			WORD attrib = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
			if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE),
										   &info)
				|| GetConsoleScreenBufferInfo(GetStdHandle(STD_ERROR_HANDLE),
											  &info)) {
				attrib = info.wAttributes;
			}
			SGR sgr     = { 0, 0, 0, 0, FALSE, FALSE };
			sgr.fgColor = attrib & 0x0F;
			sgr.bgColor = (attrib & 0xF0) >> 4;
			return sgr;
		}();
		return defaultSgr;
	}

	inline BYTE ansi2attr(BYTE rgb) noexcept
	{
		static const AttrColor rev[8]
		  = { AttrColor::black,  AttrColor::red,  AttrColor::green,
			  AttrColor::yellow, AttrColor::blue, AttrColor::magenta,
			  AttrColor::cyan,   AttrColor::gray };
		return static_cast<BYTE>(rev[rgb]);
	}

	inline void setWinSGR(rang::bg col, SGR &state) noexcept
	{
		if (col != rang::bg::reset) {
			state.bgColor = ansi2attr(static_cast<BYTE>(col) - 40);
		} else {
			state.bgColor = defaultState().bgColor;
		}
	}

	inline void setWinSGR(rang::fg col, SGR &state) noexcept
	{
		if (col != rang::fg::reset) {
			state.fgColor = ansi2attr(static_cast<BYTE>(col) - 30);
		} else {
			state.fgColor = defaultState().fgColor;
		}
	}

	inline void setWinSGR(rang::bgB col, SGR &state) noexcept
	{
		state.bgColor = (BACKGROUND_INTENSITY >> 4)
		  | ansi2attr(static_cast<BYTE>(col) - 100);
	}

	inline void setWinSGR(rang::fgB col, SGR &state) noexcept
	{
		state.fgColor
		  = FOREGROUND_INTENSITY | ansi2attr(static_cast<BYTE>(col) - 90);
	}

	inline void setWinSGR(rang::style style, SGR &state) noexcept
	{
		switch (style) {
			case rang::style::reset: state = defaultState(); break;
			case rang::style::bold: state.bold = FOREGROUND_INTENSITY; break;
			case rang::style::underline:
			case rang::style::blink:
				state.underline = BACKGROUND_INTENSITY;
				break;
			case rang::style::reversed: state.inverse = TRUE; break;
			case rang::style::conceal: state.conceal = TRUE; break;
			default: break;
		}
	}

	inline SGR &current_state() noexcept
	{
		static SGR state = defaultState();
		return state;
	}

	inline WORD SGR2Attr(const SGR &state) noexcept
	{
		WORD attrib = 0;
		if (state.conceal) {
			if (state.inverse) {
				attrib = (state.fgColor << 4) | state.fgColor;
				if (state.bold)
					attrib |= FOREGROUND_INTENSITY | BACKGROUND_INTENSITY;
			} else {
				attrib = (state.bgColor << 4) | state.bgColor;
				if (state.underline)
					attrib |= FOREGROUND_INTENSITY | BACKGROUND_INTENSITY;
			}
		} else if (state.inverse) {
			attrib = (state.fgColor << 4) | state.bgColor;
			if (state.bold) attrib |= BACKGROUND_INTENSITY;
			if (state.underline) attrib |= FOREGROUND_INTENSITY;
		} else {
			attrib = state.fgColor | (state.bgColor << 4) | state.bold
			  | state.underline;
		}
		return attrib;
	}

	template <typename T>
	inline void setWinColorAnsi(std::ostream &os, T const value)
	{
		os << "\033[" << static_cast<int>(value) << "m";
	}

	template <typename T>
	inline void setWinColorNative(std::ostream &os, T const value)
	{
		const HANDLE h = getConsoleHandle(os.rdbuf());
		if (h != INVALID_HANDLE_VALUE) {
			setWinSGR(value, current_state());
			// Out all buffered text to console with previous settings:
			os.flush();
			SetConsoleTextAttribute(h, SGR2Attr(current_state()));
		}
	}

	template <typename T>
	inline enableStd<T> setColor(std::ostream &os, T const value)
	{
		if (winTermMode() == winTerm::Auto) {
			if (supportsAnsi(os.rdbuf())) {
				setWinColorAnsi(os, value);
			} else {
				setWinColorNative(os, value);
			}
		} else if (winTermMode() == winTerm::Ansi) {
			setWinColorAnsi(os, value);
		} else {
			setWinColorNative(os, value);
		}
		return os;
	}
#else
	template <typename T>
	inline enableStd<T> setColor(std::ostream &os, T const value)
	{
		return os << "\033[" << static_cast<int>(value) << "m";
	}
#endif
}  // namespace rang_implementation

template <typename T>
inline rang_implementation::enableStd<T> operator<<(std::ostream &os,
													const T value)
{
	const control option = rang_implementation::controlMode();
	switch (option) {
		case control::Auto:
			return rang_implementation::supportsColor()
				&& rang_implementation::isTerminal(os.rdbuf())
			  ? rang_implementation::setColor(os, value)
			  : os;
		case control::Force: return rang_implementation::setColor(os, value);
		default: return os;
	}
}

inline void setWinTermMode(const rang::winTerm value) noexcept
{
	rang_implementation::winTermMode() = value;
}

inline void setControlMode(const control value) noexcept
{
	rang_implementation::controlMode() = value;
}

}  // namespace rang

#undef OS_LINUX
#undef OS_WIN
#undef OS_MAC

#endif /* ifndef RANG_DOT_HPP */
