/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.rdf.model.test;

import java.util.Random;

import org.apache.jena.rdf.model.Model ;
import org.apache.jena.rdf.model.Resource ;
import org.apache.jena.rdf.model.Statement ;
import org.apache.jena.rdf.model.test.helpers.TestingModelFactory ;
import org.apache.jena.vocabulary.RDF ;
import org.junit.Assert;

public class IsomorphicTests extends AbstractModelTestBase
{
    // This is not part of the standard test suite 
    // It's not stable enough for inclusion in the automatic test suite.
    // Often, they pass, but there is a significant number of times they don't.
    // It also seems to be machine-dependent - failures are more frequent
    // on Apache Jenkins (hardware influening "random" numbers?)
    
	/**
	 * A theoretical graph for testing purposes.
	 * All nodes are anonymous resources. All edges are labelled
	 * rdf:value.
	 * The basic DiHyperCube consists of the nodes being the
	 * corners of a hypercube (e.g. in 3D a cube)
	 * with the statements being the edges of the cube, directed
	 * from one corner labelled 2^n-1 to the opposite corner
	 * labelled 0. The labels are not present in the model.
	 * This basic graph is then extended, for test purposes
	 * by duplicating a node.
	 */
	static class DiHyperCube extends java.lang.Object
	{
		static int bitCount( final int i )
		{
			return java.math.BigInteger.valueOf(i).bitCount();
		}

		/*
		 * We have two DiHyperCube's
		 * to one we have added N a1's
		 * to the other we have added N b1's
		 * Returns true if they are equal.
		 */
		static boolean equal( final int a1, final int b1 )
		{
			return DiHyperCube.bitCount(a1) == DiHyperCube.bitCount(b1);
		}

		/*
		 * We have two DiHyperCube's
		 * to one we have added N a1's and N a2's.
		 * to the other we have added N b1's and N b2's.
		 * Returns true if they are equal.
		 */
		static boolean equal( final int a1, final int a2, final int b1,
				final int b2 )
		{
			return (DiHyperCube.bitCount(a1 ^ a2) == DiHyperCube.bitCount(b1
					^ b2))
					&& (DiHyperCube.bitCount(a1 & a2) == DiHyperCube
							.bitCount(b1 & b2))
					&& (DiHyperCube.bitCount(a1 | a2) == DiHyperCube
							.bitCount(b1 | b2))
					&& (Math.min(DiHyperCube.bitCount(a1),
							DiHyperCube.bitCount(a2)) == Math.min(
							DiHyperCube.bitCount(b1), DiHyperCube.bitCount(b2)));
		}

		final private Resource corners[];

		final private int dim;

		final private Model model;

		/** Creates new DiHyperCube */
		public DiHyperCube( final int dimension, final Model m )
		{
			dim = dimension;
			model = m;
			corners = new Resource[1 << dim];
			for (int i = 0; i < corners.length; i++)
			{
				corners[i] = m.createResource();
			}
			for (int i = 0; i < corners.length; i++)
			{
				addDown(i, corners[i]);
			}
		}

		private void addDown( final int corner, final Resource r )
		{
			for (int j = 0; j < dim; j++)
			{
				final int bit = 1 << j;
				if ((corner & bit) != 0)
				{
					model.add(r, RDF.value, corners[corner ^ bit]);
				}
			}
		}

		DiHyperCube dupe( final int corner )
		{
			final Resource dup = model.createResource();
			for (int j = 0; j < dim; j++)
			{
				final int bit = 1 << j;
				if ((corner & bit) != 0)
				{
					model.add(dup, RDF.value, corners[corner ^ bit]);
				}
				else
				{
					model.add(corners[corner ^ bit], RDF.value, dup);
				}
			}
			return this;
		}

	}

