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

#include <cstddef>
#include <cassert>
#include <vector>
#include <variant>
#include <any>
#include <tuple>
#include <optional>
#include <string_view>
#include <functional>
#include <type_traits>
#include <iterator>
#include <substrate/static_vector>
#include <substrate/index_sequence>
#include <substrate/flags>
#include <substrate/utility>

namespace substrate::commandLine
{
	enum class optionFlags_t
	{
		repeatable,
		takesParameter,
		required,
		global,
		exclusive,
	};

	enum class optionValueType_t
	{
		signedInt,
		unsignedInt,
		boolean,
		string,
		path,
		userDefined,
	};

	struct optionFlagPair_t final
	{
		std::string_view _shortFlag{};
		std::string_view _longFlag{};
	};

	struct optionValue_t final
	{
	private:
		std::string_view _metaName{};

	public:
		constexpr explicit optionValue_t(const std::string_view &metaName) noexcept : _metaName{metaName} { }
		[[nodiscard]] constexpr auto &metaName() const noexcept { return _metaName; }
	};

	struct optionSignedInt_t final
	{
		int64_t min{INT64_MIN};
		int64_t max{INT64_MAX};
	};

	struct optionUnsignedInt_t final
	{
		uint64_t min{0U};
		uint64_t max{UINT64_MAX};
	};

	struct optionAlternation_t;
	struct arguments_t;
	using optionValueParser_t = std::optional<std::any> (*)(const std::string_view &) noexcept;

	// A singular command line option such as a simple flag
	struct SUBSTRATE_CLS_API option_t final
	{
	private:
		using optionHolder_t = std::variant<std::string_view, optionFlagPair_t, optionValue_t>;
		using flagsHolder_t = bitFlags_t<uint8_t, optionFlags_t>;

		optionHolder_t _option;
		std::string_view _help;
		flagsHolder_t _flags{};
		optionValueType_t _valueType{optionValueType_t::string};
		std::variant<std::monostate, optionSignedInt_t, optionUnsignedInt_t> _valueAllowedRange{};
		optionValueParser_t _valueParserFn{nullptr};

	public:
		constexpr option_t(const optionHolder_t &option, const std::string_view &help) noexcept :
			_option{option}, _help{help} { }
		constexpr option_t(const optionValue_t &option, const std::string_view &help) noexcept :
			_option{option}, _help{help}, _flags{optionFlags_t::takesParameter} { }
		constexpr option_t(const option_t &) noexcept = default;
		constexpr option_t(option_t &&) noexcept = default;

		constexpr option_t &operator =(const option_t &) noexcept = default;
		constexpr option_t &operator =(option_t &&) noexcept = default;

		[[nodiscard]] constexpr option_t &valueType(const optionValueType_t type = optionValueType_t::string,
			const optionValueParser_t valueParserFn = nullptr) noexcept
		{
			_valueType = type;
			if (type == optionValueType_t::userDefined)
			{
				// A user-defined option must supply a parsing function to use
				assert(valueParserFn != nullptr);
				_valueParserFn = valueParserFn;
			}
			return *this;
		}

		[[nodiscard]] constexpr option_t &takesParameter(const optionValueType_t type = optionValueType_t::string,
			const optionValueParser_t valueParserFn = nullptr) noexcept
		{
			_flags.set(optionFlags_t::takesParameter);
			return valueType(type, valueParserFn);
		}

		[[nodiscard]] constexpr option_t &repeatable() noexcept
		{
			_flags.set(optionFlags_t::repeatable);
			return *this;
		}

		[[nodiscard]] constexpr option_t &required() noexcept
		{
			_flags.set(optionFlags_t::required);
			return *this;
		}

		[[nodiscard]] constexpr option_t &global() noexcept
		{
			_flags.set(optionFlags_t::global);
			return *this;
		}

		[[nodiscard]] constexpr option_t &exclusive() noexcept
		{
			_flags.set(optionFlags_t::exclusive);
			return *this;
		}

