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

#include <type_traits>
#include <utility>
#include <string>
#include <chrono>
#include <substrate/utility>
#include <substrate/internal/defs>
#include <substrate/promotion_helpers>
#if __cplusplus >= 201703L
#include <substrate/span>
#endif

namespace substrate
{
	template<typename int_t, typename valueType_t, std::size_t _length = static_cast<std::size_t>(-1), char padding = '0'>
		struct fromInt_t
	{
	public:
		constexpr static const auto npos{static_cast<std::size_t>(-1)};

	private:
		using uint_t = make_unsigned_t<int_t>;
		using arithInt_t = promoted_type_t<int_t>;
		using arithUint_t = make_unsigned_t<arithInt_t>;
		const valueType_t &_value;

		SUBSTRATE_NO_DISCARD(constexpr std::size_t calcDigits(const arithUint_t number) const noexcept)
			{ return 1U + (number < 10 ? 0U : calcDigits(number / 10U)); }

		SUBSTRATE_NO_DISCARD(constexpr std::size_t digits(const arithInt_t number) const noexcept)
		{
			return std::is_signed<int_t>::value && number < 0 ?
				1U + calcDigits(~arithUint_t(number) + 1U) : calcDigits(arithUint_t(number));
		}

		SUBSTRATE_NO_DISCARD(constexpr std::size_t power10(const std::size_t power) const noexcept)
			{ return power ? power10(power - 1U) * 10U : 1U; }

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR size_t zeros(const arithInt_t number) const noexcept)
		{
			const int_t num{number / 10};
			return num * 10U == number ? 1U + zeros(num) : 0U;
		}

		template<typename R, typename P> SUBSTRATE_CXX14_CONSTEXPR size_t
			zeros(const std::chrono::duration<R, P> &number) const noexcept
				{ return zeros(number.count()); }

		SUBSTRATE_NO_DISCARD(constexpr size_t hexDigits(const arithUint_t number) const noexcept)
			{ return 1U + (number < 16U ? 0U : hexDigits(number >> 4U)); }

		SUBSTRATE_NO_DISCARD(constexpr size_t octalDigits(const arithUint_t number) const noexcept)
			{ return 1U + (number < 8U ? 0U : octalDigits(number >> 3U)); }

		template<typename value_t> constexpr enable_if_t<!is_duration<value_t>::value, arithInt_t>
			valueAsInt(const value_t &number) const noexcept { return number; }

		template<typename R, typename P> constexpr arithInt_t
			valueAsInt(const std::chrono::duration<R, P> &number) const noexcept
				{ return number.count(); }

		SUBSTRATE_NOINLINE SUBSTRATE_CXX14_CONSTEXPR arithUint_t process(const arithUint_t number, char *const buffer,
			const std::size_t digits, const std::size_t index) const noexcept
		{
			if (number < 10)
			{
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				buffer[digits - index] = static_cast<char>(number + '0');
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				buffer[digits + 1] = 0;
			}
			else
			{
				const auto num{char(number - (process(number / 10U, buffer, digits, index + 1U) * 10U))};
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				buffer[digits - index] = static_cast<char>(num + '0');
			}
			return number;
		}

		SUBSTRATE_CXX14_CONSTEXPR void process(const arithUint_t number, char *const buffer) const noexcept
			{ process(number, buffer, digits() - 1U, 0); }
		template<typename T = int_t> SUBSTRATE_CXX14_CONSTEXPR enable_if_t<std::is_same<T, int_t>::value &&
			std::is_integral<T>::value && !is_boolean<T>::value && std::is_unsigned<T>::value && _length == npos>
			format(char *const buffer) const noexcept { process(valueAsInt(_value), buffer); }
		template<typename T = int_t> SUBSTRATE_NOINLINE SUBSTRATE_CXX14_CONSTEXPR
			enable_if_t<std::is_same<T, int_t>::value && std::is_integral<T>::value && !is_boolean<T>::value &&
			std::is_signed<T>::value && _length == npos> format(char *const buffer) const noexcept
		{
			const auto value{valueAsInt(_value)};
			if (value < 0)
			{
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				buffer[0] = '-';
				process(~arithUint_t(value) + 1U, buffer);
			}
			else
				process(arithUint_t(value), buffer);
		}

