/****************************************************************************
Copyright 2012 cocos2d-x.org
Copyright 2011 Jeff Lamarche
Copyright 2012 Goffredo Marocchi
Copyright 2012 Ricardo Quesada

http://www.cocos2d-x.org
 
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.
****************************************************************************/
package org.cocos2d.shaders;

import java.nio.IntBuffer;
import java.util.Arrays;

import org.cocos2d.CCDirector;
import org.cocos2d.include.ccConfig;
import org.cocos2d.include.ccMacros;
import org.cocos2d.kazmath.kmMat4;
import org.cocos2d.kazmath.GL.Matrix;
import org.cocos2d.utils.CCFormatter;

import android.opengl.GLES20;

/**
 * @addtogroup shaders
 * @{
 */

/** CCGLProgram
 * Class that implements a glProgram
 * 
 * 
 * @since v2.0.0
 */
public class CCGLProgram {
	private static final String TAG = CCGLProgram.class.getSimpleName();

	public static final int kCCVertexAttrib_Position = 0;
	public static final int kCCVertexAttrib_Color = 1;
	public static final int kCCVertexAttrib_TexCoords = 2;
	public static final int kCCVertexAttrib_MAX = 3;

	public static final int kCCUniformPMatrix = 0;
	public static final int kCCUniformMVMatrix = 1;
	public static final int kCCUniformMVPMatrix = 2;
	public static final int kCCUniformTime = 3;
	public static final int kCCUniformSinTime = 4;
	public static final int kCCUniformCosTime = 5;
	public static final int kCCUniformRandom01 = 6;
	public static final int kCCUniformSampler = 7;
	public static final int kCCUniform_MAX = 8;

	public static final String kCCShader_PositionTextureColor			= "ShaderPositionTextureColor";
	public static final String kCCShader_PositionTextureColorAlphaTest	= "ShaderPositionTextureColorAlphaTest";
	public static final String kCCShader_PositionColor					= "ShaderPositionColor";
	public static final String kCCShader_PositionTexture				= "ShaderPositionTexture";
	public static final String kCCShader_PositionTexture_uColor			= "ShaderPositionTexture_uColor";
	public static final String kCCShader_PositionTextureA8Color			= "ShaderPositionTextureA8Color";
	public static final String kCCShader_Position_uColor				= "ShaderPosition_uColor";
	public static final String kCCShader_PositionLengthTexureColor		= "ShaderPositionLengthTextureColor";

	// uniform names
	public static final String kCCUniformPMatrix_s			= "CC_PMatrix";
	public static final String kCCUniformMVMatrix_s			= "CC_MVMatrix";
	public static final String kCCUniformMVPMatrix_s		= "CC_MVPMatrix";
	public static final String kCCUniformTime_s				= "CC_Time";
	public static final String kCCUniformSinTime_s			= "CC_SinTime";
	public static final String kCCUniformCosTime_s			= "CC_CosTime";
	public static final String kCCUniformRandom01_s			= "CC_Random01";
	public static final String kCCUniformSampler_s			= "CC_Texture0";
	public static final String kCCUniformAlphaTestValue		= "CC_alpha_value";

	// Attribute names
	public static final String kCCAttributeNameColor		= "a_color";
	public static final String kCCAttributeNamePosition		= "a_position";
	public static final String kCCAttributeNameTexCoord		= "a_texCoord";

	public CCGLProgram() {
		m_uProgram = 0;
		m_uVertShader = 0;
		m_uFragShader = 0;
		m_pHashForUniforms = null;
		m_bUsesTime = false;

		Arrays.fill(m_uUniforms, 0);
	}

