import
	std.windows.charset,
	std.file,
	std.path,
	coneneko.all,
	sdl,
	opengl,
	std.math,
	std.string,
	std.stdio,
	std.bind;

version (D_Version2) // d2
{
	import core.memory;
	private void gcCollect() { core.memory.GC.collect(); }
}
else
{
	private void gcCollect() { std.gc.fullCollect(); }
}

void main(string[] args)
{
	try
	{
		foreach (ref string a; args) a = fromMBSz(a.ptr);
		chdir(args[0].getDirName());
		chdir(`..\resource`);
		(new TestWindow())();
	}
	catch (Exception e) writefln(e.toString);
}

interface Test
{
	void opCall(TestWindow wnd);
}

class TestWindow : Window
{
	class FpsText : TextBoard
	{
		this() { super("", -1.0, 0.9, 0.3, 0.1, Color.WHITE, 128, 32); }
		override void attach()
		{
			text = .toString(fps) ~ "fps";
			super.attach();
		}
	}
	
	class Caption : SceneGraph
	{
		this()
		{
			root = linkParallel(
				(new RenderState()).blend(true),
					new FpsText(),
					new TextBoard(`"Esc"で戻る "Enter"で次へ`, -0.4, 0.9)
			);
		}
	}
	
	class Menu : SceneGraph
	{
		this()
		{
			auto rs = (new RenderState()).blend(true);
			root = link(rs, new Clear());
			for (int i = 0; i < tests.length; i++)
			{
				auto tb = new TextBoard(
					"[" ~ cast(char)('a' + i) ~ "]" ~ testNames[i],
					(i % 2) - 1.0,
					0.9 - (2.0 / 14.0) * (i / 2 + 1)
				);
				tb.size.x *= 0.9;
				link(rs, tb);
			}
			linkAnother(rs, new Caption());
			auto pushAnyKey = format("push [a-%s] key", cast(char)('a' + tests.length - 1));
			link(
				rs,
				new TextBoard(pushAnyKey, -0.5, -1.0, 1.0, 0.1, Vector(0.0, 1.0, 0.0, 0.6))
			);
		}
	}
	
	private static Test[] tests;
	private static string[] testNames;
	
	static void add(string name, Test t)
	{
		testNames ~= name;
		tests ~= t;
	}
	
	private Caption caption;
	
	this()
	{
		caption = new Caption();
	}
	
	void opCall()
	{
		auto menu = new Menu();
		while (true)
		{
			update(menu);
			if (closed || key.pressing(SDLK_ESCAPE)) return;
			for (int i = 0; i < tests.length; i++)
			{
				if (!key.pressing('a' + i)) continue;
				try tests[i](this);
				catch (ToMenu e) {}
				finally gcCollect();
			}
		}
	}
	
	private class ToMenu {}
	
	// TODO lazyに置き換え、削除
	void waitInput(SceneGraph a, void delegate() preUpdate = null)
	{
		auto scene = new SceneGraph();
		scene.root = scene.linkAnother(new Object(), a);
		scene.linkAnother(scene.root, caption);
		while (true)
		{
			if (preUpdate !is null) preUpdate();
			update(scene);
			if (key.pressing(SDLK_ESCAPE)) throw new ToMenu();
			if (key.pressing(SDLK_RETURN)) return;
		}
	}
	
	// TODO rename waitInput
	void waitInputLazy(lazy SceneGraph a)
	{
		while (true)
		{
			update(a);
			if (key.pressing(SDLK_ESCAPE)) throw new ToMenu();
			if (key.pressing(SDLK_RETURN)) return;
		}
	}
}

template AddTest()
{
	static this()
	{
		TestWindow.add(this.classinfo.name, new typeof(this));
	}
}

class TriangleRotationTest : Test
{
	mixin AddTest;
	
	void opCall(TestWindow wnd)
	{
		auto renderState = new RenderState();
		with (renderState)
		{
			cullFace = true;
			cullFaceMode = GL_FRONT;
			depthTest = true;
		}
		auto viewPort = new ViewPort(wnd.size);
		viewPort.x = 100;
		viewPort.y = 100;
		auto viewProjection = new ViewProjection(
			Matrix.lookAt(0, 0, 2,  0, 0, 0),
			Matrix.perspectiveFov(PI / 4.0, wnd.size.x / wnd.size.y, 1.0, 100.0)
		);
		auto rotation = new class MultMatrix, TimeIterator
		{
			float f = 0.0;
			void opPostInc() { this = Matrix.rotationZ(f += 0.1); }
		};
		auto triangle = new class Unit
		{
			void attach()
			{
				glBegin(GL_TRIANGLES);
				glColor3f(1, 0, 0); glVertex3f(0, 1, 0);
				glColor3f(0, 1, 0); glVertex3f(1, 0, 0);
				glColor3f(0, 0, 1); glVertex3f(-1, 0, 0);
				glEnd();
			}
			void detach() {}
		};
		auto scene = new SceneGraph();
		scene.root = scene.linkSerial(
			renderState,
			viewPort,
			viewProjection,
			scene.linkParallel(
				rotation,
					new Clear(),
					triangle
			)
		);
		wnd.waitInput(scene, { rotation++; });
	}
}

class BillBoardTest : Test
{
	mixin AddTest;
	
	class BillBoardTest : SceneGraph
	{
		this(TestWindow wnd)
		{
			auto rs = (new RenderState()).blend(true);
			root = link(rs, new Clear(Color.LIGHT_GRAY));
			bool b;
			for (float y = -1; y < 1; y += 0.4)
			{
				for (float x = -1; x < 1; x += 0.3)
				{
					b = !b;
					if (b) continue;
					link(rs, new BillBoard(x, y, 0.3, 0.4, Color.GRAY));
				}
			}
		}
	}
	
	class TextBoardTest : SceneGraph
	{
		this(TestWindow wnd)
		{
			root = linkParallel(
				(new RenderState()).blend(true),
					new Clear(),
					new TextBoard("普通の文字列", -0.8, 0.7),
					new TextBoard("改行付き\n文字列", -0.5, 0.4, 1.2, 0.2, Color.RED, 512, 64),
					new TextBoard(200, "長さ制限付きの文字列", -0.2, 0.1, 1.2, 0.2, Color.GREEN, 512, 64),
					new TextBoard(200, "長さ制限付き改行あり\n文字列", 0.1, -0.5, 1.2, 0.4, Color.BLUE, 512, 128)
			);
		}
	}
	
