/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 */

/*
 * This file is based on, or a modified version of code from TinyGL (C) 1997-2022 Fabrice Bellard,
 * which is licensed under the MIT license (see LICENSE).
 * It also has modifications by the ResidualVM-team, which are covered under the GPLv2 (or later).
 */

#include "common/singleton.h"
#include "common/array.h"

#include "graphics/tinygl/tinygl.h"
#include "graphics/tinygl/zgl.h"
#include "graphics/tinygl/zblit.h"
#include "graphics/tinygl/zdirtyrect.h"

namespace TinyGL {

class GLContextArray : public Common::Singleton<GLContextArray> {
private:

	Common::Array<GLContext *> _glContextArray;

public:

	GLContext *createContext() {
		GLContext *ctx = new GLContext;
		_glContextArray.push_back(ctx);
		return ctx;
	}

	void destroyContext(ContextHandle *handle) {
		for (Common::Array<GLContext *>::iterator it = _glContextArray.begin(); it != _glContextArray.end(); it++) {
			if (*it == (GLContext *)handle) {
				(*it)->deinit();
				delete *it;
				_glContextArray.erase(it);
				break;
			}
		}
	}

	void destroyContexts() {
		for (Common::Array<GLContext *>::iterator it = _glContextArray.begin(); it != _glContextArray.end(); it++) {
			if (*it != nullptr) {
				(*it)->deinit();
				delete *it;
			}
		}
		_glContextArray.clear();
	}

	bool existsContexts() {
		return _glContextArray.size() != 0;
	}

	GLContext *getContext(ContextHandle *handle) {
		for (Common::Array<GLContext *>::iterator it = _glContextArray.begin(); it != _glContextArray.end(); it++) {
			if ((*it) == (GLContext *)handle) {
				return *it;
			}
		}
		return nullptr;
	}
};

} // end of namespace TinyGL

namespace Common {
DECLARE_SINGLETON(TinyGL::GLContextArray);
}

