module cg;


private {
	import core.dllStuff;
	import core.plugin;

	import core.common;
	import renderer.renderer;
	import geometry.vertex;
	import geometry.vertexData;
	import derelict.opengl.gl;
	import glExtLoader;
	import derelict.cg.cg;
	import derelict.cgGL.cgGL;
	import core.maths;
	import std.string;
	import subsystems.texture;
	
	import renderer.gpuShader;
	import renderer.typedefs;
	
	import renderer.vertexKernel;
	import renderer.gpuShaderKernel;
	import renderer.gpuShaderState;
	import renderer.textureKernel;
	import renderer.textureState;
	import renderer.renderContextBuilder;
	
	import core.engine;
	import core.engineSubsystem;
	import core.systemCaps;
	import subsystems.render;
	import subsystems.gpuShader;
}


pragma (lib, "derelictGL.lib");
pragma (lib, "derelictCg.lib");
pragma (lib, "derelictCgGL.lib");
pragma (lib, "derelictUtil.lib");



class CgException : Exception
{
	this (char[] str) {
		super(str);
	}
}


private class CgCaps : SystemCaps
{
	this ()
	{
		set("loaded", true);
		engine.systemCaps.extend(this, "Cg");
	}
}


private class CgShaderSubsystem : GpuShaderSubsystem
{
	private static void cgErrorCallback()
	{
		CGerror error = cgGetError();
		char* errStr;
		if (error == CGerror.COMPILER_ERROR) {
			errStr = cgGetLastListing(cgContext);
		} else {
			errStr = cgGetErrorString(error);
		}
		
		throw new CgException("cgError: " ~ errStr[0 .. strlen(errStr)]);
	}


	this ()
	{
		//writefln("Initializing Cg");
		
		DerelictCG_Load();
		DerelictCGGL_Load();
		
		cgContext = cgCreateContext();
		cgSetErrorCallback(&cgErrorCallback);
	}
	
	
	private bool loadCgProgram(char[] programString, CGprofile profile, out CGprogram cgProg)
	{
		if (!cgGLIsProfileSupported(profile)) {
			return false;
		}
		
		char[] progStr = programString.replace("/+", "/*").replace("+/", "*/") ~ '\0';
		
		CGprogram prog;
		
		try {
			prog = cgCreateProgram(cgContext, CGenum.SOURCE, progStr, profile, "main", null);
			cgGLLoadProgram(prog);
		}
		catch (CgException err) {
			writefln(err);
			return false;
		}		
		finally {
			delete progStr;
		}
		
		cgProg = prog;
		
		return true;
	}


	IVertexShader loadVertexShader(char[] programString)
	{
		CGprogram prog;
		if (CgShaderKernel.vertexShadersSupported && loadCgProgram(programString, vertexProfile, prog)) {
			return new CgVertexShader(prog);
		} else {
			return null;
		}
	}
	
	
	IFragmentShader loadFragmentShader(char[] programString)
	{
		CGprogram prog;
		if (CgShaderKernel.fragmentShadersSupported && loadCgProgram(programString, fragmentProfile, prog)) {
			return new CgFragmentShader(prog);
		} else {
			return null;
		}
	}


	package {
		static CGcontext	cgContext;
		static CGprofile	vertexProfile;
		static CGprofile	fragmentProfile;
	}
}


private class CgVertexKernel : VertexKernel
{
	private VertexKernel old;
	
	this (VertexKernel old) {
		this.old = old;
	}
	
	
	void enableArray(VertexDataType vdt, VertexStoreType vst, uint stride, void* ptr)
	{
		if (!CgShaderKernel.vertexShaderBound) {
			return old.enableArray(vdt, vst, stride, ptr);
		}
		
		uint		components;
		uint		type;
		
		switch (vst) {
			case VertexStoreType.FLOAT:
			case VertexStoreType.FLOAT2:
			case VertexStoreType.FLOAT3:
			case VertexStoreType.FLOAT4:
			{
				type = GL_FLOAT;
				components = vst - VertexStoreType.FLOAT + 1;
			} break;
			
			case VertexStoreType.INT:
			{
				type = GL_INT;
				components = 1;
			} break;
			
			case VertexStoreType.INT1:
			case VertexStoreType.INT2:
			case VertexStoreType.INT3:
			case VertexStoreType.INT4:
			{
				type = GL_INT;
				components = vst - VertexStoreType.INT1 + 1;
			} break;

			
			case VertexStoreType.BYTE1:
			case VertexStoreType.BYTE2:
			case VertexStoreType.BYTE3:
			case VertexStoreType.BYTE4:
			{
				type = GL_BYTE;
				components = vst - VertexStoreType.BYTE1 + 1;
			} break;

			default:
				throw new Exception("*Renderer: invalid vertex attribute format");
		}
		
		enableArray(vdt);
		cgGLSetParameterPointer(cgParams[vdt - arrayOffset], components, type, stride, ptr);
	}


