package openmods.geometry;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.primitives.Floats;
import com.google.common.primitives.Ints;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.vecmath.AxisAngle4f;
import javax.vecmath.Matrix3f;
import javax.vecmath.Matrix4f;
import javax.vecmath.Quat4f;
import net.minecraft.client.renderer.block.model.ModelRotation;
import net.minecraftforge.common.model.TRSRTransformation;
import openmods.block.BlockRotationMode;
import org.apache.commons.io.IOUtils;

// NOTE: this class is supposed to be called by Gradle task or manually
public class OrientationInfoGenerator {

	private static final File OUTPUT_DIR = new File(new File("etc"), "orientations");

	private enum Rotation {
		R0(0), R90(90), R180(180), R270(270);

		public final String name;

		public final float angle;

		private Rotation(int angle) {
			this.angle = (float)Math.toRadians(angle);
			this.name = Integer.toString(angle);
		}

		@Override
		public String toString() {
			return name;
		}
	}

	private static class XYZRotation implements Comparable<XYZRotation> {
		public final Rotation x;
		public final Rotation y;
		public final Rotation z;

		public XYZRotation(Rotation x, Rotation y, Rotation z) {
			this.x = x;
			this.y = y;
			this.z = z;
		}

		private static int countComponent(Rotation r) {
			return r == Rotation.R0? 0 : 1;
		}

		private int componentCount() {
			return countComponent(x) + countComponent(y) + countComponent(z);
		}

		private float angleSum() {
			return x.angle + y.angle + z.angle;
		}

		@Override
		public int compareTo(XYZRotation o) {
			int result = Ints.compare(componentCount(), o.componentCount());
			if (result != 0) return result;

			return Floats.compare(angleSum(), o.angleSum());
		}

		@Override
		public String toString() {
			List<String> r = Lists.newArrayList();
			if (x != Rotation.R0) r.add("X" + x);
			if (y != Rotation.R0) r.add("Y" + y);
			if (z != Rotation.R0) r.add("Z" + z);
			return Joiner.on('_').join(r);
		}
	}

	private static List<XYZRotation> sorted(final Collection<XYZRotation> l) {
		final List<XYZRotation> tmp = Lists.newArrayList(l);
		Collections.sort(tmp);
		return tmp;
	}

	private static void dumpBlockRotationsRotations(BlockRotationMode brm, Multimap<Orientation, ModelRotation> vanilla, Multimap<Orientation, XYZRotation> xyz) throws IOException {
		final File outFile = new File(OUTPUT_DIR, brm.name().toLowerCase(Locale.ROOT) + ".txt");
		System.out.println("Generating file: " + outFile.getAbsolutePath());
		PrintWriter out = null;
		try {
			out = new PrintWriter(outFile);
			for (Orientation o : Sets.newTreeSet(brm.getValidDirections())) {
				final StringBuilder line = new StringBuilder();
				line.append('"');
				line.append(o.getName().toLowerCase(Locale.ROOT));
				line.append("\": {");
				final Collection<ModelRotation> v = vanilla.get(o);
				if (!v.isEmpty()) {
					final ModelRotation m = v.iterator().next();
					line.append(modelRotationToJson(m));
				} else {
					final XYZRotation f = sorted(xyz.get(o)).iterator().next();
					line.append(forgeRotationToJson(f));

				}
				line.append("}, // front: ");
				line.append(brm.getFront(o));
				line.append(", top: ");
				line.append(brm.getTop(o));
				out.println(line.toString());
			}
		} finally {
			IOUtils.closeQuietly(out);
		}
	}

	private static String forgeRotationToJson(XYZRotation f) {
		List<String> result = Lists.newArrayList();
		if (f.z != Rotation.R0) result.add("{\"z\": " + f.z.toString() + "}");
		if (f.y != Rotation.R0) result.add("{\"y\": " + f.y.toString() + "}");
		if (f.x != Rotation.R0) result.add("{\"x\": " + f.x.toString() + "}");

		return "{ \"transform\": { \"rotation\": [" + Joiner.on(", ").join(result) + "]}}";
	}

	private static Pattern namePattern = Pattern.compile("X(\\d+)_Y(\\d+)");

	private static String modelRotationToJson(ModelRotation m) {
		final Matcher matcher = namePattern.matcher(m.name());
		Preconditions.checkState(matcher.matches());
		final String x = matcher.group(1);
		final String y = matcher.group(2);

		List<String> result = Lists.newArrayList();
		if (!x.equals("0")) result.add("\"x\": " + x);
		if (!y.equals("0")) result.add("\"y\": " + y);
		return Joiner.on(", ").join(result);
	}

