// =====================================================================================================================
//  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_TEXTURE_H
#define FENNEC_RENDERERS_OPENGL_LIB_TEXTURE_H

/* Because our implementation targets minimum OpenGL ES 3.2,
 * we are guaranteed to have the following relevant extensions (Starting from OpenGL ES 3.0):
 *
 * OES_texture_compression_astc
 * EXT_texture_border_clamp
 * OES_EGL_image_external_essl3
 * ARB_shader_image_load_store
 * ARB_stencil_texturing
 * ARG_shader_image_size
 * ARB_texture_multisample
 * ARB_texture_storage_multisample
 * ARB_sample_locations
 * OES_texture_view
 * NV_image_formats
 * EXT_render_snorm
 * EXT_render_norm16
 * EXT_color_buffer_float
 * OES_copy_image
 * OES_shader_image_atomic
 * OES_texture_border_clamp
 * OES_texture_buffer
 * OES_texture_cube_map_array
 * OES_texture_stencil8
 * OES_texture_storage_multisample_2d_array
 */


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

namespace fennec
{

namespace gl
{

template<GLint FormatV, GLboolean ImmutableV> using texture1d      = texture<TEX_1D,        FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using texture1dv     = texture<TEX_1D_V,      FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using texture2d      = texture<TEX_2D,        FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using texture2dv     = texture<TEX_2D_V,      FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using texture_rect   = texture<TEX_RECT,      FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using texture2d_ms   = texture<TEX_2D_MS,     FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using texture2dv_ms  = texture<TEX_2D_MS_V,   FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using cubemap        = texture<TEX_CUBEMAP,   FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using cubemapv       = texture<TEX_CUBEMAP_V, FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using texture3d      = texture<TEX_3D,        FormatV, ImmutableV>;
template<GLint FormatV, GLboolean ImmutableV> using buffer_texture = texture<TEX_BUFFER,    FormatV, ImmutableV>;

///
/// \brief Wrapper for OpenGL Texture Objects
/// \tparam TypeV The type of the texture
/// \tparam FormatV The internal format of the texels
/// \tparam ImmutableV Whether the texture is immutable, i.e. cannot be resized after creation
///
/// \details Immutable textures require EXT_texture_storage or ARB_texture_storage
///          Immutable multisample textures require ARB_texture_storage_multisample of OES_texture_storage_multisample_2d_array
template<GLenum TypeV, GLint FormatV, GLboolean ImmutableV>
class texture {

// Constants ===========================================================================================================

public:
	static constexpr GLenum    type         = TypeV;
	static constexpr GLint     format       = FormatV;
	static constexpr GLboolean immutable    = ImmutableV;
	static constexpr GLboolean is_rect      = type == TEX_RECT;
	static constexpr GLboolean is_buffered  = type == TEX_BUFFER;
	static constexpr GLboolean sampled      = type == TEX_2D_MS   or type == TEX_2D_MS_V;
	static constexpr GLboolean cubemap      = type == TEX_CUBEMAP or type == TEX_CUBEMAP_V;
	static constexpr GLboolean is_1d        = type == TEX_1D      or type == TEX_1D_V;
	static constexpr GLboolean is_2d        = sampled or is_rect  or type == TEX_2D    or type == TEX_2D_V    or cubemap;
	static constexpr GLboolean is_array     = TypeV == TEX_1D_V   or TypeV == TEX_2D_V or type == TEX_2D_MS_V or type == TEX_CUBEMAP_V;
	static constexpr GLboolean is_3d        = TypeV == TEX_3D;
	static constexpr GLboolean has_mipmaps  = not(sampled or is_rect or is_buffered);

	static constexpr GLboolean use_1d = is_1d and not is_array;
	static constexpr GLboolean use_2d = (is_2d and not is_array) or (is_1d and is_array) and not sampled;
	static constexpr GLboolean use_3d = is_3d or (is_2d and is_array) and not sampled and not cubemap;

	static constexpr GLint  cubemap_faces = 6;
	static constexpr GLenum base_cubemap_face = GL_TEXTURE_CUBE_MAP_POSITIVE_X;

