// SPDX-License-Identifier: BSD-3-Clause
#ifndef SUBSTRATE_CONSOLE
#define SUBSTRATE_CONSOLE

#include <cstdio>
#include <cstddef>
#include <array>
#include <type_traits>
#include <string>
#if __cplusplus >= 201703L
#	include <string_view>
#endif
#include <memory>
#include <mutex>
#include <cstring>
#ifdef _WIN32
#	include <io.h>
#endif

#include <substrate/internal/defs>
#include <substrate/utility>
#include <substrate/promotion_helpers>

#ifdef _MSC_VER
#pragma warning(push)
// (87): warning C4251: 'substrate::console_t::consoleMutex': class 'std::mutex'
// needs to have dll-interface to be used by clients of struct 'substrate::console_t'
#pragma warning(disable: 4251)
#endif

namespace substrate
{
	inline std::string operator ""_s(const char *str, const size_t len) noexcept { return {str, len}; }
	struct consoleStream_t;

	struct SUBSTRATE_CLS_API printable_t
	{
		constexpr printable_t() = default;
		virtual void operator()(const consoleStream_t &) const noexcept = 0;
		virtual ~printable_t() noexcept = default;

	protected:
		constexpr printable_t(const printable_t &) = default;
		constexpr printable_t(printable_t &&) = default;
		printable_t &operator =(const printable_t &) = default;
		printable_t &operator =(printable_t &&) = default;
	};

	struct SUBSTRATE_CLS_API consoleStream_t final
	{
	private:
		int32_t fd{-1};
		bool _tty{false};

		void checkTTY() noexcept;
#ifndef _WIN32
		void convertingWrite(const char16_t *string, const size_t stringLen) const noexcept;
#endif

	public:
		constexpr consoleStream_t() noexcept = default;
		consoleStream_t(const int32_t desc) noexcept : fd{desc} { checkTTY(); }
		SUBSTRATE_NO_DISCARD(constexpr bool valid() const noexcept) { return fd != -1; }
		SUBSTRATE_NO_DISCARD(constexpr bool isTTY() const noexcept) { return _tty; }
#ifdef _WIN32
		SUBSTRATE_NO_DISCARD(void *handle() const noexcept)
			{ return reinterpret_cast<void *>(_get_osfhandle(fd)); }
#endif

		void write(const void *buffer, size_t bufferLen) const noexcept;
		void write(const char *value) const noexcept;
		void write(const char *value, size_t valueLen) const noexcept;
#ifdef _WIN32
		void write(const wchar_t *value) const noexcept;
		void write(const wchar_t *value, size_t valueLen) const noexcept;
#endif
		void write(const char16_t *value) const noexcept;
		void write(const char16_t *value, size_t valueLen) const noexcept;
		void write(const char value) const noexcept { write(&value, 1U); }
		template<typename T> enable_if_t<!std::is_array<T>::value>
			write(const std::unique_ptr<T> &value) const noexcept
			{ value ? write(*value) : write("(null)"_s); }
		// NOLINTNEXTLINE(modernize-avoid-c-arrays, cppcoreguidelines-avoid-c-arrays)
		template<typename T> void write(const std::unique_ptr<T []> &value) const noexcept
			{ write(value.get()); }
		void write(const std::string &value) const noexcept
			{ write(value.data(), value.length()); }
#ifdef _WIN32
		void write(const std::wstring &value) const noexcept
			{ write(value.data(), value.length()); }
#endif
		void write(const std::u16string &value) const noexcept
			{ write(value.data(), value.length()); }
#if __cplusplus >= 201703L
		void write(const std::string_view &value) const noexcept
			{ write(value.data(), value.length()); }
#endif
		template<typename T, typename = enable_if_t<std::is_base_of<printable_t, T>::value>>
			void write(T &&printable) const noexcept { printable(*this); }
		template<typename T> enable_if_t<is_numeric<T>::value && !std::is_enum<T>::value>
			write(T value) const noexcept;
		template<typename T> enable_if_t<std::is_enum<T>::value> write(const T value) const noexcept
			{ write(static_cast<typename std::underlying_type<T>::type>(value)); }
		template<typename T> void write(const T *ptr) const noexcept;
		void write(bool value) const noexcept;
		template<size_t N> void write(const std::array<char, N> &value) const noexcept;
		template<typename T, size_t N> void write(const std::array<T, N> &value) const noexcept;
	};

	struct SUBSTRATE_CLS_API console_t final
	{
	private:
		mutable std::mutex consoleMutex{};
		consoleStream_t outputStream{};
		consoleStream_t errorStream{};
		bool valid_{false};
		bool showDebug_{true};

