package openmods.geometry;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import net.minecraft.util.EnumFacing;
import openmods.block.BlockRotationMode;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

public class BlockRotationModeTest {

	private static final Set<EnumFacing> HORIZONTALS = Sets.newHashSet(EnumFacing.HORIZONTALS);

	@RunWith(Parameterized.class)
	public static class BasicProperties {
		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(BlockRotationMode.values());
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void testSurfacePlacementConsistency() {
			for (EnumFacing facing : EnumFacing.values()) {
				Orientation orientation = mode.getOrientationFacing(facing);
				if (orientation != null)
					Assert.assertTrue("Orientation " + orientation + " is not valid",
							mode.getValidDirections().contains(orientation));
			}
		}

		@Test
		public void testLocalDirections() {
			for (EnumFacing facing : EnumFacing.values()) {
				Orientation orientation = mode.getOrientationFacing(facing);
				if (orientation != null)
					Assert.assertNotNull("Orientation " + orientation + " is not valid",
							mode.getLocalDirections(orientation));
			}
		}

		@Test
		public void testSerializationConsistency() {
			for (Orientation o : mode.getValidDirections()) {
				final int value = mode.toValue(o);
				Assert.assertTrue(o.toString(), value >= 0);
				Assert.assertTrue(o.toString(), value < mode.getValidDirections().size());

				final Orientation deserialized = mode.fromValue(value);
				Assert.assertEquals(o.toString(), o, deserialized);
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class OnlyHorizontalsSupported {
		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.FOUR_DIRECTIONS,
					BlockRotationMode.TWO_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void testHorizontals() {
			for (EnumFacing facing : EnumFacing.HORIZONTALS) {
				Orientation orientation = mode.getOrientationFacing(facing);
				Assert.assertNotNull(facing.toString(), orientation);
			}
		}

		@Test
		public void testUp() {
			Orientation orientation = mode.getOrientationFacing(EnumFacing.UP);
			Assert.assertNull(orientation);
		}

		@Test
		public void testDown() {
			Orientation orientation = mode.getOrientationFacing(EnumFacing.DOWN);
			Assert.assertNull(orientation);
		}
	}

	@RunWith(Parameterized.class)
	public static class AllDirectionsSupported {
		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.NONE,
					BlockRotationMode.THREE_DIRECTIONS,
					BlockRotationMode.SIX_DIRECTIONS,
					BlockRotationMode.THREE_FOUR_DIRECTIONS,
					BlockRotationMode.TWELVE_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void testAllDirectionsNonNull() {
			for (EnumFacing facing : EnumFacing.values()) {
				Orientation orientation = mode.getOrientationFacing(facing);
				Assert.assertNotNull(facing.toString(), orientation);
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class TopIsHorizontalForTopAndBottomFacings {
		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.THREE_DIRECTIONS,
					BlockRotationMode.SIX_DIRECTIONS,
					BlockRotationMode.THREE_FOUR_DIRECTIONS,
					BlockRotationMode.TWELVE_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		private static void testTopIsHorizontalForVerticalFacing(BlockRotationMode mode, EnumFacing facing) {
			final Orientation orientation = mode.getOrientationFacing(facing);
			Assert.assertNotNull(facing.toString(), orientation);
			Assert.assertTrue(HORIZONTALS.contains(mode.getTop(orientation)));
		}

		@Test
		public void testTopIsHorizontalForUpFacing() {
			testTopIsHorizontalForVerticalFacing(mode, EnumFacing.UP);
		}

		@Test
		public void testTopIsHorizontalForDownFacing() {
			testTopIsHorizontalForVerticalFacing(mode, EnumFacing.DOWN);
		}
	}

	@RunWith(Parameterized.class)
	public static class SurfacePlacementFrontWeakConsistency {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.TWO_DIRECTIONS,
					BlockRotationMode.THREE_DIRECTIONS,
					BlockRotationMode.THREE_FOUR_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		// front should be on the same axis as placement side
		@Test
		public void testSurfacePlacementFrontWeakConsistency() {
			for (EnumFacing facing : EnumFacing.values()) {
				Orientation orientation = mode.getOrientationFacing(facing);
				if (orientation != null) {
					Assert.assertEquals("Orientation " + orientation + " has invalid placement/front combination",
							facing.getAxis(), mode.getFront(orientation).getAxis());
				}
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class SurfacePlacementFrontStrongConsistency {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.FOUR_DIRECTIONS,
					BlockRotationMode.SIX_DIRECTIONS,
					BlockRotationMode.TWELVE_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		// front should be on the same direction as placement side
		@Test
		public void testSurfacePlacementFrontStrongConsistency() {
			for (EnumFacing facing : EnumFacing.values()) {
				Orientation orientation = mode.getOrientationFacing(facing);
				if (orientation != null) {
					Assert.assertEquals("Orientation " + orientation + " has invalid placement/front combination",
							facing, mode.getFront(orientation));
				}
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class TopOrientationForHorizontals {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.TWO_DIRECTIONS,
					BlockRotationMode.THREE_DIRECTIONS,
					BlockRotationMode.FOUR_DIRECTIONS,
					BlockRotationMode.SIX_DIRECTIONS,
					BlockRotationMode.THREE_FOUR_DIRECTIONS,
					BlockRotationMode.TWELVE_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void testTopEqualsUpForAllHorizontals() {
			for (EnumFacing facing : EnumFacing.HORIZONTALS) {
				final Orientation orientation = mode.getOrientationFacing(facing);
				Assert.assertNotNull(facing.toString(), orientation);
				Assert.assertEquals(facing.toString(), EnumFacing.UP, mode.getTop(orientation));
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class NonRotatingFrontClick {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.TWO_DIRECTIONS,
					BlockRotationMode.THREE_DIRECTIONS,
					BlockRotationMode.THREE_FOUR_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void toolRotationOnFrontDoesntChangeFront() {
			for (Orientation orientation : mode.getValidDirections()) {
				EnumFacing front = mode.getFront(orientation);
				final Orientation rotatedOrientation = mode.calculateToolRotation(orientation, front);
				EnumFacing rotatedFront = mode.getFront(rotatedOrientation);
				Assert.assertEquals(front, rotatedFront);
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class SwitchingFrontClick {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.FOUR_DIRECTIONS,
					BlockRotationMode.SIX_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void toolRotationOnFrontChangesFrontToOpposite() {
			for (Orientation orientation : mode.getValidDirections()) {
				EnumFacing front = mode.getFront(orientation);
				final Orientation rotatedOrientation = mode.calculateToolRotation(orientation, front);
				EnumFacing rotatedFront = mode.getFront(rotatedOrientation);
				Assert.assertEquals(front.getOpposite(), rotatedFront);
			}
		}
	}

	public static void checkFrontDirectionsAfterFourRotations(BlockRotationMode mode, EnumFacing clickedSide, Orientation orientation, EnumFacing... expectedDirections) {
		final Set<EnumFacing> results = Sets.newHashSet();
		for (int i = 0; i < 4; i++) {
			orientation = mode.calculateToolRotation(orientation, clickedSide);
			Assert.assertTrue(mode.isOrientationValid(orientation));
			results.add(mode.getFront(orientation));
		}

		Assert.assertEquals(Sets.newHashSet(expectedDirections), results);
	}

	public static void checkTopDirectionsAfterFourRotations(BlockRotationMode mode, EnumFacing clickedSide, Orientation orientation, EnumFacing... expectedDirections) {
		final Set<EnumFacing> results = Sets.newHashSet();
		for (int i = 0; i < 4; i++) {
			orientation = mode.calculateToolRotation(orientation, clickedSide);
			Assert.assertTrue(mode.isOrientationValid(orientation));
			results.add(mode.getTop(orientation));
		}

		Assert.assertEquals(Sets.newHashSet(expectedDirections), results);
	}

	@RunWith(Parameterized.class)
	public static class ToolRotationForWeakConsistency {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.TWO_DIRECTIONS,
					BlockRotationMode.THREE_DIRECTIONS,
					BlockRotationMode.THREE_FOUR_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void toolRotationOnBackDoesntChangeFront() {
			for (Orientation orientation : mode.getValidDirections()) {
				final EnumFacing front = mode.getFront(orientation);
				final EnumFacing back = front.getOpposite();
				final Orientation rotatedOrientation = mode.calculateToolRotation(orientation, back);
				final EnumFacing rotatedFront = mode.getFront(rotatedOrientation);
				Assert.assertEquals(front, rotatedFront);
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class ToolRotationForStrongConsistency {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.FOUR_DIRECTIONS,
					BlockRotationMode.SIX_DIRECTIONS,
					BlockRotationMode.TWELVE_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void toolRotationOnBackChangesFrontToOpposite() {
			for (Orientation orientation : mode.getValidDirections()) {
				final EnumFacing back = mode.getFront(orientation).getOpposite();
				final Orientation rotatedOrientation = mode.calculateToolRotation(orientation, back);
				final EnumFacing rotatedFront = mode.getFront(rotatedOrientation);
				Assert.assertEquals(back, rotatedFront);
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class UniqueOrientationForEverySide {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.TWO_DIRECTIONS,
					BlockRotationMode.THREE_DIRECTIONS,
					BlockRotationMode.FOUR_DIRECTIONS,
					BlockRotationMode.SIX_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void exactlyOneOrientationForEveryFacing() {
			final Map<EnumFacing, Orientation> stateMap = Maps.newHashMap();
			for (Orientation orientation : mode.getValidDirections()) {
				final EnumFacing front = mode.getFront(orientation);
				final Orientation prev = stateMap.put(front, orientation);
				Assert.assertNull(prev);
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class UniqueOrientationForEverySideNotToggling {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.TWO_DIRECTIONS,
					BlockRotationMode.THREE_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void toolRotationOnFrontDoesntChangeOrientation() {
			for (Orientation orientation : mode.getValidDirections()) {
				EnumFacing front = mode.getFront(orientation);
				final Orientation rotatedOrientation = mode.calculateToolRotation(orientation, front);
				Assert.assertEquals(orientation, rotatedOrientation);
			}
		}
	}

	@RunWith(Parameterized.class)
	public static class ExactHorizontalRotation {

		@Parameters(name = "{0}")
		public static Iterable<BlockRotationMode> dirs() {
			return Arrays.asList(
					BlockRotationMode.SIX_DIRECTIONS,
					BlockRotationMode.TWELVE_DIRECTIONS);
		}

		@Parameter
		public BlockRotationMode mode;

		@Test
		public void toolRotationOnHorizontalsChangesFrontToClickedSide() {
			for (Orientation initialOrientation : mode.getValidDirections()) {
				for (EnumFacing rotatedSide : EnumFacing.HORIZONTALS) {
					Orientation orientation = initialOrientation;
					final Set<Orientation> results = Sets.newHashSet();
					for (int i = 0; i < 4; i++) {
						final EnumFacing preRotationFront = mode.getFront(orientation);
						orientation = mode.calculateToolRotation(orientation, rotatedSide);
						final EnumFacing postRotationFront = mode.getFront(orientation);
						Assert.assertTrue(mode.isOrientationValid(orientation));
						if (preRotationFront == rotatedSide)
							Assert.assertEquals(postRotationFront, rotatedSide.getOpposite());
						else
							Assert.assertEquals(postRotationFront, rotatedSide);
						results.add(orientation);
					}

					Assert.assertTrue("Multiple states: " + results, results.size() == 2);
				}
			}
		}
	}

	public static class TwoDirections {

		private static final BlockRotationMode MODE = BlockRotationMode.TWO_DIRECTIONS;

		@Test
		public void toolRotationOnHorizontalChangesFrontAxisToClickedSide() {
			for (Orientation orientation : MODE.getValidDirections()) {
				for (EnumFacing rotatedSide : EnumFacing.HORIZONTALS) {
					final Orientation rotatedOrientation = MODE.calculateToolRotation(orientation, rotatedSide);
					final EnumFacing rotatedFront = MODE.getFront(rotatedOrientation);
					Assert.assertEquals(rotatedSide.getAxis(), rotatedFront.getAxis());
				}
			}
		}

		@Test
		public void toolTopRotationCoversHorizontals() {
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.UP, Orientation.XP_YP, EnumFacing.NORTH, EnumFacing.WEST);
		}

		@Test
		public void toolBottomRotationCoversHorizontals() {
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.DOWN, Orientation.XP_YP, EnumFacing.NORTH, EnumFacing.WEST);
		}
	}

	public static class ThreeDirections {

		private static final BlockRotationMode MODE = BlockRotationMode.THREE_DIRECTIONS;

		@Test
		public void toolRotationOnAnySideChangesFrontAxisToClickedSide() {
			for (Orientation orientation : MODE.getValidDirections()) {
				for (EnumFacing rotatedSide : EnumFacing.VALUES) {
					final Orientation rotatedOrientation = MODE.calculateToolRotation(orientation, rotatedSide);
					final EnumFacing rotatedFront = MODE.getFront(rotatedOrientation);
					Assert.assertEquals(rotatedSide.getAxis(), rotatedFront.getAxis());
				}
			}
		}
	}

	public static class FourDirections {
		private static final BlockRotationMode MODE = BlockRotationMode.FOUR_DIRECTIONS;

		@Test
		public void toolTopRotationCoversHorizontals() {
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.UP, Orientation.XP_YP, EnumFacing.NORTH, EnumFacing.SOUTH, EnumFacing.WEST, EnumFacing.EAST);
		}

		@Test
		public void toolBottomRotationCoversHorizontals() {
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.DOWN, Orientation.XP_YP, EnumFacing.NORTH, EnumFacing.SOUTH, EnumFacing.WEST, EnumFacing.EAST);
		}
	}

	public static class SixDirections {

		private static final BlockRotationMode MODE = BlockRotationMode.SIX_DIRECTIONS;

		@Test
		public void toolRotationOnAnySideChangesFrontToClickedSideOrOpposite() {
			for (Orientation orientation : MODE.getValidDirections()) {
				for (EnumFacing rotatedSide : EnumFacing.VALUES) {
					checkFrontDirectionsAfterFourRotations(MODE, rotatedSide, orientation, rotatedSide, rotatedSide.getOpposite());
				}
			}
		}
	}

	public static class ThreeFourDirections {

		private static final BlockRotationMode MODE = BlockRotationMode.THREE_FOUR_DIRECTIONS;

		@Test
		public void toolRotationOnTopCoversHorizontals() {
			checkTopDirectionsAfterFourRotations(MODE, EnumFacing.UP, Orientation.XP_YP, EnumFacing.HORIZONTALS);
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.UP, Orientation.XP_YP, EnumFacing.UP);
		}

		@Test
		public void toolRotationOnBottomCoversHorizontals() {
			checkTopDirectionsAfterFourRotations(MODE, EnumFacing.DOWN, Orientation.XP_YP, EnumFacing.HORIZONTALS);
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.DOWN, Orientation.XP_YP, EnumFacing.UP);
		}

		@Test
		public void toolRotationOnEastCoversFourSides() {
			checkTopDirectionsAfterFourRotations(MODE, EnumFacing.EAST, Orientation.XP_YP, EnumFacing.NORTH, EnumFacing.SOUTH, EnumFacing.UP, EnumFacing.DOWN);
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.EAST, Orientation.XP_YP, EnumFacing.WEST);
		}

		@Test
		public void toolRotationOnWestCoversFourSides() {
			checkTopDirectionsAfterFourRotations(MODE, EnumFacing.WEST, Orientation.XP_YP, EnumFacing.NORTH, EnumFacing.SOUTH, EnumFacing.UP, EnumFacing.DOWN);
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.WEST, Orientation.XP_YP, EnumFacing.WEST);
		}

		@Test
		public void toolRotationOnNorthCoversFourSides() {
			checkTopDirectionsAfterFourRotations(MODE, EnumFacing.NORTH, Orientation.XP_YP, EnumFacing.EAST, EnumFacing.WEST, EnumFacing.UP, EnumFacing.DOWN);
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.NORTH, Orientation.XP_YP, EnumFacing.NORTH);
		}

		@Test
		public void toolRotationOnSouthCoversFourSides() {
			checkTopDirectionsAfterFourRotations(MODE, EnumFacing.SOUTH, Orientation.XP_YP, EnumFacing.EAST, EnumFacing.WEST, EnumFacing.UP, EnumFacing.DOWN);
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.SOUTH, Orientation.XP_YP, EnumFacing.NORTH);
		}
	}

	public static class TwelveDirections {

		private static final BlockRotationMode MODE = BlockRotationMode.TWELVE_DIRECTIONS;

		@Test
		public void toolRotationOnTopCoversHorizontals() {
			checkTopDirectionsAfterFourRotations(MODE, EnumFacing.UP, Orientation.XP_YP, EnumFacing.HORIZONTALS);
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.UP, Orientation.XP_YP, EnumFacing.UP);
		}

		@Test
		public void toolRotationOnBottomCoversHorizontals() {
			checkTopDirectionsAfterFourRotations(MODE, EnumFacing.DOWN, Orientation.XP_YP, EnumFacing.HORIZONTALS);
			checkFrontDirectionsAfterFourRotations(MODE, EnumFacing.DOWN, Orientation.XP_YP, EnumFacing.DOWN);
		}

		@Test
		public void toolRotationOnHorizontalChangesFrontToOpposite() {
			for (EnumFacing front : EnumFacing.HORIZONTALS) {
				Orientation orientation = MODE.getOrientationFacing(front);
				final Orientation rotatedOrientation = MODE.calculateToolRotation(orientation, front);
				EnumFacing rotatedFront = MODE.getFront(rotatedOrientation);
				Assert.assertEquals(front.getOpposite(), rotatedFront);
			}
		}
	}

}