/**
render stateはdetachで値を修復しておく。
textureやshaderなど、他に影響を与えるものもdetachで修復しておく。
他に影響を与えないものは、修復しなくてもいい。
*/
module coneneko.unit;
import
	coneneko.glext,
	coneneko.math,
	std.math,
	std.string,
	std.stream,
	std.c.time,
	std.signals,
	opengl,
	openglu,
	coneneko.rgba,
	std.cstream;

///
interface Unit
{
	void attach(); ///
	void detach(); ///
}

///
interface TimeIterator
{
	void opPostInc(); /// ++
}

///
class UnitDecorator : Unit
{
	private Unit base;
	this(Unit base) { this.base = base; } ///
	void attach() { base.attach(); }
	void detach() { base.detach(); }
}

/// RenderTargetの扱いが難しくなるので、できるだけ使わないでください
class ViewPort : Unit
{
	///
	int x, y, width, height;
	
	///
	this(int width, int height)
	{
		x = y = 0;
		this.width = width;
		this.height = height;
	}
	
	///
	this(Vector size)
	{
		this(cast(int)size.x, cast(int)size.y);
	}
	
	void attach()
	{
		glPushAttrib(GL_VIEWPORT_BIT);
		glViewport(x, y, width, height);
	}
	
	void detach()
	{
		glPopAttrib();
	}
}

///
class RenderState : Unit
{
	invariant()
	{
		assert(emptyGlError());
	}
	
	bool cullFace; ///
	uint cullFaceMode = GL_BACK; /// GL_FRONT GL_BACK GL_FRONT_AND_BACK
	bool depthTest; ///
	private bool blend_;
	int blendSrc = GL_SRC_ALPHA; ///
	int blendDst = GL_ONE_MINUS_SRC_ALPHA; ///
	
	void attach()
	{
		glPushAttrib(GL_ENABLE_BIT | GL_POLYGON_BIT | GL_COLOR_BUFFER_BIT);
		cullFace ? glEnable(GL_CULL_FACE) : glDisable(GL_CULL_FACE);
		glCullFace(cullFaceMode);
		depthTest ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
		blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
		glBlendFunc(blendSrc, blendDst);
	}
	
	void detach()
	{
		glPopAttrib();
	}
	
	RenderState clone() ///
	{
		RenderState result = new RenderState();
		result.cullFace = cullFace;
		result.cullFaceMode = cullFaceMode;
		result.depthTest = depthTest;
		result.blend = blend;
		result.blendSrc = blendSrc;
		result.blendDst = blendDst;
		return result;
	}
	
	RenderState blend(bool a) ///
	{
		blend_ = a;
		return this;
	}
	
	bool blend() { return blend_; } ///
}

private string getGlError()
{
	switch (glGetError())
	{
		case GL_NO_ERROR:          return "";
		case GL_INVALID_ENUM:      return "GL_INVALID_ENUM";
		case GL_INVALID_VALUE:     return "GL_INVALID_VALUE";
		case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
		case GL_STACK_OVERFLOW:    return "GL_STACK_OVERFLOW";
		case GL_STACK_UNDERFLOW:   return "GL_STACK_UNDERFLOW";
		case GL_OUT_OF_MEMORY:     return "GL_OUT_OF_MEMORY";
		default:                   return "OpenGL unknown error";
	}
}

bool emptyGlError() /// invariant { assert(emptyGlError); }
{
	auto e = getGlError();
	if (e == "") return true;
	derr.writefln(e);
	return false;
}

void throwIfGlError(string file, long line) ///
{
	auto e = getGlError();
	if (e != "") throw new Error(format("%s(%d): %s", file, line, e));
}

///
class Clear : Unit
{
	private const MASK_DEFAULT = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT;
	private const Vector color;
	private const uint mask;
	
	///
	this(Vector color = vector(), uint mask = MASK_DEFAULT)
	{
		this.color = color;
		this.mask = mask;
	}
	
	void attach()
	{
		float[4] stock;
		glGetFloatv(GL_COLOR_CLEAR_VALUE, stock.ptr);
		glClearColor(color.x, color.y, color.z, color.w);
		glClear(mask);
		glClearColor(stock[0], stock[1], stock[2], stock[3]);
	}
	
	void detach() {}
}

///
class MultMatrix : Unit
{
	private Matrix matrix;
	
	///
	this(Matrix matrix = Matrix.identity)
	{
		this.matrix = matrix;
	}
	
	void attach()
	{
		glPushMatrix();
		glMultMatrixf(cast(float*)&matrix);
	}
	
	void detach()
	{
		glPopMatrix();
	}
	