		static void write(const consoleStream_t &stream) noexcept { stream.write('\n'); }
		void write(const consoleStream_t &, std::nullptr_t) const noexcept { }
		template<typename T, typename... U> void write(const consoleStream_t &stream,
			T &&value, U &&...values) const noexcept
		{
			stream.write(std::forward<T>(value));
			write(stream, std::forward<U>(values)...);
		}

		void _error() const noexcept;
		void _warning() const noexcept;
		void _info() const noexcept;
		void _debug() const noexcept;

	public:
		console_t() noexcept = default;
		console_t(FILE *outStream, FILE *errStream) noexcept;
		console_t(const int32_t outStream, const int32_t errStream) noexcept :
			outputStream{outStream}, errorStream{errStream}, valid_{outputStream.valid() && errorStream.valid()} { }
		console_t(const console_t &) noexcept = delete;
		console_t(console_t &&other) noexcept : console_t{} { swap(other); }
		~console_t() noexcept = default;
		console_t operator =(const console_t &) noexcept = delete;
		bool valid() const noexcept { return valid_; }

		console_t &operator =(console_t &&other) noexcept
		{
			swap(other);
			return *this;
		}

		template<typename... T> void error(T &&...values) const noexcept
		{
			SUBSTRATE_NOWARN_UNUSED(std::lock_guard<std::mutex> lock){consoleMutex};
			_error();
			write(errorStream, std::forward<T>(values)...);
		}

		template<typename... T> void warning(T &&...values) const noexcept
		{
			SUBSTRATE_NOWARN_UNUSED(std::lock_guard<std::mutex> lock){consoleMutex};
			_warning();
			write(outputStream, std::forward<T>(values)...);
		}

		template<typename... T> void warn(T &&...values) const noexcept
		{
			SUBSTRATE_NOWARN_UNUSED(std::lock_guard<std::mutex> lock){consoleMutex};
			_warning();
			write(outputStream, std::forward<T>(values)...);
		}

		template<typename... T> void info(T &&...values) const noexcept
		{
			SUBSTRATE_NOWARN_UNUSED(std::lock_guard<std::mutex> lock){consoleMutex};
			_info();
			write(outputStream, std::forward<T>(values)...);
		}

		template<typename... T> void debug(T &&...values) const noexcept
		{
			SUBSTRATE_NOWARN_UNUSED(std::lock_guard<std::mutex> lock){consoleMutex};
			if (showDebug_)
			{
				_debug();
				write(outputStream, std::forward<T>(values)...);
			}
		}

		template<typename... T> void writeln(T &&...values) const noexcept
		{
			SUBSTRATE_NOWARN_UNUSED(std::lock_guard<std::mutex> lock){consoleMutex};
			write(outputStream, std::forward<T>(values)...);
		}

		void showDebug(const bool show) noexcept { showDebug_ = show; }
		bool showDebug() const noexcept { return showDebug_; }
		void dumpBuffer();

		void swap(console_t &other) noexcept
		{
			SUBSTRATE_NOWARN_UNUSED(std::lock_guard<std::mutex> ourLock){consoleMutex};
			SUBSTRATE_NOWARN_UNUSED(std::lock_guard<std::mutex> theirLock){other.consoleMutex};

			std::swap(outputStream, other.outputStream);
			std::swap(errorStream, other.errorStream);
			std::swap(valid_, other.valid_);
			std::swap(showDebug_, other.showDebug_);
		}
	};

	template<typename int_t> struct asInt_t final : public printable_t
	{
	private:
		using uint_t = promoted_type_t<make_unsigned_t<int_t>>;
		int_t value_;

		SUBSTRATE_NOINLINE uint_t format(const consoleStream_t &stream, const uint_t number) const noexcept
		{
			if (number < 10)
				stream.write(char(number + '0'));
			else
			{
				const auto digit{number - format(stream, number / 10U) * 10U};
				const auto value{static_cast<char>(static_cast<char>(digit) + '0')};
				stream.write(value);
			}
			return number;
		}

		template<typename T> enable_if_t<std::is_same<T, int_t>::value &&
			std::is_integral<T>::value && !is_boolean<T>::value && std::is_unsigned<T>::value>
			printTo(const consoleStream_t &stream) const noexcept { format(stream, value_); }

		template<typename T> SUBSTRATE_NOINLINE enable_if_t<std::is_same<T, int_t>::value &&
			std::is_integral<T>::value && !is_boolean<T>::value && std::is_signed<T>::value>
			printTo(const consoleStream_t &stream) const noexcept
		{
			if (value_ < 0)
			{
				stream.write('-');
				format(stream, ~static_cast<uint_t>(value_) + 1U);
			}
			else
				format(stream, static_cast<uint_t>(value_));
		}

	public:
		constexpr asInt_t(const int_t value) noexcept : value_{value} { }
		constexpr asInt_t(const asInt_t &) noexcept = default;
		constexpr asInt_t(asInt_t &&) noexcept = default;
		~asInt_t() noexcept final = default;
		asInt_t &operator =(const asInt_t &) noexcept = default;
		asInt_t &operator =(asInt_t &&) noexcept = default;
		void operator()(const consoleStream_t &stream) const noexcept final
			{ printTo<int_t>(stream); }
	};

