#pragma once // No BOM and only basic ASCII in this header, or a neko will die

#include "util/types.hpp"

#include <string>

namespace fmt
{
	template <typename CharT, usz N, typename... Args>
	static std::string format(const CharT(&)[N], const Args&...);

#ifdef _WIN32
	struct win_error
	{
		unsigned long error{};
		void* module_handle{};
	};

	// Get a string for a windows error (DWORD). Optionally a module HANDLE can be passed.
	std::string win_error_to_string(unsigned long error, void* module_handle = nullptr);
	std::string win_error_to_string(const win_error& error);
#endif
}

template <typename T, typename = void>
struct fmt_unveil
{
	static_assert(sizeof(T) > 0, "fmt_unveil<> error: incomplete type");

	using type = T;

	static inline u64 get(const T& arg)
	{
		return reinterpret_cast<uptr>(&arg);
	}

	// Temporary value container (can possibly be created by other fmt_unveil<> specializations)
	struct u64_wrapper
	{
		T arg;

		// Allow implicit conversion
		operator u64() const
		{
			return reinterpret_cast<uptr>(&arg);
		}
	};

	// This overload resolution takes the precedence
	static inline u64_wrapper get(T&& arg)
	{
		return u64_wrapper{std::move(arg)};
	}
};

template <typename T>
struct fmt_unveil<T, std::enable_if_t<std::is_integral<T>::value && sizeof(T) <= 8 && alignof(T) <= 8>>
{
	using type = T;

	static inline u64 get(T arg)
	{
		return static_cast<T>(arg);
	}
};

template <typename T>
struct fmt_unveil<T, std::enable_if_t<std::is_floating_point<T>::value && sizeof(T) <= 8 && alignof(T) <= 8>>
{
	using type = T;

	// Convert FP to f64 and reinterpret as u64
	static inline u64 get(const f64& arg)
	{
		return std::bit_cast<u64>(arg);
	}
};

template <typename T>
struct fmt_unveil<T, std::enable_if_t<std::is_enum<T>::value>>
{
	using type = T;

	static inline u64 get(T arg)
	{
		return static_cast<std::underlying_type_t<T>>(arg);
	}
};

template <typename T>
struct fmt_unveil<T*, void>
{
	using type = std::add_const_t<T>*;

	static inline u64 get(type arg)
	{
		return reinterpret_cast<uptr>(arg);
	}
};

namespace fmt
{
	template <typename T>
	concept CharT = (std::is_same_v<const T, const char> || std::is_same_v<const T, const char8_t>);
}

template <fmt::CharT T, usz N>
struct fmt_unveil<T[N], void>
{
	using type = std::add_const_t<T>*;

	static inline u64 get(type arg)
	{
		return reinterpret_cast<uptr>(arg);
	}
};

template <typename T, bool Se, usz Align>
struct fmt_unveil<se_t<T, Se, Align>, void>
{
	using type = typename fmt_unveil<T>::type;

	static inline auto get(const se_t<T, Se, Align>& arg)
	{
		return fmt_unveil<T>::get(arg);
	}
};

// String type format provider, also type classifier (format() called if an argument is formatted as "%s")
template <typename T, typename = void>
struct fmt_class_string
{
	// Formatting function (must be explicitly specialized)
	static void format(std::string& out, u64 arg);

	// Helper typedef (visible in format())
	using type = T;

	// Helper function (converts arg to object reference)
	static FORCE_INLINE SAFE_BUFFERS(const T&) get_object(u64 arg)
	{
		return *reinterpret_cast<const T*>(static_cast<uptr>(arg));
	}

	// Enum -> string function type
	using convert_t = const char*(*)(T value);

	// Helper function (safely converts arg to enum value)
	static FORCE_INLINE SAFE_BUFFERS(void) format_enum(std::string& out, u64 arg, convert_t convert)
	{
		const auto value = static_cast<std::underlying_type_t<T>>(arg);

		// Check narrowing
		if (static_cast<u64>(value) == arg)
		{
			if (const char* str = convert(static_cast<T>(value)))
			{
				out += str;
				return;
			}
		}

		// Fallback to underlying type formatting
		fmt_class_string<std::underlying_type_t<T>>::format(out, static_cast<u64>(value));
	}

