module coneneko.model;
import
	std.stdio,
	coneneko.modeldata,
	coneneko.unit,
	coneneko.vertexbuffer,
	coneneko.texture,
	coneneko.math,
	opengl,
	coneneko.glext,
	coneneko.rgba,
	coneneko.pcnta,
	coneneko.shader,
	coneneko.clothpcnta;

/// world変換で内部に影響が出るので、MultMatrixではなくworldを使ってください
class Model : Unit, TimeIterator
{
	invariant()
	{
		foreach (v; textures) assert(v !is null);
		foreach (v; subsets) assert(v !is null);
		assert(shader !is null);
		assert(motion !is null);
		assert(waitingMotion !is null);
		assert(1 <= boneMatrixArray.length);
	}
	
	Texture[] textures; ///
	private ModelSubset[size_t] subsets;
	ModelShader shader; ///
	Matrix world; ///
	private MotionMatrixArray motionMatrixArray;
	AnyRoute!(MotionKeys) motion, waitingMotion;
	Matrix[] boneMatrixArray;
	size_t boneLength;
	ModelSubset opIndex(size_t i) { return subsets[i]; } ///
	bool opIn_r(size_t index) { return (index in subsets) !is null; } ///
	bool hasMotion() { return motionMatrixArray !is null; }
	size_t motionLength() { return hasMotion ? motionMatrixArray.motions.length : 0; }
	size_t[] keys() { return subsets.keys; }
	
	private this() {}
	
	///
	this(ModelData data)
	{
		shader = new DefaultModelShader();
		world = Matrix.identity;
		foreach (v; data.textures) textures ~= new Texture(v, 0);
		if (data.base !is null)
		{
			subsets[0] = data.base.hasAttribute
				? new SkinMeshSubset(this, data.base)
				: new BasicSubset(this, data.base, 0);
			subsets[0].visible = true;
		}
		auto indexOf = new BoneIndexExtractor(data.skeleton);
		foreach (i, v; data.options) subsets[i] = new BasicSubset(this, v, indexOf(v));
		foreach (i, v; data.morphs) subsets[i] = new MorphSubset(this, v, indexOf(v));
		foreach (i, v; data.clothes) subsets[i] = new ClothSubset(this, v, indexOf(v));
		if (data.motions.length != 0 && data.skeleton !is null)
		{
			motionMatrixArray = new MotionMatrixArray(data.motions, data.skeleton);
		}
		motion = new AnyRoute!(MotionKeys)();
		waitingMotion = new AnyRoute!(MotionKeys)();
		
		boneLength = data.skeleton is null ? 0 : data.skeleton.boneTriangles.length / 3;
		auto ma = new Matrix[boneLength == 0 ? 1 : boneLength];
		ma[] = Matrix.identity;
		boneMatrixArray = ma;
	}
	
	void attach()
	{
		foreach (v; subsets.values)
		{
			v.attach();
			v.detach();
		}
	}
	
	void detach() {}
	
	Model clone()
	{
		auto result = new Model();
		result.textures = textures;
		result.subsets = subsets;
		result.shader = shader;
		result.world = world;
		result.motion = motion;
		assert(false); // TODO
		return result;
	}
	
	void opPostInc()
	{
		if (motion.valid)
		{
			motion.t++;
			if (!motion.valid) waitingMotion.t = 0;
		}
		else
		{
			waitingMotion.t++;
			if (!waitingMotion.valid) waitingMotion.t = 0;
		}
		
		if (motionMatrixArray !is null) boneMatrixArray = getBoneMatrixArray();
		
		foreach (v; subsets.values) v++;
	}
	
	private Matrix[] getBoneMatrixArray()
	in
	{
		assert(motionMatrixArray !is null);
	}
	body
	{
		if (motion.valid) return motionMatrixArray.get(motion);
		else if (waitingMotion.valid) return motionMatrixArray.get(waitingMotion);
		else return motionMatrixArray.getDefault();
	}
	
	MotionKeysRoute createMotion(size_t motionIndex)
	in
	{
		assert(motionMatrixArray !is null);
	}
	out (result)
	{
		assert(result.valid);
	}
	body
	{
		auto result = new MotionKeysRoute();
		MotionKeys[] keys;
		auto keyFrames = motionMatrixArray.motions[motionIndex].positionMap.keys;
		foreach (keyFrame; keyFrames) keys ~= MotionKeys(motionIndex, keyFrame);
		result = keys;
		keyFrames.sort;
		result.span = keyFrames[$ - 1];
		return result;
	}
}

class BoneIndexExtractor
{
	private const Vector[] tris;
	
	this(SkeletonData data)
	{
		tris = data !is null ? data.boneTriangles : null;
	}
	
	size_t opCall(BasicPcnta pcnta)
	{
		if (tris.length == 0) return 0;
		return getMinIndex(computeDistances(pcnta.position));
	}
	