	class ImageBoardTest : SceneGraph
	{
		this(TestWindow wnd)
		{
			auto rs = (new RenderState()).blend(true);
			root = link(rs, new Clear());
			auto imageA = new ImageBoard("a.png", -0.8, -0.2, 0.3, 0.4);
			auto imageI = new ImageBoard("b.png", -0.5, -0.8, 0.3, 0.4);
			bool b;
			for (float y = -1; y < 1; y += 0.4)
			{
				for (float x = -1; x < 1; x += 0.3)
				{
					b = !b;
					auto t = b ? imageA.clone() : imageI.clone();
					t.position = Vector(x, y);
					link(rs, t);
				}
			}
			imageA.size = Vector(1.0, 1.0);
			imageI.size = Vector(1.0, 1.0);
			imageI.color = Vector(1, 1, 1, 0.5);
			linkParallel(
				rs,
					imageA,
					imageI,
					new ImageBoard("castle.jpg", -0.2, -0.2, 1.0, 1.0, vector(1, 1, 1, 0.8))
			);
		}
	}
	
	void opCall(TestWindow wnd)
	{
		wnd.waitInput(new BillBoardTest(wnd));
		wnd.waitInput(new TextBoardTest(wnd));
		wnd.waitInput(new ImageBoardTest(wnd));
	}
}

class VertexBufferTest : Test
{
	mixin AddTest;
	
	void opCall(TestWindow wnd)
	{
		auto scene = new SceneGraph;
		auto root = new Object();
		scene.root = root;
		scene.link(root, new Clear());
		
		auto p = new PositionBuffer([0.0, 1, 0,  1, -1, -1.1,  -1, -1, 1.1]);
		scene.link(root, p);
		wnd.waitInput(scene);
		
		auto c = new ColorBuffer([1.0, 0, 0, 1,  0, 1, 0, 1,  0, 0, 1, 1]);
		scene.cut(root, p);
		scene.linkSerial(root, c, p);
		wnd.waitInput(scene);
		
		scene.cut(root, c);
		scene.linkSerial(
			root,
			new Texture("castle.jpg", 0),
			new TexCoordBuffer([0.5, 0,  1, 1,  0, 1]),
			c
		);
		wnd.waitInput(scene);
	}
}

class ShaderTest : Test
{
	mixin AddTest;
	
	class TestShader : Shader
	{
		this()
		{
			super(
				"void main()
				{
					gl_Position = gl_ModelViewMatrix * gl_Vertex;
					gl_TexCoord[0] = gl_MultiTexCoord0;
				}",
				"uniform sampler2D texture0, texture1;
				void main()
				{
					gl_FragColor = texture2D(texture0, gl_TexCoord[0].xy) * 0.5
						+ texture2D(texture1, gl_TexCoord[0].xy) * 0.5;
				}"
			);
			this["texture0"] = 0;
			this["texture1"] = 1;
		}
	}
	
	class MonochromeTest : SceneGraph
	{
		this(Window wnd)
		{
			root = link(new MonochromeShader(), new ImageBoard("castle.jpg", -1, -1, 2, 2));
		}
	}
	
	class MonochromeShader : Shader
	{
		this()
		{
			super(
				"void main()
				{
					gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
					gl_TexCoord[0] = gl_MultiTexCoord0;
				}",
				"uniform sampler2D texture0;
				void main()
				{
					float p = dot(
						texture2D(texture0, gl_TexCoord[0].xy).rgb,
						vec3(0.3, 0.59, 0.11)
					);
					gl_FragColor = vec4(p, p, p, 1.0);
				}"
			);
			this["texture0"] = 0;
		}
	}
	
	void opCall(TestWindow wnd)
	{
		auto scene = new SceneGraph();
		scene.root = scene.link(new Object(), new Clear());
		scene.linkSerial(
			scene.root,
			new TestShader(),
			new Texture(`a.png`, 0),
			new Texture(`b.png`, 1),
			new TexCoordBuffer([ 0.5, 0,  1, 1,  0, 1]),
			new PositionBuffer([ 0, 1, 0,  1, -1, 0,  -1, -1, 0 ])
		);
		wnd.waitInput(scene);
		
		wnd.waitInput(new MonochromeTest(wnd));
	}
}

class AttributeBufferTest : Test
{
	mixin AddTest;
	
	class VertexBlendShader : Shader, TimeIterator
	{
		this()
		{
			super(
				"uniform mat4 matrixArray[2];
				attribute vec3 matrixIndex2Weight1;
				void main()
				{
					vec4 p0 = matrixArray[int(matrixIndex2Weight1.x)] * gl_Vertex;
					vec4 p1 = matrixArray[int(matrixIndex2Weight1.y)] * gl_Vertex;
					gl_Position = p0 * matrixIndex2Weight1.z + p1 * (1.0 - matrixIndex2Weight1.z);
					gl_FrontColor = vec4(matrixIndex2Weight1.z, 1.0, 1.0, 1.0);
				}",
				"void main()
				{
					gl_FragColor = gl_Color;
				}"
			);
		}
		float f = 0.0;
		void opPostInc()
		{
			this["matrixArray"] = [Matrix.identity, Matrix.rotationZ(f -= 0.04)];
		}
	}
	
	class VertexBlendTest : SceneGraph, TimeIterator
	{
		private VertexBlendShader shader;
		
		this()
		{
			Vector[] positions, miws;
			const RANGE = 0.1;
			for (float y = 0; y < 1.0; y += RANGE)
			{
				positions ~= vector(0, y, 0);
				positions ~= vector(0, y + RANGE, 0);
				positions ~= vector(0.02, y, 0);
				miws ~= vector(0, 1, 1 - y);
				miws ~= vector(0, 1, clamp(1.0 - y - RANGE, 0.0, 1.0));
				miws ~= vector(0, 1, 1 - y);
			}
			
			root = link(new Object(), new Clear());
			linkSerial(
				root,
				shader = new VertexBlendShader(),
				new AttributeBuffer(miws, 3, shader.getAttributeLocation("matrixIndex2Weight1")),
				new PositionBuffer(positions)
			);
		}
		
		void opPostInc()
		{
			shader++;
		}
	}
	