	/**
	 * A theoretical graph for testing purposes.
	 * All nodes are anonymous resources. All edges are labelled
	 * rdf:value.
	 * The basic HyperCube consists of the nodes being the
	 * corners of a hypercube (e.g. in 3D a cube)
	 * with the statements being the edges of the cube, in both
	 * directions. The labels are not present in the model.
	 * This basic graph is then extended, for test purposes
	 * by duplicating a node. Or by adding/deleting an edge between
	 * two nodes.
	 */
	static class HyperCube extends java.lang.Object
	{
		static int bitCount( final int i )
		{
			return java.math.BigInteger.valueOf(i).bitCount();
		}

		/*
		 * We have two HyperCube's
		 * to one we have added N a1's and M a2's.
		 * to the other we have added N b1's and M b2's.
		 * or we have toggled an edge between a1 and a2, and
		 * between b1 and b2.
		 * Returns true if they are equal.
		 */
		static boolean equal( final int a1, final int a2, final int b1,
				final int b2 )
		{
			return HyperCube.bitCount(a1 ^ a2) == HyperCube.bitCount(b1 ^ b2);
		}

		final private Resource corners[];

		final private int dim;

		final private Model model;

		/** Creates new DiHyperCube */
		public HyperCube( final int dimension, final Model m )
		{
			dim = dimension;
			model = m;
			corners = new Resource[1 << dim];
			for (int i = 0; i < corners.length; i++)
			{
				corners[i] = m.createResource();
			}
			for (int i = 0; i < corners.length; i++)
			{
				add(i, corners[i]);
			}
		}

		private void add( final int corner, final Resource r )
		{
			for (int j = 0; j < dim; j++)
			{
				final int bit = 1 << j;
				model.add(r, RDF.value, corners[corner ^ bit]);
			}
		}

		HyperCube dupe( final int corner )
		{
			final Resource dup = model.createResource();
			add(corner, dup);
			return this;
		}

		HyperCube toggle( final int from, final int to )
		{
			final Resource f = corners[from];
			final Resource t = corners[to];
			final Statement s = model.createStatement(f, RDF.value, t);
			if (model.contains(s))
			{
				model.remove(s);
			}
			else
			{
				model.add(s);
			}
			return this;
		}

	}

	private static int QUANTITY = 10;
	private static int DIMENSION = 6;
	private final int sz = 1 << IsomorphicTests.DIMENSION;

	private Random random;

	private Model model2;

	public IsomorphicTests( final TestingModelFactory modelFactory, final String name )
	{
		super(modelFactory, name);
	}

	@Override
	public void setUp()
	{
		super.setUp();
		random = new Random();
		model2 = createModel();
	}

	@Override
	public void tearDown()
	{
		model.close();
		super.tearDown();
	}

	private void test2DiHyperCube( int quantity, final boolean type )
	{
		if (IsomorphicTests.QUANTITY < 6)
		{
		    // (Guessing) If the number is too small, the probability
		    // of passing the test is too small.
			return;
		}
		for (int i = 0; i < quantity; i++)
		{
			int a1, b1;
			do
			{
				a1 = random.nextInt(sz);
				b1 = random.nextInt(sz);
			} while (type != DiHyperCube.equal(a1, b1));

			new DiHyperCube(IsomorphicTests.DIMENSION, model).dupe(a1).dupe(a1)
					.dupe(a1);

			new DiHyperCube(IsomorphicTests.DIMENSION, model2).dupe(b1).dupe(b1)
					.dupe(b1);

			Assert.assertEquals(type, model.isIsomorphicWith(model2));
		}
	}

	public void test2DiHyperCubeFalse()
	{
		test2DiHyperCube(IsomorphicTests.QUANTITY, false);
	}

	public void test2DiHyperCubeTrue()
	{
		test2DiHyperCube(IsomorphicTests.QUANTITY, true);
	}

	public void test2HyperCube()
	{

		for (int i = 0; i < IsomorphicTests.QUANTITY; i++)
		{
			int a1, b1;
			a1 = random.nextInt(sz);
			b1 = random.nextInt(sz);
			new HyperCube(IsomorphicTests.DIMENSION, model).dupe(a1).dupe(a1)
					.dupe(a1);
			new HyperCube(IsomorphicTests.DIMENSION, model2).dupe(b1).dupe(b1)
					.dupe(b1);
			Assert.assertTrue("Models not isomorphic",
					model.isIsomorphicWith(model2));
		}
	}