	void opAssign(Matrix a) /// =
	{
		matrix = a;
	}
}

///
class EnableDisableBase(alias T) : Unit
{
	invariant()
	{
		assert(emptyGlError());
	}
	
	private const uint cap;
	private ubyte stock;
	
	///
	this(uint cap)
	{
		this.cap = cap;
	}
	
	void attach()
	in
	{
		assert(emptyGlError());
	}
	out
	{
		assert(emptyGlError());
	}
	body
	{
		stock = glIsEnabled(cap);
		T(cap);
	}
	
	void detach()
	in
	{
		assert(emptyGlError());
	}
	out
	{
		assert(emptyGlError());
	}
	body
	{
		stock == GL_TRUE ? glEnable(cap) : glDisable(cap);
	}
}

alias EnableDisableBase!(glEnable) Enable; ///
alias EnableDisableBase!(glDisable) Disable; ///

///
abstract class QuadricObj : Unit
{
	protected GLUquadricObj* handle; ///
	Vector color; ///
	GLenum drawStyle = GLU_FILL; /// GLU_FILL GLU_LINE GLU_SILHOUETTE GLU_POINT
	GLenum normals = GLU_SMOOTH; /// GLU_NONE GLU_FLAT GLU_SMOOTH
	GLenum orientation = GLU_OUTSIDE; /// GLU_OUTSIDE GLU_INSIDE
	GLboolean textureCoords = GL_TRUE; /// GL_TRUE GL_FALSE
	
	///
	this(Vector color)
	{
		this.color = color;
		handle = gluNewQuadric();
	}
	
	~this()
	{
		gluDeleteQuadric(handle);
	}
	
	void attach()
	{
		glColor3f(color.r, color.g, color.b);
		gluQuadricDrawStyle(handle, drawStyle);
		gluQuadricNormals(handle, normals);
		gluQuadricOrientation(handle, orientation);
		gluQuadricTexture(handle, textureCoords);
		draw();
	}
	
	void detach() {}
	void draw(); ///
}

///
class Sphere : QuadricObj
{
	GLdouble radius; ///
	GLint slices; ///
	GLint stacks; ///
	
	///
	this(Vector color = Color.WHITE, GLdouble radius = 10.0, GLint slices = 16, GLint stacks = 16)
	{
		super(color);
		this.radius = radius;
		this.slices = slices;
		this.stacks = stacks;
	}
	
	void draw()
	{
		gluSphere(handle, radius, slices, stacks);
	}
}

///
class Cylinder : QuadricObj
{
	GLdouble baseRadius; ///
	GLdouble topRadius; ///
	GLdouble height; ///
	GLint slices; ///
	GLint stacks; ///
	
	///
	this(
		Vector color = Color.WHITE,
		GLdouble baseRadius = 5.0,
		GLdouble topRadius = 4.0,
		GLdouble height = 10.0,
		GLint slices = 16,
		GLint stacks = 16
	)
	{
		super(color);
		this.baseRadius = baseRadius;
		this.topRadius = topRadius;
		this.height = height;
		this.slices = slices;
		this.stacks = stacks;
	}
	
	void draw()
	{
		gluCylinder(handle, baseRadius, topRadius, height, slices, stacks);
	}
}

///
class Disk : QuadricObj
{
	GLdouble innerRadius; ///
	GLdouble outerRadius; ///
	GLint slices; ///
	GLint loops; ///
	
	///
	this(
		Vector color = Color.WHITE,
		GLdouble innerRadius = 5.0,
		GLdouble outerRadius = 10.0,
		GLint slices = 16,
		GLint loops = 16
	)
	{
		super(color);
		this.innerRadius = innerRadius;
		this.outerRadius = outerRadius;
		this.slices = slices;
		this.loops = loops;
	}
	
	void draw()
	{
		gluDisk(handle, innerRadius, outerRadius, slices, loops);
	}
}

///
class PartialDisk : Disk
{
	GLdouble startAngle; ///
	GLdouble sweepAngle; ///
	
	///
	this(
		Vector color = Color.WHITE,
		GLdouble innerRadius = 5.0,
		GLdouble outerRadius = 10.0,
		GLint slices = 16,
		GLint loops = 16,
		GLdouble startAngle = 0.0,
		GLdouble sweepAngle = 180.0
	)
	{
		super(color, innerRadius, outerRadius, slices, loops);
		this.startAngle = startAngle;
		this.sweepAngle = sweepAngle;
	}
	
	override void draw()
	{
		gluPartialDisk(handle, innerRadius, outerRadius, slices, loops, startAngle, sweepAngle);
	}
}