	template<size_t paddedWidth, char paddingChar> struct writePadding_t final
	{
		void operator ()(const consoleStream_t &stream, const uint8_t maxDigits) const noexcept
		{
			if (maxDigits < paddedWidth)
			{
				// Put out the excess padding early to keep the logic simple.
				for (size_t i{0}; i < paddedWidth - maxDigits; ++i)
					stream.write(paddingChar);
			}
		}
	};

	template<char paddingChar> struct writePadding_t<0, paddingChar> final
		{ void operator ()(const consoleStream_t &, const uint8_t) const noexcept { } };

	template<size_t paddedWidth = 0, char paddingChar = ' '> struct asHex_t final : public printable_t
	{
	private:
		uint8_t maxDigits;
		uint8_t msbShift;
		uintmax_t _value;

	public:
		template<typename T, typename = enable_if_t<std::is_unsigned<T>::value>>
			constexpr asHex_t(const T value) noexcept : maxDigits{sizeof(T) * 2},
			msbShift{uint8_t(4U * (maxDigits - 1U))}, _value(value) { }

		constexpr asHex_t(const asHex_t &) noexcept = default;
		constexpr asHex_t(asHex_t &&) noexcept = default;
		~asHex_t() noexcept final = default;
		asHex_t &operator =(const asHex_t &) noexcept = default;
		asHex_t &operator =(asHex_t &&) noexcept = default;

		SUBSTRATE_NOINLINE void operator ()(const consoleStream_t &stream) const noexcept final
		{
			uintmax_t value{_value};
			// If we've been asked to pad by more than the maximum possible length of the number
			writePadding_t<paddedWidth, paddingChar>{}(stream, maxDigits);

			uint8_t digits{maxDigits};
			// For up to the maximum number of digits, pad as needed
			for (; digits > 1; --digits)
			{
				const uint8_t nibble((value >> msbShift) & 0x0FU);
				if (nibble == 0)
					value <<= 4;
				if (digits > paddedWidth && nibble == 0)
					continue;
				else if (digits <= paddedWidth && nibble == 0)
					stream.write(paddingChar);
				else
					break;
				// if 0 and paddedWidth == 0, we don't output anything here.
			}

			for (; digits > 0; --digits)
			{
				const uint8_t nibble((value >> msbShift) & 0x0FU);
				const char digit = char(nibble + '0');
				if (digit > '9')
					stream.write(char(digit + 7));
				else
					stream.write(digit);
				value <<= 4U;
			}
		}
	};

	template<typename T> enable_if_t<is_numeric<T>::value && !std::is_enum<T>::value>
		consoleStream_t::write(const T value) const noexcept { write(asInt_t<T>{value}); }

	template<typename T> void consoleStream_t::write(const T *const ptr) const noexcept
	{
		write("0x"_s);
		write(asHex_t<8, '0'>{
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
			reinterpret_cast<uintptr_t>(ptr) // lgtm[cpp/reinterpret-cast]
		});
	}

	template<size_t N> void consoleStream_t::write(const std::array<char, N> &value) const noexcept
	{
		for (const auto &elem : value)
			write(elem);
	}

	template<typename T, size_t N> void consoleStream_t::write(const std::array<T, N> &value) const noexcept
	{
		bool comma{false};
		for (const auto &elem : value)
		{
			if (comma)
				write(", "_s);
			comma = true;
			write(asHex_t<sizeof(T) * 2, '0'>{elem});
		}
	}

	struct SUBSTRATE_CLS_API asTime_t final : public printable_t
	{
	private:
		uint64_t value;

	public:
		constexpr asTime_t(const uint64_t _value) noexcept : value{_value} { }
		constexpr asTime_t(const asTime_t &) noexcept = default;
		constexpr asTime_t(asTime_t &&) noexcept = default;
		~asTime_t() noexcept final = default;
		asTime_t &operator =(const asTime_t &) noexcept = default;
		asTime_t &operator =(asTime_t &&) noexcept = default;

		void operator ()(const consoleStream_t &stream) const noexcept final
		{
			asInt_t<uint64_t>{value / 60}(stream);
			stream.write("m "_s);
			asInt_t<uint64_t>{value % 60}(stream);
			stream.write("s"_s);
		}
	};

	SUBSTRATE_API console_t console;
} // namespace substrate

#ifdef _MSC_VER
#pragma warning(pop)
#endif

#endif /*SUBSTRATE_CONSOLE*/
/* vim: set ft=cpp ts=4 sw=4 noexpandtab: */
