/**
 * Copyright (c) 2006-2016 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

#ifndef LOVE_GRAPHICS_OPENGL_OPENGL_H
#define LOVE_GRAPHICS_OPENGL_OPENGL_H

// LOVE
#include "common/config.h"
#include "common/int.h"
#include "graphics/Color.h"
#include "graphics/Texture.h"
#include "common/Matrix.h"

// GLAD
#include "libraries/glad/gladfuncs.hpp"

// C++
#include <vector>
#include <stack>

// The last argument to AttribPointer takes a buffer offset casted to a pointer.
#define BUFFER_OFFSET(i) ((char *) NULL + (i))

namespace love
{
namespace graphics
{
namespace opengl
{

// Awful, but the library uses the namespace in order to use the functions sanely
// with proper autocomplete in IDEs while having name mangling safety -
// no clashes with other GL libraries when linking, etc.
using namespace glad;

// Vertex attribute indices used in shaders by LOVE. The values map to OpenGL
// generic vertex attribute indices.
enum VertexAttribID
{
	ATTRIB_POS = 0,
	ATTRIB_TEXCOORD,
	ATTRIB_COLOR,
	ATTRIB_CONSTANTCOLOR,
	ATTRIB_MAX_ENUM
};

enum VertexAttribFlags
{
	ATTRIBFLAG_POS = 1 << ATTRIB_POS,
	ATTRIBFLAG_TEXCOORD = 1 << ATTRIB_TEXCOORD,
	ATTRIBFLAG_COLOR = 1 << ATTRIB_COLOR,
	ATTRIBFLAG_CONSTANTCOLOR = 1 << ATTRIB_CONSTANTCOLOR
};

enum BufferType
{
	BUFFER_VERTEX = 0,
	BUFFER_INDEX,
	BUFFER_MAX_ENUM
};

/**
 * Thin layer between OpenGL and the rest of the program.
 * Internally shadows some OpenGL context state for improved efficiency and
 * accuracy (compared to glGet etc.)
 * A class is more convenient and readable than plain namespaced functions, but
 * typically only one OpenGL object should be used (singleton.)
 **/
class OpenGL
{
public:

	// OpenGL GPU vendors.
	enum Vendor
	{
		VENDOR_AMD,
		VENDOR_NVIDIA,
		VENDOR_INTEL,
		VENDOR_MESA_SOFT, // Software renderer.
		VENDOR_APPLE,     // Software renderer on desktops.
		VENDOR_MICROSOFT, // Software renderer.
		VENDOR_IMGTEC,
		VENDOR_ARM,
		VENDOR_QUALCOMM,
		VENDOR_BROADCOM,
		VENDOR_VIVANTE,
		VENDOR_UNKNOWN
	};

	enum FramebufferTarget
	{
		FRAMEBUFFER_READ = (1 << 0),
		FRAMEBUFFER_DRAW = (1 << 1),
		FRAMEBUFFER_ALL  = (FRAMEBUFFER_READ | FRAMEBUFFER_DRAW),
	};

	// A rectangle representing an OpenGL viewport or a scissor box.
	struct Viewport
	{
		int x, y;
		int w, h;

		bool operator == (const Viewport &rhs) const
		{
			return x == rhs.x && y == rhs.y && w == rhs.w && h == rhs.h;
		}
	};

	struct TextureFormat
	{
		GLenum internalformat = 0;
		GLenum externalformat = 0;
		GLenum type = 0;
	};

	struct
	{
		std::vector<Matrix4> transform;
		Matrix4 projection;
	} matrices;

	class TempTransform
	{
	public:

		TempTransform(OpenGL &gl)
			: gl(gl)
		{
			gl.pushTransform();
		}

		~TempTransform()
		{
			gl.popTransform();
		}

		Matrix4 &get()
		{
			return gl.getTransform();
		}

	private:
		OpenGL &gl;
	};

	class TempDebugGroup
	{
	public:

#if defined(LOVE_IOS)
		TempDebugGroup(const char *name)
		{
			if (GLAD_EXT_debug_marker)
				glPushGroupMarkerEXT(0, (const GLchar *) name);
		}
#else
		TempDebugGroup(const char *) {}
#endif

#if defined(LOVE_IOS)
		~TempDebugGroup()
		{
			if (GLAD_EXT_debug_marker)
				glPopGroupMarkerEXT();
		}
#endif
	};

	struct Stats
	{
		size_t textureMemory;
		int    drawCalls;
		int    shaderSwitches;
	} stats;

