module coneneko.scenegraph;
import
	coneneko.container,
	coneneko.rendertarget,
	coneneko.unit,
	coneneko.math,
	coneneko.shader,
	coneneko.rgba,
	coneneko.camera,
	coneneko.billboard,
	opengl;

///
class SceneGraph : Nodes
{
	Object root; ///
	
	Object linkAnother(Object a, SceneGraph b) /// merge(b); link(a, b.root);
	{
		merge(b);
		return super.link(a, b.root);
	}
	
	override void clear()
	{
		super.clear();
		root = null;
	}
	
	SceneGraph clone() ///
	{
		auto result = new SceneGraph();
		result.merge(this);
		result.root = root;
		return result;
	}
}

/*
VertexBuffer, Texture, FrameBufferObject等のコストの高いものを含む場合は
何度も生成しないように注意して実装する
*/

/// texcoordが上下逆のBillBoard
class FboBoard : BillBoard
{
	///
	this(float x = -1, float y = -1, float width = 2, float height = 2, Vector color = Color.WHITE)
	{
		super(x, y, width, height, color);
	}
	
	override void attach()
	{
		glBegin(GL_QUADS);
		glColor4f(color.x, color.y, color.z, color.w);
		glNormal3f(0.0, 0.0, 0.0);
		
		// 左下、左上、右上、右下
		glTexCoord2f(texCoordPosition.x, texCoordPosition.y);
		glVertex2f(position.x, position.y);
		
		glTexCoord2f(texCoordPosition.x, texCoordPosition.y + texCoordSize.y);
		glVertex2f(position.x, position.y + size.y);
		
		glTexCoord2f(texCoordPosition.x + texCoordSize.x, texCoordPosition.y + texCoordSize.y);
		glVertex2f(position.x + size.x, position.y + size.y);
		
		glTexCoord2f(texCoordPosition.x + texCoordSize.x, texCoordPosition.y);
		glVertex2f(position.x + size.x, position.y);
		glEnd();
	}
}

/**
FboをSceneGraphにする事で、そのまま描画できる。
Clearがない理由。
単体で使うなら特に必要ない。
他の要素が混ざるのなら、Clearはそれに含まれる。
*/
class FboSceneGraph : SceneGraph
{
	///
	this(FrameBufferObject fbo)
	{
		root = link(fbo, new FboBoard());
	}
}

///
class Fsaa(uint xN, FsaaNShader)
{
	private FrameBufferObject fbo;
	private SceneGraph fsaaTree;
	
	///
	this(Vector dstSize)
	{
		fbo = new FrameBufferObject(
			Rgba.toTextureLength(cast(uint)dstSize.x * xN),
			Rgba.toTextureLength(cast(uint)dstSize.y * xN),
			0,
			true
		);
		
		fsaaTree = new SceneGraph();
		fsaaTree.root = fsaaTree.linkSerial(new FsaaNShader(fbo.size), fbo, new FboBoard());
	}
	
	SceneGraph opCall(SceneGraph tree) /// treeにFsaaをかけたSceneGraphを返す
	{
		fbo.draw(tree);
		return fsaaTree;
	}
}

class Fsaa2Shader : Shader
{
	this(Vector textureSize)
	{
		super(
			"void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
				gl_TexCoord[0] = gl_MultiTexCoord0;
			}",
			"uniform sampler2D texture0;
			uniform float tpw, tph;
			void main()
			{
				vec4 tc = texture2D(texture0, gl_TexCoord[0].xy);
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(0.0, tph));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(tpw, 0.0));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(tpw, tph));
				tc /= 4.0;
				gl_FragColor = tc;
				gl_FragColor.a = 1.0;
			}"
		);
		this["texture0"] = 0;
		this["tpw"] = 1.0 / textureSize.x;
		this["tph"] = 1.0 / textureSize.y;
	}
}

typedef	Fsaa!(2, Fsaa2Shader) Fsaa2; ///