	class VertexTweeningShader : Shader, TimeIterator
	{
		this()
		{
			super(
				"uniform float weight;
				attribute vec4 position2;
				void main()
				{
					vec4 p0 = gl_ModelViewProjectionMatrix * gl_Vertex;
					vec4 p1 = gl_ModelViewProjectionMatrix * position2;
					gl_Position = p0 * weight + p1 * (1.0 - weight);
					gl_FrontColor = vec4(weight, 1.0, 1.0, 1.0);
				}",
				"void main()
				{
					gl_FragColor = gl_Color;
				}"
			);
		}
		float f = 0.0;
		void opPostInc()
		{
			f += 0.01;
			if (1.0 < f) f = 0.0;
			this["weight"] = f;
		}
	}
	
	class VertexTweeningTest : SceneGraph, TimeIterator
	{
		private VertexTweeningShader shader;
		
		this()
		{
			Vector[] positions, positions2;
			const RANGE = 0.1;
			for (float y = 0; y < 1.0; y += RANGE)
			{
				positions ~= vector(0, y, 0);
				positions ~= vector(0, y + RANGE, 0);
				positions ~= vector(0.02, y, 0);
				positions2 ~= vector(y, 0, 0,  1);
				positions2 ~= vector(y + RANGE, 0, 0,  1);
				positions2 ~= vector(y, -0.02, 0,  1);
			}
			
			root = link(new Object(), new Clear());
			linkSerial(
				root,
				shader = new VertexTweeningShader(),
				new AttributeBuffer(positions2, 4, shader.getAttributeLocation("position2")),
				new PositionBuffer(positions)
			);
		}
		
		void opPostInc()
		{
			shader++;
		}
	}
	
	void opCall(TestWindow wnd)
	{
		auto blend = new VertexBlendTest();
		wnd.waitInput(blend, { blend++; });
		
		auto tweening = new VertexTweeningTest();
		wnd.waitInput(tweening, { tweening++; });
	}
}

class FrameBufferObjectTest : Test
{
	mixin AddTest;
	
	class Triangle : Unit
	{
		void attach()
		{
			glBegin(GL_TRIANGLES);
			glColor3d(1.0, 0.0, 0.0);
			glVertex3f(0.0, 1.0, 0.0);
			glColor3d(0.0, 1.0, 0.0);
			glVertex3f(1.0, -1.0, 0.0);
			glColor3d(0.0, 0.0, 1.0);
			glVertex3f(-1.0, -1.0, 0.0);
			glEnd();
		}
		void detach() {}
	}
	
	class TriangleScene : SceneGraph
	{
		this()
		{
			root = linkParallel(
				new Object(),
					new Clear(Color.WHITE),
					new Triangle()
			);
		}
	}
	
	class Rotation : MultMatrix, TimeIterator
	{
		float angle = 0.0;
		void opPostInc() { this = Matrix.rotationY(toRadian(angle += 0.8)); }
	}
	
	class TriangleScene2 : SceneGraph, TimeIterator
	{
		private Rotation rotation;
		
		this()
		{
			rotation = new Rotation();
			auto renderState = new RenderState();
			renderState.depthTest = true;
			root = linkParallel(
				renderState,
					new Clear(Color.WHITE),
					link(rotation, new Triangle()),
					link(new MultMatrix(Matrix.translation(0.5, 0.0, 0.0)), new Triangle()) // z[-1.0, 1.0) z = -1が手前
			);
		}
		
		void opPostInc()
		{
			rotation++;
		}
	}
	
	void opCall(TestWindow wnd)
	{
		auto fbo = new FrameBufferObject(256, 256, 0, false);
		fbo.draw(new TriangleScene());
		wnd.waitInput(new FboSceneGraph(fbo));
		
		auto fbo2 = new FrameBufferObject(256, 256, 0, true);
		auto tri2 = new TriangleScene2();
		wnd.waitInput(new FboSceneGraph(fbo2), { tri2++; fbo2.draw(tri2); });
	}
}

class MqoCameraTest : Test
{
	mixin AddTest;
	
	void opCall(TestWindow wnd)
	{
		auto scene = new SceneGraph();
		scene.root = scene.linkParallel(
			new MqoCamera(wnd),
				new Clear(),
				new class Unit
				{
					void attach()
					{
						glBegin(GL_TRIANGLES);
						glColor3f(100, 0, 0); glVertex3f(0, 100, 0);
						glColor3f(0, 100, 0); glVertex3f(100, 0, 0);
						glColor3f(0, 0, 100); glVertex3f(-100, 0, 0);
						glEnd();
					}
					void detach() {}
				}
		);
		wnd.waitInput(scene);
	}
}

class NoiseTest : Test
{
	mixin AddTest;
	
	class NoiseRectangle : Unit, TimeIterator
	{
		private float f = 0.0;
		void attach()
		{
			glBegin(GL_QUADS);
			glColor4f(1.0, 0.0, 0.0, 1.0);
			glNormal3f(0.0, 0.0, 0.0);
			auto z = cos(f) * 0.5 + 0.5;
			glTexCoord3f(0.0, 0.0, z); glVertex2f(-1.0, -1.0);
			glTexCoord3f(0.0, 1.0, z); glVertex2f(-1.0,  1.0);
			glTexCoord3f(1.0, 1.0, z); glVertex2f( 1.0,  1.0);
			glTexCoord3f(1.0, 0.0, z); glVertex2f( 1.0, -1.0);
			glEnd();
		}
		void detach() {}
		void opPostInc() { f += 0.01; }
	}
	
	class NoiseRectangleTest : SceneGraph, TimeIterator
	{
		private NoiseRectangle rect;
		this()
		{
			rect = new NoiseRectangle();
			root = linkParallel(
				new NoiseTexture("noise"),
					new Clear(),
					rect
			);
		}
		void opPostInc() { rect++; }
	}
	
	class SkyShader : Shader, TimeIterator
	{
		this()
		{
			super(
				"uniform float tx, tz;
				void main()
				{
					gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
					gl_FrontColor = gl_Color;
					vec4 tc = gl_MultiTexCoord0;
					tc.x = mod(tc.x + tx, 1.0);
					tc.z = tz;
					tc *= 2.0;
					if (1.0 < tc.x) tc.x = 2.0 - tc.x;
					if (1.0 < tc.y) tc.y = 2.0 - tc.y;
					gl_TexCoord[0] = tc;
				}",
				"uniform sampler3D texture0;
				void main()
				{
					gl_FragColor.rgb = mix(
						gl_Color.rgb,
						vec3(1.0, 1.0, 1.0),
						texture3D(texture0, gl_TexCoord[0].xyz).r
					);
					gl_FragColor.a = 1.0;
				}"
			);
			this["texture0"] = 0;
		}
		
		private float f = 0.0;
		