		template<typename T> [[nodiscard]] constexpr option_t &valueRange(T min, T max) noexcept
		{
			// These perform casts only to silence conversion warnings
			if (_valueType == optionValueType_t::signedInt)
				_valueAllowedRange = {optionSignedInt_t{int64_t(min), int64_t(max)}};
			else if (_valueType == optionValueType_t::unsignedInt)
				_valueAllowedRange = {optionUnsignedInt_t{uint64_t(min), uint64_t(max)}};
			return *this;
		}

		[[nodiscard]] bool matches(const std::string_view &argument) const noexcept;
		[[nodiscard]] std::optional<std::any> parseValue(const std::string_view &value) const noexcept;

		[[nodiscard]] constexpr bool takesParameter() const noexcept
			{ return _flags.includes(optionFlags_t::takesParameter); }
		[[nodiscard]] constexpr bool isRepeatable() const noexcept
			{ return _flags.includes(optionFlags_t::repeatable); }
		[[nodiscard]] constexpr bool isRequired() const noexcept
			{ return _flags.includes(optionFlags_t::required); }
		[[nodiscard]] constexpr bool isGlobal() const noexcept
			{ return _flags.includes(optionFlags_t::global); }
		[[nodiscard]] constexpr bool isExclusive() const noexcept
			{ return _flags.includes(optionFlags_t::exclusive); }
		[[nodiscard]] constexpr bool valueOnly() const noexcept
			{ return std::holds_alternative<optionValue_t>(_option); }

		[[nodiscard]] std::string_view metaName() const noexcept;
		[[nodiscard]] std::string displayName() const noexcept;
		[[nodiscard]] size_t displayLength() const noexcept;
		void displayHelp(size_t padding = 0U) const noexcept;
		[[nodiscard]] constexpr auto &helpText() const noexcept { return _help; }

		[[nodiscard]] constexpr bool operator <(const option_t &other) const noexcept
		{
			// If either variant holds no value, consider unequal
			if (_option.valueless_by_exception() || other._option.valueless_by_exception())
				return false;
			// Check if the alternative held is numerically <
			if (_option.index() < other._option.index())
				return true;
			// Check if the alternative held is numerically >
			if (_option.index() > other._option.index())
				return false;
			// Both are the same index, so compare on what's held
			if (std::holds_alternative<std::string_view>(_option))
				return std::get<std::string_view>(_option) < std::get<std::string_view>(other._option);
			if (std::holds_alternative<optionFlagPair_t>(_option))
				return std::get<optionFlagPair_t>(_option)._shortFlag <
					std::get<optionFlagPair_t>(other._option)._shortFlag;
			// If the alternative is optionValue_t, always return false
			return false;
		}

	private:
		[[nodiscard]] std::string_view typeToValue() const noexcept;
		[[nodiscard]] std::optional<std::any> parseSignedValue(const std::string_view &value) const noexcept;
		[[nodiscard]] std::optional<std::any> parseUnsignedValue(const std::string_view &value) const noexcept;
		[[nodiscard]] static std::optional<std::any> parseBoolValue(const std::string_view &value) noexcept;
		[[nodiscard]] static std::optional<std::any> parsePathValue(const std::string_view &value) noexcept;
	};

	// A set of command line option alternations such as a set of commands
	struct SUBSTRATE_CLS_API optionSet_t final
	{
	private:
		std::string_view _metaName{};
		staticVector_t<const optionAlternation_t> _alternations;

	public:
		constexpr optionSet_t(const std::string_view &metaName,
			const staticVector_t<const optionAlternation_t> &alternations) :
				_metaName{metaName}, _alternations{alternations} { }
		constexpr optionSet_t(const optionSet_t &) noexcept = default;
		constexpr optionSet_t(optionSet_t &&) noexcept = default;

		constexpr optionSet_t &operator =(const optionSet_t &) noexcept = default;
		constexpr optionSet_t &operator =(optionSet_t &&) noexcept = default;

		[[nodiscard]] constexpr auto &metaName() const noexcept { return _metaName; }
		[[nodiscard]] std::optional<std::reference_wrapper<const optionAlternation_t>>
			matches(const std::string_view &argument) const noexcept;
		[[nodiscard]] size_t displayPadding() const noexcept;
		void displayHelp(size_t padding = 0U) const noexcept;
		bool displayHelp(const arguments_t &args, const std::string_view &usage) const noexcept;

