#pragma once

#include <psemek/gfx/gl.hpp>
#include <psemek/gfx/pixel.hpp>
#include <psemek/gfx/pixmap.hpp>
#include <psemek/gfx/buffer.hpp>
#include <psemek/math/vector.hpp>

#include <optional>

namespace psemek::gfx
{

	template <std::size_t D, GLenum Target>
	struct basic_texture
	{
		static_assert(D >= 1 && D <= 3);

		basic_texture();
		basic_texture(basic_texture &&);
		basic_texture & operator = (basic_texture &&);
		~basic_texture();

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

		static basic_texture null();

		static constexpr GLenum target = Target;

		GLuint id() const { return id_; }

		explicit operator bool() const { return id() != 0; }

		void reset();

		void bind() const;
		void bind(int texture_unit) const;

		GLint internal_format() const { return format_; }

		math::vector<std::size_t, D> size() const { return size_; }

		std::size_t width() const;
		std::size_t height() const;
		std::size_t depth() const;

		void load(GLint internal_format, math::vector<std::size_t, D> const & size, GLenum format, GLenum type, const void * data);

		template <typename Pixel>
		void load(math::vector<std::size_t, D> const & size, Pixel const * data = nullptr);

		template <typename Pixel>
		void load_srgb(math::vector<std::size_t, D> const & size, Pixel const * data = nullptr);

		template <typename Pixel>
		void load(util::ndarray<Pixel, D> const & p);

		template <typename Pixel>
		void load_srgb(util::ndarray<Pixel, D> const & p);

#ifndef PSEMEK_GLES
		void pixels(GLenum format, GLenum type, void * data, int layer = 0) const;

		template <typename Pixmap>
		void pixels(Pixmap & pixmap, int layer = 0) const;

		template <typename Pixmap>
		Pixmap pixels(int layer = 0) const;
#endif

		static basic_texture from_data(GLint internal_format, math::vector<std::size_t, D> const & size, GLenum format, GLenum type, const void * data);

		template <typename Pixmap>
		static basic_texture from_pixmap(Pixmap const & p);

		void generate_mipmap();
		void nearest_filter();
		void linear_filter();
		void linear_mipmap_filter();
		void anisotropy();

		void repeat();
		void mirrored_repeat();
		void clamp();

		bool uses_mipmaps() const { return uses_mipmaps_; }

	protected:
		GLuint id_ = 0;
		GLint format_ = 0;
		math::vector<std::size_t, D> size_ = math::vector<std::size_t, D>::zero();
		bool uses_mipmaps_ = true;

		explicit basic_texture(std::nullptr_t);
	};

	using texture_2d       = basic_texture<2, gl::TEXTURE_2D>;
	using texture_2d_array = basic_texture<3, gl::TEXTURE_2D_ARRAY>;
	using texture_3d       = basic_texture<3, gl::TEXTURE_3D>;

	extern template struct basic_texture<2, gl::TEXTURE_2D>;
	extern template struct basic_texture<3, gl::TEXTURE_2D_ARRAY>;
	extern template struct basic_texture<3, gl::TEXTURE_3D>;

	extern template struct basic_texture<2, gl::TEXTURE_CUBE_MAP>;

#ifndef PSEMEK_GLES
	using texture_1d       = basic_texture<1, gl::TEXTURE_1D>;
	using texture_1d_array = basic_texture<2, gl::TEXTURE_1D_ARRAY>;
	extern template struct basic_texture<1, gl::TEXTURE_1D>;
	extern template struct basic_texture<2, gl::TEXTURE_1D_ARRAY>;
#endif

	struct texture_cubemap
		: basic_texture<2, gl::TEXTURE_CUBE_MAP>
	{
		texture_cubemap() = default;
		texture_cubemap(texture_cubemap &&) = default;
		texture_cubemap & operator = (texture_cubemap &&) = default;
		~texture_cubemap() = default;

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

		static texture_cubemap null()
		{
			return basic_texture<2, gl::TEXTURE_CUBE_MAP>::null();
		}

		static constexpr GLenum target = gl::TEXTURE_CUBE_MAP;

		static GLenum face_to_gl(int f);

		void load(int f, GLint internal_format, math::vector<std::size_t, 2> const & size, GLenum format, GLenum type, const void * data);

		template <typename Pixel>
		void load(int f, math::vector<std::size_t, 2> const & size, Pixel const * data = nullptr)
		{
			using traits = pixel_traits<Pixel>;
			load(f, traits::internal_format, size, traits::format, traits::type, data);
		}

		template <typename Pixel>
		void load(int f, util::ndarray<Pixel, 2> const & p)
		{
			math::vector<std::size_t, 2> size;
			for (std::size_t i = 0; i < 2; ++i) size[i] = p.dim(i);
			load(f, size, p.data());
		}

		void pixels(int f, GLenum format, GLenum type, void * data) const;

		template <typename Pixmap>
		Pixmap pixels(int f) const
		{
			using traits = pixel_traits<typename Pixmap::value_type>;

			std::array<std::size_t, 2> size;
			for (std::size_t i = 0; i < 2; ++i) size[i] = size_[i];

			Pixmap p(size);
			pixels(f, traits::format, traits::type, p.data());
			return p;
		}

	private:
		texture_cubemap(basic_texture<2, gl::TEXTURE_CUBE_MAP> t)
			: basic_texture<2, gl::TEXTURE_CUBE_MAP>(std::move(t))
		{}
	};

#ifndef PSEMEK_GLES
	struct texture_2d_multisample
	{
		texture_2d_multisample();
		texture_2d_multisample(texture_2d_multisample &&);
		texture_2d_multisample & operator = (texture_2d_multisample &&);
		~texture_2d_multisample();

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