	static constexpr GLboolean compressed =  type == RGB_DXT1  or type == RGBA_DXT1  or type == RGBA_DXT3  or type == RGBA_DXT5
										  or type == SRGB_DXT1 or type == SRGBA_DXT1 or type == SRGBA_DXT3 or type == SRGBA_DXT5;


// Constructors ========================================================================================================

	///
	/// \brief 1D Texture Constructor
	/// \param width The width of the texture
	/// \param mips The number of mipmap levels
	/// \param data A pointer to a buffer containing pixel values, used as an offset if a PIXEL_UNPACK_BUFFER is bound.
	/// \param component The type of each component
	/// \param layout The layout of components in each pixel
	texture(GLsizei width, GLint mips,
        void* data = nullptr, GLenum component = BYTE, GLenum layout = R, GLsizei size = 0) requires use_1d
		: _handle(NULL)
		, _width(width), _height(1), _depth(1)
		, _samples(1), _mips(mips) {
		glGenTextures(1, &_handle);
		start();
		if constexpr(immutable) {
			glTexStorage1D(type, _mips, format, _width);
			glTexSubImage1D(type, 0, 0, _width, layout, component, data);
		} else if constexpr(compressed) {
			glCompressedTexImage1D(type, 0, format, _width, 0, size, data);
		} else {
			glTexImage1D(type, 0, format, _width, 0, layout, component, data);
		}
		genmips();
	}

	///
	/// \brief 2D Texture Constructor
	/// \param width The width of the texture
	/// \param height The height of the texture, or number of layers for arrays
	/// \param mips The number of mipmap levels
	/// \param data A pointer to a buffer containing pixel values, used as an offset if a PIXEL_UNPACK_BUFFER is bound.
	/// \param component The type of each component
	/// \param layout The layout of components in each pixel
	texture(GLsizei width, GLsizei height, GLint mips,
			void* data = nullptr, GLenum component = BYTE, GLenum layout = R, GLsizei size = 0) requires use_2d and not cubemap
		: _handle(NULL)
		, _width(width), _height(height), _depth(1)
		, _samples(1), _mips(mips) {
		glGenTextures(1, &_handle);
		start();
		if constexpr(immutable) {
			glTexStorage2D(type, _mips, format, _width, _height);
			glTexSubImage2D(type, 0, 0, 0, _width, _height, layout, component, data);
		} else if constexpr(compressed) {
			glCompressedTexImage2D(type, 0, format, _width, _height, 0, size, data);
		} else {
			glTexImage2D(type, 0, format, _width, _height, 0, layout, component, data);
		}
	}

	///
	/// \brief 2D Texture Constructor
	/// \param width The width of the texture
	/// \param height The height of the texture
	/// \param depth The depth of the texture, or number of layers for arrays
	/// \param mips The number of mipmap levels
	/// \param data A pointer to a buffer containing pixel values, used as an offset if a PIXEL_UNPACK_BUFFER is bound.
	/// \param component The type of each component
	/// \param layout The layout of components in each pixel
	texture(GLsizei width, GLsizei height, GLsizei depth, GLsizei mips,
			void* data = nullptr, GLenum component = BYTE, GLenum layout = R, GLsizei size = 0) requires use_3d and not cubemap
		: _handle(NULL)
		, _width(width), _height(height), _depth(depth)
		, _samples(1), _mips(mips) {
		glGenTextures(1, &_handle);
		start();
		if constexpr(immutable) {
			glTexStorage3D(type, _mips, format, _width, _height, _depth);
			glTexSubImage3D(type, 0, 0, 0, 0, _width, _height, _depth, layout, component, data);
		} else if constexpr(compressed) {
			glCompressedTexImage3D(type, 0, format, _width, _height, _depth, 0, size, data);
		} else {
			glTexImage3D(type, 0, format, _width, _height, _depth, 0, layout, component, data);
		}
	}

