module coneneko.mkmtranslator;
import std.stream, std.string, coneneko.mkm, coneneko.modeldata,
	coneneko.math, coneneko.mqotranslator, coneneko.mqo, coneneko.container;

class MkmTranslator : Mkm, Translator
{
	private ModelData owner;
	
	void initialize(ModelData owner)
	{
		this.owner = owner;
	}
	
	override void deserialize(Stream reader)
	in
	{
		assert(owner !is null);
		assert(reader !is null);
	}
	body
	{
		super.deserialize(reader);
		foreach (v; motions) owner.motions ~= createMotionData(v);
	}
	
	private MotionData createMotionData(MkmMotion motion)
	{
		MotionData result = new MotionData();
		foreach (v; motion.vector.keyFrames) result.positionMap[v.t] = v.value;
		foreach (v; motion.quaternions)
		{
			foreach (w; v.keyFrames) result.poseMap[w.t] ~= w.value;
		}
		return result;
	}
}

class MkmSkeleton : SkeletonData
{
	unittest
	{
		//   3      2
		// 2 1 -> 3 1
		//   0      0
		auto bones = new MkmBone[4];
		bones[0] = new MkmBone(vector(0, 0, 0),  vector(0, 2, 0),  vector(1, 0, 0));
		bones[1] = new MkmBone(vector(0, 4, 0), vector(0, 6, 0), vector(1, 4, 0));
		bones[2] = new MkmBone(vector(0, 2, 0), vector(-2, 2, 0), vector(0, 3, 0));
		bones[3] = new MkmBone(vector(0, 6, 0), vector(0, 8, 0), vector(1, 6, 0));
		auto lineLinks = new MkmLineLink[1];
		lineLinks[0] = new MkmLineLink(0, 2, 0,  0, 4, 0);
		auto skeleton = new MkmSkeleton(bones, lineLinks);
		assert(3 == skeleton.links.length);
		assert(bones[0] == skeleton.bones[0]);
		assert(bones[1] == skeleton.bones[1]);
		assert(bones[3] == skeleton.bones[2]);
		assert(bones[2] == skeleton.bones[3]);

		// 置き換え前の関連順と同じ
		assert(bones[0] == skeleton.links[0].first);
		assert(bones[1] == skeleton.links[0].second);
		assert(bones[0] == skeleton.links[1].first);
		assert(bones[2] == skeleton.links[1].second);
		assert(bones[1] == skeleton.links[2].first);
		assert(bones[3] == skeleton.links[2].second);
		
		auto routes = skeleton.createBoneRouteIndicesArray();
		assert(routes.length == 4);
		assert(routes[0] == cast(uint[])[0]);
		assert(routes[1] == cast(uint[])[1, 0]);
		assert(routes[2] == cast(uint[])[2, 1, 0]);
		assert(routes[3] == cast(uint[])[3, 0]);
	}
	
	MkmBone[] bones;
	private MkmBoneLink[] links;
	
	this(Mqo mqo)
	{
		auto boneObject = getBoneObject(mqo);
		this(getBones(mqo.material, boneObject), getLineLinks(boneObject));
	}
	
	static bool hasBone(Mqo mqo)
	{
		foreach (v; mqo.object)
		{
			if (v.name.length >= 5 && "bone:" == v.name[0..5]) return true;
		}
		return false;
	}
	
	private MqoObject getBoneObject(Mqo mqo)
	{
		foreach (v; mqo.object)
		{
			if (v.name.length >= 5 && "bone:" == v.name[0..5]) return v;
		}
		throw new Error("MkmSkeleton: not found bone");
	}
	
	private MkmBone[] getBones(MqoMaterial[] materials, MqoObject boneObject)
	out (result)
	{
		assert(1 <= result.length);
	}
	body
	{
		MkmBone[] result;
		auto vertex = boneObject.vertex;
		foreach (v; boneObject.face)
		{
			if (v.vlength != 3) continue;
			result ~= new MkmBone(
				materials[v.M].name,
				vector(vertex[v.V[0]].x, vertex[v.V[0]].y, vertex[v.V[0]].z),
				vector(vertex[v.V[1]].x, vertex[v.V[1]].y, vertex[v.V[1]].z),
				vector(vertex[v.V[2]].x, vertex[v.V[2]].y, vertex[v.V[2]].z)
			);
		}
		return result;
	}
	
	private MkmLineLink[] getLineLinks(MqoObject boneObject)
	{
		MkmLineLink[] result;
		auto vertex = boneObject.vertex;
		foreach (v; boneObject.face)
		{
			if (v.vlength != 2) continue;
			result ~= new MkmLineLink(
				vertex[v.V[0]].x, vertex[v.V[0]].y, vertex[v.V[0]].z,
				vertex[v.V[1]].x, vertex[v.V[1]].y, vertex[v.V[1]].z
			);
		}
		return result;
	}
	