		[[nodiscard]] const optionAlternation_t *begin() const noexcept;
		[[nodiscard]] const optionAlternation_t *end() const noexcept;

		[[nodiscard]] bool operator <(const optionSet_t &other) const noexcept;
	};

	template<size_t N> constexpr static inline auto
		optionAlternations(optionAlternation_t (&&alternations)[N]) noexcept
			{ return make_array<const optionAlternation_t>(std::move(alternations)); }

	namespace internal
	{
		using optionsItem_t = std::variant<option_t, optionSet_t>;

		template<size_t offset, typename... options_t>
			constexpr optionsItem_t optionAt(const size_t index, const std::tuple<options_t...> &tuple)
		{
			if (index == offset)
				return std::get<offset>(tuple);
			else if constexpr (offset + 1U == sizeof...(options_t))
				throw std::out_of_range{"index into options out of range"};
			else
				return optionAt<offset + 1U>(index, tuple);
		}

		// This type exists to store an options tuple, allow it to be accessed by the builder,
		// and provide safe access to the held options. This is used by the optionsHolder_t
		// to help provide the go-between
		template<typename... options_t> struct optionsStorage_t final
		{
		private:
			std::tuple<options_t...> _options;

		public:
			constexpr optionsStorage_t(const std::tuple<options_t...> &options) noexcept : _options{options} { }
			constexpr optionsStorage_t(std::tuple<options_t...> &&options) noexcept : _options{std::move(options)} { }
			constexpr optionsStorage_t(const optionsStorage_t &) noexcept = default;
			constexpr optionsStorage_t &operator =(const optionsStorage_t &) noexcept = default;
			constexpr optionsItem_t operator [](const size_t index) const { return optionAt<0U>(index, _options); }
			constexpr auto &options() const noexcept { return _options; }

			void displayHelp(const arguments_t &args, const std::string_view &usage,
				const std::string_view &optionsTitle) const noexcept;
		};

		template<typename... options_t> optionsStorage_t(const std::tuple<options_t...> &) ->
			optionsStorage_t<options_t...>;
		template<typename... options_t> optionsStorage_t(std::tuple<options_t...> &&) ->
			optionsStorage_t<options_t...>;

		// Type-erased options iterator
		struct optionsIterator_t final
		{
		public:
			using indexFn_t = optionsItem_t (*)(size_t index, const void *storage);

		private:
			const void *_storage;
			size_t _index;
			indexFn_t _indexFn;

		public:
			constexpr optionsIterator_t(const void *storage, const size_t index, indexFn_t indexFn) noexcept :
				_storage{storage}, _index{index}, _indexFn{indexFn} { }
			constexpr optionsIterator_t(const optionsIterator_t &) noexcept = default;
			constexpr optionsIterator_t &operator =(const optionsIterator_t &) noexcept = default;
			constexpr void operator ++() noexcept { ++_index; }
			constexpr optionsItem_t operator *() const { return _indexFn(_index, _storage); }

			constexpr bool operator ==(const optionsIterator_t &other) const noexcept
				{ return _storage == other._storage && _index == other._index; }
			constexpr bool operator !=(const optionsIterator_t &other) const noexcept
				{ return _storage != other._storage || _index != other._index; }

			using difference_type = size_t;
			using value_type = optionsItem_t;
			using pointer = optionsItem_t;
			using reference = optionsItem_t;
			using iterator_category = std::forward_iterator_tag;
		};

		// In order to create a clean split interface, we have to take the options tuple here, generate
		// a type erased pointer to it, generate functions that then turn it back into its real type
		// and assign those functions to pointers in the holder that then act as a go-between.
		struct optionsHolder_t final
		{
		private:
			const void *storage{nullptr};
			size_t length{0U};
			optionsIterator_t::indexFn_t indexFn{nullptr};

			template<typename... options_t>
				static constexpr optionsItem_t index(const size_t index, const void *const storage)
			{
				const auto &options{*static_cast<const optionsStorage_t<options_t...> *>(storage)};
				return options[index];
			}

		public:
			constexpr optionsHolder_t() noexcept = default;
			template<typename... options_t>
				constexpr optionsHolder_t(const optionsStorage_t<options_t...> &options) noexcept :
					storage{static_cast<const void *>(&options)}, length{sizeof...(options_t)},
					indexFn{index<options_t...>} { }

