module coneneko.shader;
import
	coneneko.math,
	coneneko.unit,
	coneneko.glext,
	std.string,
	coneneko.texture,
	opengl,
	std.cstream;

/// glsl
class Shader : Unit
{
	invariant()
	{
		assert(emptyGlError());
		assert(0 <= attachCounter);
	}
	
	private uint handle;
	private int attachCounter;
	
	///
	this(string vertexSource, string fragmentSource)
	{
		handle = createShader(vertexSource, fragmentSource);
	}
	
	private int createShader(string vertexSource, string fragmentSource)
	{
		GLhandleARB result;
		GLhandleARB vsObj, fsObj;
		try
		{
			result = glCreateProgramObjectARB();
			if (!result) throw new Error("glCreateProgramObjectARB");
			
			vsObj = compileShader(GL_VERTEX_SHADER_ARB, vertexSource);
			fsObj = compileShader(GL_FRAGMENT_SHADER_ARB, fragmentSource);
			
			glAttachObjectARB(result, vsObj);
			glAttachObjectARB(result, fsObj);
			
			glLinkProgramARB(result);
			GLint resultL;
			glGetObjectParameterivARB(result, GL_OBJECT_LINK_STATUS_ARB, &resultL);
			if (!resultL) throw new Error("shader link error");
		}
		catch (Exception e) { throw e; }
		finally
		{
			if (fsObj) glDeleteObjectARB(fsObj);
			if (vsObj) glDeleteObjectARB(vsObj);
		}
		return result;
	}
	
	private int compileShader(uint shaderType, string source)
	{
		GLhandleARB result = glCreateShaderObjectARB(shaderType);
		if (!result) throw new Error("glCreateShaderObjectARB");
		
		char* sourcez = cast(char*)source.toStringz(); // d2
		int sourceLength = source.length;
		glShaderSourceARB(result, 1, &sourcez, &sourceLength);
		
		int resultC;
		glCompileShaderARB(result);
		glGetObjectParameterivARB(result, GL_OBJECT_COMPILE_STATUS_ARB, &resultC);
		if (!resultC)
		{
			int infoLogLength;
			glGetObjectParameterivARB(result, GL_OBJECT_INFO_LOG_LENGTH_ARB, &infoLogLength);
			assert(0 < infoLogLength);
			
			auto infoLog = new char[infoLogLength];
			GLsizei r;
			glGetInfoLogARB(result, infoLog.length, &r, infoLog.ptr);
			assert(infoLog.length - 1 == r);
			
			throw new Error("shader compile error: " ~ cast(string)infoLog); // d2
		}
		return result;
	}
	
	~this()
	{
		glDeleteObjectARB(handle);
	}
	
	// TODO 派生クラスでattachをoverrideすると、opIndexAssign内部が変更されるので
	// なんらかの修正が必要
	void attach()
	{
		if (attachCounter == 0) glUseProgramObjectARB(handle);
		attachCounter++;
	}
	
	void detach()
	{
		attachCounter--;
		if (attachCounter == 0) glUseProgramObjectARB(0);
	}
	
	protected int getUniformLocation(string key) /// 使わない値には代入できないようなので注意
	{
		int result = glGetUniformLocationARB(handle, toStringz(key));
		if (-1 == result) throw new Error("UniformLocation: " ~ key);
		return result;
	}
	
	void opIndexAssign(float a, string key) ///
	{
		attach();
		glUniform1fARB(getUniformLocation(key), a);
		detach();
	}
	
	void opIndexAssign(int a, string key) ///
	{
		attach();
		glUniform1iARB(getUniformLocation(key), a);
		detach();
	}
	
	void opIndexAssign(Vector a, string key) ///
	{
		attach();
		glUniform4fARB(getUniformLocation(key), a.x, a.y, a.z, a.w);
		detach();
	}
	
	void opIndexAssign(Matrix[] a, string key) ///
	{
		attach();
		glUniformMatrix4fvARB(getUniformLocation(key), a.length, GL_FALSE, cast(float*)a.ptr);
		detach();
	}
	
	void opIndexAssign(Matrix a, string key) ///
	{
		Matrix[] m;
		m ~= a;
		this[key] = m;
	}
	
	uint getAttributeLocation(string key) ///
	{
		attach();
		int result = glGetAttribLocationARB(handle, toStringz(key));
		detach();
		if (-1 == result) throw new Error("getAttributeLocation: " ~ key);
		return result;
	}
}