class Fsaa3Shader : Shader
{
	this(Vector textureSize)
	{
		super(
			"void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
				gl_TexCoord[0] = gl_MultiTexCoord0;
			}",
			"uniform sampler2D texture0;
			uniform float tpw, tph;
			void main()
			{
				vec4 tc = texture2D(texture0, gl_TexCoord[0].xy);
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(0.0, tph));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(0.0, tph * 2.0));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(tpw, 0.0));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(tpw, tph));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(tpw, tph * 2.0));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(tpw * 2.0, 0.0));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(tpw * 2.0, tph));
				tc += texture2D(texture0, gl_TexCoord[0].xy + vec2(tpw * 2.0, tph * 2.0));
				tc /= 9.0;
				gl_FragColor = tc;
				gl_FragColor.a = 1.0;
			}"
		);
		this["texture0"] = 0;
		this["tpw"] = 1.0 / textureSize.x;
		this["tph"] = 1.0 / textureSize.y;
	}
}

typedef	Fsaa!(3, Fsaa3Shader) Fsaa3; ///

class MosaicShader : Shader
{
	this()
	{
		super(
			"void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
				gl_TexCoord[0] = gl_MultiTexCoord0;
			}",
			"uniform sampler2D texture0;
			void main()
			{
				vec2 t = floor(gl_TexCoord[0].xy * 50.0) * 0.02;
				gl_FragColor = texture2D(texture0, t);
			}"
		);
		this["texture0"] = 0;
	}
}

// opCall(fbo)でもfboは必要になる、has fboでopCall(tree)にしておく
/// 平面へのfilterを想定している、3Dへ用いるならthis(depth=true)で
class Mosaic
{
	private FboBoard fb;
	private FrameBufferObject fbo;
	private SceneGraph mosaicTree;
	
	///
	this(uint fboWidth, uint fboHeight, bool depth = false)
	{
		fb = new FboBoard();
		fb.size *= 0.1;
		fb.texCoordSize *= 0.1;
		
		fbo = new FrameBufferObject(fboWidth, fboHeight, 0, depth);
		
		mosaicTree = new SceneGraph();
		mosaicTree.root = mosaicTree.linkParallel(
			fbo,
				new FboBoard(),
				mosaicTree.link(new MosaicShader(), fb)
		);
	}
	
	SceneGraph opCall(SceneGraph tree, Vector position) ///
	{
		fb.position = position;
		fb.texCoordPosition = Vector(position.x * 0.5 + 0.5, position.y * 0.5 + 0.5);
		fbo.draw(tree);
		return mosaicTree;
	}
}

/// 深度を赤に、g=near, b=far
class DepthShader : Shader
{
	this(float near, float far)
	in
	{
		assert(0.0 <= near);
		assert(near < far);
	}
	body
	{
		super(
			"uniform float near, far;
			void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
				
				gl_FrontColor.r = 1.0;
				gl_FrontColor.a = 1.0;
				vec4 n = gl_ProjectionMatrix * vec4(0.0, 0.0, -near, 1.0);
				n /= n.w;
				gl_FrontColor.g = n.z * 0.5 + 0.5;
				vec4 f = gl_ProjectionMatrix * vec4(0.0, 0.0, -far, 1.0);
				f /= f.w;
				gl_FrontColor.b = f.z * 0.5 + 0.5;
			}",
			"void main()
			{
				gl_FragColor = vec4(gl_FragCoord.z, gl_Color.g, gl_Color.b, 1.0);
			}"
		);
		this["near"] = near;
		this["far"] = far;
	}
}

///
class DepthOfField
{
	private FrameBufferObject fbo, depthFbo;
	private SceneGraph dofTree;
	private DepthOfFieldShader shader;
	