			constexpr auto empty() const noexcept { return storage == nullptr; }
			constexpr auto begin() const noexcept { return optionsIterator_t{storage, 0U, indexFn}; }
			constexpr auto end() const noexcept { return optionsIterator_t{storage, length, indexFn}; }

			[[nodiscard]] size_t displayPadding() const noexcept;
			void displayHelp(const arguments_t &args, const std::string_view &usage,
				const std::string_view &optionsTitle) const noexcept;
		};

		template<typename... options_t>
			void optionsStorage_t<options_t...>::displayHelp(const arguments_t &args, const std::string_view &usage,
				const std::string_view &optionsTitle) const noexcept
		{
			optionsHolder_t holder{*this};
			holder.displayHelp(args, usage, optionsTitle);
		}
	}

	// The base container for a program's command line options
	struct options_t final
	{
	private:
		internal::optionsHolder_t _options{};

	public:
		constexpr options_t() noexcept = default;
		constexpr options_t(const internal::optionsHolder_t &options) : _options{options} { }
		constexpr options_t(const options_t &) noexcept = default;
		constexpr options_t(options_t &&) noexcept = default;

		constexpr options_t &operator =(const options_t &) noexcept = default;
		constexpr options_t &operator =(options_t &&) noexcept = default;

		[[nodiscard]] constexpr auto empty() const noexcept { return _options.empty(); }
		[[nodiscard]] constexpr auto begin() const noexcept { return _options.begin(); }
		[[nodiscard]] constexpr auto end() const noexcept { return _options.end(); }

		[[nodiscard]] size_t displayPadding() const noexcept { return _options.displayPadding(); }
		void displayHelp(const arguments_t &args, const std::string_view &usage,
			std::string_view optionsTitle = {}) const noexcept
				{ _options.displayHelp(args, usage, optionsTitle); }
	};

	namespace internal
	{
		using substrate::internal::indexSequence_t;
		using substrate::internal::makeIndexSequence;

		template<typename... options_t, size_t... indices> constexpr static inline
			std::array<const optionsItem_t, sizeof...(options_t)>
				optionTupleToArray(const std::tuple<options_t...> &tuple, indexSequence_t<indices...>)
			{ return {{std::get<indices>(tuple)...}}; }

		template<typename... options_t> constexpr static inline auto
			optionToTuple(const optionsStorage_t<options_t...> &tuple) { return tuple.options(); }
		constexpr static inline std::tuple<option_t> optionToTuple(const option_t &option) { return {option}; }
		constexpr static inline std::tuple<optionSet_t> optionToTuple(const optionSet_t &optionSet)
			{ return {optionSet}; }

		template<typename... results_t, typename option_t, typename... options_t> constexpr static inline auto
			buildOptionsTuple(const std::tuple<results_t...> &results, option_t &&option, options_t &&...options) noexcept
		{
			const auto result{std::tuple_cat(results, optionToTuple(std::forward<option_t>(option)))};
			if constexpr (sizeof...(options_t) == 0)
				return result;
			else
				return buildOptionsTuple(result, std::forward<options_t>(options)...);
		}

		template<typename... options_t> constexpr static inline auto buildOptions(options_t &&...options) noexcept
			{ return buildOptionsTuple(std::tuple<>{}, std::forward<options_t>(options)...); }
	} // namespace internal

	template<typename... options_t> constexpr static inline auto options(options_t &&...options) noexcept
		{ return internal::optionsStorage_t{internal::buildOptions(std::forward<options_t>(options)...)}; }

	// Represents a specific alternation in an optionSet_t
	struct SUBSTRATE_CLS_API optionAlternation_t final
	{
	private:
		std::string_view _value;
		std::string_view _help{};
		options_t _suboptions{};

	public:
		constexpr optionAlternation_t(const std::string_view &value, const std::string_view &help) noexcept :
			_value{value}, _help{help} { }
		template<typename... options_t> constexpr optionAlternation_t(const std::string_view &value,
			const std::string_view &help, const internal::optionsStorage_t<options_t...> &suboptions) noexcept :
				_value{value}, _help{help}, _suboptions{suboptions} { }
		constexpr optionAlternation_t(const optionAlternation_t &) noexcept = default;
		constexpr optionAlternation_t(optionAlternation_t &&) noexcept = default;