	struct Bugs
	{
		/**
		 * On AMD's Windows (and probably Linux) drivers,
		 * glBindFramebuffer + glClear + glBindFramebuffer + draw(fbo_tex) won't
		 * work unless there's some kind of draw or state change which causes
		 * the driver to update the texture's contents (just drawing the texture
		 * won't always do it, with this driver bug).
		 * Activating shader program 0 and then activating the actual program
		 * seems to always 'fix' it for me.
		 * Bug observed January 2016 with multiple AMD GPUs and driver versions.
		 * https://love2d.org/forums/viewtopic.php?f=4&t=81496
		 **/
		bool clearRequiresDriverTextureStateUpdate;

		/**
		 * AMD's Windows drivers don't always properly generate mipmaps unless
		 * glEnable(GL_TEXTURE_2D) is called directly before glGenerateMipmap.
		 * This only applies to legacy and Compatibility Profile contexts, of
		 * course.
		 * https://www.opengl.org/wiki/Common_Mistakes#Automatic_mipmap_generation
		 **/
		bool generateMipmapsRequiresTexture2DEnable;

		/**
		 * Other bugs which have workarounds that don't use conditional code at
		 * the moment:
		 *
		 * Kepler nvidia GPUs in at least OS X 10.10 and 10.11 fail to render
		 * geometry with glDrawElements if index data comes from a Buffer Object
		 * and vertex data doesn't. One workaround is to use a CPU-side index
		 * array when there's also a CPU-side vertex array.
		 * https://love2d.org/forums/viewtopic.php?f=4&t=81401&start=10
		 *
		 * Some android drivers don't seem to initialize the sampler index
		 * values of sampler uniforms in shaders to 0 (which is required by the
		 * GLSL ES specification) when linking the shader program. One
		 * workaround is to always set the values of said sampler uniforms to 0
		 * just after linking the shader program.
		 * https://love2d.org/forums/viewtopic.php?f=4&t=81458
		 **/
	} bugs;

	OpenGL();

	/**
	 * Initializes the active OpenGL context.
	 **/
	bool initContext();

	/**
	 * Sets up some required context state based on current and default OpenGL
	 * state. Call this directly after initializing an OpenGL context!
	 **/
	void setupContext();

	/**
	 * Marks current context state as invalid and deletes OpenGL objects owned
	 * by this class instance. Call this directly before potentially deleting
	 * an OpenGL context!
	 **/
	void deInitContext();

	void pushTransform();
	void popTransform();
	Matrix4 &getTransform();

	/**
	 * Set up necessary state (LOVE-provided shader uniforms, etc.) for drawing.
	 * This *MUST* be called directly before OpenGL drawing functions.
	 **/
	void prepareDraw();

	/**
	 * State-tracked glBindBuffer.
	 * NOTE: This does not account for multiple VAOs being used! Index buffer
	 * bindings are per-VAO in OpenGL, but this doesn't know about that.
	 **/
	void bindBuffer(BufferType type, GLuint buffer);

	/**
	 * glDeleteBuffers which updates our shadowed state.
	 **/
	void deleteBuffer(GLuint buffer);

	/**
	 * glDrawArrays and glDrawElements which increment the draw-call counter by
	 * themselves.
	 **/
	void drawArrays(GLenum mode, GLint first, GLsizei count);
	void drawElements(GLenum mode, GLsizei count, GLenum type, const void *indices);

	/**
	 * Sets the enabled vertex attribute arrays based on the specified attribute
	 * bits. Each bit in the uint32 represents an enabled attribute array index.
	 * For example, useVertexAttribArrays(1 << 0) will enable attribute index 0.
	 * See the VertexAttribFlags enum for the standard vertex attributes.
	 * This function *must* be used instead of glEnable/DisableVertexAttribArray.
	 **/
	void useVertexAttribArrays(uint32 arraybits);

	/**
	 * Sets the OpenGL rendering viewport to the specified rectangle.
	 * The y-coordinate starts at the top.
	 **/
	void setViewport(const Viewport &v, bool canvasActive);

	/**
	 * Gets the current OpenGL rendering viewport rectangle.
	 **/
	Viewport getViewport() const;

	/**
	 * Sets the scissor box to the specified rectangle.
	 * The y-coordinate starts at the top and is flipped internally.
	 **/
	void setScissor(const Viewport &v, bool canvasActive);

	/**
	 * Gets the current scissor box (regardless of whether scissoring is enabled.)
	 **/
	Viewport getScissor() const;

	/**
	 * Sets the global point size.
	 **/
	void setPointSize(float size);

	/**
	 * Gets the global point size.
	 **/
	float getPointSize() const;

	/**
	 * Calls glEnable/glDisable(GL_FRAMEBUFFER_SRGB).
	 **/
	void setFramebufferSRGB(bool enable);

	/**
	 * Equivalent to glIsEnabled(GL_FRAMEBUFFER_SRGB).
	 **/
	bool hasFramebufferSRGB() const;