	private this(MkmBone[] bones, MkmLineLink[] lineLinks)
	{
		this.bones = bones.dup;
		this.links = createMkmBoneLinks(lineLinks);
		relocate();
		
		boneRouteIndicesArray = createBoneRouteIndicesArray();
		foreach (v; this.bones)
		{
			boneTriangles ~= v.startPosition;
			boneTriangles ~= v.endPosition;
			boneTriangles ~= v.headPosition;
		}
	}
	
	private MkmBoneLink[] createMkmBoneLinks(MkmLineLink[] lineLinks)
	in
	{
		assert(bones !is null);
	}
	body
	{
		MkmBoneLink[] result;
		foreach (a; bones)
		{
			foreach (b; bones)
			{
				if (a is b) continue;
				if (a.endPosition == b.startPosition) result ~= new MkmBoneLink(a, b);
				else
				{
					foreach (c; lineLinks)
					{
						if (c.isLinking(a.endPosition, b.startPosition))
						{
							result ~= new MkmBoneLink(a, b);
						}
					}
				}
			}
		}
		return result;
	}
	
	// boneListを再帰的ツリーの順に並び替える
	private void relocate()
	in
	{
		assert(bones !is null);
		assert(links !is null);
	}
	body
	{
		void relocateR(ref MkmBone[] result, MkmBone currentBone) // 再帰
		{
			result ~= currentBone;
			foreach (v; links)
			{
				if (currentBone is v.first) relocateR(result, v.second);
			}
		}
		
		MkmBone[] result;
		relocateR(result, getRootBone());
		if (bones.length != result.length) throw new Error("MkmSkeleton: bone relocate error");
		bones[] = result;
	}
	
	private MkmBone getParent(MkmBone bone)
	{
		foreach (v; links)
		{
			if (v.second is bone) return v.first;
		}
		return null;
	}
	
	private MkmBone getRootBone()
	{
		MkmBone current = bones[0];
		while (true)
		{
			auto parent = getParent(current);
			if (parent is null) return current;
			current = parent;
		}
		throw new Error("MkmSkeleton.getRootBone");
	}
	
	private uint indexOf(MkmBone bone)
	{
		for (int i = 0; i < bones.length; i++)
		{
			if (bone is bones[i]) return i;
		}
		throw new Error("MkmSkeleton.indexOf");
	}
	
	uint[][] createBoneRouteIndicesArray()
	out (result)
	{
		assert(result.length == bones.length);
	}
	body
	{
		uint[][] result;
		for (int i = 0; i < bones.length; i++)
		{
			auto currentBone = bones[i];
			uint[] route;
			route ~= i;
			while (true)
			{
				auto parentBone = getParent(currentBone);
				if (parentBone is null) break;
				currentBone = parentBone;
				route ~= indexOf(currentBone);
			}
			result ~= route;
		}
		return result;
	}
}

class MkmBoneLink
{
	MkmBone first, second;
	
	this(MkmBone first, MkmBone second)
	{
		this.first = first;
		this.second = second;
	}
}

class MkmLineLink
{
	unittest
	{
		Vector a = vector(0, 0, 0);
		Vector b = vector(1, 1, 1);
		Vector c = vector(2, 2, 2);
		MkmLineLink ab = new MkmLineLink(a.x, a.y, a.z, b.x, b.y, b.z);
		assert(ab.isLinking(a, b));
		assert(ab.isLinking(b, a));
		assert(!ab.isLinking(a, c));
		assert(!ab.isLinking(b, c));
		assert(!ab.isLinking(c, a));
		assert(!ab.isLinking(c, b));
	}

	Vector first, second;
	
	this(float a0, float a1, float a2, float b0, float b1, float b2)
	{
		this.first = vector(a0, a1, a2);
		this.second = vector(b0, b1, b2);
	}
	
	bool isLinking(Vector a, Vector b)
	{
		return (first == a && second == b) || (first == b && second == a);
	}
}

unittest
{
	assert("hoge" == insertRL("hoge", 1.0));
	assert("hoge[L]" == insertRL("hoge[]", 1.0));
	assert("hoge[R]" == insertRL("hoge[]", -1.0));
}

string insertRL(string materialName, float x) // []の中のR, Lを入れる
{
	if (-1 == materialName.find("[]")) return materialName;
	return materialName.replace("[]", x >= 0 ? "[L]" : "[R]");
}