		void opPostInc()
		{
			f += 0.001;
			if (2.0 < f) f = 0.0;
			this["tz"] = f <= 1.0 ? f : 2.0 - f;
			this["tx"] = f;
		}
	}
	
	class SkyTest : SceneGraph, TimeIterator
	{
		private SkyShader shader;
		void opPostInc() { shader++; }
		this(Window wnd) // 東=x正
		{
			shader = new SkyShader();
			auto renderState = new RenderState();
			with (renderState)
			{
				depthTest = true;
				cullFace = true;
				cullFaceMode = GL_FRONT;
			}
			auto sphere = new Sphere(Color.BLUE, 400.0, 32, 32);
			root = link(renderState, new Clear());
			linkSerial(
				renderState,
				new MqoCamera(wnd),
				shader,
				new NoiseTexture("noise", 0),
				sphere
			);
		}
	}
	
	void opCall(TestWindow wnd)
	{
		auto rectTest = new NoiseRectangleTest();
		wnd.waitInput(rectTest, { rectTest++; });
		
		auto skyTest = new SkyTest(wnd);
		wnd.waitInput(skyTest, { skyTest++; });
	}
}

class SoundTest : Test
{
	mixin AddTest;
	
	class Button : TextBoard
	{
		protected Window wnd;
		protected const char key;
		protected const string fileName;
		this(Window wnd, char key, string text, float x, float y)
		{
			this.wnd = wnd;
			this.key = key;
			this.fileName = text;
			super("[" ~ key ~ "]" ~ text, x, y);
		}
	}
	
	class StopBgmButton : Button, TimeIterator
	{
		this(Window wnd, char key, string text, float x, float y) { super(wnd, key, text, x, y); }
		void opPostInc()
		{
			color = Bgm.fileName == "" ? Color.GREEN : Color.WHITE;
			if (wnd.key.pressing(key)) Bgm.stop();
		}
	}
	
	class PlayBgmButton : Button, TimeIterator
	{
		this(Window wnd, char key, string text, float x, float y) { super(wnd, key, text, x, y); }
		void opPostInc()
		{
			color = Bgm.fileName == fileName ? Color.GREEN : Color.WHITE;
			if (wnd.key.pressing(key)) Bgm.play(fileName);
		}
	}
	
	class PlaySeButton : Button, TimeIterator
	{
		this(Window wnd, char key, string text, float x, float y)
		{
			super(wnd, key, text, x, y);
			se = new Se(text);
		}
		private Se se;
		void opPostInc()
		{
			color = se.playing ? color : Color.WHITE;
			if (!wnd.key.pressing(key)) return;
			
			if (wnd.key.pressed(SDLK_LCTRL) || wnd.key.pressed(SDLK_RCTRL))
			{
				se.play();
				color = Color.BLUE;
			}
			else if (wnd.key.pressed(SDLK_LSHIFT) || wnd.key.pressed(SDLK_RSHIFT))
			{
				se.play(true);
				color = Color.RED;
			}
			else se.stop();
		}
		void stop() { se.stop(); }
	}
	
	void opCall(TestWindow wnd)
	{
		auto scene = new SceneGraph();
		auto dirStock = getcwd();
		scope (exit)
		{
			foreach (PlaySeButton v; scene) v.stop();
			chdir(dirStock);
			Bgm.stop();
		}
		
		chdir(`C:\WINDOWS\Media`);
		
		auto rs = (new RenderState()).blend(true);
		scene.root = scene.link(rs, new Clear());
		scene.link(rs, new StopBgmButton(wnd, '0', "stopBgm", -1.0, 0.8));
		auto key = 'a';
		foreach (int i, string v; curdir.listdir("*.mid"))
		{
			scene.link(rs, new PlayBgmButton(wnd, key++, v, -1.0, 0.7 - 0.1 * i));
			if (0.8 - 0.1 * i < -1.0) break;
		}
		auto t = new TextBoard(
			"デフォstop +ctrlでplay +shiftでloop",
			0.0, 0.8, 1.8, 0.1, Color.WHITE, 1024, 32);
		scene.link(rs, t);
		foreach (int i, string v; curdir.listdir("*.wav"))
		{
			scene.link(rs, new PlaySeButton(wnd, key++, v, 0.0, 0.7 - 0.1 * i));
			if (0.8 - 0.1 * i < -1.0) break;
		}
		wnd.waitInput(scene, { foreach (TimeIterator v; scene) v++; });
	}
}

class MosaicTest : Test
{
	mixin AddTest;
	
	class TestScene : SceneGraph
	{
		this()
		{
			root = linkParallel(
				new Object(),
					new Clear(),
					new ImageBoard("castle.jpg", -1, -1, 2, 2)
			);
		}
	}
	
	void opCall(TestWindow wnd)
	{
		auto tree = new TestScene();
		wnd.waitInput(tree);
		
		auto mosaic = new Mosaic(1024, 512);
		wnd.waitInputLazy(mosaic(tree, wnd.mouse.position));
	}
}

class FsaaTest : Test
{
	mixin AddTest;
	
	class TestCircle : SceneGraph
	{
		this()
		{
			root = linkParallel(
				new Camera(0, 100, 400,  0, 0, 0),
					new Clear(),
					new Cylinder(Color.WHITE, 50.0, 50.0, 4, 256)
			);
		}
	}
	
	void opCall(TestWindow wnd)
	{
		wnd.waitInput(new TestCircle());
		
		auto fsaa2 = new Fsaa2(wnd.size);
		wnd.waitInput(fsaa2(new TestCircle()));
		
		auto fsaa3 = new Fsaa3(wnd.size);
		wnd.waitInput(fsaa3(new TestCircle()));
		
		// x4は最大テクスチャ(2048x2048)でwnd.size小さくしないと無理
	}
}

class DepthOfFieldTest : Test
{
	mixin AddTest;
	
	class TestTree : SceneGraph
	{
		SceneGraph depthTree;
		
		this()
		{
			auto vp = new ViewProjection(
				Matrix.lookAt(0, 10, -100,  0, 0, -400),
				Matrix.perspectiveFov(PI / 8.0, 640.0 / 480.0, 40.0, 500.0)
			);
			root = linkParallel(
				vp,
					new Clear(),
					linkSerial(
						new Texture("castle.jpg", 0),
						new MultMatrix(Matrix.rotationX(-PI_2) * Matrix.translation(0, 0, -200)),
						new Disk(Color.WHITE, 16.0, 256.0, 256, 256) // 頂点数が少なすぎるとゆがみと深度がうまく出せない?
					)
			);
			
			depthTree = clone();
			depthTree.root = depthTree.link(new DepthShader(50, 200), depthTree.root);
		}
	}
	