	private static Multimap<Orientation, XYZRotation> calculateXyzRotations(Map<Matrix3f, Orientation> fromMatrix) {
		final Multimap<Orientation, XYZRotation> toXYZRotation = HashMultimap.create();
		for (Rotation x : Rotation.values())
			for (Rotation y : Rotation.values())
				for (Rotation z : Rotation.values()) {
					final XYZRotation rotation = new XYZRotation(x, y, z);
					Quat4f q = new Quat4f(0, 0, 0, 1);

					Quat4f tmp = new Quat4f();
					tmp.set(new AxisAngle4f(0, 0, 1, z.angle));
					q.mul(tmp);

					tmp.set(new AxisAngle4f(0, 1, 0, y.angle));
					q.mul(tmp);

					tmp.set(new AxisAngle4f(1, 0, 0, x.angle));
					q.mul(tmp);

					Matrix3f m = new Matrix3f();
					m.set(q);
					roundMatrixElements(m);
					final Orientation orientation = fromMatrix.get(m);
					Preconditions.checkNotNull(orientation, rotation);
					toXYZRotation.put(orientation, rotation);
				}

		return toXYZRotation;
	}

	private static void roundMatrixElements(Matrix3f m) {
		m.m00 = Math.round(m.m00);
		m.m01 = Math.round(m.m01);
		m.m02 = Math.round(m.m02);

		m.m10 = Math.round(m.m10);
		m.m11 = Math.round(m.m11);
		m.m12 = Math.round(m.m12);

		m.m20 = Math.round(m.m20);
		m.m21 = Math.round(m.m21);
		m.m22 = Math.round(m.m22);
	}

	private static Matrix3f roundAndReduceMatrixElements(Matrix4f m) {
		Preconditions.checkArgument(m.m30 == 0);
		Preconditions.checkArgument(m.m31 == 0);
		Preconditions.checkArgument(m.m32 == 0);

		Preconditions.checkArgument(m.m03 == 0);
		Preconditions.checkArgument(m.m13 == 0);
		Preconditions.checkArgument(m.m23 == 0);

		Preconditions.checkArgument(m.m33 == 1);

		final Matrix3f result = new Matrix3f();
		result.m00 = Math.round(m.m00);
		result.m01 = Math.round(m.m01);
		result.m02 = Math.round(m.m02);

		result.m10 = Math.round(m.m10);
		result.m11 = Math.round(m.m11);
		result.m12 = Math.round(m.m12);

		result.m20 = Math.round(m.m20);
		result.m21 = Math.round(m.m21);
		result.m22 = Math.round(m.m22);
		return result;
	}

	private static Multimap<Orientation, ModelRotation> calculateVanillaRotations(Map<Matrix3f, Orientation> fromMatrix) {
		final Multimap<Orientation, ModelRotation> toVanilla = HashMultimap.create();

		for (ModelRotation rot : ModelRotation.values()) {
			final Matrix4f rotMatrix = TRSRTransformation.toVecmath(rot.getMatrix4d());
			final Matrix3f key = roundAndReduceMatrixElements(rotMatrix);
			final Orientation orientation = fromMatrix.get(key);
			Preconditions.checkNotNull(orientation, rot);
			toVanilla.put(orientation, rot);
		}

		return toVanilla;
	}

	private static void dumpOrientations(Multimap<Orientation, ModelRotation> vanilla, Multimap<Orientation, XYZRotation> xyz) throws IOException {
		final File outFile = new File(OUTPUT_DIR, "all.txt");
		System.out.println("Generating file: " + outFile.getAbsolutePath());
		PrintWriter out = null;
		try {
			out = new PrintWriter(outFile);

			for (Orientation o : Orientation.VALUES) {
				out.println(String.format("%s = %s -> x=%s,y=%s,z=%s -> XY: %s, XYZ: %s", o.name(), o, o.x.dir, o.y.dir, o.z.dir, vanilla.get(o), sorted(xyz.get(o))));
			}
		} finally

		{
			IOUtils.closeQuietly(out);
		}
	}

	public static void main(String[] args) throws IOException {
		OUTPUT_DIR.mkdirs();
		final Map<Matrix3f, Orientation> fromMatrix = Maps.newHashMap();

		for (Orientation o : Orientation.VALUES)
			fromMatrix.put(o.getLocalToWorldMatrix(), o);

		final Multimap<Orientation, ModelRotation> vanilla = calculateVanillaRotations(fromMatrix);

		final Multimap<Orientation, XYZRotation> xyz = calculateXyzRotations(fromMatrix);

		dumpOrientations(vanilla, xyz);

		for (BlockRotationMode brm : BlockRotationMode.values())
			dumpBlockRotationsRotations(brm, vanilla, xyz);
	}

}