		static texture_2d_multisample null();

		static constexpr GLenum target = gl::TEXTURE_2D_MULTISAMPLE;

		GLuint id() const { return id_; }

		explicit operator bool() const { return id() != 0; }

		void reset();

		void bind() const;
		void bind(int texture_unit) const;

		math::vector<std::size_t, 2> size() const { return size_; }

		std::size_t width() const { return size_[0]; }
		std::size_t height() const { return size_[1]; }

		int samples() const { return samples_; }

		void load(GLint internal_format, math::vector<std::size_t, 2> const & size, int samples, bool fixed_sample_locations = true);

		template <typename Pixel>
		void load(math::vector<std::size_t, 2> const & size, int samples, bool fixed_sample_locations = true);

	protected:
		GLuint id_ = 0;
		math::vector<std::size_t, 2> size_ = {0, 0};
		int samples_ = 0;

		explicit texture_2d_multisample(std::nullptr_t);
	};
#endif

	struct buffer_texture
	{
		buffer_texture();
		buffer_texture(buffer_texture &&);
		buffer_texture & operator = (buffer_texture &&);
		~buffer_texture();

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

		static buffer_texture null();

		static constexpr GLenum target = gl::TEXTURE_BUFFER;

		GLuint id() const { return id_; }

		explicit operator bool() const { return id() != 0; }

		void reset();

		void bind() const;
		void bind(int texture_unit) const;

		void buffer(GLenum internal_format, gfx::buffer const & buffer);

		template <typename Pixel>
		void buffer(gfx::buffer const & buffer);

	protected:
		GLuint id_;

		explicit buffer_texture(std::nullptr_t);
	};

	namespace detail
	{
		std::optional<float> max_anisotropy();
	}

	template <std::size_t D, GLenum Target>
	basic_texture<D, Target>::basic_texture()
	{
		gl::GenTextures(1, &id_);
	}

	template <std::size_t D, GLenum Target>
	basic_texture<D, Target>::basic_texture(basic_texture && other)
		: id_{other.id_}
		, format_{other.format_}
		, size_{other.size_}
	{
		other.id_ = 0;
		other.format_ = 0;
		other.size_ = other.size_.zero();
	}

	template <std::size_t D, GLenum Target>
	basic_texture<D, Target> & basic_texture<D, Target>::operator = (basic_texture && other)
	{
		if (this == &other) return *this;

		reset();
		std::swap(id_, other.id_);
		std::swap(format_, other.format_);
		std::swap(size_, other.size_);
		return *this;
	}

	template <std::size_t D, GLenum Target>
	basic_texture<D, Target>::~basic_texture()
	{
		reset();
	}

