// =====================================================================================================================
//  fennec, a free and open source game engine
//  Copyright © 2025  Medusa Slockbower
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
// =====================================================================================================================

#ifndef FENNEC_RENDERERS_OPENGL_LIB_BUFFER_H
#define FENNEC_RENDERERS_OPENGL_LIB_BUFFER_H

#include <fennec/math/common.h>
#include <fennec/renderers/opengl/lib/fwd.h>
#include <fennec/renderers/opengl/lib/enum.h>

namespace fennec
{

namespace gl
{

template<GLbitfield FlagsV, GLboolean ImmutableV> using vertex_buffer             = buffer<VERTEX,             FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using element_buffer            = buffer<ELEMENT,            FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using uniform_buffer            = buffer<UNIFORM,            FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using shader_storage_buffer     = buffer<SHADER_STORAGE,     FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using query_buffer              = buffer<QUERY,              FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using texture_buffer            = buffer<TEXTURE,            FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using transform_feedback_buffer = buffer<TRANSFORM_FEEDBACK, FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using atomic_counter_buffer     = buffer<ATOMIC_COUNTER,     FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using parameter_buffer          = buffer<PARAMETER,          FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using indirect_draw_buffer      = buffer<INDIRECT_DRAW,      FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using indirect_dispatch_buffer  = buffer<INDIRECT_DISPATCH,  FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using copy_read_buffer          = buffer<COPY_READ,          FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using copy_write_buffer         = buffer<COPY_WRITE,         FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using pixel_pack_buffer         = buffer<PIXEL_PACK,         FlagsV, ImmutableV>;
template<GLbitfield FlagsV, GLboolean ImmutableV> using pixel_unpack_buffer       = buffer<PIXEL_UNPACK,       FlagsV, ImmutableV>;

template<GLenum TypeV, GLbitfield FlagsV, GLboolean ImmutableV>
class buffer {

// HELPER FUNCTIONS ====================================================================================================

private:
	static constexpr GLenum get_mutable_traits() {
		GLenum res;

		// Set READ/DRAW/COPY
		if constexpr (read) {
			res = GL_STREAM_READ;
		} else if constexpr (write) {
			res = GL_STREAM_DRAW;
		} else {
			res = GL_STREAM_COPY;
		}

		// Set STATIC/DYNAMIC/STREAM
		if constexpr (client or coherent) {
			// do nothing
		} else if constexpr (dynamic or persistent) {
			res += 6;
		} else {
			res += 3;
		}

		return res;
	}


// CONSTANTS ===========================================================================================================

public:
	static constexpr GLenum     type       = TypeV;
	static constexpr GLboolean  immutable  = ImmutableV;
	static constexpr GLbitfield flags      = FlagsV;
	static constexpr GLboolean  indexed    = type == ATOMIC_COUNTER or type == SHADER_STORAGE or type == TRANSFORM_FEEDBACK or type == UNIFORM;
	static constexpr GLboolean  map_read   = flags & READ;
	static constexpr GLboolean  map_write  = flags & WRITE;
	static constexpr GLboolean  mapped     = map_read or map_write;
	static constexpr GLboolean  dynamic    = flags & DYNAMIC;
	static constexpr GLboolean  persistent = flags & PERSISTENT;
	static constexpr GLboolean  coherent   = flags & COHERENT;
	static constexpr GLboolean  client     = flags & CLIENT;
	static constexpr GLenum     usage      = get_mutable_traits();

	static_assert(not persistent or persistent == mapped);
	static_assert(not coherent   or coherent   == persistent);


// CONSTRUCTORS & ASSIGNMENT ===========================================================================================

	constexpr buffer(const void* data, GLsizeiptr size)
		: _handle()
		, _size(size) {
		glGenBuffers(1, &_handle);
		start();
		if constexpr(immutable) {
			glBufferStorage(type, _size, data, flags);
		} else {
			glBufferData(type, _size, data, usage);
		}
	}

	constexpr buffer(buffer&& buff) noexcept
		: _handle(buff)
		, _size(buff._size)
		, _mapping(nullptr) {
	}

	constexpr buffer& operator=(buffer&& buff) noexcept {
		glDeleteBuffers(1, &_handle);
		_handle = buff._handle;
		_size = buff._size;
		return *this;
	}

	constexpr buffer(const buffer&) = delete;
	constexpr buffer& operator=(const buffer&) = delete;


// OBJECT FUNCTIONS ====================================================================================================