	private void enableArray(VertexDataType vdt)
	{
		CGparameter cgParam = cgParams[vdt - arrayOffset];
		if (cgParam) {
			debug writefln("disable vdt: %s cg param: %s", cast(uint)vdt, cast(uint)cgParam);
			cgGLEnableClientState(cgParam);
		}
	}
	
	
	void disableArray(VertexDataType vdt)
	{
		if (!CgShaderKernel.vertexShaderBound) {
			return old.disableArray(vdt);
		}
		
		CGparameter cgParam = cgParams[vdt - arrayOffset];
		if (cgParam) {
			debug writefln("disable vdt: %s cg param: %s", cast(uint)vdt, cast(uint)cgParam);
			cgGLDisableClientState(cgParam);
		}
	}


	package {
		static const uint arraySize		= cast(uint)(VertexDataType.max - VertexDataType.min + 1);
		static const uint arrayOffset	= cast(uint)VertexDataType.min;
		
		static CGparameter[arraySize]	cgParams;
	}
}


class CgShaderKernel : GPUShaderKernel
{
	private GPUShaderKernel old;
	
	this(GPUShaderKernel old)
	{
		this.old = old;
		
		if (!cgInitialized) {
			writefln("Initializing CgGL");
			
			DerelictGL_Load();
			loadAllOpenGLExtensions();

			try {
				CgShaderSubsystem.vertexProfile		= cgGLGetLatestProfile(CGGLenum.VERTEX);
				CgShaderSubsystem.fragmentProfile	= cgGLGetLatestProfile(CGGLenum.FRAGMENT);
		
				if (cgGLIsProfileSupported(CgShaderSubsystem.vertexProfile)) {
					cgGLSetOptimalOptions(CgShaderSubsystem.vertexProfile);
					vertexShadersSupported = true;
				}
					
				if (cgGLIsProfileSupported(CgShaderSubsystem.fragmentProfile)) {
					cgGLSetOptimalOptions(CgShaderSubsystem.fragmentProfile);
					fragmentShadersSupported = true;
				}
				
				writefln("CgGL initialization finished");
			}
			catch (CgException err) {
				writefln("Cg is not supported\nError: ", err);
			}

			cgInitialized = true;
		}
	}
	
	
	void apply(inout GPUShaderState state)
	{
		old.apply(state);
			
		with (state) {

			if (!applyState) {
				return;
			}
		
			vertexShaderBound		= (vertexShader !is null);
			fragmentShaderBound	= (fragmentShader !is null);
		}
	}
	
	
	void unapply(inout GPUShaderState state)
	{
		old.unapply(state);

		with (state) {
			if (!applyState) {
				return;
			}
			
			vertexShaderBound		= false;
			fragmentShaderBound	= false;
		}
	}
	
	private {
		static bool cgInitialized = false;
	}
	
	package {
		static bool vertexShadersSupported		= false;
		static bool fragmentShadersSupported	= false;

		static bool vertexShaderBound		= false;
		static bool fragmentShaderBound	= false;
	}
}


class CgTextureKernel : TextureKernel
{
	private TextureKernel old;
	
	this(TextureKernel old) {
		this.old = old;
	}
	
	
	void apply(inout TextureState state)
	{
		with (state) {
			if (!applyState) {
				return;
			}
			
			foreach (uint i, Texture t; texUnitTextures) {
				if (t !is null) {
					state.texUnits[i].bindTexture(t);
				}
			}
		}
		
		old.apply(state);
	}


	void unapply(inout TextureState state)
	{
		foreach (inout Texture t; texUnitTextures) {
			t = null;
		}
		
		old.unapply(state);
	}


	package {
		static Texture[TextureState.maxTexUnits]	texUnitTextures;
	}
}


private template MCgProgram()
{
	ShaderParam getParam(char[] name) {
		return cgGetNamedParameter(cgProgram, name ~ '\0');
	}
	
	
	void	setParam(ShaderParam param, float val) {
		cgGLSetParameter1f(cast(CGparameter)param, val);
	}
	
	
	void	setParam(ShaderParam param, vec2 val) {
		cgGLSetParameter2fv(cast(CGparameter)param, &val.x);
	}
	
	
	void	setParam(ShaderParam param, vec3 val) {
		cgGLSetParameter3fv(cast(CGparameter)param, &val.x);
	}
	
	
	void	setParam(ShaderParam param, vec4 val) {
		cgGLSetParameter4fv(cast(CGparameter)param, &val.x);
	}
	
	
	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);
	}
	
	
	package {
		CGprogram cgProgram;
	}
}


class CgVertexShader : IVertexShader
{
	mixin MCgProgram;
	
	
	private void extractVaryingParams()
	{
		CGparameter leaf = cgGetFirstLeafParameter(cgProgram, CGenum.PROGRAM);
		
		while (leaf)
		{
			CGenum var	= cgGetParameterVariability(leaf);
			CGenum dir	= cgGetParameterDirection(leaf);
			
			if (var == CGenum.VARYING && dir != CGenum.OUT && dir != CGenum.ERROR) {
				CGresource res = cgGetParameterResource(leaf);
				
				if (res >= VertexDataType.min && res <= VertexDataType.max) {
					VertexDataType vdt = cast(VertexDataType)res;
					varyingParams ~= VaryingParam(leaf, vdt);
					debug writefln("extracted a param: ", cast(uint)vdt);
				}
			}
			
			leaf = cgGetNextLeafParameter(leaf);
		}
	}


