#pragma once

#include <psemek/gfx/gl.hpp>
#include <psemek/util/span.hpp>

#include <vector>
#include <memory>

namespace psemek::gfx
{

	template <GLenum Target>
	struct basic_buffer
	{
		static constexpr GLenum target = Target;

		basic_buffer()
		{
			gl::GenBuffers(1, &id_);
		}

		basic_buffer(basic_buffer && other)
			: id_{other.id_}
			, size_{other.size_}
		{
			other.id_ = 0;
			other.size_ = 0;
		}

		basic_buffer & operator = (basic_buffer && other)
		{
			if (this == &other)
				return *this;

			reset();
			id_ = other.id_;
			size_ = other.size_;
			other.id_ = 0;
			other.size_ = 0;
			return *this;
		}

		~basic_buffer()
		{
			reset();
		}

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

		static basic_buffer null()
		{
			return basic_buffer{nullptr};
		}

		GLuint id() const { return id_; }

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

		void bind() const
		{
			gl::BindBuffer(Target, id_);
		}

		void bind(GLuint index) const
		{
			gl::BindBufferBase(Target, index, id_);
		}

		void reset()
		{
			if (id_)
				gl::DeleteBuffers(1, &id_);
			id_ = 0;
			size_ = 0;
		}

		void load(void const * data, std::size_t size, GLenum usage = gl::STREAM_DRAW)
		{
			bind();
			gl::BufferData(Target, size, data, usage);
			size_ = size;
		}

		template <typename T>
		void load(T const * data, std::size_t size, GLenum usage = gl::STREAM_DRAW)
		{
			load(static_cast<void const *>(data), size * sizeof(T), usage);
		}

		template <typename T>
		void load(std::vector<T> const & data, GLenum usage = gl::STREAM_DRAW)
		{
			load(data.data(), data.size(), usage);
		}

		template <typename T, std::size_t N>
		void load(std::array<T, N> const & data, GLenum usage = gl::STREAM_DRAW)
		{
			load(data.data(), data.size(), usage);
		}

		template <typename T, std::size_t N>
		void load(T const (&data)[N], GLenum usage = gl::STREAM_DRAW)
		{
			load(data, N, usage);
		}

		template <typename T>
		void load(util::span<T> const & data, GLenum usage = gl::STREAM_DRAW)
		{
			load(data.data(), data.size(), usage);
		}

		void load_subdata(std::size_t offset, void const * data, std::size_t size)
		{
			bind();
			gl::BufferSubData(Target, offset, size, data);
		}

		template <typename T>
		void load_subdata(std::size_t offset, T const * data, std::size_t size)
		{
			load_subdata(offset * sizeof(T), static_cast<void const *>(data), size * sizeof(T));
		}

		template <typename T>
		void load_subdata(std::size_t offset, std::vector<T> const & data)
		{
			load_subdata(offset, data.data(), data.size());
		}

		template <typename T, std::size_t N>
		void load_subdata(std::size_t offset, std::array<T, N> const & data)
		{
			load_subdata(offset, data.data(), data.size());
		}

		template <typename T, std::size_t N>
		void load_subdata(std::size_t offset, T const (&data)[N])
		{
			load_subdata(offset, data, N);
		}

		template <typename T>
		void load_subdata(std::size_t offset, util::span<T> const & data)
		{
			load_subdata(offset, data.data(), data.size());
		}

		std::size_t size() const
		{
			return size_;
		}

		template <typename T>
		std::shared_ptr<T[]> map(GLbitfield access)
		{
			if (auto p = mapped_.lock())
				return std::static_pointer_cast<T[]>(p);

			bind();
			std::shared_ptr<T[]> p(reinterpret_cast<T *>(gl::MapBufferRange(Target, 0, size_, access)), [this](T *){
				bind();
				gl::UnmapBuffer(Target);
			});
			mapped_ = p;
			return p;
		}

	private:
		GLuint id_ = 0;
		std::size_t size_ = 0;
		std::weak_ptr<void> mapped_;

		explicit basic_buffer(std::nullptr_t)
		{}
	};

	using buffer = basic_buffer<gl::ARRAY_BUFFER>;
	using uniform_buffer = basic_buffer<gl::UNIFORM_BUFFER>;

	template <GLenum Target>
	std::size_t memory_usage(basic_buffer<Target> const & buffer)
	{
		return buffer.size();
	}

}
