#pragma once

#include <psemek/gfx/gl.hpp>
#include <psemek/gfx/drawable.hpp>
#include <psemek/gfx/array.hpp>
#include <psemek/gfx/buffer.hpp>
#include <psemek/gfx/attribs.hpp>
#include <psemek/gfx/armature.hpp>

#include <psemek/math/vector.hpp>
#include <psemek/math/point.hpp>
#include <psemek/math/simplex.hpp>
#include <psemek/math/matrix.hpp>

#include <psemek/util/assert.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/hash_table.hpp>

#include <cstddef>

namespace psemek::gfx
{

	namespace detail
	{

		template <typename T>
		struct index_type_to_gl_enum;

		template <>
		struct index_type_to_gl_enum<std::uint8_t>
		{
			static constexpr GLenum value = gl::UNSIGNED_BYTE;
		};

		template <>
		struct index_type_to_gl_enum<std::uint16_t>
		{
			static constexpr GLenum value = gl::UNSIGNED_SHORT;
		};

		template <>
		struct index_type_to_gl_enum<std::uint32_t>
		{
			static constexpr GLenum value = gl::UNSIGNED_INT;
		};

		template <typename T>
		static constexpr GLenum index_type_to_gl_enum_v = index_type_to_gl_enum<T>::value;

		inline std::size_t index_size(GLenum type)
		{
			switch (type)
			{
			case gl::UNSIGNED_BYTE: return 1;
			case gl::UNSIGNED_SHORT: return 2;
			case gl::UNSIGNED_INT: return 4;
			}
			assert(false);
			return 0;
		}

		template <std::size_t N>
		GLenum get_primitive_type()
		{
			if constexpr (N == 0)
			{
				return gl::POINTS;
			}
			else if constexpr (N == 1)
			{
				return gl::LINES;
			}
			else if constexpr (N == 2)
			{
				return gl::TRIANGLES;
			}
			else
			{
				static_assert("unknown primitive type");
			}
		}

		inline std::optional<std::size_t> get_primitive_type_vertex_count(GLenum type)
		{
			switch (type)
			{
			case gl::LINES:
				return 2;
			case gl::TRIANGLES:
				return 3;
			default:
				return std::nullopt;
			}
		}

	}

	struct imported_mesh;

	/* A generic mesh class
	 * Supports both indexed & non-indexed rendering
	 * Supports both instanced & non-instanced rendering
	 * Checks vertex & instance size, may throw on load
	 * Strong exception guarantees
	 */

