package resonantinduction.electrical.wire.flat;

import java.util.Arrays;
import java.util.LinkedList;

import net.minecraft.util.Icon;
import codechicken.lib.lighting.LightModel;
import codechicken.lib.math.MathHelper;
import codechicken.lib.render.CCModel;
import codechicken.lib.render.ColourModifier;
import codechicken.lib.render.ColourMultiplier;
import codechicken.lib.render.IUVTransformation;
import codechicken.lib.render.IVertexModifier;
import codechicken.lib.render.IconTransformation;
import codechicken.lib.render.RenderUtils;
import codechicken.lib.render.UV;
import codechicken.lib.render.UVScale;
import codechicken.lib.render.UVTranslation;
import codechicken.lib.render.Vertex5;
import codechicken.lib.vec.Cuboid6;
import codechicken.lib.vec.Rotation;
import codechicken.lib.vec.Transformation;
import codechicken.lib.vec.Translation;
import codechicken.lib.vec.Vector3;

public class RenderFlatWire
{
	public static Icon flatWireTexture;

	public static class UVT implements IUVTransformation
	{
		public Transformation t;
		private Vector3 vec = new Vector3();

		public UVT(Transformation t)
		{
			this.t = t;
		}

		@Override
		public void transform(UV uv)
		{
			vec.set(uv.u, 0, uv.v).apply(t);
			uv.set(vec.x, vec.z);
		}
	}

	public static int[] reorientSide = new int[] { 0, 3, 3, 0, 0, 3 };

	/*
	 * All generations are done on side 0 so know that for rotation r
	 * 0 = side 3 = +Z = SOUTH
	 * 1 = side 4 = -X = WEST
	 * 2 = side 2 = -Z = NORTH
	 * 3 = side 5 = +X = EAST
	 */

	private static class WireModelGenerator
	{
		int side;
		int tw;
		int th;
		double w;
		double h;
		int mask;
		int connMask;
		int connCount;
		CCModel model;
		int i = 0;
		boolean inv;

		public static int countConnections(int connMask)
		{
			int n = 0;
			for (int r = 0; r < 4; r++)
				if ((connMask & 1 << r) != 0)
					n += 1;
			return n;
		}

		public int numFaces()
		{
			if (inv)
				return 22;

			int conns;
			if (connCount <= 2)
				conns = 2;
			else
				conns = connCount;

			int faces = conns * 3 + 5;
			for (int i = 0; i < 4; i++)
				if ((mask >> i & 0x11) == 1)
					faces++;

			return faces;
		}

		public CCModel generateInvModel(int thickness)
		{
			return generateModel(modelKey(0, thickness, 0xF0), true);
		}

		public CCModel generateModel(int key, boolean inv)
		{
			this.inv = inv;

			side = (key >> 8) % 6;
			tw = (key >> 8) / 6 + 1;
			w = tw / 16D;
			th = tw + 1;
			h = th / 16D;
			mask = key & 0xFF;
			connMask = (mask & 0xF0) >> 4 | mask & 0xF;
			connCount = countConnections(connMask);
			model = CCModel.quadModel(numFaces() * 4);
			i = 0;

			generateCenter();
			for (int r = 0; r < 4; r++)
				generateSide(r);

			model.apply(Rotation.sideOrientation(side, 0).at(Vector3.center));

			return finishModel(model);
		}

		private void generateSide(int r)
		{
			int type = mask >> r & 0x11;

			Vertex5[] verts;
			if (inv)
				verts = generateSideInv(r);

			else if (connCount == 0)
				if (r % 2 == 1)
					verts = generateStub(r);
				else
					verts = generateFlat(r);

			else if (connCount == 1)
				if (connMask == 1 << (r + 2) % 4)// this side is opposite the one with a connection
					verts = generateStub(r);
				else
					verts = generateSideFromType(type, r);

			else
				verts = generateSideFromType(type, r);

			Transformation t = Rotation.quarterRotations[r].at(Vector3.center);
			for (Vertex5 vert : verts)
				vert.apply(t);

			i = addVerts(model, verts, i);
		}