interface ModelShader
{
	Shader getBasic(); ///
	
	/**
		attribute vec3 nextPosition;
		uniform float interpolater; // [0.0, 1.0], lerp用
	*/
	Shader getMorph();
	
	/**
		attribute vec3 matrixIndexWeight;
		uniform mat4 boneMatrixArray[?]; // ?は任意、当分は? < 32ぐらいで
	*/
	Shader getSkinMesh();
}

class DefaultModelShader : ModelShader
{
	invariant()
	{
		assert(basic !is null);
		assert(morph !is null);
		assert(skinMesh !is null);
	}
	
	DefaultShader basic;
	DefaultMorphShader morph;
	DefaultSkinMeshShader skinMesh;
	
	this()
	{
		basic = new DefaultShader();
		morph = new DefaultMorphShader();
		skinMesh = new DefaultSkinMeshShader();
	}
	
	Shader getBasic() { return basic; }
	Shader getMorph() { return morph; }
	Shader getSkinMesh() { return skinMesh; }
}

class DefaultShader : Shader
{
	protected this(string vertexSource)
	{
		super(
			vertexSource,
			"uniform sampler2D texture0;
			void main()
			{
				gl_FragColor = gl_Color * texture2D(texture0, gl_TexCoord[0].xy);
			}"
		);
		this["texture0"] = 0;
	}
	
	this()
	{
		this(
			"void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
				gl_FrontColor = gl_Color;
				gl_TexCoord[0] = gl_MultiTexCoord0;
			}"
		);
	}
}

class DefaultMorphShader : DefaultShader
{
	this()
	{
		super(
			"attribute vec3 nextPosition;
			uniform float interpolater;
			void main()
			{
				vec4 p0 = gl_ModelViewProjectionMatrix * gl_Vertex;
				vec4 p1 = gl_ModelViewProjectionMatrix * vec4(nextPosition, 1.0);
				gl_Position = p0 * (1.0 - interpolater) + p1 * interpolater;
				gl_FrontColor = gl_Color;
				gl_TexCoord[0] = gl_MultiTexCoord0;
			}"
		);
	}
}

class DefaultSkinMeshShader : DefaultShader
{
	this()
	{
		super(
			"attribute vec3 matrixIndexWeight;
			uniform mat4 boneMatrixArray[34];
			vec4 blend(vec4 p)
			{
				return boneMatrixArray[int(matrixIndexWeight.x)] * p * matrixIndexWeight.z
					+ boneMatrixArray[int(matrixIndexWeight.y)] * p * (1.0 - matrixIndexWeight.z);
			}
			void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * blend(gl_Vertex);
				gl_FrontColor = gl_Color;
				gl_TexCoord[0] = gl_MultiTexCoord0;
			}"
		);
	}
}

class EdgeModelShader : ModelShader
{
	invariant()
	{
		assert(basic !is null);
		assert(morph !is null);
		assert(skinMesh !is null);
	}
	
	EdgeShader basic;
	EdgeMorphShader morph;
	EdgeSkinMeshShader skinMesh;
	
	this()
	{
		basic = new EdgeShader();
		morph = new EdgeMorphShader();
		skinMesh = new EdgeSkinMeshShader();
		size = 0.2;
	}
	
	Shader getBasic() { return basic; }
	Shader getMorph() { return morph; }
	Shader getSkinMesh() { return skinMesh; }
	
	void size(float a)
	{
		basic["size"] = a;
		morph["size"] = a;
		skinMesh["size"] = a;
	}
}

class EdgeShader : Shader
{
	protected this(string vertexSource)
	{
		super(
			vertexSource,
			"uniform float size;
			void main()
			{
				float d = dot(gl_Color.xyz * 2.0 - 1.0, vec3(0.0, 0.0, -1.0));
				if (size < d) discard;
				gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
			}"
		);
	}
	
	this()
	{
		this(
			"void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
				gl_FrontColor.xyz = gl_NormalMatrix * gl_Normal * 0.5 + 0.5;
			}"
		);
	}
}

class EdgeMorphShader : EdgeShader
{
	this()
	{
		super(
			"attribute vec3 nextPosition;
			uniform float interpolater;
			void main()
			{
				vec4 p0 = gl_ModelViewProjectionMatrix * gl_Vertex;
				vec4 p1 = gl_ModelViewProjectionMatrix * vec4(nextPosition, 1.0);
				gl_Position = p0 * (1.0 - interpolater) + p1 * interpolater;
				gl_FrontColor.xyz = gl_NormalMatrix * gl_Normal * 0.5 + 0.5;
			}"
		);
	}
}