namespace TinyGL {

GLContext *gl_ctx;

GLContext *gl_get_context() {
	assert(gl_ctx);
	return gl_ctx;
}

ContextHandle *createContext(int screenW, int screenH, Graphics::PixelFormat pixelFormat, int textureSize,
							 bool enableStencilBuffer, bool dirtyRectsEnable, uint32 drawCallMemorySize) {
	gl_ctx = GLContextArray::instance().createContext();
	gl_ctx->init(screenW, screenH, pixelFormat, textureSize, enableStencilBuffer,
				 dirtyRectsEnable, drawCallMemorySize);
	return (ContextHandle *)gl_ctx;
}

void destroyContext() {
	GLContextArray::instance().destroyContexts();
	GLContextArray::destroy();
	gl_ctx = nullptr;
}

void destroyContext(ContextHandle *handle) {
	GLContextArray::instance().destroyContext(handle);
	if ((GLContext *)handle == gl_ctx)
		gl_ctx = nullptr;
	if (!GLContextArray::instance().existsContexts())
		GLContextArray::destroy();
}

void setContext(ContextHandle *handle) {
	GLContext *ctx = GLContextArray::instance().getContext(handle);
	if (ctx == nullptr) {
		error("TinyGL: makeContextCurrent() Failed get context");
	}
	gl_ctx = ctx;
}

void GLContext::initSharedState() {
	GLSharedState *s = &shared_state;
	s->lists = (GLList **)gl_zalloc(sizeof(GLList *) * MAX_DISPLAY_LISTS);
	s->texture_hash_table = (GLTexture **)gl_zalloc(sizeof(GLTexture *) * TEXTURE_HASH_TABLE_SIZE);
}

void GLContext::endSharedState() {
	GLSharedState *s = &shared_state;

	for (int i = 0; i < MAX_DISPLAY_LISTS; i++) {
		// TODO
	}
	gl_free(s->lists);

	gl_free(s->texture_hash_table);
}

void GLContext::init(int screenW, int screenH, Graphics::PixelFormat pixelFormat, int textureSize,
	             bool enableStencilBuffer, bool dirtyRectsEnable, uint32 drawCallMemorySize) {
	GLViewport *v;

	_enableDirtyRectangles = dirtyRectsEnable;
	stencil_buffer_supported = enableStencilBuffer;

	fb = new TinyGL::FrameBuffer(screenW, screenH, pixelFormat, enableStencilBuffer);
	renderRect = Common::Rect(0, 0, screenW, screenH);

	if ((textureSize & (textureSize - 1)))
		error("glInit: texture size not power of two: %d", textureSize);
	if (textureSize <= 1 || textureSize > 4096)
		error("glInit: texture size not allowed: %d", textureSize);
	_textureSize = textureSize;
	fb->setTextureSizeAndMask(textureSize, (textureSize - 1) << ZB_POINT_ST_FRAC_BITS);

	// allocate GLVertex array
	vertex_max = POLYGON_MAX_VERTEX;
	vertex = (GLVertex *)gl_malloc(POLYGON_MAX_VERTEX * sizeof(GLVertex));

	// viewport
	v = &viewport;
	v->xmin = 0;
	v->ymin = 0;
	v->xsize = screenW;
	v->ysize = screenH;
	v->updated = 1;

	// shared state
	initSharedState();

	// lists
	exec_flag = 1;
	compile_flag = 0;
	print_flag = 0;

	in_begin = 0;

	// lights
	for (int i = 0; i < T_MAX_LIGHTS; i++) {
		GLLight *l = &lights[i];
		l->ambient = Vector4(0, 0, 0, 1);
		if (i == 0) {
			l->diffuse = Vector4(1, 1, 1, 1);
			l->specular = Vector4(1, 1, 1, 1);
			l->has_specular = true;
		} else {
			l->diffuse = Vector4(0, 0, 0, 1);
			l->specular = Vector4(0, 0, 0, 1);
			l->has_specular = false;
		}
		l->position = Vector4(0, 0, 1, 0);
		l->spot_direction = Vector3(0, 0, -1);
		l->spot_exponent = 0;
		l->spot_cutoff = 180;
		l->attenuation[0] = 1;
		l->attenuation[1] = 0;
		l->attenuation[2] = 0;
		l->cos_spot_cutoff = -1.0f;
		l->norm_spot_direction = Vector3(0, 0, -1);
		l->norm_position = Vector3(0, 0, 1);
		l->enabled = 0;
		l->next = nullptr;
		l->prev = nullptr;
	}
	first_light = nullptr;
	ambient_light_model = Vector4(0.2f, 0.2f, 0.2f, 1);
	local_light_model = 0;
	lighting_enabled = 0;
	light_model_two_side = 0;

	// default materials */
	for (int i = 0; i < 2; i++) {
		GLMaterial *m = &materials[i];
		m->emission = Vector4(0, 0, 0, 1);
		m->ambient = Vector4(0.2f, 0.2f, 0.2f, 1);
		m->diffuse = Vector4(0.8f, 0.8f, 0.8f, 1);
		m->specular = Vector4(0, 0, 0, 1);
		m->has_specular = false;
		m->shininess = 0;
	}
	current_color_material_mode = TGL_FRONT_AND_BACK;
	current_color_material_type = TGL_AMBIENT_AND_DIFFUSE;
	color_material_enabled = 0;

	// textures
	texture_2d_enabled = false;
	current_texture = default_texture = alloc_texture(0);
	maxTextureName = 0;
	texture_mag_filter = TGL_LINEAR;
	texture_min_filter = TGL_NEAREST_MIPMAP_LINEAR;
#if defined(SCUMM_LITTLE_ENDIAN)
	colorAssociationList.push_back({Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24), TGL_RGBA, TGL_UNSIGNED_BYTE});
	colorAssociationList.push_back({Graphics::PixelFormat(3, 8, 8, 8, 0, 0, 8, 16, 0),  TGL_RGB,  TGL_UNSIGNED_BYTE});
#else
	colorAssociationList.push_back({Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0), TGL_RGBA, TGL_UNSIGNED_BYTE});
	colorAssociationList.push_back({Graphics::PixelFormat(3, 8, 8, 8, 0, 16, 8, 0, 0),  TGL_RGB,  TGL_UNSIGNED_BYTE});
