/* Copyright (c) 2010 Wildfire Games
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * load and link together shaders; provides hotload support.
 */

#include "precompiled.h"
#include "ogl_shader.h"

#include "lib/ogl.h"

#include "ps/Filesystem.h"
#include "ps/CStr.h"
#include "ps/CLogger.h"
#include "ps/XML/Xeromyces.h"

#include "lib/timer.h"
#include "lib/res/h_mgr.h"
#include "lib/file/vfs/vfs.h"

static const StatusDefinition oglShaderStatusDefs[] = {
	{ ERR::SHDR_CREATE,     L"Shader creation failed" },
	{ ERR::SHDR_COMPILE,    L"Shader compile failed" },
	{ ERR::SHDR_NO_SHADER,  L"Invalid shader reference" },
	{ ERR::SHDR_LINK,       L"Shader linking failed" },
	{ ERR::SHDR_NO_PROGRAM, L"Invalid shader program reference" }
};
STATUS_ADD_DEFINITIONS(oglShaderStatusDefs);


// Convert a shader object type into a descriptive string.
// If the type enum is not known, the given buffer is used as scratch space
// to format the type number. If buf is null, a generic string is returned.
static const wchar_t* shader_type_to_string(GLenum type, wchar_t* buf, size_t buflen)
{
	switch(type)
	{
	case GL_VERTEX_SHADER_ARB: return L"VERTEX_SHADER";
	case GL_FRAGMENT_SHADER_ARB: return L"FRAGMENT_SHADER";
	}
	
	if (!buf)
		return L"unknown type enum";
	
	swprintf_s(buf, buflen, L"%u", type);
	return buf;
}

// Return the OpenGL shader type enum for the given string,
// or 0 if the shader type is not known.
static GLenum string_to_shader_type(const char* name)
{
	if (!strcasecmp(name, "VERTEX_SHADER"))
		return GL_VERTEX_SHADER_ARB;
	if (!strcasecmp(name, "FRAGMENT_SHADER"))
		return GL_FRAGMENT_SHADER_ARB;
	return 0;
}


//----------------------------------------------------------------------------
// Handle type implementation

// Data for an Ogl_Shader object
struct Ogl_Shader {
	// Type of shader (e.g. GL_VERTEX_SHADER_ARB)
	GLenum type;

	// ID of the OpenGL shader object
	GLhandleARB id;
};


H_TYPE_DEFINE(Ogl_Shader);


// One-time initialization, called once by h_alloc, which is
// in turn called by ogl_shader_load
static void Ogl_Shader_init(Ogl_Shader* shdr, va_list args)
{
	shdr->type = va_arg(args, GLenum);
}

TIMER_ADD_CLIENT(tc_compileShader);
TIMER_ADD_CLIENT(tc_linkProgram);

// Reload the shader object from the source file.
//
// TODO: The OpenGL specification says that all changes to shader objects
// have absolutely no effect on a program object that contains these shaders
// when the program object is already linked.
// So, how can we inform the "parent object" (i.e. the program object) of our change?
static Status Ogl_Shader_reload(Ogl_Shader* shdr, const PIVFS& vfs, const VfsPath& pathname, Handle UNUSED(h))
{
	Status err  = ERR::FAIL;

	if (shdr->id)
		return INFO::OK;

	shared_ptr<u8> file; size_t file_size;
	RETURN_STATUS_IF_ERR(vfs->LoadFile(pathname, file, file_size));

	ogl_WarnIfError();

	shdr->id = pglCreateShaderObjectARB(shdr->type);
	if (!shdr->id)
	{
		// May be out of memory, but bad shdr->type is also possible.
		// In any case, checking OpenGL error state will help spot
		// bad code.
		ogl_WarnIfError();
		
		WARN_RETURN(ERR::SHDR_CREATE);
	}
	
	{
		const GLchar* strings[] = { (const GLchar*)file.get() };
		const GLint tmp = (GLint)file_size;
		pglShaderSourceARB(shdr->id, 1, strings, &tmp);

		// Some drivers (Mesa i915 on 945GM) give GL_INVALID_ENUM after calling
		// CompileShader on a GL_FRAGMENT_PROGRAM, because the hardware doesn't support
		// fragment programs. I can't find a better way to detect that situation in advance,
		// so detect the error afterwards and return failure.

		ogl_WarnIfError();

		{
			TIMER_ACCRUE(tc_compileShader);
			pglCompileShaderARB(shdr->id);
		}

		if(ogl_SquelchError(GL_INVALID_ENUM))
			goto fail_shadercreated;
	}

	{
	wchar_t typenamebuf[32];
	CStrW type(shader_type_to_string(shdr->type, typenamebuf, ARRAY_SIZE(typenamebuf)));

	GLint log_length;
	GLint compile_success;
	pglGetShaderiv(shdr->id, GL_OBJECT_COMPILE_STATUS_ARB, &compile_success);
	pglGetShaderiv(shdr->id, GL_OBJECT_INFO_LOG_LENGTH_ARB, &log_length);
	if (log_length > 1)
	{
		char* infolog = new char[log_length];
		pglGetShaderInfoLog(shdr->id, log_length, 0, infolog);
		debug_printf(L"Compile log for shader %ls (type %ls):\n%hs", pathname.string().c_str(), type.c_str(), infolog);
		delete[] infolog;
	}

	if (!compile_success)
	{
		// Compilation failure caused by syntax errors and similar
		// errors at the GLSL level does not set OpenGL error state
		// according to the spec, but this might still prove to be
		// useful some time.
		ogl_WarnIfError();
	
		debug_printf(L"Failed to compile shader %ls (type %ls)\n", pathname.string().c_str(), type.c_str());
		
		err = ERR::SHDR_COMPILE;
		goto fail_shadercreated;
	}
	}

	return INFO::OK;

fail_shadercreated:
	pglDeleteShader(shdr->id);
	shdr->id = 0;
	return err;
}


