package se.liu.ida.rdfstar.tools.conversion;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.jena.graph.Node;
import org.apache.jena.graph.Triple;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryFactory;
import org.apache.jena.query.Syntax;
import org.apache.jena.sparql.core.TriplePath;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.syntax.Element;
import org.apache.jena.sparql.syntax.ElementBind;
import org.apache.jena.sparql.syntax.ElementGroup;
import org.apache.jena.sparql.syntax.ElementPathBlock;
import org.junit.Test;

import se.liu.ida.rdfstar.tools.sparqlstar.lang.SPARQLStar;

/**
 * 
 * @author Olaf Hartig
 */
public class ElementTransformSPARQLStarTest
{
	@Test
	public void transformElementPathBlock_NoNeedToConvert()
	{
		final String queryString = "SELECT * WHERE { <http://example.org/a> ?p _:b1 }";
		final String expected =    "SELECT * WHERE { <http://example.org/a> ?p _:b2 }";
		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementPathBlock_NestedSubject()
	{
		final String queryString = "SELECT * WHERE { << <http://ex.org/a> ?p ?o >> ?p2 ?o2 }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
				"  _:b rdf:type rdf:Statement ." +
				"  _:b rdf:subject   <http://ex.org/a> ." +
				"  _:b rdf:predicate ?p ." +
				"  _:b rdf:object    ?o ." +
				"  _:b ?p2 ?o2 ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementPathBlock_NestedObject()
	{
		final String queryString = "SELECT * WHERE { ?s2 ?p2 << <http://ex.org/a> ?p ?o >> }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
				"  _:b rdf:type rdf:Statement ." +
				"  _:b rdf:subject   <http://ex.org/a> ." +
				"  _:b rdf:predicate ?p ." +
				"  _:b rdf:object    ?o ." +
				"  ?s2 ?p2 _:b ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementPathBlock_NestedSubjectAndObject()
	{
		final String queryString = "SELECT * WHERE { << ?s ?p <http://ex.org/a> >> ?p2 << <http://ex.org/a> ?p ?o >> }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  ?s ?p <http://ex.org/a> ." +
		        "  <http://ex.org/a> ?p ?o ." +
				"  _:bs rdf:type rdf:Statement ." +
				"  _:bs rdf:subject   ?s ." +
				"  _:bs rdf:predicate ?p ." +
				"  _:bs rdf:object    <http://ex.org/a> ." +
				"  _:bo rdf:type rdf:Statement ." +
				"  _:bo rdf:subject   <http://ex.org/a> ." +
				"  _:bo rdf:predicate ?p ." +
				"  _:bo rdf:object    ?o ." +
				"  _:bs ?p2 _:bo ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementPathBlock_DoubleNestedSubject()
	{
		final String queryString = "SELECT * WHERE { << << <http://ex.org/a> ?p ?o >> ?p2 ?o2 >> ?p3 _:bnode }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
		        "  _:bss ?p2 ?o2 ." +
				"  _:bss rdf:type rdf:Statement ." +
				"  _:bss rdf:subject   <http://ex.org/a> ." +
				"  _:bss rdf:predicate ?p ." +
				"  _:bss rdf:object    ?o ." +
				"  _:bs rdf:type rdf:Statement ." +
				"  _:bs rdf:subject   _:bss ." +
				"  _:bs rdf:predicate ?p2 ." +
				"  _:bs rdf:object    ?o2 ." +
				"  _:bs ?p3 _:bnode ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementPathBlock_NestedSubjectTwice()
	{
		final String queryString = "SELECT * WHERE { << <http://ex.org/a> ?p ?o >> ?p2 ?o2 ; ?p3 ?o3 }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
				"  _:b rdf:type rdf:Statement ." +
				"  _:b rdf:subject   <http://ex.org/a> ." +
				"  _:b rdf:predicate ?p ." +
				"  _:b rdf:object    ?o ." +
				"  _:b ?p2 ?o2 ." +
				"  _:b ?p3 ?o3 ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementPathBlock_NestedObjectTwice()
	{
		final String queryString = "SELECT * WHERE { ?s2 ?p2 << <http://ex.org/a> ?p ?o >> . ?s3 ?p3 << <http://ex.org/a> ?p ?o >> }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
				"  _:b rdf:type rdf:Statement ." +
				"  _:b rdf:subject   <http://ex.org/a> ." +
				"  _:b rdf:predicate ?p ." +
				"  _:b rdf:object    ?o ." +
				"  ?s2 ?p2 _:b ." +
				"  ?s3 ?p3 _:b ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementPathBlock_NestedSubjectAsNestedObject()
	{
		final String queryString = "SELECT * WHERE { << <http://ex.org/a> ?p ?o >> ?p2 ?o2 . ?s3 ?p3 << <http://ex.org/a> ?p ?o >> }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
				"  _:b rdf:type rdf:Statement ." +
				"  _:b rdf:subject   <http://ex.org/a> ." +
				"  _:b rdf:predicate ?p ." +
				"  _:b rdf:object    ?o ." +
				"  _:b ?p2 ?o2 ." +
				"  ?s3 ?p3 _:b ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementBind_NoNeedToConvert()
	{
		final String queryString = "SELECT * WHERE { BIND ( <http://example.org/a> AS ?t ) }";
		final String expected =    "SELECT * WHERE { BIND ( <http://example.org/a> AS ?t ) }";
		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementBind_Simple()
	{
		final String queryString = "SELECT * WHERE { BIND( << <http://ex.org/a> ?p ?o >> AS ?t ) }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
				"  ?t rdf:type rdf:Statement ." +
				"  ?t rdf:subject   <http://ex.org/a> ." +
				"  ?t rdf:predicate ?p ." +
				"  ?t rdf:object    ?o ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementBind_NestedSubject()
	{
		final String queryString = "SELECT * WHERE { BIND( << << <http://ex.org/a> ?p ?o >> ?p2 ?o2 >> AS ?t ) }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
		        "  _:bs ?p2 ?o2 ." +
				"  _:bs rdf:type rdf:Statement ." +
				"  _:bs rdf:subject   <http://ex.org/a> ." +
				"  _:bs rdf:predicate ?p ." +
				"  _:bs rdf:object    ?o ." +
				"  ?t rdf:type rdf:Statement ." +
				"  ?t rdf:subject   _:bs ." +
				"  ?t rdf:predicate ?p2 ." +
				"  ?t rdf:object    ?o2 ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementBind_NestedObject()
	{
		final String queryString = "SELECT * WHERE { BIND( << ?s2 ?p2 << <http://ex.org/a> ?p ?o >> >> AS ?t ) }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
		        "  ?s2 ?p2 _:bo ." +
				"  _:bo rdf:type rdf:Statement ." +
				"  _:bo rdf:subject   <http://ex.org/a> ." +
				"  _:bo rdf:predicate ?p ." +
				"  _:bo rdf:object    ?o ." +
				"  ?t rdf:type rdf:Statement ." +
				"  ?t rdf:subject   ?s2 ." +
				"  ?t rdf:predicate ?p2 ." +
				"  ?t rdf:object    _:bo ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transformElementBind_NestedSubjectAndObject()
	{
		final String queryString = "SELECT * WHERE { BIND( << << <http://ex.org/a> ?p _:bnode >> ?p2 << <http://ex.org/a> ?p ?o >> >> AS ?t ) }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p _:bnode ." +
		        "  <http://ex.org/a> ?p ?o ." +
		        "  _:bs ?p2 _:bo ." +
				"  _:bs rdf:type rdf:Statement ." +
				"  _:bs rdf:subject   <http://ex.org/a> ." +
				"  _:bs rdf:predicate ?p ." +
				"  _:bs rdf:object    _:bnode ." +
				"  _:bo rdf:type rdf:Statement ." +
				"  _:bo rdf:subject   <http://ex.org/a> ." +
				"  _:bo rdf:predicate ?p ." +
				"  _:bo rdf:object    ?o ." +
				"  ?t rdf:type rdf:Statement ." +
				"  ?t rdf:subject   _:bs ." +
				"  ?t rdf:predicate ?p2 ." +
				"  ?t rdf:object    _:bo ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transform_BindVarUse()
	{
		final String queryString = "SELECT * WHERE { BIND( << <http://ex.org/a> ?p ?o >> AS ?t ) . ?t ?p2 ?o2 }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
				"  ?t rdf:type rdf:Statement ." +
				"  ?t rdf:subject   <http://ex.org/a> ." +
				"  ?t rdf:predicate ?p ." +
				"  ?t rdf:object    ?o ." +
				"  ?t ?p2 ?o2 ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}

	@Test
	public void transform_BindSameAsNested()
	{
		final String queryString = "SELECT * WHERE { BIND( << <http://ex.org/a> ?p ?o >> AS ?t ) . << <http://ex.org/a> ?p ?o >> ?p2 ?o2 }";

		final String expected = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " +
		        "SELECT * WHERE { " +
		        "  <http://ex.org/a> ?p ?o ." +
				"  ?t rdf:type rdf:Statement ." +
				"  ?t rdf:subject   <http://ex.org/a> ." +
				"  ?t rdf:predicate ?p ." +
				"  ?t rdf:object    ?o ." +
				"  ?t ?p2 ?o2 ." +
				"}";

		checkBgpAndBindOnlyQuery(queryString, expected);
	}


	// ---- helpers ----

	protected Query convert( String sparqlstarQueryString )
	{
		final String baseIRI = null;

		final Query sparqlstarQuery = QueryFactory.create(sparqlstarQueryString, baseIRI, SPARQLStar.syntax);

		final Query sparqlQuery = new SPARQLStar2SPARQL().convert(sparqlstarQuery);

		assertEquals( Syntax.syntaxSPARQL, sparqlQuery.getSyntax() );

		return sparqlQuery;
	}

	protected void checkBgpAndBindOnlyQuery( String sparqlstarQueryString, String expectedResultQuery )
	{
		final String baseIRI = null;
		final Query expectedQuery = QueryFactory.create(expectedResultQuery, baseIRI, Syntax.syntaxSPARQL);
		final ElementGroup expectedEG = (ElementGroup) expectedQuery.getQueryPattern();

		final Query convertedQuery = convert( sparqlstarQueryString );
		final ElementGroup convertedEG = (ElementGroup) convertedQuery.getQueryPattern();
		
		checkForEquivalence( expectedEG, mergeElementPathBlocks(convertedEG) );
	}

	protected void checkForEquivalence( ElementGroup expectedEG, ElementGroup testEG )
	{
		assertEquals( expectedEG.size(), testEG.size() );
		assertEquals( expectedEG.get(0).getClass(), testEG.get(0).getClass() );

		if ( expectedEG.get(0) instanceof ElementPathBlock ) {
			final ElementPathBlock expectedEPB = (ElementPathBlock) expectedEG.get(0);
			final ElementPathBlock convertedEPB = (ElementPathBlock) testEG.get(0);
			checkForEquivalence( expectedEPB, convertedEPB );
		}
		else if ( expectedEG.get(0) instanceof ElementBind ) {
			final ElementBind expectedEB = (ElementBind) expectedEG.get(0);
			final ElementBind convertedEB = (ElementBind) testEG.get(0);
			checkForEquivalence( expectedEB, convertedEB );
		}
		else
			fail();
	}

	protected void checkForEquivalence( ElementPathBlock expectedEPB, ElementPathBlock testEPB )
	{
		final IsomorphismMap isoMap = new IsomorphismMap();

		final Iterator<TriplePath> eit = expectedEPB.getPattern().iterator();
		while ( eit.hasNext() )
		{
			final Triple expcTP = eit.next().asTriple();

			boolean found = false;
			final Iterator<TriplePath> rit = testEPB.getPattern().iterator();
			while ( rit.hasNext() )
			{
				final Triple testTP = rit.next().asTriple();

				if (    isoMap.canBeIsomorphic(expcTP.getSubject(),   testTP.getSubject())
				     && isoMap.canBeIsomorphic(expcTP.getPredicate(), testTP.getPredicate())
				     && isoMap.canBeIsomorphic(expcTP.getObject(),    testTP.getObject()) )
					found = true;
			}

			if ( ! found )
				System.err.println( "Expected triple pattern not found (" + expcTP.toString() + ")" );

			assertTrue(found);
		}
	}

	protected void checkForEquivalence( ElementBind expectedEB, ElementBind testEB )
	{
		assertTrue( expectedEB.equalTo(testEB,null) );
	}

	protected ElementGroup mergeElementPathBlocks( ElementGroup eg )
	{
		if ( eg.size() == 1 )
			return eg;

		final ElementGroup result = new ElementGroup();

		final Iterator<Element> it = eg.getElements().iterator();
		Element prevEl = it.next();

		while ( it.hasNext() )
		{
			final Element currEl = it.next();
			if (    (currEl instanceof ElementPathBlock)
			     && (prevEl instanceof ElementPathBlock) )
			{
				final ElementPathBlock currPB = (ElementPathBlock) currEl;
				final ElementPathBlock prevPB = (ElementPathBlock) prevEl;
				prevPB.getPattern().addAll( currPB.getPattern() );
			}
			else {
				result.addElement(prevEl);
				prevEl = currEl;
			}
		}

		result.addElement(prevEl);

		return result;
	}

	protected class IsomorphismMap
	{
	    final protected Map<Node, Node> map = new HashMap<Node, Node>();
	    final protected Set<Node> valueSet = new HashSet<Node>();

	    public boolean canBeIsomorphic(Node n1, Node n2) {
	        if (    (n1.isBlank() || Var.isBlankNodeVar(n1))
	             && (n2.isBlank() || Var.isBlankNodeVar(n2)) ) {
	            final Node other = map.get(n1);
	            if ( other == null ) {
	            	if ( valueSet.contains(n2) )
	            		return false;

	                map.put(n1, n2);
	                valueSet.add(n2);
	                return true;
	            }
	            return other.equals(n2);
	        }
	        return n1.equals(n2);
	    }
	} // end of class IsomorphismMap

}