	/** Initializes the CCGLProgram with a vertex and fragment with bytes array */
	public boolean initWithVertexShaderByteArray(final String vShaderByteArray, final String fShaderByteArray) {
		m_uProgram = GLES20.glCreateProgram();
		ccMacros.CHECK_GL_ERROR_DEBUG();

		m_uVertShader = m_uFragShader = 0;

		if (vShaderByteArray != null) {
			IntBuffer buffVShader = IntBuffer.allocate(1);
			if (! compileShader(buffVShader, GLES20.GL_VERTEX_SHADER, vShaderByteArray)) {
				ccMacros.CCLOG(TAG, "cocos2d: ERROR: Failed to compile vertex shader");
			} else {
				m_uVertShader = buffVShader.get(0);
			}
		}

		// Create and compile fragment shader
		if (fShaderByteArray != null) {
			IntBuffer buffFShader = IntBuffer.allocate(1);
			if (! compileShader(buffFShader, GLES20.GL_FRAGMENT_SHADER, fShaderByteArray)) {
				ccMacros.CCLOG(TAG, "cocos2d: ERROR: Failed to compile fragment shader");
			} else {
				m_uFragShader = buffFShader.get(0);
			}
		}

		if (m_uVertShader != 0) {
			GLES20.glAttachShader(m_uProgram, m_uVertShader);
		}
		ccMacros.CHECK_GL_ERROR_DEBUG();

		if (m_uFragShader != 0) {
			GLES20.glAttachShader(m_uProgram, m_uFragShader);
		}
		m_pHashForUniforms = null;

		ccMacros.CHECK_GL_ERROR_DEBUG();

		return true;
	}

	/** Initializes the CCGLProgram with a vertex and fragment with contents of filenames */
	public boolean initWithVertexShaderFilename(final String vShaderFilename, final String fShaderFilename) {
// TODO		final String vertexSource = (GLchar*) CCString::createWithContentsOfFile(CCFileUtils::sharedFileUtils()->fullPathForFilename(vShaderFilename).c_str())->getCString();
// TODO		final String fragmentSource = (GLchar*) CCString::createWithContentsOfFile(CCFileUtils::sharedFileUtils()->fullPathForFilename(fShaderFilename).c_str())->getCString();
			final String vertexSource = "";
			final String fragmentSource = "";

		return initWithVertexShaderByteArray(vertexSource, fragmentSource);
	}

	/**  It will add a new attribute to the shader */
	void addAttribute(final String attributeName, int index) {
		GLES20.glBindAttribLocation(m_uProgram, index, attributeName);
	}

	/** links the glProgram */
	public boolean link() {
		ccMacros.CCAssert(m_uProgram != 0, "Cannot link invalid program");

		int status = GLES20.GL_TRUE;

		GLES20.glLinkProgram(m_uProgram);

		if(m_uVertShader != 0) {
			GLES20.glDeleteShader(m_uVertShader);
		}

		if(m_uFragShader != 0) {
			GLES20.glDeleteShader(m_uFragShader);
		}

		m_uVertShader = m_uFragShader = 0;

		if(ccConfig.COCOS2D_DEBUG != 0) {
			IntBuffer lisk_status = IntBuffer.allocate(1);
			GLES20.glGetProgramiv(m_uProgram, GLES20.GL_LINK_STATUS, lisk_status);
			status = lisk_status.get(0);

			if (status == GLES20.GL_FALSE) {
				ccMacros.CCLOG(TAG, CCFormatter.format("cocos2d: ERROR: Failed to link program: %i", m_uProgram));
				ccGLStateCache.ccGLDeleteProgram(m_uProgram);
				m_uProgram = 0;
			}
		}

		return (status == GLES20.GL_TRUE);
	}

	/** it will call glUseProgram() */
	public void use() {
		ccGLStateCache.ccGLUseProgram(m_uProgram);
	}

