#pragma once

#include <psemek/io/stream.hpp>
#include <psemek/gfx/color.hpp>
#include <psemek/math/vector.hpp>
#include <psemek/math/quaternion.hpp>
#include <psemek/math/affine_transform.hpp>
#include <psemek/math/easing.hpp>
#include <psemek/util/hstring.hpp>
#include <psemek/util/hash_table.hpp>

#include <vector>
#include <string>
#include <optional>
#include <variant>

namespace psemek::gfx
{

	struct gltf_asset
	{
		using extra = std::variant<bool, float, std::vector<float>, std::string>;
		using extras_map = util::hash_map<util::hstring, extra>;

		struct node
		{
			std::string name;
			std::optional<std::size_t> parent;
			std::optional<std::size_t> mesh;
			std::optional<std::size_t> skin;
			std::vector<std::size_t> children;
			std::optional<std::size_t> light; // KHR_lights_punctual

			math::vector<float, 3> translation;
			math::quaternion<float> rotation;
			math::vector<float, 3> scale;
			math::affine_transform<float, 3, 3> transform;

			extras_map extras;
		};

		struct mesh
		{
			struct primitive
			{
				std::optional<std::size_t> position;
				std::optional<std::size_t> normal;
				std::optional<std::size_t> texcoord;
				std::optional<std::size_t> color;
				std::optional<std::size_t> joints;
				std::optional<std::size_t> weights;
				std::size_t indices;
				std::optional<std::size_t> material;
			};

			std::vector<primitive> primitives;
		};

		struct material
		{
			std::string name;
			bool two_sided;
			std::optional<color_4f> albedo;
			std::optional<float> metallic;
			std::optional<float> roughness;
			std::optional<std::size_t> texture;
			std::optional<color_3f> emission;
			std::optional<std::size_t> emission_texture;
			std::optional<std::size_t> material_texture;

			extras_map extras;
		};

		struct texture
		{
			std::optional<std::string> uri;
			std::optional<std::size_t> buffer_view;
		};

		struct skin
		{
			std::vector<std::size_t> joints;
			std::optional<std::size_t> inverse_bind_matrices; // accessor
			std::string name;
		};

		struct animation
		{
			struct channel
			{
				std::size_t sampler;
				std::size_t target; // node

				enum path_t
				{
					scale,
					rotation,
					translation,
				} path;
			};

			struct sampler
			{
				std::size_t input; // keyframes accessor
				std::size_t output; // values accessor
				math::easing_type interpolation;
			};

			std::string name;
			std::vector<channel> channels;
			std::vector<sampler> samplers;
		};

		struct accessor
		{
			std::size_t buffer_view;
			enum component_type_t
			{
				byte = 5120,
				unsigned_byte = 5121,
				_short = 5122,
				unsigned_short = 5123,
				unsigned_int = 5125,
				_float = 5126,
			} component_type;
			std::size_t count;

			enum type_t
			{
				scalar,
				vec2,
				vec3,
				vec4,
				mat2,
				mat3,
				mat4,
			} type;

			bool normalized;
		};

		struct buffer_view
		{
			std::size_t buffer;
			std::size_t offset;
			std::size_t length;
			std::size_t stride;
		};

		struct buffer
		{
			std::size_t length;
			std::string uri;

			std::optional<util::blob> data; // for GLB buffer
		};

		// KHR_lights_punctual
		struct light
		{
			enum type_t
			{
				directional,
				point,
				spot,
			} type;

			gfx::color_3f color;
			float intensity;
			float range;
			math::interval<float> cone_angle;

			extras_map extras;
		};

		std::vector<node> nodes;
		std::vector<mesh> meshes;
		std::vector<material> materials;
		std::vector<texture> textures;
		std::vector<skin> skins;
		std::vector<animation> animations;
		std::vector<accessor> accessors;
		std::vector<buffer_view> buffer_views;
		std::vector<buffer> buffers;
		std::vector<light> lights; // KHR_lights_punctual

		util::hash_map<std::string, std::size_t> node_index; // node by name
		util::hash_map<std::string, std::size_t> material_index; // material by name

		struct animation_and_channel
		{
			std::size_t animation;
			std::size_t channel;
		};

		std::vector<std::vector<animation_and_channel>> node_animations; // node to list of animation channels that affect it
	};

	gltf_asset parse_gltf(io::istream && stream);
	gltf_asset parse_glb(io::istream && stream);

	inline std::size_t component_size(gltf_asset::accessor::component_type_t type)
	{
		switch (type)
		{
		case gltf_asset::accessor::byte: return 1;
		case gltf_asset::accessor::unsigned_byte: return 1;
		case gltf_asset::accessor::_short: return 2;
		case gltf_asset::accessor::unsigned_short: return 2;
		case gltf_asset::accessor::unsigned_int: return 4;
		case gltf_asset::accessor::_float: return 4;
		default: throw util::exception("Unknown glTF component type");
		}
	}

	inline std::size_t attribute_size(gltf_asset::accessor::type_t type)
	{
		using type_t = gltf_asset::accessor::type_t;
		switch (type)
		{
		case type_t::scalar: return 1;
		case type_t::vec2: return 2;
		case type_t::vec3: return 3;
		case type_t::vec4: return 4;
		case type_t::mat2: return 4;
		case type_t::mat3: return 9;
		case type_t::mat4: return 16;
		default: throw util::exception("Unsupported attribute type");
		}
	}

}
