module renderer.api.opengl.CgKernel;




template MCgKernel() {
	private import renderer.cg.cgGL;
	private import utils.StructClass : simpleStructCtor;
	private import std.file : read, exists;


	private template MCgProgram() {
		/+void	setParam(ShaderParam param, mat4 val) {
			cgGLSetMatrixParameterfr(cast(CGparameter)param, &val.data[0]);
		}

		void setStateMatrixParam(ShaderParam param, StateMatrix sm, MatrixTransform mt)
		{
			cgGLSetStateMatrixParameter(cast(CGparameter)param, cast(CGGLenum)sm, cast(CGGLenum)mt);
		}+/

		final CGparameter getParam(char[] name) {
			{
				CGparameter* p = name in namedParams;
				if (p) return *p;
			}
			
			CGparameter p = cgGetNamedParameter(cgProgram, toStringz(name));
			if (p is null) throw new Exception(`Unknown shader param: '` ~ name ~ `'`);
			namedParams[name.dup] = p;
			namedParams.rehash;
			
			return p;
		}


		override void	opIndexAssign(float val, char[] name) {
			cgGLSetParameter1f(getParam(name), val);
		}
		
		
		override void	opIndexAssign(vec2 val, char[] name) {
			cgGLSetParameter2fv(getParam(name), &val.x);
		}


		override void	opIndexAssign(vec3 val, char[] name) {
			cgGLSetParameter3fv(getParam(name), &val.x);
		}
		
		
		override void	opIndexAssign(vec4 val, char[] name) {
			cgGLSetParameter4fv(getParam(name), &val.x);
		}
		

		override void opIndexAssign(StateMatrix val, char[] name) {
			static CGGLenum[] typeMap = [
				CGGLenum.CG_GL_MODELVIEW_MATRIX,
				CGGLenum.CG_GL_PROJECTION_MATRIX,
				CGGLenum.CG_GL_TEXTURE_MATRIX,
				CGGLenum.CG_GL_MODELVIEW_PROJECTION_MATRIX
			];
				
			static CGGLenum[] xformMap = [
				CGGLenum.CG_GL_MATRIX_IDENTITY,
				CGGLenum.CG_GL_MATRIX_TRANSPOSE,
				CGGLenum.CG_GL_MATRIX_INVERSE,
				CGGLenum.CG_GL_MATRIX_INVERSE_TRANSPOSE
			];
			
			cgGLSetStateMatrixParameter(getParam(name), typeMap[cast(int)val.type], xformMap[cast(int)val.transform]);
		}
		
		
		protected {
			CGprofile   					cgProfile;
			CGprogram					cgProgram;
			CGparameter[char[]]		namedParams;
		}
	}
	
	
	class CgVertexShader : HwVertexShader {
		mixin MCgProgram;

		private void extractVaryingParams() {
			CGparameter leaf = cgGetFirstLeafParameter(cgProgram, CGenum.CG_PROGRAM);
			
			while (leaf) {
				CGenum var	= cgGetParameterVariability(leaf);
				CGenum dir	= cgGetParameterDirection(leaf);
				
				if (var == CGenum.CG_VARYING && dir != CGenum.CG_OUT && dir != CGenum.CG_ERROR) {
					CGresource res = cgGetParameterResource(leaf);
					
					if (res >= VertexDataType.min && res <= VertexDataType.max) {
						VertexDataType vdt = cast(VertexDataType)res;
						varyingParams ~= VaryingParam(leaf, vdt);
						writefln("CgVertexShader: extracted a param: ", cast(uint)vdt);
					}
				}
				
				leaf = cgGetNextLeafParameter(leaf);
			}
		}


		void bind() {
			foreach (inout VaryingParam vpar; varyingParams) {
				CgKernel.vdtCgParams[vpar.vdt - CgKernel.vdtArrayOffset] = vpar.param;
			}
			
			//writefln(`enabling vertex profile: `, cast(int)this.cgProfile);
			cgGLEnableProfile(this.cgProfile);
			cgGLBindProgram(this.cgProgram);
		}
		
		
		void unbind() {
			foreach (inout VaryingParam vpar; varyingParams) {
				CgKernel.vdtCgParams[vpar.vdt - CgKernel.vdtArrayOffset] = null;
			}
			
			cgGLUnbindProgram(this.cgProfile);
			cgGLDisableProfile(this.cgProfile);
		}


		this (CGprogram prog, CGprofile prof) {
			this.cgProgram = prog;
			this.cgProfile = prof;
			extractVaryingParams();
		}


		override bool isVertexDataNeeded(VertexDataType vdt) {
			foreach (inout VaryingParam vp; varyingParams) {
				if (vp.vdt is vdt) {
					return true;
				}
			}
			
			return false;
		}
		

		/+void setTexture(ShaderParam param, Texture tex) {
			assert(false);		// not implemented
		}+/


		package {
			struct VaryingParam {
				CGparameter		param;
				VertexDataType		vdt;
				mixin simpleStructCtor;
			}
			
			VaryingParam[]	varyingParams;
		}
	}
	
	
	class CgFragmentShader : HwFragmentShader {
		mixin MCgProgram;
		
		
		private void extractTexBindings() {
			CGparameter leaf = cgGetFirstLeafParameter(cgProgram, CGenum.CG_PROGRAM);
			
			while (leaf) {
				CGenum var	= cgGetParameterVariability(leaf);
				CGenum dir	= cgGetParameterDirection(leaf);
				
				if (var != CGenum.CG_VARYING && dir != CGenum.CG_OUT && dir != CGenum.CG_ERROR) {
					CGresource res = cgGetParameterResource(leaf);
					
					if (res >= CGresource.CG_TEXUNIT0 && res <= CGresource.CG_TEXUNIT15) {
						texBindings ~= TexBinding(leaf, res - CGresource.CG_TEXUNIT0);
					}
				}
				
				leaf = cgGetNextLeafParameter(leaf);
			}
		}


		this (CGprogram prog, CGprofile prof) {
			this.cgProgram = prog;
			this.cgProfile = prof;
			extractTexBindings();
		}


		/+void setTexture(ShaderParam param, Texture tex) {							TODO
			if (tex !is null && tex.id != -1) {
				foreach (inout TexBinding tb; texBindings) {
					if (tb.pred == cast(CGparameter)param) {
						CgTextureKernel.texUnitTextures[tb.succ] = tex;
						return;
					}
				}
			}
		}+/


		void bind() {
			//writefln(`enabling fragment profile: `, cast(int)this.cgProfile);
			cgGLEnableProfile(this.cgProfile);
			cgGLBindProgram(this.cgProgram);
		}
		
		
		void unbind() {
			cgGLUnbindProgram(this.cgProfile);
			cgGLDisableProfile(this.cgProfile);
		}



		package {
			struct TexBinding {
				CGparameter	param;
				uint					unit;
				mixin simpleStructCtor;
			}
			
			TexBinding[]	texBindings;
		}
	}
	

	static class CgException : Exception {
		this (char[] str) {
			super(str);
		}
	}

	
	class CgKernel : HwShaderKernel {
		private static void cgErrorCallback() {
			CGerror error = cgGetError();
			char* errStr;
			
			if (error == CGerror.CG_COMPILER_ERROR) {
				errStr = cgGetLastListing(cgContext);
			} else {
				errStr = cgGetErrorString(error);
			}
			
			throw new CgException("cgError: " ~ .toString(errStr));
		}


		override bool canLoadVertexShader(char[] filename, char[] profile) {
			return
				cgContext !is null &&
				cgVertexProfile !is CGprofile.CG_PROFILE_UNKNOWN &&
				filename.length > 3 && tolower(filename[$-3..$]) == `.cg` &&
				std.file.exists(filename);
		}


		override bool canLoadFragmentShader(char[] filename, char[] profile) {
			return
				cgContext !is null &&
				cgFragmentProfile !is CGprofile.CG_PROFILE_UNKNOWN &&
				filename.length > 3 && tolower(filename[$-3..$]) == `.cg` &&
				std.file.exists(filename);
		}


		private bool loadCgProgramFile(char[] programString, CGprofile profile, out CGprogram cgProg) {
			CGprogram prog;
			bool res = loadCgProgram(cast(char[])std.file.read(programString), profile, prog);
			if (res) cgProg = prog;
			return res;
		}


		private bool loadCgProgram(char[] programString, CGprofile profile, out CGprogram cgProg) {
			if (!cgGLIsProfileSupported(profile)) {
				return false;
			}
			
			char[] progStr = programString;//programString.replace("/+", "/*").replace("+/", "*/") ~ '\0';
			
			CGprogram prog;
			
			try {
				cgGLSetOptimalOptions(profile);
				prog = cgCreateProgram(cgContext, CGenum.CG_SOURCE, .toStringz(progStr), profile, "main", null);
				cgGLEnableProfile(profile);
				cgGLLoadProgram(prog);
				cgGLDisableProfile(profile);
			}
			catch (CgException err) {
				writefln(err);
				return false;
			}
			
			cgProg = prog;
			
			return true;
		}


		override HwVertexShader loadVertexShader(char[] filename, char[] profileName) {
			writefln(`CgKernel.loadVertexShader (%s)`, filename);
			CGprofile profile = this.cgVertexProfile;
			if (profileName !is null) profile = cgGetProfile(.toStringz(profileName));
			
			assert (profile != CGprofile.CG_PROFILE_UNKNOWN);
			
			CGprogram prog;
			if (canLoadVertexShader(filename, profileName) && loadCgProgramFile(filename, profile, prog)) {
				return new CgVertexShader(prog, profile);
			} else {
				return null;
			}
		}


		override HwFragmentShader loadFragmentShader(char[] filename, char[] profileName) {
			writefln(`CgKernel.loadFragmentShader (%s)`, filename);
			CGprofile profile = this.cgFragmentProfile;
			if (profileName !is null) profile = cgGetProfile(.toStringz(profileName));

			assert (profile != CGprofile.CG_PROFILE_UNKNOWN);

			CGprogram prog;
			if (canLoadFragmentShader(filename, profileName) && loadCgProgramFile(filename, profile, prog)) {
				return new CgFragmentShader(prog, profile);
			} else {
				return null;
			}
		}
		
		
		protected void enableVertexArrayCallback(VertexDataType vdt, uint components, uint glType, uint stride, void* ptr) {
			CGparameter cgParam = vdtCgParams[vdt - vdtArrayOffset];
			
			if (cgParam) {
				//writefln(`enabling cg array: `, cast(int)vdt);
				cgGLEnableClientState(cgParam);
				cgGLSetParameterPointer(cgParam, components, glType, stride, ptr);
			}
		}
		
		
		protected void disableVertexArrayCallback(VertexDataType vdt) {
			CGparameter cgParam = vdtCgParams[vdt - vdtArrayOffset];
			
			if (cgParam) {
				//writefln(`disabling cg array: `, cast(int)vdt);
				cgGLDisableClientState(cgParam);
			}
		}
		
		
		this() {
			renderer.cg.cgGL.initCg();
			renderer.cg.cgGL.initCgGl();
			writefln(`Loaded the CgGL shared library`);

			if (cgCreateContext !is null) {
				cgContext = cgCreateContext();
				
				assert (cgGLGetLatestProfile !is null);
				
				cgVertexProfile = cgGLGetLatestProfile(CGGLenum.CG_GL_VERTEX);
				if (cgVertexProfile !is CGprofile.CG_PROFILE_UNKNOWN) {
					assert (cgGLSetOptimalOptions !is null);
					cgGLSetOptimalOptions(cgVertexProfile);
					//checkForCgError("selecting vertex profile");
					assert (cgGetProfileString !is null);
					writefln("Cg fragment profile: ", .toString(cgGetProfileString(cgVertexProfile)));
				} else writefln("No Cg fragment shader support");

				cgFragmentProfile = cgGLGetLatestProfile(CGGLenum.CG_GL_FRAGMENT);
				if (cgFragmentProfile !is CGprofile.CG_PROFILE_UNKNOWN) {
					assert (cgGLSetOptimalOptions !is null);
					cgGLSetOptimalOptions(cgFragmentProfile);
					//checkForCgError("selecting vertex profile");
					assert (cgGetProfileString !is null);
					writefln("Cg vertex profile: ", .toString(cgGetProfileString(cgFragmentProfile)));
				} else writefln("No Cg vertex shader support");
				
				if (cgSetErrorCallback !is null) cgSetErrorCallback(&cgErrorCallback);
				
				addEnableArrayCallback(&enableVertexArrayCallback);
				addDisableArrayCallback(&disableVertexArrayCallback);
			}			
		}
		
		
		static {
			CGcontext	cgContext;
			CGprofile   cgVertexProfile;
			CGprofile   cgFragmentProfile;


			static const uint vdtArraySize		= cast(uint)(VertexDataType.max - VertexDataType.min + 1);
			static const uint vdtArrayOffset	= cast(uint)VertexDataType.min;			
			static CGparameter[vdtArraySize]	vdtCgParams;
		}
	}
	
	
	protected final void initCgKernel() {
		writefln(`Initializing the Cg kernel`);
		addHwShaderKernel(new CgKernel);
	}
}