class EdgeSkinMeshShader : EdgeShader
{
	this()
	{
		super(
			"attribute vec3 matrixIndexWeight;
			uniform mat4 boneMatrixArray[34];
			vec4 blend(vec4 p)
			{
				return boneMatrixArray[int(matrixIndexWeight.x)] * p * matrixIndexWeight.z
					+ boneMatrixArray[int(matrixIndexWeight.y)] * p * (1.0 - matrixIndexWeight.z);
			}
			vec3 nblend(vec3 n)
			{
				vec4 n2 = vec4(n, 0);
				vec4 result = boneMatrixArray[int(matrixIndexWeight.x)] * n2 * matrixIndexWeight.z
					+ boneMatrixArray[int(matrixIndexWeight.y)] * n2 * (1.0 - matrixIndexWeight.z);
				return vec3(result);
			}
			void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * blend(gl_Vertex);
				gl_FrontColor.xyz = gl_NormalMatrix * nblend(gl_Normal) * 0.5 + 0.5;
			}"
		);
	}
}

class ToonModelShader : ModelShader
{
	invariant()
	{
		assert(basic !is null);
		assert(morph !is null);
		assert(skinMesh !is null);
	}
	
	ToonShader basic;
	ToonMorphShader morph;
	ToonSkinMeshShader skinMesh;
	
	this()
	{
		basic = new ToonShader();
		morph = new ToonMorphShader();
		skinMesh = new ToonSkinMeshShader();
		lightDirection = Vector(-1.0, -1.0, -1.0);
	}
	
	Shader getBasic() { return basic; }
	Shader getMorph() { return morph; }
	Shader getSkinMesh() { return skinMesh; }
	
	void lightDirection(Vector a) // mv後
	in
	{
		assert(scalar(a) != 0.0);
	}
	body
	{
		auto na = normalize(a);
		basic["lightDirection"] = na;
		morph["lightDirection"] = na;
		skinMesh["lightDirection"] = na;
	}
}

class ToonShader : Shader
{
	const VERTEX = "
	uniform vec4 lightDirection;
	void main()
	{
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
		gl_FrontColor = gl_Color;
		gl_TexCoord[0] = gl_MultiTexCoord0;
		gl_TexCoord[1].x = clamp(
			dot(gl_NormalMatrix * gl_Normal, lightDirection.xyz),
			0.0,
			1.0
		);
	}";
	
	this(string vertexSource = VERTEX)
	{
		super(
			vertexSource,
			"uniform sampler2D texture0;
			void main()
			{
				gl_FragColor = gl_Color * texture2D(texture0, gl_TexCoord[0].xy);
				if (gl_TexCoord[1].x < 0.05) gl_FragColor.rgb *= 0.95;
			}"
		);
		this["texture0"] = 0;
	}
}

class ToonMorphShader : ToonShader
{
	this() { super(VERTEX); }
	
	const VERTEX = "
	uniform vec4 lightDirection;
	attribute vec3 nextPosition;
	uniform float interpolater;
	void main()
	{
		vec4 p0 = gl_ModelViewProjectionMatrix * gl_Vertex;
		vec4 p1 = gl_ModelViewProjectionMatrix * vec4(nextPosition, 1.0);
		gl_Position = p0 * (1.0 - interpolater) + p1 * interpolater;
		gl_FrontColor = gl_Color;
		gl_TexCoord[0] = gl_MultiTexCoord0;
		gl_TexCoord[1].x = clamp(
			dot(gl_NormalMatrix * gl_Normal, lightDirection.xyz),
			0.0,
			1.0
		);
	}";
}

class ToonSkinMeshShader : ToonShader
{
	this() { super(VERTEX); }
	