	void opCall(TestWindow wnd)
	{
		auto tt = new TestTree();
		wnd.waitInput(tt.depthTree);
		
		wnd.waitInput(tt);
		
		auto dof = new DepthOfField(1024, 512);
		wnd.waitInput(dof(tt, tt.depthTree));
	}
}

class ShadowBufferTest : Test
{
	mixin AddTest;
	
	class TestScene : SceneGraph
	{
		this(ViewProjection camera, Vector viewportSize, Shader shader = null, Texture texture = null)
		{
			auto rs = new RenderState();
			rs.depthTest = true;
			
			if (shader is null && texture is null) root = link(rs, camera);
			else if (shader !is null && texture is null) root = linkSerial(rs, shader, camera);
			else if (shader !is null && texture !is null) root = linkSerial(rs, shader, texture, camera);
			else assert(false);
			linkParallel(
				camera,
					new Clear(),
					link(new MultMatrix(Matrix.rotationX(-PI_2)), new Disk(Color.WHITE, 0.5, 50.0, 256, 256)),
					link(new MultMatrix(Matrix.translation(15, 10, 0)), new Cylinder(Color.RED, 5.0, 4.0, 10.0, 256, 256)),
					link(new MultMatrix(Matrix.translation(0, 20, -10)), new Sphere(Color.GREEN, 10.0, 256, 256))
			);
		}
	}
	
	class TestTree : SceneGraph
	{
		SceneGraph shadowBufferTree, shadowLayerTree;
		
		this(FrameBufferObject shadowBuffer)
		{
			auto rs = new RenderState();
			rs.depthTest = true;
			root = link(createCamera(), rs);
			linkParallel(
				rs,
					new Clear(),
					link(new MultMatrix(Matrix.rotationX(-PI_2)), new Disk(Color.WHITE, 0.5, 50.0, 256, 256)),
					link(new MultMatrix(Matrix.translation(15, 10, 0)), new Cylinder(Color.RED, 5.0, 4.0, 10.0, 256, 256)),
					link(new MultMatrix(Matrix.translation(0, 20, -10)), new Sphere(Color.GREEN, 10.0, 256, 256))
			);
			
			shadowBufferTree = clone();
			shadowBufferTree.remove(root);
			shadowBufferTree.root = shadowBufferTree.linkSerial(
				createLightCamera(), new ShadowBufferShader(), rs
			);
			
			shadowLayerTree = clone();
			shadowLayerTree.remove(root);
			auto identity = new ViewProjection(Matrix.identity, Matrix.identity); // 不要?
			shadowLayerTree.root = shadowLayerTree.linkSerial(
				new ShadowShader(createCamera(), createLightCamera()), shadowBuffer, identity, rs
			);
		}
		
		private ViewProjection createCamera()
		{
			return new Camera(0, 50, 150,  0, 0, 0);
		}
		
		private ViewProjection createLightCamera()
		{
			return new ViewProjection(
				Matrix.lookAt(200, 200, 0,  0, 0, 0),
				Matrix.perspectiveFov(PI / 8.0, 640.0 / 480.0, 180.0, 350.0)
			);
		}
	}
	
	void opCall(TestWindow wnd)
	{
		//auto shadowBuffer = new FrameBufferObject(1024, 512, 0, true);
		auto shadowBuffer = new FrameBufferObject(2048, 2048, 0, true); // きれいな影に
		//auto shadowBuffer = new FrameBufferObjectRgba32f(2048, 2048, 0, true); // 少しだけ精度が増す
		auto tt = new TestTree(shadowBuffer);
		
		wnd.waitInput(tt);
		
		wnd.waitInput(tt.shadowBufferTree);
		
		shadowBuffer.draw(tt.shadowBufferTree);
		wnd.waitInput(new FboSceneGraph(shadowBuffer));
		
		wnd.waitInput(tt.shadowLayerTree);
	}
}

class MqoTest : Test
{
	mixin AddTest;
	
	void opCall(TestWindow wnd)
	{
		auto mqo = new ModelData();
		mqo.translator = new MqoTranslator();
		readFile("mqo_test.mqo", mqo);
		
		auto renderState = new RenderState();
		renderState.depthTest = true;
		
		auto scene = new SceneGraph();
		scene.root = scene.link(renderState, new Clear());
		scene.linkSerial(renderState, new MqoCamera(wnd), new Model(mqo));
		wnd.waitInput(scene);
	}
}

class MqoExTest : Test
{
	mixin AddTest;
	
	class TestScene : SceneGraph
	{
		this(Window wnd, Model model)
		{
			auto renderState = new RenderState();
			renderState.depthTest = true;
			root = linkParallel(
				renderState,
					new Clear(),
					link(new MqoCamera(wnd), model)
			);
		}
	}
	
	void opCall(TestWindow wnd)
	{
		auto mqo = new ModelData();
		mqo.translator = new MqoExTranslator();
		readFile("mqoex_test.mqo", mqo);
		auto model = new Model(mqo);
		model[1].visible = true;
		auto scene = new TestScene(wnd, model);
		wnd.waitInput(scene);
		
		model[1].visible = false;
		model[2].visible = true;
		model[2].motion = [0, 1];
		model[2].motion.span = 30;
		model[2].waitingMotion = [0, 1, 0];
		model[2].waitingMotion.span = 60;
		wnd.waitInput(scene, { model++; });
		
		model[2].visible = false;
		model[3].visible = true;
		float f = 0.0;
		wnd.waitInput(
			scene,
			{
				model[3].world = Matrix.translation(cos(f += 0.04) * 50.0, 0, 0);
				model++;
			}
		);
	}
}

class FaceTest : Test
{
	mixin AddTest;
	