		SUBSTRATE_CXX14_CONSTEXPR void formatFixed(char *const buffer, const arithUint_t value) const noexcept
		{
			const auto length{calcDigits(value)};
			if (length <=  _length)
			{
				const auto offset{_length - length};
				const auto _buffer{std::fill_n(buffer, offset, padding)};
				process(value, _buffer, uint8_t(length - 1U), 0U);
			}
			else
				std::fill_n(buffer, _length, padding);
		}

		template<typename T = int_t> SUBSTRATE_CXX14_CONSTEXPR enable_if_t<std::is_same<T, int_t>::value &&
			std::is_integral<T>::value && !is_boolean<T>::value && std::is_unsigned<T>::value && _length != npos>
			format(char *const buffer) const noexcept { formatFixed(buffer, valueAsInt(_value)); }

		template<typename T = int_t> SUBSTRATE_NOINLINE SUBSTRATE_CXX14_CONSTEXPR
			enable_if_t<std::is_same<T, int_t>::value && std::is_integral<T>::value && !is_boolean<T>::value &&
			std::is_signed<T>::value && _length != npos> format(char *const buffer) const noexcept
		{
			const auto value{valueAsInt(_value)};
			if (value < 0)
			{
				const auto length{digits(value)};
				if (length <= _length)
				{
					const auto offset{_length - length};
					// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
					const auto _buffer{std::fill_n(buffer + 1, offset, padding)};
					process(~arithUint_t(value) + 1U, _buffer, length - 2U, 0U);
				}
				else
					std::fill_n(buffer, _length, padding);
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				buffer[0] = '-';
			}
			else
				formatFixed(buffer, uint_t(value));
		}

#if __cplusplus >= 201703L
		void formatHex(span<char> result, const bool upperCase, const ptrdiff_t offset) const noexcept
#else
		void formatHex(std::string &result, const bool upperCase, const ptrdiff_t offset) const noexcept
#endif
		{
			auto value{valueAsInt(_value)};
			auto digit{result.rbegin() + offset};
			while (digit != result.rend())
			{
				auto hex{arithUint_t(value & 0x0FU)};
				value >>= 4U;
				if (hex > 9U)
				{
					hex += 0x07U;
					if (!upperCase)
						hex += 0x20U;
				}
				*digit++ = char(hex + 0x30U);
				if (!value)
					break;
			}
			std::fill_n(result.begin(), result.rend() - digit, padding);
		}

#if __cplusplus >= 201703L
		void formatOctal(span<char> result, const ptrdiff_t offset) const noexcept
#else
		void formatOctal(std::string &result, const ptrdiff_t offset) const noexcept
#endif
		{
			auto value{valueAsInt(_value)};
			auto digit{result.rbegin() + offset};
			while (digit != result.rend())
			{
				auto hex{arithUint_t(value & 0x07U)};
				value >>= 3U;
				*digit++ = char(hex + 0x30U);
				if (!value)
					break;
			}
			std::fill_n(result.begin(), result.rend() - digit, padding);
		}

		// This function aims to do two things
		// If the number of digits in the number is less than maxDigits,
		// pad the front of the number with 0's and chop any trailing 0's off
		// If the number of digits in the number is more, truncate to maxDigits and chop trailing 0's off.
		// In the former case, 0 is a special-case that results in a single 0
		// In the latter case, if the result of truncation is 0, we also trunate the output to a single 0.
		SUBSTRATE_CXX14_CONSTEXPR void formatFraction(const uint8_t maxDigits, char *const buffer) const noexcept
		{
			const arithInt_t value{valueAsInt(_value)};
			const auto digits_{digits(value)};
			// TODO: This technically hides crimes for signed numbers.. We need to consider if this is right or not
			const auto fraction{static_cast<arithUint_t>(value)};
			if (!fraction)
				buffer[0] = '0';
			else if (digits_ >= maxDigits)
			{
				const auto truncatedFraction{fraction - ((fraction / power10(maxDigits)) * power10(maxDigits))};
				if (!truncatedFraction)
					buffer[0] = '0';
				else
				{
					const auto trailingZeros_{trailingZeros()};
					process(static_cast<arithUint_t>(truncatedFraction / power10(trailingZeros_)),
						buffer, maxDigits - trailingZeros_ - 1U, 0U);
				}
			}
			else
			{
				const auto trailingZeros_{trailingZeros()};
				const auto leadingZeros{maxDigits - digits_};
				for (size_t i{0}; i < leadingZeros; ++i)
					// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
					buffer[i] = '0';
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				process(static_cast<arithUint_t>(fraction / power10(trailingZeros_)), buffer + leadingZeros,
					digits_ - trailingZeros_ - 1U, 0U);
			}
		}

