module coneneko.clothpcnta;
import coneneko.pcnta, coneneko.math, coneneko.serializer, std.stream;

protected interface Positions
{
	Vector opIndex(size_t i);
	void opIndexAssign(Vector value, size_t i);
	size_t length();
}

private class Link
{
	invariant()
	{
		assert(positions[firstIndex].w == 0.0f);
		assert(positions[secondIndex].w == 0.0f);
	}
	
	private Positions positions;
	const uint firstIndex, secondIndex;
	Vector firstPosition() { return positions[firstIndex]; }
	Vector secondPosition() { return positions[secondIndex]; }
	void firstPosition(Vector a) { positions[firstIndex] = a; }
	void secondPosition(Vector a) { positions[secondIndex] = a; }
	
	this(Positions positions, uint firstIndex, uint secondIndex)
	in
	{
		assert(firstIndex != secondIndex);
		assert(firstIndex < positions.length);
		assert(secondIndex < positions.length);
	}
	body
	{
		this.positions = positions;
		this.firstIndex = firstIndex;
		this.secondIndex = secondIndex;
	}
}

private class MirrorLink : Link // secondPositionがfirstPositionに依存
{
	this(Positions p, uint fi, uint si) { super(p, fi, si); }
	void sync() { secondPosition = firstPosition; }
}

private class AnyFloatingLink : Link
{
	const Vector firstInit, secondInit;
	
	this(Positions p, uint fi, uint si)
	{
		super(p, fi, si);
		firstInit = firstPosition;
		secondInit = secondPosition;
	}
	
	Vector springForce(Matrix world, float spring)
	out (result)
	{
		assert(result.w == 0.0f);
	}
	body
	{
		auto target = mulW(secondInit, world) - mulW(firstInit, world);
		return (target - getCurrentFirstToSecond(world)) * spring / 60.0;
	}
	
	protected Vector reflect(Matrix world) // 長さ制限
	out (result)
	{
		assert(result.w == 0.0f);
	}
	body
	{
		auto currentFirstToSecond = getCurrentFirstToSecond(world);
		auto diff = scalar(currentFirstToSecond) - distance(firstInit, secondInit);
		if (diff <= 0.0) return vector(0.0, 0.0, 0.0); // error?
		return -normalize(currentFirstToSecond) * diff;
	}
	
	abstract Vector getCurrentFirstToSecond(Matrix world);
}

private class FixingFloatingLink : AnyFloatingLink
{
	this(Positions p, uint fi, uint si) { super(p, fi, si); }
	Vector getCurrentFirstToSecond(Matrix w) { return secondPosition - mulW(firstPosition, w); }
	void doReflect(Matrix world) { secondPosition = secondPosition + reflect(world); }
}

private class FloatingFloatingLink : AnyFloatingLink
{
	this(Positions p, uint fi, uint si) { super(p, fi, si); }
	Vector getCurrentFirstToSecond(Matrix w) { return secondPosition - firstPosition; }
	void doReflect(Matrix world)
	{
		auto r = reflect(world) * 0.5;
		firstPosition = firstPosition - r;
		secondPosition = secondPosition + r;
	}
}

/// vertexのattributeとは違う
class ClothAttributes : Serializer
{
	const FIXING = uint.max, FLOATING = uint.max - 1, MIRROR_MAX = uint.max - 2;
	private uint[] attributes;
	bool isFixing(size_t index) { return attributes[index] == FIXING; }
	bool isFloating(size_t index) { return attributes[index] == FLOATING; }
	bool isMirror(size_t index) { return attributes[index] <= MIRROR_MAX; } // floatingのみ
	size_t length() { return attributes.length; }
	this() {}
	private const HEADER = "ClothAttributes";
	
	this(Positions p, Vector[] areaTriangles)
	{
		for (int i = 0; i < p.length; i++)
		{
			attributes ~= innerArea(areaTriangles, p[i]) ? FLOATING : FIXING;
		}
		for (int i = 0; i < p.length - 1; i++)
		{
			if (!isFloating(i)) continue;
			for (int j = i + 1; j < p.length; j++)
			{
				if (isFloating(j) && p[i] == p[j]) attributes[j] = i;
			}
		}
	}
	
	uint targetIndex(size_t index)
	in
	{
		assert(isMirror(index));
	}
	body
	{
		return attributes[index];
	}
	
	void serialize(Stream writer)
	{
		with (new SerializeStream(writer))
		{
			write(cast(char[])HEADER); // d2
			write(attributes);
		}
	}
	
	void deserialize(Stream reader)
	{
		with (new SerializeStream(reader))
		{
			throwIfHeaderError(HEADER);
			attributes = readUints();
		}
	}
}

private Link[] createLinks(Positions p) // triangleで全てのlinkを作る
{
	Link[] result;
	for (int i = 0; i < p.length; i += 3)
	{
		result ~= new Link(p, i + 0, i + 1);
		result ~= new Link(p, i + 1, i + 2);
		result ~= new Link(p, i + 2, i + 0);
	}
	return result;
}

private MirrorLink[] createMirrorLinks(Positions p, ClothAttributes ca)
{
	MirrorLink[] result;
	for (int i = 0; i < ca.length; i++)
	{
		if (ca.isMirror(i)) result ~= new MirrorLink(p, ca.targetIndex(i), i);
	}
	return result;
}