	// Helper function (bitset formatting)
	static FORCE_INLINE SAFE_BUFFERS(void) format_bitset(std::string& out, u64 arg, const char* prefix, const char* delim, const char* suffix, void (*fmt)(std::string&, u64))
	{
		// Start from raw value
		fmt_class_string<u64>::format(out, arg);

		out += prefix;

		for (u64 i = 0; i < 63; i++)
		{
			const u64 mask = 1ull << i;

			if (arg & mask)
			{
				fmt(out, i);

				if (arg >> (i + 1))
				{
					out += delim;
				}
			}
		}

		if (arg & (1ull << 63))
		{
			fmt(out, 63);
		}

		out += suffix;
	}

	// Helper constant (may be used in format_enum as lambda return value)
	static constexpr const char* unknown = nullptr;
};

template <>
struct fmt_class_string<const void*, void>
{
	static void format(std::string& out, u64 arg);
};

template <typename T>
struct fmt_class_string<T*, void> : fmt_class_string<const void*, void>
{
	// Classify all pointers as const void*
};

template <>
struct fmt_class_string<const char*, void>
{
	static void format(std::string& out, u64 arg);
};

template <>
struct fmt_class_string<char*, void> : fmt_class_string<const char*>
{
	// Classify char* as const char*
};

template <>
struct fmt_class_string<const char8_t*, void> : fmt_class_string<const char*>
{
};

template <>
struct fmt_class_string<char8_t*, void> : fmt_class_string<const char8_t*>
{
};

template <>
struct fmt_class_string<const wchar_t*, void>
{
	static void format(std::string& out, u64 arg);
};

template <>
struct fmt_class_string<wchar_t*, void> : fmt_class_string<const wchar_t*>
{
};

namespace fmt
{
	template <typename T>
	concept StringConvertible = requires (T & t)
	{
		{ t.to_string() } -> std::convertible_to<std::string>;
	};
}

template <fmt::StringConvertible T>
struct fmt_class_string<T, void>
{
	static FORCE_INLINE SAFE_BUFFERS(const T&) get_object(u64 arg)
	{
		return *reinterpret_cast<const T*>(static_cast<uptr>(arg));
	}

	static void format(std::string& out, u64 arg)
	{
		out += get_object(arg).to_string();
	}
};

namespace fmt
{
	// Both uchar and std::byte are allowed
	template <typename T>
	concept ByteArray = requires (T& t) { const_cast<std::conditional_t<std::is_same_v<decltype(std::as_const(t[0])), const std::byte&>, std::byte, uchar>&>(std::data(t)[0]); };
}

template <fmt::ByteArray T>
struct fmt_class_string<T, void>
{
	static FORCE_INLINE SAFE_BUFFERS(const T&) get_object(u64 arg)
	{
		return *reinterpret_cast<const T*>(static_cast<uptr>(arg));
	}

	static void format(std::string& out, u64 arg)
	{
		const auto& obj = get_object(arg);

		void format_byte_array(std::string&, const uchar*, usz);
		format_byte_array(out, reinterpret_cast<const uchar*>(std::data(obj)), std::size(obj));
	}
};

struct fmt_type_info
{
	decltype(&fmt_class_string<int>::format) fmt_string;

	template <typename T>
	static constexpr fmt_type_info make()
	{
		return fmt_type_info
		{
			&fmt_class_string<T>::format,
		};
	}
};

// Argument array type (each element generated via fmt_unveil<>)
template <typename... Args>
using fmt_args_t = const u64(&&)[sizeof...(Args) + 1];

template <typename Arg>
using fmt_unveil_t = typename fmt_unveil<Arg>::type;

namespace fmt
{
	// Base-57 format helper
	struct base57
	{
		const uchar* data;
		usz size;

		template <typename T>
		base57(const T& arg)
			: data(reinterpret_cast<const uchar*>(&arg))
			, size(sizeof(T))
		{
		}