	constexpr void start() const {
		glBindBuffer(type, _handle);
	}

	constexpr void end() const {
		glBindBuffer(type, NULL);
	}

	constexpr void bind(GLuint i, GLsizeiptr size = -1, GLintptr offset = 0) {
		offset = max(offset, GLintptr(0));
		size = size < 0 ? _size : size;
		size = min(size, _size - offset);
		if (size <= 0) return;
		glBindBufferRange(type, i, _handle, offset, size);
	}


// BASIC BUFFER FUNCTIONS ==============================================================================================

	constexpr void resize(GLsizei size) requires not immutable {
		unmap();
		glBufferData(type, _size = size, nullptr, usage);
	}

	constexpr void* map(GLbitfield access, GLsizeiptr size, GLintptr offset = 0) {
		if (_mapping) {
			return _mapping;
		}
		offset = max(offset, GLintptr(0));
		size = size < 0 ? _size : size;
		size = min(size, _size - offset);
		if (size <= 0) return nullptr;
		return _mapping = glMapBufferRange(type, offset, size, _mapflags = access);
	}

	constexpr void unmap() {
		glUnmapBuffer(type);
		_mapping = nullptr;
	}

	constexpr void clear(GLsizeiptr size = -1, GLintptr offset = 0, const void* data = nullptr, GLenum data_type = BYTE, GLenum format = R, GLenum internal = R8) {
		offset = max(offset, GLintptr(0));
		size = size < 0 ? _size : size;
		size = min(size, _size - offset);
		if (size <= 0) return;
		glClearBufferSubData(type, internal, offset, size, format, type, data);
	}

	template<GLenum OTypeV, GLbitfield OFlagsV, GLboolean OImmutableV>
	constexpr void copy(const buffer<OTypeV, OFlagsV, OImmutableV>& cpy, GLsizeiptr size = -1, GLintptr write_offset = 0, GLintptr read_offset = 0) {
		write_offset = max(write_offset, GLintptr(0));
		read_offset  = max(read_offset,  GLintptr(0));
		size = size < 0 ? _size : size;
		size = fennec::min(size, cpy._size - read_offset);
		size = fennec::min(size, _size - write_offset);
		if (size <= 0) return;
		glBindBuffer(COPY_READ, cpy._handle);
		glBindBuffer(COPY_WRITE, _handle);
		glCopyBufferSubData(COPY_READ, COPY_WRITE, read_offset, write_offset, size);
		glBindBuffer(COPY_READ,  NULL);
		glBindBuffer(COPY_WRITE, NULL);
	}

	constexpr void read(void* data, GLsizeiptr size, GLintptr offset = 0) const {
		static_assert(dynamic or persistent and not (client or coherent));
		offset = max(offset, GLintptr(0));
		size = size < 0 ? _size : size;
		size = min(size, _size - offset);
		if (size <= 0) return;
		glGetBufferSubData(type, offset, size, data);
	}

	constexpr void write(const void* data, GLsizeiptr size, GLintptr offset = 0) {
		static_assert(dynamic or persistent and not (client or coherent));
		offset = max(offset, GLintptr(0));
		size = size < 0 ? _size : size;
		size = min(size, _size - offset);
		if (size <= 0) return;
		glBufferSubData(type, offset, size, data);
	}

	constexpr void flush(GLsizeiptr size = -1, GLintptr offset = 0) {
		if (not _mapping) return;
		if (not (_mapflags & EXPLICIT_FLUSH)) return;
		offset = max(offset, GLintptr(0));
		size = size < 0 ? _size : size;
		size = min(size, _size - offset);
		if (size <= 0) return;
		glFlushMappedBufferRange(type, offset, size);
	}

	constexpr void invalidate(GLsizeiptr size, GLintptr offset = 0) {
		offset = max(offset, GLintptr(0));
		size = size < 0 ? _size : size;
		size = min(size, _size - offset);
		if (size <= 0) return;
		glInvalidateBufferSubData(type, offset, size);
	}


// TYPED FUNCTIONS =====================================================================================================

	template<typename TypeT>
	constexpr TypeT* map(GLbitfield access, GLsizeiptr n, GLintptr offset = 0) {
		return static_cast<TypeT*>(map(access, n * sizeof(TypeT), offset * sizeof(TypeT)));
	}

private:
	GLuint     _handle;
	GLsizeiptr _size;
	void*      _mapping;
	GLbitfield _mapflags;
};

}

}

#endif // FENNEC_PLATFORM_OPENGL_LIB_H