		constexpr optionAlternation_t &operator =(const optionAlternation_t &) noexcept = default;
		constexpr optionAlternation_t &operator =(optionAlternation_t &&) noexcept = default;

		[[nodiscard]] bool matches(const std::string_view &argument) const noexcept;
		[[nodiscard]] constexpr auto &suboptions() const noexcept { return _suboptions; }

		[[nodiscard]] constexpr auto &displayName() const noexcept { return _value; }
		[[nodiscard]] constexpr auto displayLength() const noexcept { return _value.length(); }
		[[nodiscard]] constexpr auto &helpText() const noexcept { return _help; }
	};

	namespace internal
	{
		struct optionsRoot_t final
		{
		private:
			options_t _options;
			std::string_view _usage{};
			std::string_view _helpHeader{};
			std::string_view _helpFooter{};

		public:
			constexpr optionsRoot_t(const optionsHolder_t &options) : _options{options} { }
			constexpr optionsRoot_t(const optionsRoot_t &other, const optionsHolder_t &options) noexcept :
				_options{options}, _usage{other._usage}, _helpHeader{other._helpHeader}, _helpFooter{other._helpFooter}
					{ }
			constexpr optionsRoot_t(optionsRoot_t &&) noexcept = default;

			constexpr optionsRoot_t &operator =(const optionsRoot_t &) noexcept = default;
			constexpr optionsRoot_t &operator =(optionsRoot_t &&) noexcept = default;

			[[nodiscard]] constexpr auto &options() const noexcept { return _options; }

			[[nodiscard]] constexpr auto empty() const noexcept { return _options.empty(); }
			[[nodiscard]] constexpr auto begin() const noexcept { return _options.begin(); }
			[[nodiscard]] constexpr auto end() const noexcept { return _options.end(); }

			constexpr void usage(const std::string_view &usage) noexcept { _usage = usage; }
			constexpr void helpHeader(const std::string_view &header) noexcept { _helpHeader = header; }
			constexpr void helpFooter(const std::string_view &footer) noexcept { _helpFooter = footer; }

			void displayHelp(const arguments_t &args) const noexcept;
		};
	} // namespace internal

	template<typename... options_t> struct optionsRoot_t final
	{
	private:
		internal::optionsStorage_t<options_t...> _optionsStorage;
		internal::optionsRoot_t _options{_optionsStorage};

	public:
		constexpr optionsRoot_t(const internal::optionsStorage_t<options_t...> &options) : _optionsStorage{options} { }
		constexpr optionsRoot_t(const optionsRoot_t &other) noexcept : _optionsStorage{other._optionsStorage},
			_options{other._options, _optionsStorage} { }
		constexpr optionsRoot_t(optionsRoot_t &&) noexcept = default;

		constexpr optionsRoot_t &operator =(const optionsRoot_t &) noexcept = default;
		constexpr optionsRoot_t &operator =(optionsRoot_t &&) noexcept = default;
		[[nodiscard]] constexpr auto &options() const noexcept { return _options.options(); }

		[[nodiscard]] constexpr auto empty() const noexcept { return _options.empty(); }
		[[nodiscard]] constexpr auto begin() const noexcept { return _options.begin(); }
		[[nodiscard]] constexpr auto end() const noexcept { return _options.end(); }

		[[nodiscard]] constexpr auto &usage(const std::string_view &usage) noexcept
		{
			_options.usage(usage);
			return *this;
		}

		[[nodiscard]] constexpr auto &helpHeader(const std::string_view &header) noexcept
		{
			_options.helpHeader(header);
			return *this;
		}

		[[nodiscard]] constexpr auto &helpFooter(const std::string_view &footer) noexcept
		{
			_options.helpFooter(footer);
			return *this;
		}

		void displayHelp(const arguments_t &args) const noexcept { _options.displayHelp(args); }
	};

	template<typename... options_t> optionsRoot_t(options_t...) -> optionsRoot_t<options_t...>;
} // namespace substrate::commandLine

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