	/**
	 * Binds a Framebuffer Object to the specified target.
	 **/
	void bindFramebuffer(FramebufferTarget target, GLuint framebuffer);
	GLuint getFramebuffer(FramebufferTarget target) const;
	void deleteFramebuffer(GLuint framebuffer);

	/**
	 * Calls glUseProgram.
	 **/
	void useProgram(GLuint program);

	/**
	 * This will usually be 0 (system drawable), but some platforms require a
	 * non-zero FBO for rendering.
	 **/
	GLuint getDefaultFBO() const;

	/**
	 * Gets the ID for love's default texture (used for "untextured" primitives.)
	 **/
	GLuint getDefaultTexture() const;

	/**
	 * Helper for setting the active texture unit.
	 *
	 * @param textureunit Index in the range of [0, maxtextureunits-1]
	 **/
	void setTextureUnit(int textureunit);

	/**
	 * Helper for binding a texture to a specific texture unit.
	 *
	 * @param textureunit Index in the range of [0, maxtextureunits-1]
	 * @param restoreprev Restore previously bound texture unit when done.
	 **/
	void bindTextureToUnit(GLuint texture, int textureunit, bool restoreprev);

	/**
	 * Helper for deleting an OpenGL texture.
	 * Cleans up if the texture is currently bound.
	 **/
	void deleteTexture(GLuint texture);

	/**
	 * Sets the texture filter mode for the currently bound texture.
	 * The anisotropy parameter of the argument is set to the actual amount of
	 * anisotropy that was used.
	 **/
	void setTextureFilter(graphics::Texture::Filter &f);

	/**
	 * Sets the texture wrap mode for the currently bound texture.
	 **/
	void setTextureWrap(const graphics::Texture::Wrap &w);

	bool isClampZeroTextureWrapSupported() const;
	bool isPixelShaderHighpSupported() const;

	/**
	 * Returns the maximum supported width or height of a texture.
	 **/
	int getMaxTextureSize() const;

	/**
	 * Returns the maximum supported number of simultaneous render targets.
	 **/
	int getMaxRenderTargets() const;

	/**
	 * Returns the maximum supported number of MSAA samples for renderbuffers.
	 **/
	int getMaxRenderbufferSamples() const;

	/**
	 * Returns the maximum number of accessible texture units.
	 **/
	int getMaxTextureUnits() const;

	/**
	 * Returns the maximum point size.
	 **/
	float getMaxPointSize() const;

	/**
	 * Returns the maximum anisotropic filtering value that can be used for
	 * Texture filtering.
	 **/
	float getMaxAnisotropy() const;


	void updateTextureMemorySize(size_t oldsize, size_t newsize);

	/**
	 * Get the GPU vendor of this OpenGL context.
	 **/
	Vendor getVendor() const;

	static GLenum getGLBufferType(BufferType type);
	static GLint getGLWrapMode(Texture::WrapMode wmode);

	static TextureFormat convertPixelFormat(PixelFormat pixelformat, bool renderbuffer, bool &isSRGB);
	static bool isPixelFormatSupported(PixelFormat pixelformat, bool rendertarget, bool isSRGB);
	static bool hasTextureFilteringSupport(PixelFormat pixelformat);

	static const char *errorString(GLenum errorcode);
	static const char *framebufferStatusString(GLenum status);

	// Get human-readable strings for debug info.
	static const char *debugSeverityString(GLenum severity);
	static const char *debugSourceString(GLenum source);
	static const char *debugTypeString(GLenum type);

private:

	void initVendor();
	void initOpenGLFunctions();
	void initMaxValues();
	void initMatrices();
	void createDefaultTexture();

	bool contextInitialized;

	bool pixelShaderHighpSupported;
	float maxAnisotropy;
	int maxTextureSize;
	int maxRenderTargets;
	int maxRenderbufferSamples;
	int maxTextureUnits;
	float maxPointSize;

	Vendor vendor;

	// Tracked OpenGL state.
	struct
	{
		GLuint boundBuffers[BUFFER_MAX_ENUM];

		// Texture unit state (currently bound texture for each texture unit.)
		std::vector<GLuint> boundTextures;

		// Currently active texture unit.
		int curTextureUnit;

		uint32 enabledAttribArrays;

		Viewport viewport;
		Viewport scissor;

		float pointSize;

		GLuint boundFramebuffers[2];

		bool framebufferSRGBEnabled;

		GLuint defaultTexture;

		Matrix4 lastProjectionMatrix;
		Matrix4 lastTransformMatrix;

	} state;

}; // OpenGL

// OpenGL class instance singleton.
extern OpenGL gl;

} // opengl
} // graphics
} // love

#endif // LOVE_GRAPHICS_OPENGL_OPENGL_H