private FixingFloatingLink[] createFixingFloatingLinks(Positions p, ClothAttributes ca, float spring)
{
	FixingFloatingLink[] result;
	foreach (v; createLinks(p))
	{
		auto firstIsFixing = ca.isFixing(v.firstIndex);
		auto secondIsFixing = ca.isFixing(v.secondIndex);
		
		if (firstIsFixing && secondIsFixing) continue;
		if (!firstIsFixing && !secondIsFixing) continue;
		
		auto fixingIndex = firstIsFixing ? v.firstIndex : v.secondIndex;
		auto floatingIndex = firstIsFixing ? v.secondIndex : v.firstIndex;
		
		result ~= new FixingFloatingLink(
			p,
			fixingIndex,
			ca.isMirror(floatingIndex) ? ca.targetIndex(floatingIndex) : floatingIndex
		);
	}
	return result;
}

private FloatingFloatingLink[] createFloatingFloatingLinks(Positions p, ClothAttributes ca, float spring)
{
	FloatingFloatingLink[] result;
	foreach (v; createLinks(p))
	{
		if (ca.isFixing(v.firstIndex) || ca.isFixing(v.secondIndex)) continue;
		result ~= new FloatingFloatingLink(
			p,
			ca.isFloating(v.firstIndex) ? v.firstIndex : ca.targetIndex(v.firstIndex),
			ca.isFloating(v.secondIndex) ? v.secondIndex : ca.targetIndex(v.secondIndex)
		);
	}
	return result;
}

private class ClothPositions : Positions
{
	private ClothPcnta cp;
	private Vector[] mirror;
	Vector opIndex(size_t i) { return mirror[i]; }
	void opIndexAssign(Vector value, size_t i) { mirror[i] = value; }
	size_t length() { return mirror.length; }
	
	this(ClothPcnta cp)
	{
		this.cp = cp;
		foreach (v; cp.keys) mirror ~= cp[v].positions;
	}
	
	void commit()
	{
		int i = 0;
		foreach (v; cp.keys)
		{
			foreach (ref w; cp[v].positions) w = mirror[i++];
		}
	}
}

class ClothPcnta : BasicPcnta, Serializer
{
	float spring, resistance; /// 係数、ばねと抵抗
	private ClothAttributes attributes;
	private ClothPositions positions;
	Vector outsideForce; /// [cm/ss], 頂点に与える力
	private MirrorLink[] mirrorLinks;
	private FixingFloatingLink[] fixingFloatingLinks;
	private FloatingFloatingLink[] floatingFloatingLinks;
	private Vector[] initPositions;
	private Vector[] prePositions;
	private const HEADER = "ClothPcnta";
	
	this() {}
	
	///
	this(BasicPcnta basic, float spring, float resistance, Vector[] areaTriangles)
	{
		foreach (v; basic.keys) pcntaMap[v] = basic[v];
		this.spring = spring;
		this.resistance = resistance;
		positions = new ClothPositions(this);
		attributes = new ClothAttributes(positions, areaTriangles);
		initialize();
	}
	
	private void initialize()
	{
		outsideForce = vector(0.0, 0.0, 0.0);
		mirrorLinks = createMirrorLinks(positions, attributes);
		fixingFloatingLinks = createFixingFloatingLinks(positions, attributes, spring);
		floatingFloatingLinks = createFloatingFloatingLinks(positions, attributes, spring);
		
		initPositions.length = prePositions.length = positions.length;
		for (int i = 0; i < positions.length; i++) initPositions[i] = prePositions[i] = positions[i];
	}
	
	void tick(Matrix world) ///
	{
		for (int i = 0; i < positions.length; i++)
		{
			if (attributes.isFixing(i)) positions[i] = initPositions[i];
		}
		
		Vector[] accelerations = new Vector[positions.length];
		accelerations[] = outsideForce / 60.0;
		foreach (v; fixingFloatingLinks)
		{
			accelerations[v.secondIndex] += v.springForce(world, spring);
		}
		foreach (v; floatingFloatingLinks)
		{
			auto force = v.springForce(world, spring);
			accelerations[v.firstIndex] -= force * 0.5;
			accelerations[v.secondIndex] += force * 0.5;
		}
		
		for (int i = 0; i < positions.length; i++)
		{
			if (!attributes.isFloating(i)) continue;
			auto velocityPer60 = positions[i] - prePositions[i];
			prePositions[i] = positions[i];
			velocityPer60 -= velocityPer60 * resistance;
			velocityPer60 += accelerations[i];
			positions[i] = prePositions[i] + velocityPer60; // 無効になる? positions[i] += velocityPer60;
		}
		
		foreach (v; fixingFloatingLinks) v.doReflect(world);
		foreach (v; floatingFloatingLinks) v.doReflect(world);
		
		foreach (v; mirrorLinks) v.sync();
		
		for (int i = 0; i < positions.length; i++)
		{
			if (attributes.isFixing(i)) positions[i] = mulW(positions[i], world);
		}
		positions.commit();
	}
	
	void serialize(Stream writer)
	{
		writer.write(cast(char[])HEADER); // d2
		writer.write(spring);
		writer.write(resistance);
		attributes.serialize(writer);
		super.serialize(writer);
	}
	
	void deserialize(Stream reader)
	{
		with (new SerializeStream(reader))
		{
			throwIfHeaderError(HEADER);
			read(spring);
			read(resistance);
			attributes = new ClothAttributes();
			attributes.deserialize(reader);
			super.deserialize(reader);
			positions = new ClothPositions(this);
		}
		initialize();
	}
}