	/** It will create 4 uniforms:
	 *     - kCCUniformPMatrix
	 *     - kCCUniformMVMatrix
	 *     - kCCUniformMVPMatrix
	 *     - kCCUniformSampler
	 * 
	 * And it will bind "kCCUniformSampler" to 0
	 */
	public void updateUniforms() {
		m_uUniforms[kCCUniformPMatrix] = GLES20.glGetUniformLocation(m_uProgram, kCCUniformPMatrix_s);
		m_uUniforms[kCCUniformMVMatrix] = GLES20.glGetUniformLocation(m_uProgram, kCCUniformMVMatrix_s);
		m_uUniforms[kCCUniformMVPMatrix] = GLES20.glGetUniformLocation(m_uProgram, kCCUniformMVPMatrix_s);

		m_uUniforms[kCCUniformTime] = GLES20.glGetUniformLocation(m_uProgram, kCCUniformTime_s);
		m_uUniforms[kCCUniformSinTime] = GLES20.glGetUniformLocation(m_uProgram, kCCUniformSinTime_s);
		m_uUniforms[kCCUniformCosTime] = GLES20.glGetUniformLocation(m_uProgram, kCCUniformCosTime_s);

		m_bUsesTime = (
				m_uUniforms[kCCUniformTime] != -1 ||
				m_uUniforms[kCCUniformSinTime] != -1 ||
				m_uUniforms[kCCUniformCosTime] != -1
				);

		m_uUniforms[kCCUniformRandom01] = GLES20.glGetUniformLocation(m_uProgram, kCCUniformRandom01_s);

		m_uUniforms[kCCUniformSampler] = GLES20.glGetUniformLocation(m_uProgram, kCCUniformSampler_s);

		this.use();

		// Since sample most probably won't change, set it to 0 now.
		this.setUniformLocationWith1i(m_uUniforms[kCCUniformSampler], 0);
	}

	/** calls retrieves the named uniform location for this shader program. */
	public int getUniformLocationForName(final String name) {
		ccMacros.CCAssert(name != null, "Invalid uniform name");
		ccMacros.CCAssert(m_uProgram != 0, "Invalid operation. Cannot get uniform location when program is not initialized");

		return GLES20.glGetUniformLocation(m_uProgram, name);
	}