	void opCall(TestWindow wnd)
	{
		auto data = new ModelData();
		data.translator = new MqoExTranslator();
		readFile("face_test.mqo", data);
		auto model = new Model(data);
		auto scene = new SceneGraph();
		auto renderState = new RenderState();
		renderState.depthTest = true;
		scene.root = scene.link(new Object(), new Clear());
		scene.linkSerial(scene.root, renderState, new MqoCamera(wnd, 0, 0, 100,  0, 0, 0), model);
		scene.linkSerial(
			scene.root,
			(new RenderState()).blend(true),
			new TextBoard(`a-hキーで変化`, -1.0, 0.8)
		);
		
		model[1].visible = true;
		model[2].visible = true;
		model[3].visible = true;
		model[4].visible = true;
		
		void set(char key, size_t modelIndex, uint[] route, uint span)
		{
			if (!wnd.key.pressing(key)) return;
			model[modelIndex].waitingMotion = route;
			model[modelIndex].waitingMotion.span = span;
		}
		
		wnd.waitInput(
			scene,
			{
				set('a', 1, [0, 1, 0], 10);
				set('b', 1, [2, 3, 2], 20);
				set('c', 2, [0, 1, 0], 30);
				set('d', 2, [2, 3, 2], 40);
				set('e', 3, [0, 1, 0], 50);
				set('f', 3, [0, 1, 0], 100);
				set('g', 3, [0, 1, 0], 50);
				set('h', 3, [0, 1, 0], 100);
				set('e', 4, [0, 1, 0], 50);
				set('f', 4, [2, 3, 2], 100);
				set('g', 4, [4, 5, 4], 50);
				set('h', 4, [6, 7, 6], 100);
				model++;
			}
		);
	}
}

class MkmTest : Test
{
	mixin AddTest;
	
	private ModelData createTestModelData()
	{
		auto result = new ModelData();
		if (exists("mkmmqo_test.3d"))
		{
			readFile("mkmmqo_test.3d", result);
		}
		else
		{
			result.translator = new MkmTranslator();
			readFile("mkmmqo_test.mkm", result);
			result.translator = new MkmMqoTranslator();
			readFile("mkmmqo_test.mqo", result);
			
			result.translator = new ZlibTranslator();
			writeFile("mkmmqo_test.3d", result);
		}
		return result;
	}
	
	void opCall(TestWindow wnd)
	{
		auto model = new Model(createTestModelData());
		model.waitingMotion = model.createMotion(0);
		
		auto renderState = new RenderState();
		renderState.depthTest = true;
		
		auto scene = new SceneGraph();
		scene.root = scene.linkParallel(
			renderState,
				new Clear(),
				scene.link(new MqoCamera(wnd, vector(0, 0, 500)), model)
		);
		wnd.waitInput(scene, { model++; });
		
		model.motion = [MotionKeys(0, 0), MotionKeys(4, 0), MotionKeys(4, 30)];
		model.motion.span = 60;
		model.waitingMotion = model.createMotion(1);
		wnd.waitInput(scene, { model++; });
		
		model[1].visible = true;
		model[2].visible = true;
		model[3].visible = true;
		model[2].waitingMotion = [0, 1, 0];
		model[2].waitingMotion.span = 30;
		wnd.waitInput(scene, { model++; });
	}
}

class PointPickTest : Test
{
	mixin AddTest;
	
	void opCall(TestWindow wnd)
	{
		auto scene = new SceneGraph();
		auto mousePointer = new PointSprite(Vector(), 8.0, Color.WHITE);
		auto camera = new MqoCamera(wnd, 10, 10, 10,  0, 0, 0);
		auto a = new PointSprite(vector(0.0, 0.0, 0.0), 10.0, Color.GREEN);
		auto b = new PointSprite(vector(2.0, 0.0, 0.0), 10.0, Color.GREEN);
		auto c = new PointSprite(vector(0.0, 2.0, 0.0), 10.0, Color.GREEN);
		auto d = new PointSprite(vector(0.0, 0.0, 2.0), 10.0, Color.GREEN);
		scene.root = scene.linkParallel(
			new Object(),
				new Clear(),
				mousePointer,
				scene.linkParallel(
					camera,
						a,
						b,
						c,
						d
				)
		);
		wnd.waitInput(
			scene,
			{
				mousePointer.position = wnd.mouse.position; // xy共に[-1.0, 1.0)
				foreach (v; [a, b, c, d])
				{
					const R = 0.1;
					auto p = v.position;
					p.w = 1.0;
					auto pc = p * camera.toMatrix();
					if (pc.w != 0) pc /= pc.w;
					pc.z = pc.w = 0.0;
					v.color = distance(pc, mousePointer.position) < R ? Color.RED : Color.GREEN;
				}
			}
		);
	}
}

class DraggingTest : Test
{
	mixin AddTest;
	
	private ModelData createTestModelData()
	{
		auto result = new ModelData();
		if (exists("dragging_test.3d"))
		{
			readFile("dragging_test.3d", result);
		}
		else
		{
			result.translator = new MkmTranslator();
			readFile("dragging_test.mkm", result);
			result.translator = new MkmMqoTranslator();
			readFile("dragging_test.mqo", result);
			
			result.translator = new ZlibTranslator();
			writeFile("dragging_test.3d", result);
		}
		return result;
	}
	
	// mouse座標[-1, 0)に
	private Vector toMousePosition(Vector worldPosition, Matrix viewProjection)
	{
		auto result = worldPosition;
		result.w = 1.0;
		result *= viewProjection;
		if (result.w != 0) result /= result.w;
		result.z = result.w = 0.0;
		return result;
	}
	
	void opCall(TestWindow wnd)
	{
		auto model = new Model(createTestModelData());
		model.waitingMotion = model.createMotion(5);
		model[1].visible = true;
		model[2].visible = true;
		model[3].visible = true;
		model[4].visible = true;
		auto ps = new PointSprite(Vector(0.0, 30.0, 50.0), 8.0, Color.RED);
		auto scene = new SceneGraph();
		auto mrs = new RenderState();
		mrs.depthTest = true;
		auto camera = new class MqoCamera
		{
			this() { super(wnd); }
			override void leftDown() {}
			override void leftUp() {}
		};
		scene.root = scene.linkParallel(
			camera,
				new Clear(),
				scene.link(mrs, model),
				ps
		);
		bool dragging;
		Vector beginningMousePosition;
		wnd.waitInput(
			scene,
			{
				model++;
				
				if (!dragging)
				{
					auto psm = toMousePosition(ps.position, camera.toMatrix());
					const RADIUS = 0.1;
					auto near = distance(psm, wnd.mouse.position) < RADIUS;
					if (near && wnd.mouse.pressing(SDL_BUTTON_LEFT))
					{
						dragging = true;
						beginningMousePosition = wnd.mouse.position;
						ps.color = Color.BLUE;
					}
					else ps.color = near ? Color.GREEN : Color.RED;
				}
				else
				{
					if (wnd.mouse.releasing(SDL_BUTTON_LEFT))
					{
						(cast(ClothSubset)model[3]).clothPcnta.outsideForce = Vector();
						dragging = false;
					}
					else
					{
						auto a = wnd.mouse.position - beginningMousePosition;
						(cast(ClothSubset)model[3]).clothPcnta.outsideForce = a * 500;
					}
				}
			}
		);
		
		float beginningPsPositionY;
		float unyuY = 0.0;
		wnd.waitInput(
			scene,
			{
				model++;
				
				if (!dragging)
				{
					auto cpm = toMousePosition(ps.position, camera.toMatrix());
					const RADIUS = 0.1;
					auto near = distance(cpm, wnd.mouse.position) < RADIUS;
					if (near && wnd.mouse.pressing(SDL_BUTTON_LEFT))
					{
						dragging = true;
						beginningMousePosition = wnd.mouse.position;
						beginningPsPositionY = ps.position.y;
						ps.color = Color.BLUE;
					}
					else ps.color = near ? Color.GREEN : Color.RED;
				}
				else
				{
					auto dy = (wnd.mouse.position.y - beginningMousePosition.y) * 100;
					if (wnd.mouse.releasing(SDL_BUTTON_LEFT))
					{
						unyuY += dy;
						dragging = false;
					}
					else
					{
						ps.position.y = beginningPsPositionY + dy;
						model[3].world = Matrix.translation(0, unyuY + dy, 0);
					}
				}
			}
		);
	}
}