	const VERTEX = "
	uniform vec4 lightDirection;
	attribute vec3 matrixIndexWeight;
	uniform mat4 boneMatrixArray[34];
	vec4 blend(vec4 p)
	{
		return boneMatrixArray[int(matrixIndexWeight.x)] * p * matrixIndexWeight.z
			+ boneMatrixArray[int(matrixIndexWeight.y)] * p * (1.0 - matrixIndexWeight.z);
	}
	vec3 nblend(vec3 n)
	{
		vec4 n2 = vec4(n, 0);
		vec4 result = boneMatrixArray[int(matrixIndexWeight.x)] * n2 * matrixIndexWeight.z
			+ boneMatrixArray[int(matrixIndexWeight.y)] * n2 * (1.0 - matrixIndexWeight.z);
		return vec3(result);
	}
	void main()
	{
		gl_Position = gl_ModelViewProjectionMatrix * blend(gl_Vertex);
		gl_FrontColor = gl_Color;
		gl_TexCoord[0] = gl_MultiTexCoord0;
		gl_TexCoord[1].x = clamp(
			dot(gl_NormalMatrix * nblend(gl_Normal), lightDirection.xyz),
			0.0,
			1.0
		);
	}";
}

class HsvToonModelShader : ModelShader
{
	invariant()
	{
		assert(basic !is null);
		assert(morph !is null);
		assert(skinMesh !is null);
	}
	
	HsvToonShader basic;
	HsvToonMorphShader morph;
	HsvToonSkinMeshShader skinMesh;
	
	this()
	{
		basic = new HsvToonShader();
		morph = new HsvToonMorphShader();
		skinMesh = new HsvToonSkinMeshShader();
		lightDirection = Vector(-1.0, -1.0, -1.0);
	}
	
	Shader getBasic() { return basic; }
	Shader getMorph() { return morph; }
	Shader getSkinMesh() { return skinMesh; }
	
	void lightDirection(Vector a) // mv後
	in
	{
		assert(scalar(a) != 0.0);
	}
	body
	{
		auto na = normalize(a);
		basic["lightDirection"] = na;
		morph["lightDirection"] = na;
		skinMesh["lightDirection"] = na;
	}
}

class HsvToonShader : Shader
{
	this(string vertexSource = ToonShader.VERTEX)
	{
		super(
			vertexSource,
			"uniform sampler2D texture0;
			vec3 toHsv(vec3 rgb)
			{
				vec3 result;
				
				float max = max(rgb.r, max(rgb.g, rgb.b));
				float min = min(rgb.r, min(rgb.g, rgb.b));
				float delta = max - min;
				
				result.z = max;
				result.y = max != 0.0 ? delta / max : 0.0;
				
				result.x =
					rgb.r == max ? (rgb.g - rgb.b) / delta :
					rgb.g == max ? 2.0 + (rgb.b - rgb.r) / delta :
					4.0 + (rgb.r - rgb.g) / delta;
				result.x /= 6.0;
				if (result.x < 0.0) result.x += 1.0;
				
				return result;
			}
			vec3 toRgb(vec3 hsv)
			{
				if (hsv.y == 0.0) return vec3(hsv.z, hsv.z, hsv.z);
				
				if (1.0 <= hsv.x) hsv.x -= 1.0;
				hsv.x *= 6.0;
				float i = floor(hsv.x);
				float f = hsv.x - i;
				float aa = hsv.z * (1.0 - hsv.y);
				float bb = hsv.z * (1.0 - (hsv.y * f));
				float cc = hsv.z * (1.0 - (hsv.y * (1.0 - f)));
				
				if (i < 1.0) return vec3(hsv.z, cc, aa);
				else if (i < 2.0) return vec3(bb, hsv.z, aa);
				else if (i < 3.0) return vec3(aa, hsv.z, cc);
				else if (i < 4.0) return vec3(aa, bb, hsv.z);
				else if (i < 5.0) return vec3(cc, aa, hsv.z);
				else return vec3(hsv.z, aa, bb);
			}
			void main()
			{
				vec4 base = gl_Color * texture2D(texture0, gl_TexCoord[0].xy);
				vec3 hsv = toHsv(base.rgb);
				hsv.x -= floor(1.05 - gl_TexCoord[1].x) * 0.05;
				if (hsv.x < 0.0) hsv.x += 1.0;
				gl_FragColor.rgb = toRgb(hsv);
				gl_FragColor.a = base.a;
			}"
		);
		this["texture0"] = 0;
	}
}

class HsvToonMorphShader : HsvToonShader
{
	this()
	{
		super(ToonMorphShader.VERTEX);
	}
}

class HsvToonSkinMeshShader : HsvToonShader
{
	this()
	{
		super(ToonSkinMeshShader.VERTEX);
	}
}