	template <std::size_t D, GLenum Target>
	basic_texture<D, Target> basic_texture<D, Target>::null()
	{
		return basic_texture(nullptr);
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::reset()
	{
		if (id_ != 0)
			gl::DeleteTextures(1, &id_);
		id_ = 0;
		format_ = 0;
		size_ = size_.zero();
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::bind() const
	{
		gl::BindTexture(Target, id());
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::bind(int texture_unit) const
	{
		gl::ActiveTexture(gl::TEXTURE0 + texture_unit);
		gl::BindTexture(Target, id());
	}

	template <std::size_t D, GLenum Target>
	std::size_t basic_texture<D, Target>::width() const
	{
		return size_[0];
	}

	template <std::size_t D, GLenum Target>
	std::size_t basic_texture<D, Target>::height() const
	{
		if constexpr (D >= 2)
		{
			return size_[1];
		}
		else
		{
			return 1;
		}
	}

	template <std::size_t D, GLenum Target>
	std::size_t basic_texture<D, Target>::depth() const
	{
		if constexpr (D >= 3)
		{
			return size_[2];
		}
		else
		{
			return 1;
		}
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::load(GLint internal_format, math::vector<std::size_t, D> const & size, GLenum format, GLenum type, const void * data)
	{
		bind();

		if constexpr (D == 1)
		{
#ifndef PSEMEK_GLES
			gl::TexImage1D(Target, 0, internal_format, size[0], 0, format, type, data);
#endif
		}
		else if (D == 2)
		{
			gl::TexImage2D(Target, 0, internal_format, size[0], size[1], 0, format, type, data);
		}
		else if (D == 3)
		{
			gl::TexImage3D(Target, 0, internal_format, size[0], size[1], size[2], 0, format, type, data);
		}

		format_ = internal_format;
		size_ = size;
	}

	template <std::size_t D, GLenum Target>
	template <typename Pixel>
	void basic_texture<D, Target>::load(math::vector<std::size_t, D> const & size, Pixel const * data)
	{
		using traits = pixel_traits<Pixel>;
		load(traits::internal_format, size, traits::format, traits::type, data);
	}

	template <std::size_t D, GLenum Target>
	template <typename Pixel>
	void basic_texture<D, Target>::load_srgb(math::vector<std::size_t, D> const & size, Pixel const * data)
	{
		using traits = pixel_traits<srgb<Pixel>>;
		load(traits::internal_format, size, traits::format, traits::type, data);
	}

	template <std::size_t D, GLenum Target>
	template <typename Pixel>
	void basic_texture<D, Target>::load(util::ndarray<Pixel, D> const & p)
	{
		math::vector<std::size_t, D> size;
		for (std::size_t i = 0; i < D; ++i) size[i] = p.dim(i);
		load(size, p.data());
	}

	template <std::size_t D, GLenum Target>
	template <typename Pixel>
	void basic_texture<D, Target>::load_srgb(util::ndarray<Pixel, D> const & p)
	{
		math::vector<std::size_t, D> size;
		for (std::size_t i = 0; i < D; ++i) size[i] = p.dim(i);
		load_srgb(size, p.data());
	}

#ifndef PSEMEK_GLES
	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::pixels(GLenum format, GLenum type, void * data, int layer) const
	{
		bind();
		gl::GetTexImage(Target, layer, format, type, data);
	}

	template <std::size_t D, GLenum Target>
	template <typename Pixmap>
	void basic_texture<D, Target>::pixels(Pixmap & pixmap, int layer) const
	{
		using traits = pixel_traits<typename Pixmap::value_type>;

		std::array<std::size_t, D> size;
		for (std::size_t i = 0; i < D; ++i) size[i] = std::max<std::size_t>(1, size_[i] >> layer);
		pixmap.resize(size);
		pixels(traits::format, traits::type, pixmap.data(), layer);
	}

	template <std::size_t D, GLenum Target>
	template <typename Pixmap>
	Pixmap basic_texture<D, Target>::pixels(int layer) const
	{
		Pixmap pixmap;
		pixels(pixmap, layer);
		return pixmap;
	}
#endif

	template <std::size_t D, GLenum Target>
	basic_texture<D, Target> basic_texture<D, Target>::from_data(GLint internal_format, math::vector<std::size_t, D> const & size, GLenum format, GLenum type, const void * data)
	{
		basic_texture result;
		result.load(internal_format, size, format, type, data);
		return result;
	}

	template <std::size_t D, GLenum Target>
	template <typename Pixmap>
	basic_texture<D, Target> basic_texture<D, Target>::from_pixmap(Pixmap const & p)
	{
		basic_texture result;
		result.load(p);
		return result;
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::generate_mipmap()
	{
		bind();
		gl::GenerateMipmap(Target);
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::nearest_filter()
	{
		bind();
		gl::TexParameteri(Target, gl::TEXTURE_MIN_FILTER, gl::NEAREST);
		gl::TexParameteri(Target, gl::TEXTURE_MAG_FILTER, gl::NEAREST);
		uses_mipmaps_ = false;
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::linear_filter()
	{
		bind();
		gl::TexParameteri(Target, gl::TEXTURE_MIN_FILTER, gl::LINEAR);
		gl::TexParameteri(Target, gl::TEXTURE_MAG_FILTER, gl::LINEAR);
		uses_mipmaps_ = false;
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::linear_mipmap_filter()
	{
		bind();
		gl::TexParameteri(Target, gl::TEXTURE_MIN_FILTER, gl::LINEAR_MIPMAP_LINEAR);
		gl::TexParameteri(Target, gl::TEXTURE_MAG_FILTER, gl::LINEAR);
		uses_mipmaps_ = true;
	}


	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::anisotropy()
	{
		auto level = detail::max_anisotropy();
		if (level)
		{
			bind();
			gl::TexParameterf(Target, gl::TEXTURE_MAX_ANISOTROPY, *level);
		}
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::repeat()
	{
		bind();

		if constexpr (D >= 1) gl::TexParameteri(Target, gl::TEXTURE_WRAP_S, gl::REPEAT);
		if constexpr (D >= 2) gl::TexParameteri(Target, gl::TEXTURE_WRAP_T, gl::REPEAT);
		if constexpr (D >= 3) gl::TexParameteri(Target, gl::TEXTURE_WRAP_R, gl::REPEAT);
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::mirrored_repeat()
	{
		bind();

		if constexpr (D >= 1) gl::TexParameteri(Target, gl::TEXTURE_WRAP_S, gl::MIRRORED_REPEAT);
		if constexpr (D >= 2) gl::TexParameteri(Target, gl::TEXTURE_WRAP_T, gl::MIRRORED_REPEAT);
		if constexpr (D >= 3) gl::TexParameteri(Target, gl::TEXTURE_WRAP_R, gl::MIRRORED_REPEAT);
	}

	template <std::size_t D, GLenum Target>
	void basic_texture<D, Target>::clamp()
	{
		bind();

		if constexpr (D >= 1) gl::TexParameteri(Target, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE);
		if constexpr (D >= 2) gl::TexParameteri(Target, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE);
		if constexpr (D >= 3) gl::TexParameteri(Target, gl::TEXTURE_WRAP_R, gl::CLAMP_TO_EDGE);
	}

	template <std::size_t D, GLenum Target>
	basic_texture<D, Target>::basic_texture(std::nullptr_t)
	{}

#ifndef PSEMEK_GLES
	template <typename Pixel>
	void texture_2d_multisample::load(math::vector<std::size_t, 2> const & size, int samples, bool fixed_sample_locations)
	{
		load(pixel_traits<Pixel>::internal_format, size, samples, fixed_sample_locations);
	}
#endif

	inline buffer_texture::buffer_texture()
	{
		gl::GenTextures(1, &id_);
	}

	inline buffer_texture::buffer_texture(buffer_texture && other)
		: id_(other.id_)
	{
		other.id_ = 0;
	}

	inline buffer_texture & buffer_texture::operator = (buffer_texture && other)
	{
		if (this != &other)
		{
			reset();
			id_ = other.id_;
			other.id_ = 0;
		}

		return *this;
	}

	inline buffer_texture::~buffer_texture()
	{
		reset();
	}

	inline buffer_texture buffer_texture::null()
	{
		return buffer_texture(nullptr);
	}

	inline void buffer_texture::reset()
	{
		if (id_ != 0)
			gl::DeleteTextures(1, &id_);
	}

	inline void buffer_texture::bind() const
	{
		gl::BindTexture(target, id_);
	}

	inline void buffer_texture::bind(int texture_unit) const
	{
		gl::ActiveTexture(gl::TEXTURE0 + texture_unit);
		bind();
	}

	inline void buffer_texture::buffer(GLenum internal_format, gfx::buffer const & buffer)
	{
		buffer.bind();
		bind();
		gl::TexBuffer(target, internal_format, buffer.id());
	}

	template <typename Pixel>
	void buffer_texture::buffer(gfx::buffer const & buffer)
	{
		this->buffer(pixel_traits<Pixel>::internal_format, buffer);
	}

	inline buffer_texture::buffer_texture(std::nullptr_t)
		: id_(0)
	{}

	template <std::size_t D>
	std::size_t mipmap_count(math::vector<std::size_t, D> const & size)
	{
		std::size_t s = 0;
		for (std::size_t i = 0; i < D; ++i)
			s = std::max(s, size[i]);

		std::size_t result = 1;
		while (s > 1)
		{
			s /= 2;
			++result;
		}
		return result;
	}

	template <std::size_t D, GLenum Target>
	std::size_t memory_usage(basic_texture<D, Target> const & texture)
	{
		auto size = texture.width() * texture.height() * texture.depth();
		if (size == 0)
			return 0;

		float mipmap_factor = 1.f;
		if (texture.uses_mipmaps())
		{
			switch (Target)
			{
#ifndef PSEMEK_GLES
			case gl::TEXTURE_1D:
			case gl::TEXTURE_1D_ARRAY:
				mipmap_factor = 2.f;
				break;
#endif
			case gl::TEXTURE_2D:
			case gl::TEXTURE_2D_ARRAY:
				mipmap_factor = 4.f / 3.f;
				break;
			case gl::TEXTURE_3D:
				mipmap_factor = 8.f / 7.f;
				break;
			default:
				break;
			}
		}

		return std::ceil(mipmap_factor * size * pixel_size(texture.internal_format()));
	}

}