		base57(const uchar* data, usz size)
			: data(data)
			, size(size)
		{
		}
	};

	template <typename... Args>
	constexpr const fmt_type_info type_info_v[sizeof...(Args) + 1]{fmt_type_info::make<fmt_unveil_t<Args>>()...};

	// Internal formatting function
	void raw_append(std::string& out, const char*, const fmt_type_info*, const u64*) noexcept;

	// Formatting function
	template <typename CharT, usz N, typename... Args>
	FORCE_INLINE SAFE_BUFFERS(void) append(std::string& out, const CharT(&fmt)[N], const Args&... args)
	{
		raw_append(out, reinterpret_cast<const char*>(fmt), type_info_v<Args...>, fmt_args_t<Args...>{fmt_unveil<Args>::get(args)...});
	}

	// Formatting function
	template <typename CharT, usz N, typename... Args>
	FORCE_INLINE SAFE_BUFFERS(std::string) format(const CharT(&fmt)[N], const Args&... args)
	{
		std::string result;
		append(result, fmt, args...);
		return result;
	}

	// Internal exception message formatting template, must be explicitly specialized or instantiated in cpp to minimize code bloat
	[[noreturn]] void raw_throw_exception(const src_loc&, const char*, const fmt_type_info*, const u64*);

	// Throw exception with formatting
	template <typename CharT, usz N, typename... Args>
	struct throw_exception
	{
		[[noreturn]] FORCE_INLINE SAFE_BUFFERS() throw_exception(const CharT(&fmt)[N], const Args&... args,
			u32 line = __builtin_LINE(),
			u32 col = __builtin_COLUMN(),
			const char* file = __builtin_FILE(),
			const char* func = __builtin_FUNCTION())
		{
			raw_throw_exception({line, col, file, func}, reinterpret_cast<const char*>(fmt), type_info_v<Args...>, fmt_args_t<Args...>{fmt_unveil<Args>::get(args)...});
		}

#ifndef _MSC_VER
		[[noreturn]] ~throw_exception();
#endif
	};

	template <typename CharT, usz N, typename... Args>
	throw_exception(const CharT(&)[N], const Args&...) -> throw_exception<CharT, N, Args...>;

	// Helper template: pack format variables
	template <typename Arg = void, typename... Args>
	struct tie
	{
		// Universal reference
		std::add_rvalue_reference_t<Arg> arg;

		tie<Args...> next;

		// Store only references, unveil op is postponed
		tie(Arg&& arg, Args&&... args) noexcept
			: arg(std::forward<Arg>(arg))
			, next(std::forward<Args>(args)...)
		{
		}

		using type = std::remove_cvref_t<Arg>;

		// Storage for fmt_unveil (deferred initialization)
		decltype(fmt_unveil<type>::get(std::declval<Arg>())) value;

		void init(u64 to[])
		{
			value = fmt_unveil<type>::get(arg);
			to[0] = value;
			next.init(to + 1);
		}
	};

	template <>
	struct tie<void>
	{
		void init(u64 to[]) const
		{
			// Isn't really null terminated, this value has no meaning
			to[0] = 0;
		}
	};

	template <typename... Args>
	tie(Args&&... args) -> tie<Args...>;

	// Ensure with formatting
	template <typename T, typename CharT, usz N, typename... Args>
	decltype(auto) ensure(T&& arg, const CharT(&fmt)[N], tie<Args...> args,
		u32 line = __builtin_LINE(),
		u32 col = __builtin_COLUMN(),
		const char* file = __builtin_FILE(),
		const char* func = __builtin_FUNCTION()) noexcept
	{
		if (std::forward<T>(arg)) [[likely]]
		{
			return std::forward<T>(arg);
		}

		// Prepare u64 array
		u64 data[sizeof...(Args) + 1];
		args.init(data);

		raw_throw_exception({line, col, file, func}, reinterpret_cast<const char*>(fmt), type_info_v<std::remove_cvref_t<Args>...>, +data);
	}
}