class RangeTest : Test
{
	mixin AddTest;
	
	void opCall(TestWindow wnd)
	{
		auto rect1 = new LineRectangle(0.0, 0.0, 0.999, 0.999, Color.RED);
		auto rect2 = new LineRectangle(-0.999, -0.999, 0.999, 0.999, Color.GREEN);
		auto scene = new SceneGraph();
		scene.root = scene.linkParallel(
			new Object(),
				new Clear(),
				rect1,
				rect2
		);
		wnd.waitInput(scene);
		
		rect1.size = Vector(1.0, 1.0);
		rect2.position = Vector(-1.0, -1.0);
		rect2.size = Vector(1.0, 1.0);
		wnd.waitInput(scene); // xyの範囲は[-1.0, 1.0)
		
		auto pr = new PointSprite(Vector(0.0, 0.0, 0.0), 10.0, Color.RED);
		auto pg = new PointSprite(Vector(-0.1, 0.0, -1.0), 10.0, Color.GREEN);
		auto pb = new PointSprite(Vector(0.1, 0.0, 1.0), 10.0, Color.BLUE);
		auto scene2 = new SceneGraph();
		scene2.root = scene2.linkParallel(
			new Object(),
				new Clear(),
				pr,
				pg,
				pb
		);
		wnd.waitInput(scene2);
		
		pg.position.z = -1.0001;
		pb.position.z = 1.0001;
		wnd.waitInput(scene2); // zの範囲は[-1.0, 1.0], fboは[-1.0, 1.0)で異なる、精度による違い?
	}
}

class RoutingTest : Test
{
	mixin AddTest;
	
	void opCall(TestWindow wnd)
	{
		auto scene = new SceneGraph();
		scene.root = scene.link(new Object(), new Clear());
		
		auto modelData = new ModelData();
		modelData.translator = new MqoTranslator();
		readFile("routing_test.mqo", modelData);
		
		auto rs = new RenderState();
		rs.depthTest = true;
		
		auto camera = new MqoCamera(wnd, Vector(-600, 600, 600), Vector(0, 50, 0));
		camera.projection = Matrix.perspectiveFov(PI / 8.0, 640.0 / 480.0, 1.0, 2000.0);
		
		scene.linkSerial(
			scene.root,
			rs,
			camera,
			new Model(modelData)
		);
		
		auto ps = new PointSprite(Vector(0.0, 0.0, 0.0), 16.0, Color.RED);
		scene.link(camera, ps);
		
		Vector[] route;
		size_t routeIterator;
		auto triangles = modelData.base[0].positions;
		
		wnd.waitInput(
			scene,
			{
				if (wnd.key.pressing('a'))
				{
					route = getRoute(triangles, ps.position, Vector(190, 10, -190));
					routeIterator = 0;
				}
				else if (wnd.key.pressing('b'))
				{
					route = getRoute(triangles, ps.position, Vector(-190, 10, 190));
					routeIterator = 0;
				}
				else if (wnd.key.pressing('c'))
				{
					route = getRoute(triangles, ps.position, Vector(170, 154, 190));
					routeIterator = 0;
				}
				
				if (route.length <= routeIterator) return;
				
				if (distance(ps.position, route[routeIterator]) < 8.0) routeIterator++;
				else ps.position += normalize(route[routeIterator] - ps.position) * 8;
			}
		);
	}
	
	Vector[] getRoute(Vector[] triangles, Vector beginPoint, Vector endPoint)
	{
		// 開始位置と、終了位置を追加
		Vector[] crossPoints;
		crossPoints ~= beginPoint;
		crossPoints ~= endPoint;
		
		// 格子でのポイントを列挙
		crossPoints ~= Routing.getCrossPoints(triangles, -200, -200, 400, 400, 20);
		
		// 重複を取り除く
		crossPoints = Routing.removeUnunique(crossPoints);
		
		// yが低い斜面の床と衝突するので
		foreach (ref v; crossPoints[2..$]) v.y += 10.0;
		
		// 隣接ポイントをリンク
		auto links = Routing.getLinks(crossPoints, 400.0 / 20.0 * 1.99);
		
		// 傾斜が一定以上のリンクを削除
		links = Routing.removeTangent(links, PI_4);
		
		// 交差判定があるリンクを削除
		links = Routing.removeIntersect(links, triangles);
		
		if (crossPoints.length < 2
			|| beginPoint != crossPoints[0]
			|| endPoint != crossPoints[1]) return null;
		
		// 探索
		return Routing.searchRoute(crossPoints, links, 0, 1);
	}
	
	static bool triangleIntersectRay3(Vector t1, Vector t2, Vector t3, Vector rp, Vector rd, out Vector crossP)
	{
		float distance;
		auto result = triangleIntersectRay2(
			t1, t2, t3,
			rp, rd,
			distance
		);
		if (result) crossP = rp + rd * distance;
		return result;
	}
	
