#pragma once

#include <psemek/gfx/gltf_parser.hpp>
#include <psemek/util/range.hpp>

#include <type_traits>

namespace psemek::gfx
{

	namespace detail
	{

		template <typename T, bool IsArithmetic>
		struct accessor_traits_helper
		{
			static constexpr bool is_floating_point = std::is_floating_point_v<T>;
			static constexpr std::size_t components = 1;
			using component_type = T;

			static auto pointer(T & value)
			{
				return &value;
			}

			static void finalize(T &)
			{}
		};

		template <typename T>
		struct accessor_traits_helper<T, false>
		{};

		template <typename T>
		struct accessor_traits
			: accessor_traits_helper<T, std::is_arithmetic_v<T>>
		{};

		template <typename T, std::size_t N>
		struct accessor_traits<math::vector<T, N>>
		{
			static constexpr bool is_floating_point = accessor_traits<T>::is_floating_point;
			static constexpr std::size_t components = N;
			using component_type = T;

			static auto pointer(math::vector<T, N> & value)
			{
				return &value[0];
			}

			static void finalize(math::vector<T, N> &)
			{}
		};

		template <typename T, std::size_t N>
		struct accessor_traits<math::point<T, N>>
		{
			static constexpr bool is_floating_point = accessor_traits<T>::is_floating_point;
			static constexpr std::size_t components = N;
			using component_type = T;

			static auto pointer(math::point<T, N> & value)
			{
				return &value[0];
			}

			static void finalize(math::point<T, N> &)
			{}
		};

		template <typename T>
		struct accessor_traits<math::quaternion<T>>
		{
			static constexpr bool is_floating_point = accessor_traits<T>::is_floating_point;
			static constexpr std::size_t components = 4;
			using component_type = T;

			static auto pointer(math::quaternion<T> & value)
			{
				return &value[0];
			}

			static void finalize(math::quaternion<T> &)
			{}
		};

		template <typename T, std::size_t R, std::size_t C>
		struct accessor_traits<math::matrix<T, R, C>>
		{
			static constexpr bool is_floating_point = accessor_traits<T>::is_floating_point;
			static constexpr std::size_t components = R * C;
			using component_type = T;

			static auto pointer(math::matrix<T, R, C> & value)
			{
				return value.values().begin();
			}

			static void finalize(math::matrix<T, R, C> & value)
			{
				value = math::transpose(value);
			}
		};

		template <typename T>
		void safe_cast(gltf_asset::accessor::component_type_t type, bool normalized, T & dst, char const * src)
		{
			using component_type_t = gltf_asset::accessor::component_type_t;
			if (type == gltf_asset::accessor::component_type_t::_float)
			{
				dst = *(float const *)(src);
			}
			else
			{
				if constexpr (std::is_floating_point_v<T>)
				{
					if (normalized)
					{
						switch (type)
						{
						case component_type_t::byte:
							dst = std::max<std::int8_t>(-127, *(std::int8_t const *)(src)) / 127.f;
							break;
						case component_type_t::unsigned_byte:
							dst = *(std::uint8_t const *)(src) / 255.f;
							break;
						case component_type_t::_short:
							dst = std::max<std::int16_t>(-32767, *(std::int16_t const *)(src)) / 32767.f;
							break;
						case component_type_t::unsigned_short:
							dst = *(std::uint16_t const *)(src) / 65535.f;
							break;
						case component_type_t::unsigned_int:
							dst = *(std::uint32_t const *)(src) / 4294967296.f;
							break;
						default:
							break;
						}
					}
					else
					{
						switch (type)
						{
						case component_type_t::byte:
							dst = *(std::int8_t const *)(src);
							break;
						case component_type_t::unsigned_byte:
							dst = *(std::uint8_t const *)(src);
							break;
						case component_type_t::_short:
							dst = *(std::int16_t const *)(src);
							break;
						case component_type_t::unsigned_short:
							dst = *(std::uint16_t const *)(src);
							break;
						case component_type_t::unsigned_int:
							dst = *(std::uint32_t const *)(src);
							break;
						default:
							break;
						}
					}
				}

				if constexpr (std::is_integral_v<T>)
				{
					std::int64_t value = 0;
					switch (type)
					{
					case component_type_t::byte:
						value = *(std::int8_t const *)(src);
						break;
					case component_type_t::unsigned_byte:
						value = *(std::uint8_t const *)(src);
						break;
					case component_type_t::_short:
						value = *(std::int16_t const *)(src);
						break;
					case component_type_t::unsigned_short:
						value = *(std::uint16_t const *)(src);
						break;
					case component_type_t::unsigned_int:
						value = *(std::uint32_t const *)(src);
						break;
					default:
						break;
					}

					if (value < (std::int64_t)std::numeric_limits<T>::min() || value > (std::int64_t)std::numeric_limits<T>::max())
						throw util::exception("Value for glTF accessor iterator is out of range");

					dst = value;
				}
			}
		}

		template <typename T>
		T  accessor_cast(gltf_asset::accessor const & accessor, char const * ptr)
		{
			using traits = accessor_traits<T>;

			if (traits::components != attribute_size(accessor.type))
				throw util::exception("glTF accessor component count mismatch");

			T result;
			auto result_ptr = traits::pointer(result);

			if constexpr (!traits::is_floating_point)
			{
				if (accessor.component_type == gltf_asset::accessor::_float)
					throw util::exception("Cannot read floating point accessor as integral type");
			}

			for (std::size_t i = 0; i < traits::components; ++i)
			{
				safe_cast(accessor.component_type, accessor.normalized, result_ptr[i], ptr);
				ptr += component_size(accessor.component_type);
			}

			traits::finalize(result);

			return result;
		}

	}

	template <typename T>
	struct accessor_iterator
	{
		using value_type = T;
		using pointer = T *;
		using reference = T &;
		using difference_type = std::ptrdiff_t;
		using iterator_category = std::input_iterator_tag;

		accessor_iterator(gltf_asset const & asset, std::size_t accessor_id, std::size_t element_index)
		{
			accessor_ = asset.accessors[accessor_id];
			auto const & buffer_view = asset.buffer_views[accessor_.buffer_view];
			auto const & buffer = asset.buffers[buffer_view.buffer];

			if (buffer_view.stride == 0)
				stride_ = component_size(accessor_.component_type) * attribute_size(accessor_.type);
			else
				stride_ = buffer_view.stride;

			if (!buffer.data)
				throw util::exception("Buffer data not loaded for glTF accessor iterator");

			ptr_ = buffer.data->data() + buffer_view.offset;
			ptr_ += stride_ * element_index;
		}

		T operator *() const
		{
			return detail::accessor_cast<T>(accessor_, ptr_);
		}

		accessor_iterator & operator ++()
		{
			ptr_ += stride_;
			return *this;
		}

		accessor_iterator operator ++(int)
		{
			auto copy = *this;
			operator++();
			return copy;
		}

		friend bool operator == (accessor_iterator<T> const & it1, accessor_iterator<T> const & it2)
		{
			return it1.ptr_ == it2.ptr_;
		}

	private:
		gltf_asset::accessor accessor_;
		std::size_t stride_;
		char const * ptr_;
	};

	template <typename T>
	auto accessor_range(gltf_asset const & asset, std::size_t accessor_id)
	{
		return util::range<accessor_iterator<T>>{
			accessor_iterator<T>(asset, accessor_id, 0),
			accessor_iterator<T>(asset, accessor_id, asset.accessors[accessor_id].count)
		};
	}

}