	size_t opCall(MorphPcnta pcnta)
	{
		if (tris.length == 0) return 0;
		return getMinIndex(computeDistances(pcnta.position));
	}
	
	private float[] computeDistances(Vector position)
	{
		float[] result;
		for (int i = 0; i < tris.length; i += 3)
		{
			float a = distanceOfPointAndLineSegment(position, tris[i], tris[i + 1]);
			float b = distance(tris[i], tris[i + 2]);
			result ~= a - b;
		}
		return result;
	}
	
	private size_t getMinIndex(float[] a)
	{
		size_t result = 0;
		for (int i = 1; i < a.length; i++)
		{
			if (a[i] < a[result]) result = i;
		}
		return result;
	}
}

abstract class ModelSubset : Unit, TimeIterator
{
	invariant()
	{
		assert(owner !is null);
	}
	
	protected Model owner;
	bool visible;
	Matrix world;
	size_t boneIndex;
	void detach() {}
	AnyRoute!(uint) motion, waitingMotion; /// morph専用
	
	this(Model owner, size_t boneIndex)
	{
		this.owner = owner;
		this.boneIndex = boneIndex;
		world = Matrix.identity;
	}
}

class BasicSubset : ModelSubset
{
	invariant()
	{
		assert(buffers.length <= owner.textures.length);
		foreach (v; buffers) assert(v !is null);
	}
	
	protected PcntaBuffer[size_t] buffers; // [textureIndex]
	
	this(Model owner, BasicPcnta data, size_t boneIndex)
	{
		super(owner, boneIndex);
		foreach (textureIndex; data.keys) buffers[textureIndex] = new PcntaBuffer(data[textureIndex]);
	}
	
	void attach()
	{
		if (!visible) return;
		
		auto mwm = new MultMatrix(owner.boneMatrixArray[boneIndex] * world * owner.world);
		mwm.attach();
		scope (exit) mwm.detach();
		
		auto shader = owner.shader.getBasic();
		shader.attach();
		foreach (textureIndex; buffers.keys)
		{
			owner.textures[textureIndex].attach();
			buffers[textureIndex].attach();
			buffers[textureIndex].detach();
			owner.textures[textureIndex].detach();
		}
		shader.detach();
	}
	
	void opPostInc() {}
}

class MorphSubset : ModelSubset
{
	invariant()
	{
		assert(motion !is null);
		assert(waitingMotion !is null);
	}
	
	private PcntaBuffer[size_t][size_t] buffers; // [target][textureIndex]
	private AttributeBuffer[size_t][size_t] attributePositions;
	bool opIn_r(size_t target) { return (target in buffers) !is null; }
	size_t[] keys() { return buffers.keys; }
	
	this(Model owner, MorphPcnta data, size_t boneIndex)
	{
		super(owner, boneIndex);
		foreach (target; data.keys)
		{
			foreach (textureIndex; data[target].keys)
			{
				buffers[target][textureIndex] = new PcntaBuffer(data[target][textureIndex]);
				attributePositions[target][textureIndex] = new AttributeBuffer(data[target][textureIndex].positions, 3);
			}
		}
		motion = new AnyRoute!(uint)();
		waitingMotion = new AnyRoute!(uint)();
	}
	
	void attach()
	{
		if (!visible) return;
		auto m = motion.valid ? motion : waitingMotion;
		if (!m.valid) return;
		
		auto mwm = new MultMatrix(owner.boneMatrixArray[boneIndex] * world * owner.world);
		mwm.attach();
		scope (exit) mwm.detach();
		
		auto shader = owner.shader.getMorph();
		shader["interpolater"] = m.interpolater;
		shader.attach();
		auto currentPcnt = buffers[m.current];
		auto nextA = attributePositions[m.next];
		if (currentPcnt.length == 1 && nextA.length == 1)
		{
			// morph subsetのtextureを一つだけに制限した場合のみtextureアニメーションが可能
			auto currentTextureIndex = currentPcnt.keys[0];
			auto nextTextureIndex = nextA.keys[0];
			auto texture = owner.textures[currentTextureIndex];
			auto attribute = nextA[nextTextureIndex];
			auto pcnt = currentPcnt[currentTextureIndex];
			texture.attach();
			attribute.location = shader.getAttributeLocation("nextPosition");
			attribute.attach();
			pcnt.attach();
			pcnt.detach();
			attribute.detach();
			texture.detach();
		}
		else
		{
			foreach (textureIndex; currentPcnt.keys)
			{
				auto texture = owner.textures[textureIndex];
				auto attribute = nextA[textureIndex];
				auto pcnt = currentPcnt[textureIndex];
				texture.attach();
				attribute.location = shader.getAttributeLocation("nextPosition");
				attribute.attach();
				pcnt.attach();
				pcnt.detach();
				attribute.detach();
				texture.detach();
			}
		}
		shader.detach();
	}
	