class MkmBone
{
	unittest
	{
		Vector p0 = vector(0, 0, 0);
		Vector p1 = vector(0, 2, 0);
		Vector p2 = vector(1, 0, 0);
		auto a = new MkmBone(p0, p1, p2);
		assert(p0 == a.startPosition);
		assert(p1 == a.endPosition);
		assert(p2 == a.headPosition);
		auto b = new MkmBone(p1, p2, p0);
		assert(p0 == b.startPosition);
		assert(p1 == b.endPosition);
		assert(p2 == b.headPosition);
		assert(a == b);
		assert(a != new MkmBone(p0, p0, p0));
	}
	
	Vector startPosition, endPosition, headPosition;
	private alias coneneko.math.max max;
	private alias coneneko.math.min min;
	string name; // [] R, L付き
	
	this(Vector p0, Vector p1, Vector p2)
	{
		float d01 = .distance(p0, p1);
		float d02 = .distance(p0, p2);
		float d12 = .distance(p1, p2);
		float dmax = max(max(d01, d02), d12);
		float dmin = min(min(d01, d02), d12);
		startPosition = dmax == d01 ? p2 : dmax == d02 ? p1 : p0;
		endPosition = dmin == d01 ? p2 : dmin == d02 ? p1 : p0;
		headPosition = p0 != startPosition && p0 != endPosition ? p0 :
			p1 != startPosition && p1 != endPosition ? p1 : p2;
	}
	
	this(string materialName, Vector p0, Vector p1, Vector p2)
	{
		this.name = insertRL(materialName, p0.x);
		this(p0, p1, p2);
	}
	
	int opEquals(MkmBone a)
	{
		return startPosition == a.startPosition
			&& endPosition == a.endPosition
			&& headPosition == a.headPosition;
	}
	
	float distance(Vector position) // 線分
	{
		return coneneko.math.distanceOfPointAndLineSegment(position, startPosition, endPosition);
	}
}

interface Indicator
{
	void advance(float percent);
}

class NullIndicator : Indicator
{
	void advance(float percent) {}
}

class MkmMqoTranslator : MqoExTranslator
{
	private Indicator indicator;
	
	this(Indicator indicator = new NullIndicator())
	{
		this.indicator = indicator;
	}
	
	override void deserialize(Stream reader)
	{
		super.deserialize(reader);
		if (!MkmSkeleton.hasBone(this)) return;
		
		auto mkmSkeleton = new MkmSkeleton(this);
		owner.skeleton = mkmSkeleton;
		
		float total = 0, current = 0;
		foreach (v; owner.base.keys) total += owner.base[v].length;
		
		auto generator = new MatrixIndexAndWeightGenerator(mkmSkeleton.bones);
		foreach (v; new Iteration())
		{
			if (!v.getObject().visible) continue;
			auto mi = generator.getMatrixIndex(v);
			owner.base.addA(
				textures.getIndex(v.getTextureFileName()),
				vector(mi.x, mi.y, generator.getWeight(mi, v.getPosition))
			);
			current += 1.0;
			indicator.advance(current * 100.0 / total);
		}
	}
	
	class MatrixIndexAndWeightGenerator
	{
		private MkmBone[] bones;
		private Anchors anchors;
		
		this(MkmBone[] bones)
		{
			this.bones = bones;
			auto anchorsBuilder = new AnchorsBuilder();
			foreach (Iterator v; new Iteration())
			{
				anchorsBuilder.add(
					v.getObject().name,
					v.getMaterialName(),
					v.getPosition()
				);
			}
			anchors = anchorsBuilder.build();
		}
		
		Vector getMatrixIndex(Iterator v)
		out (result)
		{
			assert(0 <= result.x && result.x < bones.length);
			assert(0 <= result.y && result.y < bones.length);
		}
		body
		{
			if (isSdef(v.getObject().name))
			{
				try
				{
					auto anchorNames = anchors.innerAnchorNames(
						removeSdef(v.getObject().name), v.getPosition()
					);
					if (2 <= anchorNames.length)
					{
						auto indices = getBoneIndices(anchorNames);
						auto firstIndex = getNearBoneIndex(indices, v.getPosition());
						indices.remove(firstIndex);
						auto secondIndex = getNearBoneIndex(indices, v.getPosition());
						return vector(cast(float)firstIndex, cast(float)secondIndex);
					}
					else if (anchorNames.length == 1)
					{
						auto index = getBoneIndex(anchorNames[0]);
						return vector(cast(float)index, cast(float)index);
					}
				}
				catch (Exception e) {} // anchorに入ってるのに、anchorとlinkしたboneが存在しない
			}
			
			auto index = getNearBoneIndex(v.getPosition());
			return vector(cast(float)index, cast(float)index);
		}
		