	///
	/// \brief 2D Multisample Texture Constructor
	/// \param width The width of the texture
	/// \param height The height of the texture
	/// \param samples The number of samples per pixel
	/// \param fixed When true, a fixed set of sample locations is used
	texture(GLsizei width, GLsizei height, GLsizei samples, GLboolean fixed = true) requires sampled and not is_array
		: _handle(NULL)
		, _width(width), _height(height), _depth(1)
		, _samples(samples), _mips(0) {
		glGenTextures(1, &_handle);
		start();
		if constexpr(immutable) {
			glTexStorage2DMultisample(type, _samples, format, _width, _height, fixed);
		} else {
			glTexImage2DMultisample(type, _samples, format, _width, _height, fixed);
		}
	}

	///
	/// \brief 2D Multisample Array Texture Constructor
	/// \param width The width of the texture
	/// \param height The height of the texture
	/// \param depth The number of layers in the array
	/// \param samples The number of samples per pixel
	/// \param fixed When true, a fixed set of sample locations is used
	texture(GLsizei width, GLsizei height, GLsizei depth, GLsizei samples, GLboolean fixed = true) requires sampled and is_array
		: _handle(NULL)
		, _width(width), _height(height), _depth(depth)
		, _samples(samples), _mips(0) {
		glGenTextures(1, &_handle);
		start();
		if constexpr(immutable) {
			glTexStorage3DMultisample(type, _samples, format, _width, _height, _depth, fixed);
		} else {
			glTexImage3DMultisample(type, _samples, format, _width, _height, _depth, fixed);
		}
	}

	///
	/// \brief Cubemap Constructor
	/// \param size The size of each face texture
	/// \param mips The number of mipmap layers
	/// \param faces An array of pointers to textures containing pixel data for each face
	/// \param component The component type of the data
	/// \param layout The layout of the components in the pixel
	texture(GLsizei size, GLsizei mips,
        const void* faces[6], GLenum component = BYTE, GLenum layout = R, GLsizei size = 0) requires is_2d and cubemap
		: _handle(NULL)
		, _width(size), _height(size), _depth(1)
		, _samples(1), _mips(mips) {
		glGenTextures(1, &_handle);
		start();
		if constexpr(immutable) {
			glTexStorage2D(type, _mips, format, _width, _height);
			for (int i = 0; i < cubemap_faces; ++i) {
				glTexSubImage2D(base_cubemap_face + i, 0, 0, 0, _width, _height, layout, component, faces[i]);
			}
		} else if constexpr(compressed) {
		} else {
			for (int i = 0; i < cubemap_faces; ++i) {
				glTexImage2D(base_cubemap_face + i, 0, format, _width, _height, 0, layout, component, faces[i]);
			}
		}
	}

	///
	/// \brief Cubemap Array Constructor
	/// \param size The size of each face texture
	/// \param depth The number of layers in the array
	/// \param mips The number of mipmap layers
	/// \param data A pointer to a buffer containing image data
	/// \param component The component type of the data
	/// \param layout The layout of the components in the pixel
	///
	/// \details Requires OES_texture_cube_map_array
	texture(GLsizei size, GLsizei depth, GLsizei mips,
			const void* data, GLenum component = BYTE, GLenum layout = R, GLsizei size = 0) requires is_2d and cubemap
		: _handle(NULL)
		, _width(size), _height(size), _depth(depth)
		, _samples(1), _mips(mips) {
		glGenTextures(1, &_handle);
		start();
		if constexpr(immutable) {
			glTexStorage3D(type, _mips, format, _width, _height, _depth * 6);
			glTexSubImage3D(type, 0, 0, 0, 0, _width, _height, _depth * 6, layout, component, data);
		} else if constexpr(compressed) {
		} else {
			glTexImage3D(type, 0, format, _width, _height, _depth * 6, 0, layout, component, data);
		}
	}


// Basic Functions =====================================================================================================

	void start() {
		glBindTexture(type, _handle);
	}

	void end() {
		glBindTexture(type, NULL);
	}

	void bind(GLint i) {
		glActiveTexture(GL_TEXTURE0 + i);
		start();
	}

	void genmips() {
		glGenerateMipmap(type);
	}

private:
	GLuint  _handle;
	GLsizei _width;
	GLsizei _height;
	GLsizei _depth;
	GLsizei _samples;
	GLint   _mips;
};

}

}

#endif // FENNEC_RENDERERS_OPENGL_LIB_TEXTURE_H