		private Vertex5[] generateSideInv(int r)
		{
			return withBottom(generateStraight(r), 4, 4);
		}

		private Vertex5[] generateSideFromType(int type, int r)
		{
			if (type == 0x00)
				return generateFlat(r);
			else if (type == 0x01)
				return generateCorner(r);
			else if (type == 0x10)
				return generateStraight(r);
			else
				return generateInternal(r);
		}

		private Vertex5[] generateFlat(int r)
		{
			Vertex5[] verts = new Vertex5[] { new Vertex5(0.5 - w, 0, 0.5 + w, 16, 16 + tw), new Vertex5(0.5 + w, 0, 0.5 + w, 16, 16 - tw), new Vertex5(0.5 + w, h, 0.5 + w, 16 - th, 16 - tw), new Vertex5(0.5 - w, h, 0.5 + w, 16 - th, 16 + tw) };

			if (Rotation.rotateSide(side, r) % 2 == 0) // red is on the negative side
			{
				UVT uvt = new UVT(Rotation.quarterRotations[2].at(new Vector3(8, 0, 16)));
				for (Vertex5 vert : verts)
					vert.apply(uvt);
			}
			return verts;
		}

		private Vertex5[] generateStub(int r)
		{
			Vertex5[] verts = generateExtension(4);

			for (int i = 0; i < 4; i++)
				verts[i].vec.z -= 0.002;// pull the stub in a little so it
										// doesn't z fight with jacketed cables

			reflectSide(verts, r);
			return verts;
		}

		private Vertex5[] generateStraight(int r)
		{
			Vertex5[] verts = generateExtension(8);

			reflectSide(verts, r);
			return verts;
		}

		private Vertex5[] generateCorner(int r)
		{
			Vertex5[] verts = generateExtension(8 + th);

			// retexture cap
			for (int i = 0; i < 4; i++)
				verts[i].apply(new UVTranslation(0, -th));

			// add end face extending around block
			verts = Arrays.copyOf(verts, 20);
			verts[16] = new Vertex5(0.5 - w, 0, 1, 8 - tw, 24 + 2 * th);
			verts[17] = new Vertex5(0.5 + w, 0, 1, 8 + tw, 24 + 2 * th);
			verts[18] = new Vertex5(0.5 + w, 0, 1 + h, 8 + tw, 24 + th);
			verts[19] = new Vertex5(0.5 - w, 0, 1 + h, 8 - tw, 24 + th);

			reflectSide(verts, r);
			return verts;
		}

		private Vertex5[] generateInternal(int r)
		{
			Vertex5[] verts = generateExtension(8);

			// retexture cap
			verts[0].uv.set(8 + tw, 24);
			verts[1].uv.set(8 - tw, 24);
			verts[2].uv.set(8 - tw, 24 + tw);
			verts[3].uv.set(8 + tw, 24 + tw);

			reflectSide(verts, r);

			// offset side textures
			for (int i = 4; i < 16; i++)
				verts[i].apply(new UVTranslation(16, 0));

			return verts;
		}

		private Vertex5[] generateExtension(int tl)
		{
			double l = tl / 16D;

			return new Vertex5[] {// cap
			new Vertex5(0.5 - w, 0, 0.5 + l, 8 - tw, 24 + 2 * th), new Vertex5(0.5 + w, 0, 0.5 + l, 8 + tw, 24 + 2 * th), new Vertex5(0.5 + w, h, 0.5 + l, 8 + tw, 24 + th), new Vertex5(0.5 - w, h, 0.5 + l, 8 - tw, 24 + th),

					// top
			new Vertex5(0.5 - w, h, 0.5 + l, 8 - tw, 16 + tl), new Vertex5(0.5 + w, h, 0.5 + l, 8 + tw, 16 + tl), new Vertex5(0.5 + w, h, 0.5 + w, 8 + tw, 16 + tw), new Vertex5(0.5 - w, h, 0.5 + w, 8 - tw, 16 + tw),

					// left
			new Vertex5(0.5 - w, 0, 0.5 + w, 0, 16 + tw), new Vertex5(0.5 - w, 0, 0.5 + l, 0, 16 + tl), new Vertex5(0.5 - w, h, 0.5 + l, th, 16 + tl), new Vertex5(0.5 - w, h, 0.5 + w, th, 16 + tw),

					// right
			new Vertex5(0.5 + w, 0, 0.5 + l, 16, 16 + tl), new Vertex5(0.5 + w, 0, 0.5 + w, 16, 16 + tw), new Vertex5(0.5 + w, h, 0.5 + w, 16 - th, 16 + tw), new Vertex5(0.5 + w, h, 0.5 + l, 16 - th, 16 + tl) };
		}