	void opPostInc()
	{
		if (!visible) return;
		if (motion.valid)
		{
			motion.t++;
			if (!motion.valid) waitingMotion.t = 0;
		}
		else
		{
			waitingMotion.t++;
			if (!waitingMotion.valid) waitingMotion.t = 0;
		}
	}
}

class ClothSubset : ModelSubset
{
	ClothPcnta clothPcnta;
	
	this(Model owner, ClothPcnta data, size_t boneIndex)
	{
		super(owner, boneIndex);
		clothPcnta = data;
	}
	
	void attach()
	{
		if (!visible) return;
		auto shader = owner.shader.getBasic();
		shader.attach();
		foreach (textureIndex; clothPcnta.keys)
		{
			owner.textures[textureIndex].attach();
			draw(clothPcnta[textureIndex]);
			owner.textures[textureIndex].detach();
		}
		shader.detach();
	}
	
	void opPostInc()
	{
		if (visible) clothPcnta.tick(owner.boneMatrixArray[boneIndex] * world * owner.world);
	}
}

class SkinMeshSubset : BasicSubset
{
	this(Model owner, BasicPcnta data)
	{
		super(owner, data, 0);
	}
	
	override void attach()
	{
		if (!visible) return;
		
		auto mwm = new MultMatrix(world * owner.world);
		mwm.attach();
		scope (exit) mwm.detach();
		
		auto shader = owner.shader.getSkinMesh();
		shader["boneMatrixArray"] = owner.boneMatrixArray;
		shader.attach();
		foreach (textureIndex; buffers.keys)
		{
			owner.textures[textureIndex].attach();
			buffers[textureIndex].attributeLocation = shader.getAttributeLocation("matrixIndexWeight");
			buffers[textureIndex].attach();
			buffers[textureIndex].detach();
			owner.textures[textureIndex].detach();
		}
		shader.detach();
	}
}

class MotionMatrixArray
{
	MotionData[] motions;
	private SkeletonData skeleton;
	
	this(MotionData[] motions, SkeletonData skeleton)
	{
		this.motions = motions;
		this.skeleton = skeleton;
	}
	
	Matrix[] get(AnyRoute!(MotionKeys) route)
	in
	{
		assert(route.valid);
	}
	out (result)
	{
		assert(result.length == boneLength);
	}
	body
	{
		Matrix[] result = new Matrix[boneLength];
		for (int i = 0; i < boneLength; i++)
		{
			auto boneRouteIndices = skeleton.boneRouteIndicesArray[i];
			auto m = getLookAtMatrix(boneRouteIndices[0]);
			for (int j = 0; j < boneRouteIndices.length; j++)
			{
				auto boneIndex = boneRouteIndices[j];
				assert(boneIndex < boneLength);
				m *= rotation(route, boneIndex);
				
				if (boneRouteIndices[j] == 0) break;
				
				auto parentBoneIndex = boneRouteIndices[j + 1];
				auto start = skeleton.boneTriangles[3 * boneIndex];
				m *= Matrix.translation(mulW(start, getLookAtMatrix(parentBoneIndex)));
			}
			m *= translation(route);
			result[i] = m;
		}
		return result;
	}
	
	uint boneLength()
	{
		return skeleton.boneTriangles.length / 3;
	}
	
	private Matrix getLookAtMatrix(uint boneIndex)
	{
		auto start = skeleton.boneTriangles[3 * boneIndex];
		auto end = skeleton.boneTriangles[3 * boneIndex + 1];
		auto head = skeleton.boneTriangles[3 * boneIndex + 2];
		Vector up = normalize(head - start);
		return Matrix.lookAtLH(
			start.x, start.y, start.z,  end.x, end.y, end.z,  up.x, up.y, up.z
		);
	}
	
	private Matrix translation(AnyRoute!(MotionKeys) route)
	{
		return Matrix.translation(
			linear(
				motions[route.current.motionIndex].positionMap[route.current.keyFrame],
				motions[route.next.motionIndex].positionMap[route.next.keyFrame],
				route.interpolater
			)
		);
	}
	
	private Matrix rotation(AnyRoute!(MotionKeys) route, uint boneIndex)
	in
	{
		assert(boneIndex < motions[0].poseMap[0].length);
	}
	body
	{
		return createMatrixFromQuaternion(
			createSlerpQuaternion(
				motions[route.current.motionIndex].poseMap[route.current.keyFrame][boneIndex],
				motions[route.next.motionIndex].poseMap[route.next.keyFrame][boneIndex],
				route.interpolater
			)
		);
	}
	
	Matrix[] getDefault()
	{
		Matrix[] result = new Matrix[boneLength];
		result[] = Matrix.identity;
		return result;
	}
}