// Free associated resources
static void Ogl_Shader_dtor(Ogl_Shader* shdr)
{
	// shdr->id is 0 when reload has failed
	if (shdr->id)
	{
		pglDeleteShader(shdr->id);
		shdr->id = 0;
	}
}

static Status Ogl_Shader_validate(const Ogl_Shader* UNUSED(shdr))
{
	// TODO
	return INFO::OK;
}

static Status Ogl_Shader_to_string(const Ogl_Shader* shdr, wchar_t* buf)
{
	swprintf_s(buf, H_STRING_LEN, L"Ogl_Shader %p", shdr);
	return INFO::OK;
}


//----------------------------------------------------------------------------
// Public API

// Create, load and compile a shader object of the given type
// (e.g. GL_VERTEX_SHADER_ARB). The given file will be used as
// source code for the shader.
Handle ogl_shader_load(const PIVFS& vfs, const VfsPath& pathname, GLenum type)
{
	return h_alloc(H_Ogl_Shader, vfs, pathname, 0, type);
}


// Free all resources associated with the given handle (subject
// to refcounting).
void ogl_shader_free(Handle& h)
{
	h_free(h, H_Ogl_Shader);
}

// Attach a shader to the given OpenGL program.
Status ogl_shader_attach(GLhandleARB program, Handle& h)
{
	H_DEREF(h, Ogl_Shader, shdr);

	if (!shdr->id)
		WARN_RETURN(ERR::SHDR_NO_SHADER);

	pglAttachObjectARB(program, shdr->id);

	return INFO::OK;
}



//----------------------------------------------------------------------------
// Program type implementation

struct Ogl_Program {
	// ID of the OpenGL program object
	GLhandleARB id;
};


H_TYPE_DEFINE(Ogl_Program);


// One-time initialization, called once by h_alloc, which is
// in turn called by ogl_program_load
static void Ogl_Program_init(Ogl_Program* UNUSED(p), va_list UNUSED(args))
{
}


// Load the shader associated with one Shader element,
// and attach it to our program object.
static Status do_load_shader(
		Ogl_Program* p, const VfsPath& pathname, Handle UNUSED(h),
		const CXeromyces& XeroFile, const XMBElement& Shader)
{
#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
	AT(type);
#undef AT
	
	CStr Type = Shader.GetAttributes().GetNamedItem(at_type);

	if (Type.empty())
	{
		LOGERROR(L"%ls: Missing attribute \"type\" in element \"Shader\".", pathname.string().c_str());
		WARN_RETURN(ERR::CORRUPTED);
	}

	GLenum shadertype = string_to_shader_type(Type.c_str());
	
	if (!shadertype)
	{
		LOGERROR(L"%ls: Unknown shader type \"%hs\" (valid are: VERTEX_SHADER, FRAGMENT_SHADER).", pathname.string().c_str(), Type.c_str());
		WARN_RETURN(ERR::CORRUPTED);
	}

	CStr pathnameShader = Shader.GetText();
	
	if (pathnameShader.empty())
	{
		LOGERROR(L"%ls: Missing shader name.", pathname.string().c_str());
		WARN_RETURN(ERR::CORRUPTED);
	}
	
	Handle hshader = ogl_shader_load(g_VFS, pathnameShader.FromUTF8(), shadertype);
	RETURN_STATUS_IF_ERR(hshader);

	ogl_shader_attach(p->id, hshader);

	// According to the OpenGL specification, a shader object's deletion
	// will not be final as long as the shader object is attached to a
	// container object.
	// TODO: How will this work with automatic reload?
	ogl_shader_free(hshader);

	return INFO::OK;
}