	struct mesh
		: drawable
	{
		mesh() = default;
		mesh(mesh &&);
		mesh & operator = (mesh &&);

		mesh(mesh const &) = delete;
		mesh & operator = (mesh const &) = delete;

		void bind() const;

		void setup(attribs_description const & attribs);

		template <typename ... Attribs>
		void setup();

		static mesh create(imported_mesh const & m);

		// Non-indexed vertex data

		void load_raw(void const * vertices, std::size_t vertex_size, std::size_t count, GLenum primitive_type, GLenum usage = gl::STREAM_DRAW);

		template <typename Vertex>
		void load(Vertex const * vertices, std::size_t count, GLenum primitive_type, GLenum usage = gl::STREAM_DRAW);

		template <typename Vertex>
		void load(std::vector<Vertex> const & vertices, GLenum primitive_type, GLenum usage = gl::STREAM_DRAW);

		template <typename Vertex, std::size_t N>
		void load(math::simplex<Vertex, N> const * simplices, std::size_t count, GLenum usage = gl::STREAM_DRAW);

		template <typename Vertex, std::size_t N>
		void load(std::vector<math::simplex<Vertex, N>> const & simplices, GLenum usage = gl::STREAM_DRAW);

		// Indexed vertex data

		void load_raw(void const * vertices, std::size_t vertex_size, std::size_t vertex_count,
			void const * indices, GLenum index_type, std::size_t index_count,
			GLenum primitive_type, GLenum usage = gl::STREAM_DRAW);

		template <typename Vertex, typename Index>
		void load(Vertex const * vertices, std::size_t vertex_count, Index const * indices, std::size_t index_count, GLenum primitive_type, GLenum usage = gl::STREAM_DRAW);

		template <typename Vertex, typename Index>
		void load(std::vector<Vertex> const & vertices, std::vector<Index> const & indices, GLenum primitive_type, GLenum usage = gl::STREAM_DRAW);

		template <typename Vertex, typename Index, std::size_t N>
		void load(Vertex const * vertices, std::size_t vertex_count, math::simplex<Index, N> const * simplices, std::size_t simplex_count, GLenum usage = gl::STREAM_DRAW);

		template <typename Vertex, typename Index, std::size_t N>
		void load(std::vector<Vertex> const & vertices, std::vector<math::simplex<Index, N>> const & simplices, GLenum usage = gl::STREAM_DRAW);

		void load_raw(imported_mesh const & m);

		// Index-only vertex data

		template <typename Index>
		void load_index(Index const * indices, std::size_t index_count, GLenum primitive_type, GLenum usage = gl::STREAM_DRAW);

		template <typename Index>
		void load_index(std::vector<Index> const & indices, GLenum primitive_type, GLenum usage = gl::STREAM_DRAW);

		template <typename Index, std::size_t N>
		void load_index(math::simplex<Index, N> const * simplices, std::size_t simplex_count, GLenum usage = gl::STREAM_DRAW);

		template <typename Index, std::size_t N>
		void load_index(std::vector<math::simplex<Index, N>> const & simplices, GLenum usage = gl::STREAM_DRAW);

		// Instance data

		template <typename Instance>
		void load_instance(Instance const * instances, std::size_t count, GLenum usage = gl::STREAM_DRAW);

		template <typename Instance>
		void load_instance(std::vector<Instance> const & instances, GLenum usage = gl::STREAM_DRAW);

		// Info

		bool is_indexed() const { return info_.indexed_; }
		bool is_instanced() const { return info_.instanced_; }

		std::size_t vertex_count() const { return info_.vertex_count_; }
		std::size_t index_count() const { return info_.index_count_; }
		std::size_t instance_count() const { return info_.instance_count_; }

		GLenum primitive_type() const { return info_.primitive_type_; }
		GLenum index_type() const { return info_.index_type_; }

		buffer & vertex_buffer() { return vertex_buffer_; }
		auto & index_buffer() { return index_buffer_; }
		buffer & instance_buffer() { return instance_buffer_; }

		buffer const & vertex_buffer() const { return vertex_buffer_; }
		auto const & index_buffer() const { return index_buffer_; }
		buffer const & instance_buffer() const { return instance_buffer_; }

		// Drawing commands

		void draw() const override;

		void draw(std::size_t first, std::size_t count, std::size_t instance_count = 0) const;

	private:
		array array_;
		buffer vertex_buffer_ = buffer::null();
		basic_buffer<gl::ELEMENT_ARRAY_BUFFER> index_buffer_ = basic_buffer<gl::ELEMENT_ARRAY_BUFFER>::null();
		buffer instance_buffer_ = buffer::null();

		struct mesh_info
		{
			bool indexed_ = false;
			bool instanced_ = false;

			std::size_t vertex_count_ = 0;
			std::size_t index_count_ = 0;
			std::size_t instance_count_ = 0;

			std::size_t stride_ = 0;
			std::size_t instance_stride_ = 0;

			GLenum primitive_type_;
			GLenum index_type_;

			std::size_t max_attribute_index_ = 0;
		};

		mesh_info info_;
	};

	template <typename ... Attribs>
	void mesh::setup()
	{
		setup(make_attribs_description<Attribs...>());
	}

	template <typename Vertex>
	void mesh::load(Vertex const * vertices, std::size_t count, GLenum primitive_type, GLenum usage)
	{
		load_raw(vertices, sizeof(Vertex), count, primitive_type, usage);
	}

	template <typename Vertex>
	void mesh::load(std::vector<Vertex> const & vertices, GLenum primitive_type, GLenum usage)
	{
		load(vertices.data(), vertices.size(), primitive_type, usage);
	}

	template <typename Vertex, std::size_t N>
	void mesh::load(math::simplex<Vertex, N> const * simplices, std::size_t count, GLenum usage)
	{
		static_assert(sizeof(Vertex) * (N + 1) == sizeof(simplices[0]));
		load(reinterpret_cast<Vertex const *>(simplices), count * (N + 1), detail::get_primitive_type<N>(), usage);
	}