	///
	this(uint fboWidth, uint fboHeight)
	{
		fbo = new FrameBufferObject(fboWidth, fboHeight, 0, true);
		depthFbo = new FrameBufferObject(fboWidth, fboHeight, 1, true);
		shader = new DepthOfFieldShader(Vector(fboWidth, fboHeight));
		
		dofTree = new SceneGraph();
		dofTree.root = dofTree.linkSerial(shader, fbo, depthFbo, new FboBoard());
	}
	
	/// depthTreeはDepthShaderを使った深度バッファ
	SceneGraph opCall(SceneGraph tree, SceneGraph depthTree)
	{
		fbo.draw(tree);
		depthFbo.draw(depthTree);
		return dofTree;
	}
}

class DepthOfFieldShader : Shader
{
	this(Vector textureSize)
	{
		super(
			"void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
				gl_TexCoord[0] = gl_MultiTexCoord0;
			}",
			"uniform sampler2D texture0, texture1;
			uniform float tpw, tph;
			void main()
			{
				vec3 c = texture2D(texture0, gl_TexCoord[0].xy).rgb;
				vec3 dnf = texture2D(texture1, gl_TexCoord[0].xy).rgb; // depth near far
				if (dnf.r < dnf.g || dnf.b < dnf.r)
				{
					c += texture2D(texture0, gl_TexCoord[0].xy + vec2( tpw, -tph)).rgb;
					c += texture2D(texture0, gl_TexCoord[0].xy + vec2( 0.0, -tph)).rgb;
					c += texture2D(texture0, gl_TexCoord[0].xy + vec2(-tpw, -tph)).rgb;
					c += texture2D(texture0, gl_TexCoord[0].xy + vec2( tpw,  0.0)).rgb;
					c += texture2D(texture0, gl_TexCoord[0].xy + vec2(-tpw,  0.0)).rgb;
					c += texture2D(texture0, gl_TexCoord[0].xy + vec2( tpw,  tph)).rgb;
					c += texture2D(texture0, gl_TexCoord[0].xy + vec2( 0.0,  tph)).rgb;
					c += texture2D(texture0, gl_TexCoord[0].xy + vec2(-tpw,  tph)).rgb;
					c /= 9.0;
				}
				gl_FragColor.rgb = c.rgb;
				gl_FragColor.a = 1.0;
			}"
		);
		this["texture0"] = 0;
		this["texture1"] = 1;
		this["tpw"] = 1.0 / textureSize.x;
		this["tph"] = 1.0 / textureSize.y;
	}
}

/// r=depth
class ShadowBufferShader : Shader
{
	///
	this()
	{
		super(
			"void main()
			{
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
			}",
			"void main()
			{
				gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 1.0);
			}"
		);
	}
}

/// texture0のshadowBufferを使って影を作る
class ShadowShader : Shader
{
	///
	this(ViewProjection camera, ViewProjection lightCamera)
	{
		super(
			"uniform mat4 viewProjection, lightViewProjection;
			void main()
			{
				gl_Position = viewProjection * gl_ModelViewMatrix * gl_Vertex;
				
				vec4 positionFromLight = lightViewProjection * gl_ModelViewMatrix * gl_Vertex;
				gl_TexCoord[0].xyz = 0.5 * positionFromLight.xyz + 0.5 * positionFromLight.w; // uv, 光と頂点の距離
				gl_TexCoord[0].w = positionFromLight.w;
			}",
			"uniform sampler2D shadowBuffer;
			void main()
			{
				vec3 tc = gl_TexCoord[0].xyz / gl_TexCoord[0].w;
				float shadowBuffer = texture2D(shadowBuffer, tc.xy).r;
				shadowBuffer += 0.01; // ずらす
				float c = tc.z > shadowBuffer ? 0.5 : 1.0;
				gl_FragColor = vec4(c, c, c, 1.0);
			}"
		);
		this["viewProjection"] = camera.toMatrix();
		this["lightViewProjection"] = lightCamera.toMatrix();
		this["shadowBuffer"] = 0;
	}
}