		private uint[] getBoneIndices(string[] names) // anchorName == boneName
		{
			uint[] result;
			foreach (v; names) result ~= getBoneIndex(v);
			return result;
		}
		
		private uint getBoneIndex(string name) // MaterialName == BoneName
		{
			for (int i = 0; i < bones.length; i++)
			{
				if (bones[i].name == name) return i;
			}
			throw new Exception("MatrixIndexAndWeightGenerator.getBoneIndex: not found");
		}
		
		private uint getNearBoneIndex(Vector p)
		{
			uint[] indices;
			for (int i = 0; i < bones.length; i++)
			{
				if (innerPosition(bones[i], p)) indices ~= i;
			}
			return getNearBoneIndex(indices.length != 0 ? indices : allBoneIndices, p);
		}
		
		private bool innerPosition(MkmBone bone, Vector p)
		{
			return bone.distance(p) <= distance(bone.startPosition, bone.headPosition);
		}
		
		private uint[] allBoneIndices()
		{
			uint[] result;
			for (int i = 0; i < bones.length; i++) result ~= i;
			return result;
		}
		
		private uint getNearBoneIndex(uint[] boneIndices, Vector p)
		in
		{
			assert(boneIndices.length != 0);
		}
		body
		{
			uint result = boneIndices[0];
			float resultDistance = bones[result].distance(p);
			foreach (i; boneIndices[1..$])
			{
				auto a = bones[i].distance(p);
				if (a < resultDistance)
				{
					resultDistance = a;
					result = i;
				}
			}
			return result;
		}
		
		float getWeight(Vector matrixIndex, Vector p)
		out (result)
		{
			assert(0.5 <= result && result <= 1.0);
		}
		body
		{
			auto firstIndex = cast(uint)matrixIndex.x;
			auto secondIndex = cast(uint)matrixIndex.y;
			if (firstIndex == secondIndex) return 1.0;
			auto fd = bones[firstIndex].distance(p);
			auto sd = bones[secondIndex].distance(p);
			return sd / (fd + sd);
		}
		
		unittest
		{
			assert(isSdef("sdef:hoge"));
			assert(!isSdef("aaa:hoge"));
		}
		
		private static bool isSdef(string objectName)
		{
			return "sdef:".length <= objectName.length && "sdef:" == objectName[0..5];
		}
		
		unittest
		{
			assert("hoge" == removeSdef("sdef:hoge"));
			assert("" == removeSdef("sdef:"));
		}
		
		private static string removeSdef(string sdefObjectName)
		in
		{
			assert(isSdef(sdefObjectName));
		}
		body
		{
			return sdefObjectName[5..$];
		}
	}
}

class AnchorsBuilder
{
	private Vector[][string][string] areaTriangles; // [targetObjectName][materialName(LR付き)]
	
	void add(string objectName, string materialName, Vector position)
	{
		if (!isAnchor(objectName)) return;
		auto target = getRelationalObjectName(objectName);
		auto materialLR = insertRL(materialName, position.x);
		areaTriangles[target][materialLR] ~= position;
	}
	
	unittest
	{
		assert(isAnchor("anchor1|hoge"));
		assert(isAnchor("anchor2|hoge"));
		assert(!isAnchor("hoge"));
	}
	
	private static bool isAnchor(string objectName)
	{
		return 6 <= objectName.length && objectName[0..6] == "anchor";
	}
	
	unittest
	{
		assert("hoge" == getRelationalObjectName("anchorhoge|hoge"));
		assert("aaa" == getRelationalObjectName("anchorabc|aaa"));
		assert("abc" != getRelationalObjectName("anchorabc|aaa"));
		assert("" == getRelationalObjectName("anchorhoge"));
	}
	
	private static string getRelationalObjectName(string anchorObjectName)
	in
	{
		assert(isAnchor(anchorObjectName));
	}
	body
	{
		auto a = anchorObjectName.split("|");
		return 2 <= a.length ? a[1] : "";
	}
	
	Anchors build()
	{
		return new Anchors(areaTriangles);
	}
}

class Anchors
{
	private Vector[][string][string] areaTriangles;
	
	this(Vector[][string][string] areaTriangles)
	{
		this.areaTriangles = areaTriangles;
	}
	
	string[] innerAnchorNames(string objectName, Vector a) // AnchorName == MaterialName
	{
		string[] result;
		auto materialToAreaTriangles = areaTriangles[objectName];
		foreach (key; materialToAreaTriangles.keys)
		{
			if (innerArea(materialToAreaTriangles[key], a)) result ~= key;
		}
		return result;
	}
}