	public:
		constexpr fromInt_t(const valueType_t &value) noexcept : _value(value) { }

		// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
		SUBSTRATE_NO_DISCARD(operator std::unique_ptr<char []>() const noexcept)
		{
			// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
			auto number{make_unique_nothrow<char []>(length())};
			if (!number)
				return nullptr;
			format(number.get());
			return number;
		}

		SUBSTRATE_NO_DISCARD(explicit operator const char *() const noexcept)
		{
			// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
			std::unique_ptr<char []> number{*this};
			return number.release();
		}

		SUBSTRATE_NO_DISCARD(operator std::string() const)
		{
			std::string number(length(), '\0');
#if __cplusplus >= 201703L
			format(number.data());
#else
			format(&number.front());
#endif
			return number;
		}

		template<typename T = int_t> enable_if_t<std::is_same<T, int_t>::value && std::is_unsigned<T>::value,
			std::string> toHex(const bool upper = true) const noexcept
		{
			std::string number(hexLength(), '\0');
			formatHex(number, upper, 1);
			return number;
		}

		template<typename T = int_t> enable_if_t<std::is_same<T, int_t>::value && std::is_unsigned<T>::value,
			std::string> toOctal() const noexcept
		{
			std::string number(octalLength(), '\0');
			formatOctal(number, 1);
			return number;
		}

		SUBSTRATE_NO_DISCARD(std::string toDec() const noexcept) { return *this; }
		SUBSTRATE_NO_DISCARD(constexpr std::size_t digits() const noexcept) { return digits(valueAsInt(_value)); }
		SUBSTRATE_NO_DISCARD(constexpr std::size_t length() const noexcept)
			{ return _length == npos ? digits() + 1U : _length + 1U; }
		SUBSTRATE_NO_DISCARD(constexpr std::size_t hexLength() const noexcept)
			{ return _length == npos ? hexDigits(valueAsInt(_value)) + 1U : _length + 1U; }
		SUBSTRATE_NO_DISCARD(constexpr std::size_t octalLength() const noexcept)
			{ return _length == npos ? octalDigits(valueAsInt(_value)) + 1U : _length + 1U; }
		SUBSTRATE_CXX14_CONSTEXPR void formatTo(char *const buffer) const noexcept { format(buffer); }

#if __cplusplus >= 201703L
		SUBSTRATE_CXX14_CONSTEXPR bool formatToHex(span<char> buffer, const bool upper = true) const noexcept
		{
			// Check there's enough buffer in the provided string view
			const auto length{hexLength() - 1U};
			if (length > buffer.size_bytes())
				return false;
			// If there is, then format the value into the buffer
			formatHex(buffer, upper, 0);
			return true;
		}

		SUBSTRATE_CXX14_CONSTEXPR bool formatToOctal(span<char> buffer) const noexcept
		{
			// Check there's enough buffer in the provided string view
			const auto length{octalLength() - 1U};
			if (length > buffer.size_bytes())
				return false;
			// If there is, then format the value into the buffer
			formatOctal(buffer, 0);
			return true;
		}
#endif

		// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
		SUBSTRATE_NO_DISCARD(std::unique_ptr<char []> formatFraction(const uint8_t maxDigits) const noexcept)
		{
			// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
			auto number{make_unique_nothrow<char []>(fractionLength(maxDigits))};
			if (!number)
				return nullptr;
			formatFraction(maxDigits, number.get());
			return number;
		}

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR size_t fractionDigits(const uint8_t maxDigits) const noexcept)
		{
			const auto digits_{digits()};
			if (digits_ > maxDigits)
				return maxDigits;
			return (maxDigits - digits_) + (digits_ - trailingZeros());
		}

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR size_t trailingZeros() const noexcept)
			{ return valueAsInt(_value) ? zeros(_value) : 0U; }
		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR uint8_t fractionLength(const uint8_t maxDigits) const noexcept)
			{ return uint8_t(fractionDigits(maxDigits) + 1U); }
		void formatFractionTo(const uint8_t maxDigits, char *const buffer) const noexcept
			{ formatFraction(maxDigits, buffer); }
	};

#if __cplusplus >= 201703L
	template<typename value_t> fromInt_t(value_t) -> fromInt_t<value_t, value_t>;