	void bind() {
		foreach (inout VaryingParam vpar; varyingParams) {
			CgVertexKernel.cgParams[vpar.succ - CgVertexKernel.arrayOffset] = vpar.pred;
		}
		
		cgGLEnableProfile(CgShaderSubsystem.vertexProfile);
		cgGLBindProgram(cgProgram);
	}
	
	
	void unbind() {
		foreach (inout VaryingParam vpar; varyingParams) {
			CgVertexKernel.cgParams[vpar.succ - CgVertexKernel.arrayOffset] = null;
		}
		
		cgGLUnbindProgram(CgShaderSubsystem.vertexProfile);
		cgGLDisableProfile(CgShaderSubsystem.vertexProfile);
	}


	this (CGprogram prog)
	{
		this.cgProgram = prog;
		extractVaryingParams();
	}


	bool isVertexDataNeeded(VertexDataType vdt)
	{
		foreach (inout VaryingParam vp; varyingParams) {
			if (vp.succ == vdt) {
				return true;
			}
		}
		
		return false;
	}
	

	void setTexture(ShaderParam param, Texture tex) {
		assert	(false);		// not implemented
	}


	package {
		alias tuple!(CGparameter, VertexDataType)	VaryingParam;
		
		VaryingParam[]	varyingParams;
	}
}


class CgFragmentShader : IFragmentShader
{
	mixin MCgProgram;
	
	
	private void extractTexBindings()
	{
		CGparameter leaf = cgGetFirstLeafParameter(cgProgram, CGenum.PROGRAM);
		
		while (leaf)
		{
			CGenum var	= cgGetParameterVariability(leaf);
			CGenum dir	= cgGetParameterDirection(leaf);
			
			if (var != CGenum.VARYING && dir != CGenum.OUT && dir != CGenum.ERROR) {
				CGresource res = cgGetParameterResource(leaf);
				
				if (res >= CGresource.TEXUNIT0 && res <= CGresource.TEXUNIT15) {
					texBindings ~= TexBinding(leaf, res - CGresource.TEXUNIT0);
				}
			}
			
			leaf = cgGetNextLeafParameter(leaf);
		}
	}


	this (CGprogram prog)
	{
		this.cgProgram = prog;
		extractTexBindings();
	}


	void setTexture(ShaderParam param, Texture tex) {
		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() {
		cgGLEnableProfile(CgShaderSubsystem.fragmentProfile);
		cgGLBindProgram(cgProgram);
	}
	
	
	void unbind() {
		cgGLUnbindProgram(CgShaderSubsystem.fragmentProfile);
		cgGLDisableProfile(CgShaderSubsystem.fragmentProfile);
	}



	package {
		alias tuple!(CGparameter, uint)	TexBinding;
		
		TexBinding[]	texBindings;
	}
}


class CgVertexKernelFactory : VertexKernelFactory
{
	VertexKernel create() {
		return new CgVertexKernel(old.create);
	}
	
	this (VertexKernelFactory old) {
		this.old = old;
	}
	
	private VertexKernelFactory old;
}


class CgShaderKernelFactory : GPUShaderKernelFactory
{
	GPUShaderKernel create() {
		return new CgShaderKernel(old.create);
	}
	
	this (GPUShaderKernelFactory old) {
		this.old = old;
	}
	
	private GPUShaderKernelFactory old;
}


class CgTextureKernelFactory : TextureKernelFactory
{
	TextureKernel create() {
		return new CgTextureKernel(old.create);
	}
	
	this (TextureKernelFactory old) {
		this.old = old;
	}
	
	private TextureKernelFactory old;
}


void initializeCg()
{
	engine.registerSubsystem(new CgShaderSubsystem);
	
	with (subsystem!(RenderSubsystem).renderContextBuilder()) {
		vertexKF			= new CgVertexKernelFactory(vertexKF);
		gpuShaderKF		= new CgShaderKernelFactory(gpuShaderKF);
		textureKF			= new CgTextureKernelFactory(textureKF);
	}
	
	new CgCaps;		// tell the engine that Cg is supported from now on
}



class CgPlugin : IPlugin
{
	static class CgComponent : IPluginComponent
	{
		char[] name() {
			return "opengl.cg";
		}

		char[][] dependencies() {
			static char[][] deps = ["opengl.window", "opengl.renderer"];
			return deps;
		}

		bool initialize(IPluginInterface pi) {
			initializeCg();
			return true;
		}
	}


	IPluginComponent[] components()
	{
		static IPluginComponent[1] comp;
		if (comp[0] is null) {
			comp[0] = new CgComponent;
		}
		return comp;
	}
}


export extern(C) IPlugin createPlugin()
{
	//writefln("cg.createPlugin called");
	return new CgPlugin;
}