	template <typename Vertex, std::size_t N>
	void mesh::load(std::vector<math::simplex<Vertex, N>> const & simplices, GLenum usage)
	{
		load(simplices.data(), simplices.size(), usage);
	}

	template <typename Vertex, typename Index>
	void mesh::load(Vertex const * vertices, std::size_t vertex_count, Index const * indices, std::size_t index_count, GLenum primitive_type, GLenum usage)
	{
		load_raw(vertices, sizeof(Vertex), vertex_count, indices, detail::index_type_to_gl_enum_v<Index>, index_count, primitive_type, usage);
	}

	template <typename Vertex, typename Index>
	void mesh::load(std::vector<Vertex> const & vertices, std::vector<Index> const & indices, GLenum primitive_type, GLenum usage)
	{
		load(vertices.data(), vertices.size(), indices.data(), indices.size(), primitive_type, usage);
	}

	template <typename Vertex, typename Index, std::size_t N>
	void mesh::load(Vertex const * vertices, std::size_t vertex_count, math::simplex<Index, N> const * simplices, std::size_t simplex_count, GLenum usage)
	{
		static_assert(sizeof(Index) * (N + 1) == sizeof(simplices[0]));
		load(vertices, vertex_count, reinterpret_cast<Index const *>(simplices), (N + 1) * simplex_count, detail::get_primitive_type<N>(), usage);
	}

	template <typename Vertex, typename Index, std::size_t N>
	void mesh::load(std::vector<Vertex> const & vertices, std::vector<math::simplex<Index, N>> const & simplices, GLenum usage)
	{
		load(vertices.data(), vertices.size(), simplices.data(), simplices.size(), usage);
	}

	template <typename Index>
	void mesh::load_index(Index const * indices, std::size_t index_count, GLenum primitive_type, GLenum usage)
	{
		if (auto n = detail::get_primitive_type_vertex_count(primitive_type); n)
			assert((index_count % (*n)) == 0);

		if (!index_buffer_)
			index_buffer_ = {};
		array_.bind();
		index_buffer_.load(indices, index_count, usage);
		info_.index_count_ = index_count;
		info_.indexed_ = true;
		info_.primitive_type_ = primitive_type;
		info_.index_type_ = detail::index_type_to_gl_enum_v<Index>;
	}

	template <typename Index>
	void mesh::load_index(std::vector<Index> const & indices, GLenum primitive_type, GLenum usage)
	{
		load_index(indices.data(), indices.size(), primitive_type, usage);
	}

	template <typename Index, std::size_t N>
	void mesh::load_index(math::simplex<Index, N> const * simplices, std::size_t simplex_count, GLenum usage)
	{
		static_assert(sizeof(Index) * (N + 1) == sizeof(simplices[0]));
		load_index(reinterpret_cast<Index const *>(simplices), simplex_count, detail::get_primitive_type<N>(), usage);
	}

	template <typename Index, std::size_t N>
	void mesh::load_index(std::vector<math::simplex<Index, N>> const & simplices, GLenum usage)
	{
		load_index(simplices.data(), simplices.size(), usage);
	}

	template <typename Instance>
	void mesh::load_instance(Instance const * instances, std::size_t count, GLenum usage)
	{
		assert(info_.instance_stride_ == sizeof(Instance));

		assert(instance_buffer_);
		instance_buffer_.load(instances, count, usage);
		info_.instance_count_ = count;
	}

	template <typename Instance>
	void mesh::load_instance(std::vector<Instance> const & instances, GLenum usage)
	{
		load_instance(instances.data(), instances.size(), usage);
	}

	using pose_ref = util::span<bone_transform<float> const>;
	using pose_library = util::hash_map<std::string_view, pose_ref>;

	struct imported_mesh
	{
		attribs_description attribs;
		util::span<char const> vertices;
		util::span<std::uint32_t const> indices;

		util::span<bone const> bones;

		pose_library poses;
	};

	imported_mesh load_mesh(std::string_view data);

	inline std::size_t memory_usage(mesh const & mesh)
	{
		return memory_usage(mesh.vertex_buffer()) + memory_usage(mesh.index_buffer()) + memory_usage(mesh.instance_buffer());
	}

}