		private void generateCenter()
		{
			int tex;// 0 = straight n/s, 1 = straight e/w, 2 = circle
			if (connCount == 0)
				tex = 1;
			else if (connCount == 1)
				tex = (connMask & 5) != 0 ? 0 : 1;// if there is one connection, and it is
													// north/south then north/south, otherwise
													// east/west
			else if (connMask == 5)
				tex = 0;
			else if (connMask == 10)
				tex = 1;
			else
				tex = 2;

			Vertex5[] verts = new Vertex5[] { new Vertex5(0.5 - w, h, 0.5 + w, 8 - tw, 16 + tw), new Vertex5(0.5 + w, h, 0.5 + w, 8 + tw, 16 + tw), new Vertex5(0.5 + w, h, 0.5 - w, 8 + tw, 16 - tw), new Vertex5(0.5 - w, h, 0.5 - w, 8 - tw, 16 - tw) };

			if (tex == 0 || tex == 1)
				tex = (tex + reorientSide[side]) % 2;

			int r = reorientSide[side];
			if (tex == 1)
				r += 3;

			if (r != 0)
			{
				IUVTransformation uvt = new UVT(Rotation.quarterRotations[r % 4].at(new Vector3(8, 0, 16)));
				for (Vertex5 vert : verts)
					vert.apply(uvt);
			}

			if (tex == 2)
			{// circle (translate across to u = 24)
				UVTranslation uvt = new UVTranslation(16, 0);
				for (Vertex5 vert : verts)
					vert.apply(uvt);
			}

			if (inv)
				verts = withBottom(verts, 0, 4);

			i = addVerts(model, verts, i);
		}

		private static UVT sideReflect = new UVT(Rotation.quarterRotations[2].at(new Vector3(8, 0, 16)));

		private void reflectSide(Vertex5[] verts, int r)
		{
			if ((r + reorientSide[side]) % 4 >= 2)// rotate the texture about
													// the y center
				for (Vertex5 vert : verts)
					vert.apply(sideReflect);
		}

		/**
		 * Returns a copy of vertices with the bottom face added at the start.
		 * 
		 * @param start The index of the first vertex making up the top face
		 * @param count The number of vertices making up the top face
		 */
		private Vertex5[] withBottom(Vertex5[] verts, int start, int count)
		{
			Vertex5[] i_verts = new Vertex5[verts.length + count];

			// add the bottom face, just a copy of the top, rotated about the z
			// axis
			Transformation r = new Rotation(MathHelper.pi, 0, 0, 1).at(new Vector3(0.5, h / 2, 0));
			for (int i = 0; i < count; i++)
				i_verts[i] = verts[i + start].copy().apply(r);
			System.arraycopy(verts, 0, i_verts, count, verts.length);
			return i_verts;
		}
	}

	/**
	 * Array of all built models. These will be generated on demand.
	 */
	public static CCModel[] wireModels = new CCModel[3 * 6 * 256];
	public static CCModel[] invModels = new CCModel[3];
	private static WireModelGenerator gen_inst = new WireModelGenerator();

	/**
	 * Puts verts into model m starting at index k
	 */
	public static int addVerts(CCModel m, Vertex5[] verts, int k)
	{
		for (int i = 0; i < verts.length; i++)
			m.verts[k + i] = verts[i];

		return k + verts.length;
	}