	private void test4DiHyperCube( int quantity, final boolean type )
	{

		for (int i = 0; i < quantity; i++)
		{
			int a1, b1, a2, b2;
			do
			{
				a1 = random.nextInt(sz);
				b1 = random.nextInt(sz);
				a2 = random.nextInt(sz);
				b2 = random.nextInt(sz);
			} while (type != DiHyperCube.equal(a1, a2, b1, b2));

			new DiHyperCube(IsomorphicTests.DIMENSION, model).dupe(a1).dupe(a1)
					.dupe(a1).dupe(a2).dupe(a2).dupe(a2);

			new DiHyperCube(IsomorphicTests.DIMENSION, model2).dupe(b1).dupe(b1)
					.dupe(b1).dupe(b2).dupe(b2).dupe(b2);
			final String msg = "(" + a1 + "," + a2 + "),(" + b1 + "," + b2
					+ ")";
			Assert.assertEquals(msg, type, model.isIsomorphicWith(model2));
		}

	}

	public void test4DiHyperCubeFalse()
	{
		test4DiHyperCube(IsomorphicTests.QUANTITY, false);
	}

	public void test4DiHyperCubeTrue()
	{
		test4DiHyperCube(IsomorphicTests.QUANTITY, true);
	}

	private void test4HyperCube( int quantity, final boolean type )
	{

		for (int i = 0; i < quantity; i++)
		{
			int a1, b1, a2, b2;
			do
			{
				a1 = random.nextInt(sz);
				b1 = random.nextInt(sz);
				a2 = random.nextInt(sz);
				b2 = random.nextInt(sz);
			} while (type != HyperCube.equal(a1, a2, b1, b2));

			new HyperCube(IsomorphicTests.DIMENSION, model).dupe(a1).dupe(a1)
					.dupe(a1).dupe(a2).dupe(a2).dupe(a2);
			new HyperCube(IsomorphicTests.DIMENSION, model2).dupe(b1).dupe(b1)
					.dupe(b1).dupe(b2).dupe(b2).dupe(b2);

			final String msg = "(" + a1 + "," + a2 + "),(" + b1 + "," + b2
					+ ")";
			Assert.assertEquals(msg, type, model.isIsomorphicWith(model2));
		}
	}

	public void test4HyperCubeFalse()
	{
	    // Pragmatically, needs more loops
		test4HyperCube(2 * IsomorphicTests.QUANTITY, false);
	}

	public void test4HyperCubeTrue()
	{
		test4HyperCube(IsomorphicTests.QUANTITY, true);
	}

	private void test4ToggleHyperCube( int quantity, final boolean type )
	{

		for (int i = 0; i < quantity; i++)
		{
			int a1, b1, a2, b2;
			do
			{
				a1 = random.nextInt(sz);
				b1 = random.nextInt(sz);
				a2 = random.nextInt(sz);
				b2 = random.nextInt(sz);
			} while (type != HyperCube.equal(a1, a2, b1, b2));
			new HyperCube(IsomorphicTests.DIMENSION, model).toggle(a1, a2);

			new HyperCube(IsomorphicTests.DIMENSION, model2).toggle(b1, b2);

			final String msg = "(" + a1 + "," + a2 + "),(" + b1 + "," + b2
					+ ")";
			Assert.assertEquals(msg, type, model.isIsomorphicWith(model2));
		}
	}

	public void test4ToggleHyperCubeFalse()
	{
		test4ToggleHyperCube(2*IsomorphicTests.QUANTITY, false);
	}

	public void test4ToggleHyperCubeTrue()
	{
		test4ToggleHyperCube(IsomorphicTests.QUANTITY, true);
	}

}