// Reload the program object from the source file.
static Status Ogl_Program_reload(Ogl_Program* p, const PIVFS& vfs, const VfsPath& pathname, Handle h)
{
	if (p->id)
		return INFO::OK;

	ogl_WarnIfError();
	
	// Check that we're not accidentally using shaders when they're not supported
	ENSURE(pglCreateProgramObjectARB != NULL);

	p->id = pglCreateProgramObjectARB();
	if (!p->id)
	{
		// The spec doesn't mention any error state that can be set
		// here, but it may still help spot bad code.
		ogl_WarnIfError();
		
		WARN_RETURN(ERR::SHDR_CREATE);
	}
	
	CXeromyces XeroFile;
	if (XeroFile.Load(vfs, pathname) != PSRETURN_OK)
		WARN_RETURN(ERR::CORRUPTED); // more informative error message?

	// Define all the elements and attributes used in the XML file
#define EL(x) int el_##x = XeroFile.GetElementID(#x)
	EL(program);
	EL(shaders);
	EL(shader);
#undef EL

	XMBElement Root = XeroFile.GetRoot();

	if (Root.GetNodeName() != el_program)
	{
		LOGERROR(L"%ls: XML root was not \"Program\".", pathname.string().c_str());
		WARN_RETURN(ERR::CORRUPTED);
	}

	XMBElementList RootChildren = Root.GetChildNodes();

	for(int i = 0; i < RootChildren.Count; ++i)
	{
		XMBElement Child = RootChildren.Item(i);
	
		int ChildName = Child.GetNodeName();
		if (ChildName == el_shaders)
		{
			XMBElementList Shaders = Child.GetChildNodes();
			
			for(int j = 0; j < Shaders.Count; ++j)
			{
				XMBElement Shader = Shaders.Item(j);
				
				if (Shader.GetNodeName() != el_shader)
				{
					LOGERROR(L"%ls: Only \"Shader\" may be child of \"Shaders\".", pathname.string().c_str());
					WARN_RETURN(ERR::CORRUPTED);
				}
				
				RETURN_STATUS_IF_ERR(do_load_shader(p, pathname, h, XeroFile, Shader));
			}
		}
		else
		{
			LOGWARNING(L"%ls: Unknown child of \"Program\".", pathname.string().c_str());
		}
	}

	{
		TIMER_ACCRUE(tc_linkProgram);
		pglLinkProgramARB(p->id);
	}

	GLint log_length;
	GLint linked;
	
	pglGetProgramiv(p->id, GL_OBJECT_LINK_STATUS_ARB, &linked);
	pglGetProgramiv(p->id, GL_OBJECT_INFO_LOG_LENGTH_ARB, &log_length);
	if (log_length > 1)
	{
		char* infolog = new char[log_length];
		pglGetProgramInfoLog(p->id, log_length, 0, infolog);
		debug_printf(L"Linker log for %ls:\n%hs\n", pathname.string().c_str(), infolog);
		delete[] infolog;
	}

	if (!linked)
	{
		debug_printf(L"Link failed for %ls\n", pathname.string().c_str());
		WARN_RETURN(ERR::SHDR_LINK);
	}

	return INFO::OK;
}


// Free associated resources
static void Ogl_Program_dtor(Ogl_Program* p)
{
	if (p->id)
	{
		pglDeleteProgram(p->id);
		p->id = 0;
	}
}

static Status Ogl_Program_validate(const Ogl_Program* UNUSED(p))
{
	// TODO
	return INFO::OK;
}

static Status Ogl_Program_to_string(const Ogl_Program* p, wchar_t* buf)
{
	swprintf_s(buf, H_STRING_LEN, L"Ogl_Program %p", p);
	return INFO::OK;
}


//----------------------------------------------------------------------------
// Public API

// Load a program object based on the given XML file description.
// Shader objects are loaded and attached automatically.
Handle ogl_program_load(const PIVFS& vfs, const VfsPath& pathname)
{
	return h_alloc(H_Ogl_Program, vfs, pathname, 0);
}

// Free all resources associated with the given program handle.
void ogl_program_free(Handle& h)
{
	h_free(h, H_Ogl_Program);
}


// Activate the program (glUseProgramObjectARB).
// h may be 0, in which case program objects are disabled.
Status ogl_program_use(Handle h)
{
	if (!h)
	{
		pglUseProgramObjectARB(0);
		return INFO::OK;
	}

	Ogl_Program* p = H_USER_DATA(h, Ogl_Program);
	if (!p || !p->id)
	{
		pglUseProgramObjectARB(0);
		WARN_RETURN(ERR::INVALID_HANDLE);
	}

	pglUseProgramObjectARB(p->id);
	return INFO::OK;
}


// Query uniform information
GLint ogl_program_get_uniform_location(Handle h, const char* name)
{
	H_DEREF(h, Ogl_Program, p);
	
	return pglGetUniformLocationARB(p->id, name);
}


// Query vertex attribute information
GLint ogl_program_get_attrib_location(Handle h, const char* name)
{
	H_DEREF(h, Ogl_Program, p);
	
	return pglGetAttribLocationARB(p->id, name);
}