#endif
	colorAssociationList.push_back({Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0),  TGL_RGB,  TGL_UNSIGNED_SHORT_5_6_5});
	colorAssociationList.push_back({Graphics::PixelFormat(2, 5, 5, 5, 1, 11, 6, 1, 0),  TGL_RGBA, TGL_UNSIGNED_SHORT_5_5_5_1});
	colorAssociationList.push_back({Graphics::PixelFormat(2, 4, 4, 4, 4, 12, 8, 4, 0),  TGL_RGBA, TGL_UNSIGNED_SHORT_4_4_4_4});

	// default state
	current_color = Vector4(1.0f, 1.0f, 1.0f, 1.0f);

	current_normal = Vector4(1.0f, 0.0f, 0.0f, 0.0f);

	current_edge_flag = 1;

	current_tex_coord = Vector4(0.0f, 0.0f, 0.0f, 1.0f);

	polygon_mode_front = TGL_FILL;
	polygon_mode_back = TGL_FILL;

	current_front_face = 0; // 0 = GL_CCW  1 = GL_CW
	current_cull_face = TGL_BACK;
	current_shade_model = TGL_SMOOTH;
	cull_face_enabled = 0;

	// fog
	fog_enabled = false;
	fog_mode = TGL_EXP;
	fog_color = Vector4(0.0f, 0.0f, 0.0f, 0.0f);
	fog_density = 1.0f;
	fog_start = 0.0f;
	fog_end = 0.0f;

	// clear
	clear_color = Vector4(0.0f, 0.0f, 0.0f, 0.0f);
	clear_depth = 1.0f;
	clear_stencil = 0;

	// selection
	render_mode = TGL_RENDER;
	select_buffer = nullptr;
	name_stack_size = 0;

	// blending
	blending_enabled = false;
	source_blending_factor = TGL_ONE;
	destination_blending_factor = TGL_ZERO;

	// alpha test
	alpha_test_enabled = false;
	alpha_test_func = TGL_ALWAYS;
	alpha_test_ref_val = 0;

	// depth test
	depth_test_enabled = false;
	depth_func = TGL_LESS;
	depth_write_mask = true;

	// stipple
	polygon_stipple_enabled = false;
	memset(polygon_stipple_pattern, 0xff, sizeof(polygon_stipple_pattern));

	// stencil
	stencil_test_enabled = false;
	stencil_test_func = TGL_ALWAYS;
	stencil_ref_val = 0;
	stencil_mask = 0xff;
	stencil_write_mask = 0xff;
	stencil_sfail = TGL_KEEP;
	stencil_dpfail = TGL_KEEP;
	stencil_dppass = TGL_KEEP;

	// matrix
	matrix_mode = 0;

	matrix_stack_depth_max[0] = MAX_MODELVIEW_STACK_DEPTH;
	matrix_stack_depth_max[1] = MAX_PROJECTION_STACK_DEPTH;
	matrix_stack_depth_max[2] = MAX_TEXTURE_STACK_DEPTH;

	for (int i = 0; i < 3; i++) {
		matrix_stack[i] = (Matrix4 *)gl_zalloc(matrix_stack_depth_max[i] * sizeof(Matrix4));
		matrix_stack_ptr[i] = matrix_stack[i];
	}

	tglMatrixMode(TGL_PROJECTION);
	tglLoadIdentity();
	tglMatrixMode(TGL_TEXTURE);
	tglLoadIdentity();
	tglMatrixMode(TGL_MODELVIEW);
	tglLoadIdentity();

	matrix_model_projection_updated = 1;

	// opengl 1.1 arrays
	client_states = 0;

	// opengl 1.1 polygon offset
	offset_states = 0;
	offset_factor = 0.0f;
	offset_units = 0.0f;

	// clear the resize callback function pointer
	gl_resize_viewport = nullptr;

	// specular buffer
	specbuf_first = nullptr;
	specbuf_used_counter = 0;
	specbuf_num_buffers = 0;

	// color mask
	color_mask_red = color_mask_green = color_mask_blue = color_mask_alpha = true;

	_currentAllocatorIndex = 0;
	_drawCallAllocator[0].initialize(drawCallMemorySize);
	_drawCallAllocator[1].initialize(drawCallMemorySize);
	_debugRectsEnabled = false;
	_profilingEnabled = false;

	TinyGL::Internal::tglBlitResetScissorRect();
}

void GLContext::deinit() {
	disposeDrawCallLists();
	disposeResources();

	specbuf_cleanup();
	for (int i = 0; i < 3; i++)
		gl_free(matrix_stack[i]);
	free_texture(default_texture);
	endSharedState();
	gl_free(vertex);
	delete fb;
}

} // end of namespace TinyGL