	/** calls glUniform1i only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWith1i(int location, int i1) {
/* TODO
		boolean updated = updateUniformLocation(location, &i1, sizeof(i1)*1);

		if(updated) {
			GLES20.glUniform1i(location, i1);
		}
*/
		GLES20.glUniform1i(location, i1);
	}

	/** calls glUniform1f only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWith1f(int location, float f1) {
/* TODO
		boolean updated = updateUniformLocation(location, &f1, sizeof(f1)*1);

		if( updated ) {
			GLES20.glUniform1f(location, f1);
		}
*/
		GLES20.glUniform1f(location, f1);
	}


	/** calls glUniform2f only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWith2f(int location, float f1, float f2) {
/* TODO
		float [] floats = {f1,f2};
		boolean updated = updateUniformLocation(location, floats, sizeof(floats));

		if( updated ) {
			GLES20.glUniform2f(location, f1, f2);
		}
*/
		GLES20.glUniform2f(location, f1, f2);
	}


	/** calls glUniform3f only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWith3f(int location, float f1, float f2, float f3) {
/* TODO
		float [] floats = {f1,f2,f3};
		boolean updated = updateUniformLocation(location, floats, sizeof(floats));

		if( updated ) {
			GLES20.glUniform3f(location, f1, f2, f3);
		}
*/
		GLES20.glUniform3f(location, f1, f2, f3);
	}

	/** calls glUniform4f only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWith4f(int location, float f1, float f2, float f3, float f4) {
/* TODO
		float [] floats = {f1,f2,f3,f4};
		boolean updated = updateUniformLocation(location, floats, sizeof(floats));

		if( updated ) {
			GLES20.glUniform4f(location, f1, f2, f3, f4);
		}
*/
		GLES20.glUniform4f(location, f1, f2, f3, f4);
	}


	/** calls glUniform2fv only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWith2fv(int location, float [] floats, int numberOfArrays) {
/* TODO
		boolean updated = updateUniformLocation(location, floats, sizeof(float)*2*numberOfArrays);

		if( updated ) {
			GLES20.glUniform2fv(location, numberOfArrays, floats, 0);
		}
*/
		GLES20.glUniform2fv(location, numberOfArrays, floats, 0);
	}

	/** calls glUniform3fv only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWith3fv(int location, float [] floats, int numberOfArrays) {
/* TODO
		boolean updated = updateUniformLocation(location, floats, sizeof(float)*3*numberOfArrays);

		if( updated ) {
			GLES20.glUniform3fv(location, numberOfArrays, floats, 0);
		}
*/
		GLES20.glUniform3fv(location, numberOfArrays, floats, 0);
	}


	/** calls glUniform4fv only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWith4fv(int location, float [] floats, int numberOfArrays) {
/* TODO
		boolean updated = updateUniformLocation(location, floats, sizeof(float)*4*numberOfArrays);

		if( updated ) {
			GLES20.glUniform4fv(location, numberOfArrays, floats, 0);
		}
*/
		GLES20.glUniform4fv(location, numberOfArrays, floats, 0);
	}

	/** calls glUniformMatrix4fv only if the values are different than the previous call for this same shader program. */
	public void setUniformLocationWithMatrix4fv(int location, float [] matrixArray, int numberOfMatrices) {
/* TODO
		boolean updated = updateUniformLocation(location, matrixArray, sizeof(float)*16*numberOfMatrices);

		if( updated ) {
			GLES20.glUniformMatrix4fv(location, numberOfMatrices, false, matrixArray, 0);
		}
*/
		GLES20.glUniformMatrix4fv(location, numberOfMatrices, false, matrixArray, 0);
	}

	/** will update the builtin uniforms if they are different than the previous call for this same shader program. */
	public void setUniformsForBuiltins() {
		kmMat4 matrixP = new kmMat4();
		kmMat4 matrixMV = new kmMat4();
		kmMat4 matrixMVP = new kmMat4();

		Matrix.kmGLGetMatrix(Matrix.KM_GL_PROJECTION, matrixP);
		Matrix.kmGLGetMatrix(Matrix.KM_GL_MODELVIEW, matrixMV);

		kmMat4.kmMat4Multiply(matrixMVP, matrixP, matrixMV);

		setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformPMatrix], matrixP.mat, 1);
		setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformMVMatrix], matrixMV.mat, 1);
		setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformMVPMatrix], matrixMVP.mat, 1);

		if(m_bUsesTime) {
			CCDirector director = CCDirector.sharedDirector();
			// This doesn't give the most accurate global time value.
			// Cocos2D doesn't store a high precision time value, so this will have to do.
			// Getting Mach time per frame per shader using time could be extremely expensive.
			float time = (float) (director.getTotalFrames() * director.getAnimationInterval());

			setUniformLocationWith4f(m_uUniforms[kCCUniformTime], time/10.0f, time, time*2, time*4);
			setUniformLocationWith4f(m_uUniforms[kCCUniformSinTime], time/8.0f, time/4.0f, time/2.0f, (float) Math.sin(time));
			setUniformLocationWith4f(m_uUniforms[kCCUniformCosTime], time/8.0f, time/4.0f, time/2.0f, (float) Math.cos(time));
		}

		if (m_uUniforms[kCCUniformRandom01] != -1) {
			setUniformLocationWith4f(m_uUniforms[kCCUniformRandom01], ccMacros.CCRANDOM_0_1(), ccMacros.CCRANDOM_0_1(), ccMacros.CCRANDOM_0_1(), ccMacros.CCRANDOM_0_1());
		}
	}

	/** returns the vertexShader error log */
	public String vertexShaderLog() {
		return GLES20.glGetShaderInfoLog(m_uVertShader);
	}

	/** returns the fragmentShader error log */
	public String fragmentShaderLog() {
		return GLES20.glGetShaderInfoLog(m_uFragShader);
	}

	/** returns the program error log */
	public String programLog() {
		return GLES20.glGetProgramInfoLog(m_uProgram);
	}

	// reload all shaders, this function is designed for android
	// when opengl context lost, so don't call it.
	public void reset() {
		m_uVertShader = m_uFragShader = 0;
		Arrays.fill(m_uUniforms, 0);

		// it is already deallocated by android
		//ccGLDeleteProgram(m_uProgram);
		m_uProgram = 0;
/* TODO
		tHashUniformEntry *current_element, *tmp;

		// Purge uniform hash
		HASH_ITER(hh, m_pHashForUniforms, current_element, tmp) 
		{
			HASH_DEL(m_pHashForUniforms, current_element);
			free(current_element->value);
			free(current_element);
		}
*/
		m_pHashForUniforms = null;
	}

	public int getProgram() {
		return m_uProgram;
	}

	private class tHashUniformEntry {
		Object			value;		// value
		int				location;	// Key
// TODO		UT_hash_handle	hh;			// hash entry
	}

	private boolean updateUniformLocation(int location, byte [] data, int bytes) {
		if (location < 0) {
			return false;
		}

		boolean updated = true;
/* TODO
		tHashUniformEntry *element = NULL;
		HASH_FIND_INT(m_pHashForUniforms, &location, element);

		if (element == null) {
			element = (tHashUniformEntry*)malloc( sizeof(*element) );

			// key
			element.location = location;

			// value
			element->value = malloc( bytes );
			memcpy(element->value, data, bytes );

			HASH_ADD_INT(m_pHashForUniforms, location, element);
		} else {
			if (memcmp(element->value, data, bytes) == 0) {
				updated = false;
			} else {
				memcpy(element->value, data, bytes);
			}
		}
*/
		return updated;
	}

	private String description() {
		return CCFormatter.format(
				"<CCGLProgram = %08X | Program = %i, VertexShader = %i, FragmentShader = %i>",
				this, m_uProgram, m_uVertShader, m_uFragShader);
	}

	private boolean compileShader(IntBuffer compiledShader, int type, final String source) {
		int status;

		if (source == null) {
			return false;
		}

		final String sources[] = {
			(type == GLES20.GL_VERTEX_SHADER ? "precision highp float;\n" : "precision mediump float;\n"),
			"uniform mat4 CC_PMatrix;\n" +
			"uniform mat4 CC_MVMatrix;\n" +
			"uniform mat4 CC_MVPMatrix;\n" +
			"uniform vec4 CC_Time;\n" +
			"uniform vec4 CC_SinTime;\n" +
			"uniform vec4 CC_CosTime;\n" +
			"uniform vec4 CC_Random01;\n" +
			"//CC INCLUDES END\n\n",
			source,
		};

		int shader;
		shader = GLES20.glCreateShader(type);
		compiledShader.put(0, shader);
// TODO		GLES20.glShaderSource(shader, sizeof(sources)/sizeof(*sources), sources, NULL);
		GLES20.glShaderSource(shader, source);
		GLES20.glCompileShader(shader);

		IntBuffer params = IntBuffer.allocate(1);
		GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, params);
		status = params.get(0);

		if (status == GLES20.GL_FALSE) {
/* TODO
			IntBuffer bufferLength = IntBuffer.allocate(1);

			int length;
			GLES20.glGetShaderiv(shader, GLES20.GL_SHADER_SOURCE_LENGTH, bufferLength);
			length = bufferLength.get(0);

			byte [] src = new byte[length];
			GLES20.glGetShaderSource(shader, length, bufferLength, src);
			ccMacros.CCLOG(TAG, CCFormatter.format("cocos2d: ERROR: Failed to compile shader:\n%s", src));

			if (type == GLES20.GL_VERTEX_SHADER) {
				ccMacros.CCLOG(TAG, CCFormatter.format("cocos2d: %s", vertexShaderLog()));
			} else {
				ccMacros.CCLOG(TAG, CCFormatter.format("cocos2d: %s", fragmentShaderLog()));
			}
			free(src);

			abort();
*/
		}
		return (status == GLES20.GL_TRUE);
	}

	private int			m_uProgram;
	private int			m_uVertShader;
	private int			m_uFragShader;
	private int []		m_uUniforms = new int[kCCUniform_MAX];
	private tHashUniformEntry m_pHashForUniforms;
	private boolean		m_bUsesTime;
}

// end of shaders group
/// @}