#endif

	template<typename value_t>
		constexpr inline fromInt_t<value_t, value_t> fromInt(const value_t &value) { return {value}; }
	template<typename int_t, typename value_t>
		constexpr inline fromInt_t<int_t, value_t> fromInt(const value_t &value) { return {value}; }
	template<size_t length, typename value_t>
		constexpr inline fromInt_t<value_t, value_t, length> fromInt(const value_t &value) noexcept { return {value}; }
	template<size_t length, char padding, typename value_t>
		constexpr inline fromInt_t<value_t, value_t, length, padding>
			fromInt(const value_t &value) noexcept { return {value}; }

#ifdef _MSC_VER
#pragma warning(push)
// (386): warning C4146: unary minus operator applied to unsigned type,
// result still unsigned
#pragma warning(disable: 4146)
#endif

	template<typename int_t> struct toInt_t
	{
	private:
		using uint_t = make_unsigned_t<int_t>;
		using arithInt_t = promoted_type_t<int_t>;
		using arithUint_t = make_unsigned_t<arithInt_t>;
		const char *const _value{nullptr};
		const std::size_t _length{};
		constexpr static const bool _isSigned = std::is_signed<int_t>::value;

		SUBSTRATE_NO_DISCARD(constexpr static bool isNumber(const char x) noexcept) { return x >= '0' && x <= '9'; }
		SUBSTRATE_NO_DISCARD(constexpr static bool isHex(const char x) noexcept)
			{ return isNumber(x) || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F'); }
		SUBSTRATE_NO_DISCARD(constexpr static bool isOct(const char x) noexcept) { return x >= '0' && x <= '7'; }

		template<bool isFunc(const char)> SUBSTRATE_CXX14_CONSTEXPR bool checkValue() const noexcept
		{
			for (size_t i{}; i < _length; ++i)
			{
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				if (!isFunc(_value[i]))
					return false;
			}
			return true;
		}

	public:
		SUBSTRATE_CXX17_CONSTEXPR toInt_t(const char *const value) noexcept :
			_value{value}, _length{std::char_traits<char>::length(value)} { }
		constexpr toInt_t(const char *const value, const std::size_t subLength) noexcept :
			_value{value}, _length{subLength} { }
		constexpr std::size_t length() const noexcept { return _length; }

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR bool isDec() const noexcept)
		{
			SUBSTRATE_IF_CONSTEXPR (_isSigned) {
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				if(_length > 0 && _value[0] == '-' && length() == 1)
					return false;
			}
			for (size_t i{}; i < _length; ++i)
			{
				SUBSTRATE_IF_CONSTEXPR (_isSigned) {
					// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
					if(i == 0 && _value[i] == '-')
						continue;
				}
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				if (!isNumber(_value[i]))
					return false;
			}
			return true;
		}

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR bool isHex() const noexcept) { return checkValue<isHex>(); }
		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR bool isOct() const noexcept) { return checkValue<isOct>(); }
		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR int_t fromDec() const noexcept) { return *this; }

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR operator int_t() const noexcept)
		{
			if (!isDec())
				return {};
			arithUint_t result{};
			for (size_t i{}; i < _length; ++i)
			{
				SUBSTRATE_IF_CONSTEXPR (_isSigned) {
					// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
					if(i == 0U && _value[i] == '-')
						continue;
				}
				result *= 10U;
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				result += arithUint_t(_value[i] - '0');
			}

			SUBSTRATE_IF_CONSTEXPR (_isSigned) {
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				if (_length > 0 && _value[0] == '-')
					return static_cast<int_t>(-result);
			}

			return static_cast<int_t>(result);
		}

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR int_t fromHex() const noexcept)
		{
			if (!isHex())
				return {};
			arithUint_t value{};
			for (size_t i{}; i < _length; ++i)
			{
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				auto hex{arithUint_t(_value[i])};
				if (hex >= 'a' && hex <= 'f')
					hex -= 0x20U;
				value <<= 4U;
				hex -= 0x30U;
				if (hex > 9U)
					hex -= 0x07U;
				value += hex;
			}
			return int_t(value);
		}

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR int_t fromOct() const noexcept)
		{
			if (!isOct())
				return {};
			arithUint_t result{};
			for (size_t i{}; i < _length; ++i)
			{
				result <<= 3U;
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				result += arithUint_t{uint8_t(_value[i])} - 0x30U;
			}
			return int_t(result);
		}
	};

#ifdef _MSC_VER
#pragma warning(pop)
#endif
}

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