	public static CCModel finishModel(CCModel m)
	{
		m.apply(new UVScale(1 / 32D));
		m.shrinkUVs(0.0005);
		m.computeNormals();
		m.computeLighting(LightModel.standardLightModel);

		return m;
	}

	/**
	 * Returns a tightly packed unique index for the specific model represented
	 * by this wire. The mask is split into 3 sections the combination of
	 * corresponding bits from the two lowest nybbles gives the connection type
	 * in that direction.
	 * 00 = none
	 * 01 = corner
	 * 10 = straight
	 * 11 = internal The
	 * second byte contains the thickness*6+side
	 * 
	 * @param side The side the wire is attached to
	 * @param thickness The thickness of the wire -1 in 1/8th blocks. Supported
	 * values 0, 1, 2
	 * @param connMap The connection mask of the wire
	 */
	public static int modelKey(int side, int thickness, int connMap)
	{
		int key = connMap & 0xFF;// take the straight and corner connections

		int renderCorner = connMap >> 20 & 0xF;
		key |= (renderCorner ^ key & 0xF) << 4;// any corner connections that aren't rendered
												// convert to straight
		key &= ~0xF | renderCorner;// set corners to renderCorners

		int internal = (connMap & 0xF00) >> 8;// internal connections
		key |= internal << 4 | internal;// if internal is set, set both straight and corner to 1

		key |= side + thickness * 6 << 8;// add side and thickness
		return key;
	}

	public static int modelKey(PartFlatWire w)
	{
		return modelKey(w.side, w.getThickness(), w.connMap);
	}

	public static CCModel getOrGenerateModel(int key)
	{
		CCModel m = wireModels[key];
		if (m == null)
			wireModels[key] = m = gen_inst.generateModel(key, false);
		return m;
	}

	public static void render(PartFlatWire w, Vector3 pos)
	{
		IVertexModifier m = w.getColour().pack() == -1 ? ColourModifier.instance : new ColourMultiplier(w.getColour());
		CCModel model = getOrGenerateModel(modelKey(w));
		model.render(new Translation(pos), new IconTransformation(w.getIcon()), m);
	}

	public static void renderInv(int thickness, Transformation t, Icon icon)
	{
		CCModel m = invModels[thickness];
		if (m == null)
			invModels[thickness] = m = gen_inst.generateInvModel(thickness);

		m.render(t, new IconTransformation(icon));
	}

	public static void renderBreakingOverlay(Icon icon, PartFlatWire wire)
	{
		int key = modelKey(wire);
		int side = (key >> 8) % 6;
		double w = ((key >> 8) / 6 + 1) / 16D;
		double h = w + 1 / 16D;
		int mask = key & 0xFF;
		int connMask = (mask & 0xF0) >> 4 | mask & 0xF;
		int connCount = WireModelGenerator.countConnections(connMask);

		LinkedList<Cuboid6> boxes = new LinkedList<Cuboid6>();
		boxes.add(new Cuboid6(0.5 - w, 0, 0.5 - w, 0.5 + w, h, 0.5 + w).apply(Rotation.sideRotations[side].at(Vector3.center)));// center
		for (int r = 0; r < 4; r++)
		{
			int length;
			if (connCount == 0)
			{
				if (r % 2 == 1)
					length = 4;
				else
					length = 0;
			}
			else if (connCount == 1)
			{
				if (connMask == 1 << (r + 2) % 4)// this side is opposite the one with a connection
					length = 4;
				else if (connMask == 1 << r)
					length = 8;
				else
					length = 0;
			}
			else
				length = (connMask & 1 << r) != 0 ? 8 : 0;

			if (length > 0)
			{
				double l = length / 16D;
				boxes.add(new Cuboid6(0.5 - w, 0, 0.5 + w, 0.5 + w, h, 0.5 + l).apply(Rotation.sideOrientation(side, r).at(Vector3.center)));
			}
		}

		for (Cuboid6 box : boxes)
			RenderUtils.renderBlock(box, 0, new Translation(wire.x(), wire.y(), wire.z()), new IconTransformation(icon), null);
	}
}