module coneneko.camera;
import
	coneneko.math,
	std.math,
	std.cstream,
	coneneko.unit,
	coneneko.glext,
	opengl;

///
class ViewProjection : Unit
{
	invariant()
	{
		assert(emptyGlError());
	}
	
	protected Matrix _view, _projection;
	
	///
	this(float x, float y, float z, float atX, float atY, float atZ)
	{
		this(vector(x, y, z), vector(atX, atY, atZ));
	}
	
	///
	this(Vector position = vector(0.0, 0.0, 400.0), Vector at = vector())
	{
		this(Matrix.lookAt(position, at), Matrix.perspectiveFov());
	}
	
	///
	this(Matrix view, Matrix projection)
	{
		_view = view;
		_projection = projection;
	}
	
	void attach()
	{
		glPushAttrib(GL_TRANSFORM_BIT);
		glMatrixMode(GL_PROJECTION);
		glPushMatrix();
		glLoadMatrixf(cast(float*)&_projection);
		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		glLoadMatrixf(cast(float*)&_view);
	}
	
	void detach()
	{
		glMatrixMode(GL_MODELVIEW);
		glPopMatrix();
		glMatrixMode(GL_PROJECTION);
		glPopMatrix();
		glPopAttrib();
	}
	
	Matrix toMatrix() ///
	{
		return view * projection;
	}
	
	Matrix view() ///
	{
		return _view;
	}
	
	void view(Matrix a) ///
	{
		_view = a;
	}
	
	Matrix projection() ///
	{
		return _projection;
	}
	
	void projection(Matrix a) ///
	{
		_projection = a;
	}
}

///	at, positionを更新する場合、at, positionの順に設定しないとエラーになる
class Camera : ViewProjection
{
	Vector at; ///
	float head = 0.0; /// y軸、左右の回転
	float pitch = 0.0; /// x軸、上下の回転
	float bank = 0.0; /// z軸、upの傾き
	float zoom = 400.0; /// p, atの距離
	
	///
	this(float x, float y, float z, float atX, float atY, float atZ)
	{
		this(vector(x, y, z), vector(atX, atY, atZ));
	}
	
	///
	this(Vector position = vector(0.0, 0.0, 400.0), Vector at = vector())
	out
	{
		assert(nearly(this.position, position));
		assert(nearly(this.at, at));
	}
	body
	{
		super(position, at);
		this.at = at;
		this.position = position;
	}
	
	override Matrix view() /// bank * pitch * head
	{
		scope (failure) { position.print; at.print; }
		return Matrix.rotationZ(bank) * Matrix.lookAtRH(position, at, vector(0.0, 1.0, 0.0));
	}
	
	override void view(Matrix a) /// 使用禁止
	{
		throw new Error("Camera.view");
	}
	
	override void attach()
	{
		_view = view;
		super.attach();
	}
	
	Vector position() ///
	{
		return computePosition(at, head, pitch, zoom);
	}
	
	void position(Vector p) ///
	{
		computeHpz(p, at, head, pitch, zoom);
	}
	
	unittest
	{
		bool test(float px, float py, float pz, float ax, float ay, float az)
		{
			auto pos = vector(px, py, pz);
			auto at = vector(ax, ay, az);
			float h, p, z;
			computeHpz(pos, at, h, p, z);
			auto result = nearly(pos, computePosition(at, h, p, z));
			if (!result)
			{
				std.stdio.writefln("%f %f %f", h, p, z);
				std.stdio.writefln(pos, computePosition(at, h, p, z));
			}
			return result;
		}
		assert(test(0, 0, 100,  0, 0, 0));
		assert(test(0, 0, -50,  0, 0, 0));
		assert(test(0, 0, 0,  100, 0, 0));
		assert(test(10, 15, 0,  -20, 0, 0));
		assert(test(1, 2, 3,  4, 5, 6));
		assert(test(300, -200, 100,  -100, 200, -300));
	}
	
	private static void computeHpz(
		Vector position, Vector at, out float head, out float pitch, out float zoom
	)
	{
		auto ap = position - at;
		
		auto xz = vector(ap.x, 0, ap.z);
		if (scalar(xz) == 0.0) head = 0;
		else
		{
			head = acos(dot(vector(0, 0, 1), normalize(xz)));
			if (ap.x < 0.0) head = PI * 2 - head;
		}
		
		if (scalar(ap) == 0.0) pitch = 0;
		else
		{
			auto v = Vector(0, 0, 1, 1) * Matrix.rotationY(head);
			if (v.w != 0) v /= v.w;
			v.w = 0;
			pitch = acos(dot(v, normalize(ap)));
			if (ap.y < 0.0) pitch = PI * 2 - pitch;
		}
		
		zoom = scalar(ap);
	}
	
	private static Vector computePosition(Vector at, float head, float pitch, float zoom)
	{
		auto result = Vector(0, 0, 0, 1)
			* Matrix.translation(0, 0, zoom)
			* Matrix.rotationX(-pitch)
			* Matrix.rotationY(head)
			* Matrix.translation(at);
		if (result.w != 0.0) result /= result.w;
		result.w = 0;
		return result;
	}
}