	class Routing
	{
		// TODO crossだと外積と重複するのでintersectにrename
		static Vector[] getCrossPoints(
			Vector[] trianglePositions, float x, float z, float width, float depth, float step
		)
		{
			Vector[] result;
			for (float cz = z; cz < z + depth; cz += step)
			{
				for (float cx = x; cx < x + width; cx += step)
				{
					for (int i = 0; i < trianglePositions.length; i += 3)
					{
						Vector crossP;
						auto b = triangleIntersectRay3(
							trianglePositions[i],
							trianglePositions[i + 1],
							trianglePositions[i + 2],
							Vector(cx, 100, cz),
							Vector(0, -1, 0),
							crossP
						);
						if (b) result ~= crossP;
					}
				}
			}
			return result;
		}
		
		static Vector[] removeUnunique(Vector[] array)
		{
			Vector[] result;
			foreach (v; array)
			{
				if (!contain(result, v)) result ~= v;
			}
			return result;
		}
		
		private static bool contain(Vector[] array, Vector a)
		{
			foreach (v; array)
			{
				if (a == v) return true;
			}
			return false;
		}
		
		static class VectorPair
		{
			Vector first, second;
			size_t firstIndex, secondIndex;
			this(Vector f, Vector s, size_t fi, size_t si)
			{
				first = f;
				second = s;
				firstIndex = fi;
				secondIndex =si;
			}
		}
		
		static VectorPair[] getLinks(Vector[] array, float range)
		{
			VectorPair[] result;
			for (int i = 0; i < array.length - 1; i++)
			{
				for (int j = i + 1; j < array.length; j++)
				{
					auto a = Vector(array[i].x, array[i].z);
					auto b = Vector(array[j].x, array[j].z);
					if (distance(a, b) < range) result ~= new VectorPair(array[i], array[j], i, j);
				}
			}
			return result;
		}
		
		static VectorPair[] removeTangent(VectorPair[] array, float s)
		{
			auto ts = tan(s);
			VectorPair[] result;
			foreach (v; array)
			{
				auto width = distance(
					Vector(v.first.x, v.first.z),
					Vector(v.second.x, v.second.z)
				);
				if (width == 0) continue;
				auto height = fabs(v.first.y - v.second.y);
				if (height / width < ts) result ~= v;
			}
			return result;
		}
		
		static VectorPair[] removeIntersect(VectorPair[] array, Vector[] trianglePositions)
		{
			VectorPair[] result;
			foreach (v; array)
			{
				if (!trianglesIntersectLineSegment(trianglePositions, v.first, v.second)) result ~= v;
			}
			return result;
		}
		
		static bool trianglesIntersectLineSegment(
			Vector[] trianglePositions, Vector lineSegmentA, Vector lineSegmentB
		)
		{
			for (int i = 0; i < trianglePositions.length; i += 3)
			{
				auto b = triangleIntersectLineSegment(
					trianglePositions[i],
					trianglePositions[i + 1],
					trianglePositions[i + 2],
					lineSegmentA,
					lineSegmentB
				);
				if (b) return true;
			}
			return false;
		}
		
		static bool triangleIntersectLineSegment(
			Vector t0, Vector t1, Vector t2, Vector lineSegmentA, Vector lineSegmentB
		)
		{
			auto ab = lineSegmentB - lineSegmentA;
			float d;
			auto b = triangleIntersectRay2(t0, t1, t2, lineSegmentA, normalize(ab), d);
			return b && 0 < d && d <= scalar(ab);
		}
		
		static Vector[] searchRoute(Vector[] nodes, VectorPair[] links, size_t beginIndex, size_t endIndex)
		in
		{
			assert(beginIndex != endIndex);
		}
		body
		{
			bool[] nodeFlags = new bool[nodes.length];
			size_t[][] routes;
			routes ~= [beginIndex];
			
			for (int i = 0; i < routes.length; i++)
			{
				auto currentRoute = routes[i];
				auto currentRouteLastIndex = routes[i][$ - 1];
				
				if (currentRouteLastIndex == endIndex)
				{
					Vector[] result;
					foreach (v; currentRoute) result ~= nodes[v];
					return result;
				}
				
				if (nodeFlags[currentRouteLastIndex]) continue;
				
				foreach (v; links)
				{
					if (v.firstIndex == currentRouteLastIndex) routes ~= currentRoute ~ v.secondIndex;
					else if (v.secondIndex == currentRouteLastIndex) routes ~= currentRoute ~ v.firstIndex;
				}
				nodeFlags[currentRouteLastIndex] = true;
			}
			return null;
		}
	}
}

class InputTest : Test
{
	mixin AddTest;
	
	void throwExceptionIfPressingReturnOrEscape(TestWindow wnd)
	{
		if (wnd.key.pressing(SDLK_RETURN)
			|| wnd.key.pressing(SDLK_ESCAPE)) throw new Exception("InputTest");
	}
	
	void opCall(TestWindow wnd)
	{
		auto tb = new TextBoard("null", 0, 0);
		auto tree = new SceneGraph();
		tree.root = tree.linkParallel(
			(new RenderState).blend(true),
				new Clear(),
				tb
		);
		
		void anyTest(string text, int buttonId, InputState anyState)
		{
			tb.text = text ~ "押せ";
			while (true)
			{
				throwExceptionIfPressingReturnOrEscape(wnd);
				if (anyState.pressing(buttonId)) break;
				wnd.update(tree);
			}
			tb.text = text ~ "離せ";
			while (true)
			{
				throwExceptionIfPressingReturnOrEscape(wnd);
				if (anyState.releasing(buttonId)) break;
				wnd.update(tree);
			}
		}
		void keyTest(string text, int buttonId) { anyTest(text, buttonId, wnd.key); }
		void mouseTest(string text, int buttonId) { anyTest(text, buttonId, wnd.mouse); }
		void joyTest(string text, int buttonId) { anyTest(text, buttonId, wnd.joy); }
		
		try
		{
			keyTest("a", 'a');
			keyTest("b", 'b');
			keyTest("c", 'c');
			mouseTest("マウス左ボタン", SDL_BUTTON_LEFT);
			mouseTest("マウス右ボタン", SDL_BUTTON_RIGHT);
			joyTest("ジョイ上", Joy.UP);
			joyTest("ジョイ下", Joy.DOWN);
			joyTest("ジョイ右", Joy.RIGHT);
			joyTest("ジョイ左", Joy.LEFT);
			joyTest("ジョイ0ボタン", 0);
			joyTest("ジョイ1ボタン", 1);
			joyTest("ジョイ2ボタン", 2);
			joyTest("ジョイ3ボタン", 3);
		}
		catch (Exception e) writefln(e.toString);
	}
}
