/*
 *  Licensed 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.
 *
 *  See the NOTICE file distributed with this work for additional
 *  information regarding copyright ownership.
 */

package org.topbraid.jenax.util;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;

import org.apache.jena.enhanced.EnhGraph;
import org.apache.jena.graph.Factory;
import org.apache.jena.graph.Graph;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.compose.MultiUnion;
import org.apache.jena.ontology.OntModel;
import org.apache.jena.ontology.OntModelSpec;
import org.apache.jena.query.ARQ;
import org.apache.jena.query.Dataset;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryExecution;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.query.QuerySolutionMap;
import org.apache.jena.query.ResultSet;
import org.apache.jena.rdf.model.Literal;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.NodeIterator;
import org.apache.jena.rdf.model.Property;
import org.apache.jena.rdf.model.RDFList;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.rdf.model.Statement;
import org.apache.jena.rdf.model.StmtIterator;
import org.apache.jena.rdf.model.impl.PropertyImpl;
import org.apache.jena.rdf.model.impl.StmtIteratorImpl;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.sparql.ARQConstants;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.engine.ExecutionContext;
import org.apache.jena.sparql.engine.binding.Binding;
import org.apache.jena.sparql.engine.binding.BindingHashMap;
import org.apache.jena.sparql.engine.binding.BindingRoot;
import org.apache.jena.sparql.expr.E_Function;
import org.apache.jena.sparql.expr.Expr;
import org.apache.jena.sparql.expr.ExprEvalException;
import org.apache.jena.sparql.expr.ExprList;
import org.apache.jena.sparql.expr.ExprTransform;
import org.apache.jena.sparql.expr.ExprTransformer;
import org.apache.jena.sparql.expr.ExprVar;
import org.apache.jena.sparql.expr.NodeValue;
import org.apache.jena.sparql.expr.nodevalue.NodeFunctions;
import org.apache.jena.sparql.function.FunctionEnv;
import org.apache.jena.sparql.graph.NodeTransform;
import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransform;
import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransformSubst;
import org.apache.jena.sparql.syntax.syntaxtransform.ExprTransformNodeElement;
import org.apache.jena.sparql.syntax.syntaxtransform.QueryTransformOps;
import org.apache.jena.sparql.util.Context;
import org.apache.jena.sparql.util.NodeFactoryExtra;
import org.apache.jena.sparql.util.NodeUtils;
import org.apache.jena.vocabulary.OWL;
import org.apache.jena.vocabulary.RDF;
import org.apache.jena.vocabulary.RDFS;
import org.apache.jena.vocabulary.XSD;
import org.topbraid.jenax.progress.ProgressMonitor;


/**
 * Some convenience methods to operate on Jena Models.
 * 
 * These methods are not as stable as the rest of the API, but
 * they may be of general use.
 * 
 * @author Holger Knublauch
 */
public class JenaUtil {

	// Unstable
	private static JenaUtilHelper helper = new JenaUtilHelper();

	// Leave this line under the helper line above!
	private static Model dummyModel = JenaUtil.createDefaultModel();
	
	public static final String WITH_IMPORTS_PREFIX = "http://rdfex.org/withImports?uri=";
	
	

	/**
	 * Sets the helper which allows the behavior of some JenaUtil
	 * methods to be modified by the system using the SPIN library.
	 * Note: Should not be used outside of TopBraid - not stable.
	 * @param h  the JenaUtilHelper
	 * @return the old helper
	 */
	public static JenaUtilHelper setHelper(JenaUtilHelper h) {
		JenaUtilHelper old = helper;
		helper = h;
		return old;
	}
	
	
	/**
	 * Gets the current helper object.
	 * Note: Should not be used outside of TopBraid - not stable.
	 * @return the helper
	 */
	public final static JenaUtilHelper getHelper() {
		return helper;
	}
	
	
	/**
	 * Populates a result set of resources reachable from a subject via zero or more steps with a given predicate.
	 * Implementation note: the results set need only implement {@link Collection#add(Object)}.
	 * @param results  The transitive objects reached from subject via triples with the given predicate
	 * @param subject  the subject to start traversal at
	 * @param predicate  the predicate to walk
	 */
	public static void addTransitiveObjects(Set<Resource> results, Resource subject, Property predicate) {
		helper.setGraphReadOptimization(true);
		try {
			addTransitiveObjects(results, new HashSet<Resource>(), subject, predicate);
		}
		finally {
			helper.setGraphReadOptimization(false);
		}
	}


	private static void addTransitiveObjects(Set<Resource> resources, Set<Resource> reached,
			Resource subject, Property predicate) {
		resources.add(subject);
		reached.add(subject);
		StmtIterator it = subject.listProperties(predicate);
		try {
			while (it.hasNext()) {
				RDFNode object = it.next().getObject();
				if (object instanceof Resource) {
					if (!reached.contains(object)) {
						addTransitiveObjects(resources, reached, (Resource)object, predicate);
					}
				}
			}
		}
		finally {
			it.close();
		}
	}


	private static void addTransitiveSubjects(Set<Resource> reached, Resource object,
			Property predicate, ProgressMonitor monitor) {
		if (object != null) {
			reached.add(object);
			StmtIterator it = object.getModel().listStatements(null, predicate, object);
			try {
				while (it.hasNext()) {
					if (monitor != null && monitor.isCanceled()) {
						it.close();
						return;
					}
					Resource subject = it.next().getSubject();
					if (!reached.contains(subject)) {
						addTransitiveSubjects(reached, subject, predicate, monitor);
					}
				}
			}
			finally {
				it.close();
			}
		}
	}
	
	
	/**
	 * Turns a QuerySolution into a Binding. 
	 * @param map  the input QuerySolution
	 * @return a Binding or null if the input is null
	 */
	public static Binding asBinding(final QuerySolution map) {
		if(map != null) {
			BindingHashMap result = new BindingHashMap();
			Iterator<String> varNames = map.varNames();
			while(varNames.hasNext()) {
				String varName = varNames.next();
				RDFNode node = map.get(varName);
				if(node != null) {
					result.add(Var.alloc(varName), node.asNode());
				}
			}
			return result;
		}
		else {
			return null;
		}
	}
	

	/**
	 * Turns a Binding into a QuerySolutionMap.
	 * @param binding  the Binding to convert
	 * @return a QuerySolutionMap
	 */
	public static QuerySolutionMap asQuerySolutionMap(Binding binding) {
		QuerySolutionMap map = new QuerySolutionMap();
		Iterator<Var> vars = binding.vars();
		while(vars.hasNext()) {
			Var var = vars.next();
			Node node = binding.get(var);
			if(node != null) {
				map.add(var.getName(), dummyModel.asRDFNode(node));
			}
		}
		return map;
	}
	
	
	/**
	 * Returns a set of resources reachable from an object via one or more reversed steps with a given predicate.
	 * @param object  the object to start traversal at
	 * @param predicate  the predicate to walk
	 * @param monitor  an optional progress monitor to allow cancelation
	 * @return the reached resources
	 */
	public static Set<Resource> getAllTransitiveSubjects(Resource object, Property predicate, ProgressMonitor monitor) {
		Set<Resource> set = new HashSet<>();
		helper.setGraphReadOptimization(true);
		try {
		   addTransitiveSubjects(set, object, predicate, monitor);
		}
		finally {
			helper.setGraphReadOptimization(false);
		}
		set.remove(object);
		return set;
	}
	
	
	/**
	 * Casts a Resource into a Property.
	 * @param resource  the Resource to cast
	 * @return resource as an instance of Property
	 */
	public static Property asProperty(Resource resource) {
		if(resource instanceof Property) {
			return (Property) resource;
		}
		else {
			return new PropertyImpl(resource.asNode(), (EnhGraph)resource.getModel());
		}
	}


	public static void collectBaseGraphs(Graph graph, Set<Graph> baseGraphs) {
		if(graph instanceof MultiUnion) {
			MultiUnion union = (MultiUnion)graph;
			collectBaseGraphs(union.getBaseGraph(), baseGraphs);
			for(Object subGraph : union.getSubGraphs()) {
				collectBaseGraphs((Graph)subGraph, baseGraphs);
			}
		}
		else if(graph != null) {
			baseGraphs.add(graph);
		}
	}
	

	/**
	 * Creates a new Graph.  By default this will deliver a plain in-memory graph,
	 * but other implementations may deliver graphs with concurrency support and
	 * other features.
	 * @return a default graph
	 * @see #createDefaultModel()
	 */
	public static Graph createDefaultGraph() {
		return helper.createDefaultGraph();
	}


	/**
	 * Wraps the result of {@link #createDefaultGraph()} into a Model and initializes namespaces. 
	 * @return a default Model
	 * @see #createDefaultGraph()
	 */
	public static Model createDefaultModel() {
		Model m = ModelFactory.createModelForGraph(createDefaultGraph());
		initNamespaces(m);
		return m;
	}
	
	
	/**
	 * Creates a memory Graph with no reification.
	 * @return a new memory graph
	 */
	public static Graph createMemoryGraph() {
		return Factory.createDefaultGraph();
	}
	
	
	/**
	 * Creates a memory Model with no reification.
	 * @return a new memory Model
	 */
	public static Model createMemoryModel() {
		return ModelFactory.createModelForGraph(createMemoryGraph());
	}

	
	public static MultiUnion createMultiUnion() {
		return helper.createMultiUnion();
	}

	
	public static MultiUnion createMultiUnion(Graph[] graphs) {
		return helper.createMultiUnion(graphs);
	}

	
	public static MultiUnion createMultiUnion(Iterator<Graph> graphs) {
		return helper.createMultiUnion(graphs);
	}
	
	
	/**
	 * Gets all instances of a given class and its subclasses.
	 * @param cls  the class to get the instances of
	 * @return the instances
	 */
	public static Set<Resource> getAllInstances(Resource cls) {
		JenaUtil.setGraphReadOptimization(true);
		try {
			Model model = cls.getModel();
			Set<Resource> classes = getAllSubClasses(cls);
			classes.add(cls);
			Set<Resource> results = new HashSet<>();
			for(Resource subClass : classes) {
				StmtIterator it = model.listStatements(null, RDF.type, subClass);
				while (it.hasNext()) {
					results.add(it.next().getSubject());
				}
			}
			return results;
		}
		finally {
			JenaUtil.setGraphReadOptimization(false);
		}
	}

	
	public static Set<Resource> getAllSubClasses(Resource cls) {
		return getAllTransitiveSubjects(cls, RDFS.subClassOf);
	}

	
	/**
	 * Returns a set consisting of a given class and all its subclasses.
	 * Similar to rdfs:subClassOf*.
	 * @param cls  the class to return with its subclasses
	 * @return the Set of class resources
	 */
	public static Set<Resource> getAllSubClassesStar(Resource cls) {
		Set<Resource> results = getAllTransitiveSubjects(cls, RDFS.subClassOf);
		results.add(cls);
		return results;
	}
	
	
	public static Set<Resource> getAllSubProperties(Property superProperty) {
		return getAllTransitiveSubjects(superProperty, RDFS.subPropertyOf);
	}

	
	public static Set<Resource> getAllSuperClasses(Resource cls) {
		return getAllTransitiveObjects(cls, RDFS.subClassOf);
	}

	
	/**
	 * Returns a set consisting of a given class and all its superclasses.
	 * Similar to rdfs:subClassOf*.
	 * @param cls  the class to return with its superclasses
	 * @return the Set of class resources
	 */
	public static Set<Resource> getAllSuperClassesStar(Resource cls) {
		Set<Resource> results = getAllTransitiveObjects(cls, RDFS.subClassOf);
		results.add(cls);
		return results;
	}
	
	
	public static Set<Resource> getAllSuperProperties(Property subProperty) {
		return getAllTransitiveObjects(subProperty, RDFS.subPropertyOf);
	}

	
	/**
	 * Returns a set of resources reachable from a subject via one or more steps with a given predicate.
	 * @param subject  the subject to start at
	 * @param predicate  the predicate to traverse
	 * @return the reached resources
	 */
	public static Set<Resource> getAllTransitiveObjects(Resource subject, Property predicate) {
		Set<Resource> set = new HashSet<>();
		addTransitiveObjects(set, subject, predicate);
		set.remove(subject);
		return set;
	}

	
	private static Set<Resource> getAllTransitiveSubjects(Resource object, Property predicate) {
		return getAllTransitiveSubjects(object, predicate, null);
	}

	
	public static Set<Resource> getAllTypes(Resource instance) {
		Set<Resource> types = new HashSet<>();
		StmtIterator it = instance.listProperties(RDF.type);
		try {
			while (it.hasNext()) {
				Resource type = it.next().getResource();
				types.add(type);
				types.addAll(getAllSuperClasses(type));
			}
		}
		finally {
			it.close();
		}
		return types;
	}


	/**
	 * Gets the "base graph" of a Model, walking into MultiUnions if needed.
	 * @param model  the Model to get the base graph of
	 * @return the base graph or null if the model contains a MultiUnion that doesn't declare one
	 */
	public static Graph getBaseGraph(final Model model) {
		return getBaseGraph(model.getGraph());
	}
	
	
	public static Graph getBaseGraph(Graph graph) {
		Graph baseGraph = graph;
		while(baseGraph instanceof MultiUnion) {
			baseGraph = ((MultiUnion)baseGraph).getBaseGraph();
		}
		return baseGraph;
	}
	
	
	public static Model getBaseModel(Model model) {
		Graph baseGraph = getBaseGraph(model);
		if(baseGraph == model.getGraph()) {
			return model;
		}
		else {
			return ModelFactory.createModelForGraph(baseGraph);
		}
	}


	/**
	 * For a given subject resource and a given collection of (label/comment) properties this finds the most
	 * suitable value of either property for a given list of languages (usually from the current user's preferences).
	 * For example, if the user's languages are [ "en-AU" ] then the function will prefer "mate"@en-AU over
	 * "friend"@en and never return "freund"@de.  The function falls back to literals that have no language
	 * if no better literal has been found.
	 * @param resource  the subject resource
	 * @param langs  the allowed languages
	 * @param properties  the properties to check
	 * @return the best suitable value or null
	 */
	public static Literal getBestStringLiteral(Resource resource, List<String> langs, Iterable<Property> properties) {
		return getBestStringLiteral(resource, langs, properties, (r,p) -> r.listProperties(p));
	}
	
	
	public static Literal getBestStringLiteral(Resource resource, List<String> langs, Iterable<Property> properties, BiFunction<Resource,Property,Iterator<Statement>> getter) {
		Literal label = null;
		int bestLang = -1;
		for(Property predicate : properties) {
			Iterator<Statement> it = getter.apply(resource, predicate);
			while(it.hasNext()) {
				RDFNode object = it.next().getObject();
				if(object.isLiteral()) {
					Literal literal = (Literal)object;
					String lang = literal.getLanguage();
					if(lang.length() == 0 && label == null) {
						label = literal;
					}
					else {
						// 1) Never use a less suitable language
						// 2) Never replace an already existing label (esp: skos:prefLabel) unless new lang is better
						// 3) Fall back to more special languages if no other was found (e.g. use en-GB if only "en" is accepted)
						int startLang = bestLang < 0 ? langs.size() - 1 : (label != null ? bestLang - 1 : bestLang);
						for(int i = startLang; i >= 0; i--) {
							String langi = langs.get(i);
							if(langi.equals(lang)) {
								label = literal;
								bestLang = i;
							}
							else if(lang.contains("-") && NodeFunctions.langMatches(lang, langi) && label == null) {
								label = literal;
							}
						}
					}
				}
			}
		}
		return label;
	}


	/**
	 * Gets the "first" declared rdfs:range of a given property.
	 * If multiple ranges exist, the behavior is undefined.
	 * Note that this method does not consider ranges defined on
	 * super-properties.
	 * @param property  the property to get the range of
	 * @return the "first" range Resource or null
	 */
	public static Resource getFirstDirectRange(Resource property) {
		return property.getPropertyResourceValue(RDFS.range);
	}


	private static Resource getFirstRange(Resource property, Set<Resource> reached) {
		Resource directRange = getFirstDirectRange(property);
		if(directRange != null) {
			return directRange;
		}
		StmtIterator it = property.listProperties(RDFS.subPropertyOf);
		while (it.hasNext()) {
			Statement ss = it.next();
			if (ss.getObject().isURIResource()) {
				Resource superProperty = ss.getResource();
				if (!reached.contains(superProperty)) {
					reached.add(superProperty);
					Resource r = getFirstRange(superProperty, reached);
					if (r != null) {
						it.close();
						return r;
					}
				}
			}
		}
		return null;
	}


	/**
	 * Gets the "first" declared rdfs:range of a given property.
	 * If multiple ranges exist, the behavior is undefined.
	 * This method walks up to super-properties if no direct match exists.
	 * @param property  the property to get the range of
	 * @return the "first" range Resource or null
	 */
	public static Resource getFirstRange(Resource property) {
		return getFirstRange(property, new HashSet<>());
	}
	
	
	public static Set<Resource> getImports(Resource graph) {
		Set<Resource> results = new HashSet<>();
		for(Property importProperty : ImportProperties.get().getImportProperties()) {
			results.addAll(JenaUtil.getResourceProperties(graph, importProperty));
		}
		return results;
	}

	
	public static Integer getIntegerProperty(Resource subject, Property predicate) {
		Statement s = subject.getProperty(predicate);
		if(s != null && s.getObject().isLiteral()) {
			return s.getInt();
		}
		else {
			return null;
		}
	}
	
	
	public static RDFList getListProperty(Resource subject, Property predicate) {
		Statement s = subject.getProperty(predicate);
		if(s != null && s.getObject().canAs(RDFList.class)) {
			return s.getResource().as(RDFList.class);
		}
		else {
			return null;
		}
	}
	
	
	public static List<Literal> getLiteralProperties(Resource subject, Property predicate) {
		List<Literal> results = new LinkedList<>();
		StmtIterator it = subject.listProperties(predicate);
		while(it.hasNext()) {
			Statement s = it.next();
			if(s.getObject().isLiteral()) {
				results.add(s.getLiteral());
			}
		}
		return results;
	}
	
	
	/**
	 * Walks up the class hierarchy starting at a given class until one of them
	 * returns a value for a given Function.
	 * @param cls  the class to start at
	 * @param function  the Function to execute on each class
	 * @param <T>  the requested result type
	 * @return the "first" non-null value, or null
	 */
	public static <T> T getNearest(Resource cls, java.util.function.Function<Resource,T> function) {
		T result = function.apply(cls);
		if(result != null) {
			return result;
		}
		return getNearest(cls, function, new HashSet<>());
	}

	
	private static <T> T getNearest(Resource cls, java.util.function.Function<Resource,T> function, Set<Resource> reached) {
		reached.add(cls);
		StmtIterator it = cls.listProperties(RDFS.subClassOf);
		while(it.hasNext()) {
			Statement s = it.next();
			if(s.getObject().isResource() && !reached.contains(s.getResource())) {
				T result = function.apply(s.getResource());
				if(result == null) {
					result = getNearest(s.getResource(), function, reached);
				}
				if(result != null) {
					it.close();
					return result;
				}
			}
		}
		return null;
	}

	
	/**
	 * Overcomes a design mismatch with Jena: if the base model does not declare a default namespace then the
	 * default namespace of an import is returned - this is not desirable for TopBraid-like scenarios.
	 * @param model the Model to operate on
	 * @param prefix the prefix to get the URI of
	 * @return the URI of prefix
	 */
	public static String getNsPrefixURI(Model model, String prefix) {
		if ("".equals(prefix) && model.getGraph() instanceof MultiUnion) {
			Graph baseGraph = ((MultiUnion)model.getGraph()).getBaseGraph();
			if(baseGraph != null) {
				return baseGraph.getPrefixMapping().getNsPrefixURI(prefix);
			}
			else {
				return model.getNsPrefixURI(prefix);
			}
		}
		else {
			return model.getNsPrefixURI(prefix);
		}
	}
	
	
	public static RDFNode getProperty(Resource subject, Property predicate) {
		Statement s = subject.getProperty(predicate);
		if(s != null) {
			return s.getObject();
		}
		else {
			return null;
		}
	}
	
	
	public static Resource getResourcePropertyWithType(Resource subject, Property predicate, Resource type) {
		StmtIterator it = subject.listProperties(predicate);
		while(it.hasNext()) {
			Statement s = it.next();
			if(s.getObject().isResource() && JenaUtil.hasIndirectType(s.getResource(), type)) {
				it.close();
				return s.getResource();
			}
		}
		return null;
	}
	
	
	public static List<Resource> getResourceProperties(Resource subject, Property predicate) {
		List<Resource> results = new LinkedList<>();
		StmtIterator it = subject.listProperties(predicate);
		while(it.hasNext()) {
			Statement s = it.next();
			if(s.getObject().isResource()) {
				results.add(s.getResource());
			}
		}
		return results;
	}
	
	
	public static Resource getURIResourceProperty(Resource subject, Property predicate) {
		Statement s = subject.getProperty(predicate);
		if(s != null && s.getObject().isURIResource()) {
			return s.getResource();
		}
		else {
			return null;
		}
	}
	
	
	public static List<Resource> getURIResourceProperties(Resource subject, Property predicate) {
		List<Resource> results = new LinkedList<>();
		StmtIterator it = subject.listProperties(predicate);
		while(it.hasNext()) {
			Statement s = it.next();
			if(s.getObject().isURIResource()) {
				results.add(s.getResource());
			}
		}
		return results;
	}
	
	
	public static String getStringProperty(Resource subject, Property predicate) {
		Statement s = subject.getProperty(predicate);
		if(s != null && s.getObject().isLiteral()) {
			return s.getString();
		}
		else {
			return null;
		}
	}

	
	public static boolean getBooleanProperty(Resource subject, Property predicate) {
		Statement s = subject.getProperty(predicate);
		if(s != null && s.getObject().isLiteral()) {
			return s.getBoolean();
		}
		else {
			return false;
		}
	}
	
	
	public static Double getDoubleProperty(Resource subject, Property predicate) {
		Statement s = subject.getProperty(predicate);
		if(s != null && s.getObject().isLiteral()) {
			return s.getDouble();
		}
		else {
			return null;
		}
	}
	
	
	public static double getDoubleProperty(Resource subject, Property predicate, double defaultValue) {
		Double d = getDoubleProperty(subject, predicate);
		if(d != null) {
			return d;
		}
		else {
			return defaultValue;
		}
	}
	
	
	public static List<Graph> getSubGraphs(MultiUnion union) {
		List<Graph> results = new LinkedList<>();
		Graph baseGraph = union.getBaseGraph();
		if(baseGraph != null) {
			results.add(baseGraph);
		}
		results.addAll(union.getSubGraphs());
		return results;
	}
	
	
	/**
	 * Gets a Set of all superclasses (rdfs:subClassOf) of a given Resource. 
	 * @param subClass  the subClass Resource
	 * @return a Collection of class resources
	 */
	public static Collection<Resource> getSuperClasses(Resource subClass) {
		NodeIterator it = subClass.getModel().listObjectsOfProperty(subClass, RDFS.subClassOf);
		Set<Resource> results = new HashSet<>();
		while (it.hasNext()) {
			RDFNode node = it.nextNode();
			if (node instanceof Resource) {
				results.add((Resource)node);
			}
		}
		return results;
	}
	

	/**
	 * Gets the "first" type of a given Resource.
	 * @param instance  the instance to get the type of
	 * @return the type or null
	 */
	public static Resource getType(Resource instance) {
		return instance.getPropertyResourceValue(RDF.type);
	}
	
	
	/**
	 * Gets a Set of all rdf:types of a given Resource. 
	 * @param instance  the instance Resource
	 * @return a Collection of type resources
	 */
	public static List<Resource> getTypes(Resource instance) {
		return JenaUtil.getResourceProperties(instance, RDF.type);
	}
	
	
	/**
	 * Checks whether a given Resource is an instance of a given type, or
	 * a subclass thereof.  Make sure that the expectedType parameter is associated
	 * with the right Model, because the system will try to walk up the superclasses
	 * of expectedType.  The expectedType may have no Model, in which case
	 * the method will use the instance's Model.
	 * @param instance  the Resource to test
	 * @param expectedType  the type that instance is expected to have
	 * @return true if resource has rdf:type expectedType
	 */
	public static boolean hasIndirectType(Resource instance, Resource expectedType) {
		
		if(expectedType.getModel() == null) {
			expectedType = expectedType.inModel(instance.getModel());
		}
		
		StmtIterator it = instance.listProperties(RDF.type);
		while(it.hasNext()) {
			Statement s = it.next();
			if(s.getObject().isResource()) {
				Resource actualType = s.getResource();
				if(actualType.equals(expectedType) || JenaUtil.hasSuperClass(actualType, expectedType)) {
					it.close();
					return true;
				}
			}
		}
		return false;
	}
	

	/**
	 * Checks whether a given class has a given (transitive) super class.
	 * @param subClass  the sub-class
	 * @param superClass  the super-class
	 * @return true if subClass has superClass (somewhere up the tree)
	 */
	public static boolean hasSuperClass(Resource subClass, Resource superClass) {
		return hasSuperClass(subClass, superClass, new HashSet<>());
	}
	
	
	private static boolean hasSuperClass(Resource subClass, Resource superClass, Set<Resource> reached) {
		StmtIterator it = subClass.listProperties(RDFS.subClassOf);
		while(it.hasNext()) {
			Statement s = it.next();
			if(superClass.equals(s.getObject())) {
				it.close();
				return true;
			}
			else if(!reached.contains(s.getResource())) {
				reached.add(s.getResource());
				if(hasSuperClass(s.getResource(), superClass, reached)) {
					it.close();
					return true;
				}
			}
		}
		return false;
	}
	

	/**
	 * Checks whether a given property has a given (transitive) super property.
	 * @param subProperty  the sub-property
	 * @param superProperty  the super-property
	 * @return true if subProperty has superProperty (somewhere up the tree)
	 */
	public static boolean hasSuperProperty(Property subProperty, Property superProperty) {
		return getAllSuperProperties(subProperty).contains(superProperty);
	}


	/**
	 * Sets the usual default namespaces for rdf, rdfs, owl and xsd.
	 * @param graph  the Graph to modify
	 */
	public static void initNamespaces(Graph graph) {
		PrefixMapping prefixMapping = graph.getPrefixMapping();
		initNamespaces(prefixMapping);
	}
	
	
	/**
	 * Sets the usual default namespaces for rdf, rdfs, owl and xsd.
	 * @param prefixMapping  the Model to modify
	 */
	public static void initNamespaces(PrefixMapping prefixMapping) {
	    ensurePrefix(prefixMapping, "rdf",  RDF.getURI());
	    ensurePrefix(prefixMapping, "rdfs", RDFS.getURI());
	    ensurePrefix(prefixMapping, "owl",  OWL.getURI());
	    ensurePrefix(prefixMapping, "xsd",  XSD.getURI());
	}

	private static void ensurePrefix(PrefixMapping prefixMapping, String prefix, String uristr) {
	    // set if not present, or if different
	    if (!uristr.equals(prefixMapping.getNsPrefixURI(prefix))) {
	        prefixMapping.setNsPrefix(prefix, uristr);
	    }
	}

	/**
	 * Checks whether a given graph (possibly a MultiUnion) only contains
	 * GraphMemBase instances.
	 * @param graph  the Graph to test
	 * @return true  if graph is a memory graph
	 */
	public static boolean isMemoryGraph(Graph graph) {
		if(graph instanceof MultiUnion) {
			for(Graph subGraph : JenaUtil.getSubGraphs((MultiUnion)graph)) {
				if(!isMemoryGraph(subGraph)) {
					return false;
				}
			}
			return true;
		}
		else  {
			return helper.isMemoryGraph(graph);
		}
	}
	
	
	/**
	 * Gets an Iterator over all Statements of a given property or its sub-properties
	 * at a given subject instance.  Note that the predicate and subject should be
	 * both attached to a Model to avoid NPEs.
	 * @param subject  the subject (may be null)
	 * @param predicate  the predicate
	 * @return a StmtIterator
	 */
	public static StmtIterator listAllProperties(Resource subject, Property predicate) {
		List<Statement> results = new LinkedList<>();
		helper.setGraphReadOptimization(true);
		try {
			listAllProperties(subject, predicate, new HashSet<>(), results);
		}
		finally {
			helper.setGraphReadOptimization(false);
		}
		return new StmtIteratorImpl(results.iterator());
	}
	
	
	private static void listAllProperties(Resource subject, Property predicate, Set<Property> reached,
			List<Statement> results) {
		reached.add(predicate);
		StmtIterator sit;
		Model model;
		if (subject != null) {
			sit = subject.listProperties(predicate);
			model = subject.getModel();
		}
		else {
			model = predicate.getModel();
			sit = model.listStatements(null, predicate, (RDFNode)null);
		}
		while (sit.hasNext()) {
			results.add(sit.next());
		}

		// Iterate into direct subproperties
		StmtIterator it = model.listStatements(null, RDFS.subPropertyOf, predicate);
		while (it.hasNext()) {
			Statement sps = it.next();
			if (!reached.contains(sps.getSubject())) {
				Property subProperty = asProperty(sps.getSubject());
				listAllProperties(subject, subProperty, reached, results);
			}
		}
	}
	
	
	/**
	 * This indicates that no further changes to the model are needed.
	 * Some implementations may give runtime exceptions if this is violated.
	 * @param m  the Model to get as a read-only variant
	 * @return A read-only model
	 */
	public static Model asReadOnlyModel(Model m) {
		return helper.asReadOnlyModel(m);
	}
	
	
	/**
	 * This indicates that no further changes to the graph are needed.
	 * Some implementations may give runtime exceptions if this is violated.
	 * @param g  the Graph to get as a read-only variant
	 * @return a read-only graph
	 */
	public static Graph asReadOnlyGraph(Graph g) {
		return helper.asReadOnlyGraph(g);
	}
	
	
	// Internal to TopBraid only
	public static OntModel createOntologyModel(OntModelSpec spec, Model base) {
		return helper.createOntologyModel(spec,base);
	}
	
	
	/**
	 * Allows some environments, e.g. TopBraid, to prioritize
	 * a block of code for reading graphs, with no update occurring.
	 * The top of the block should call this with <code>true</code>
	 * with a matching call with <code>false</code> in a finally
	 * block.
	 * 
	 * Note: Unstable - don't use outside of TopBraid.
	 * 
	 * @param onOrOff  true to switch on
	 */
	public static void setGraphReadOptimization(boolean onOrOff) {
		helper.setGraphReadOptimization(onOrOff);
	}

	
	/**
	 * Ensure that we there is a read-only, thread safe version of the
	 * graph.  If the graph is not, then create a deep clone that is
	 * both.
	 * 
	 * Note: Unstable - don't use outside of TopBraid.
	 * 
	 * @param g The given graph
	 * @return A read-only, thread safe version of the given graph.
	 */
	public static Graph deepCloneForReadOnlyThreadSafe(Graph g) {
		return helper.deepCloneReadOnlyGraph(g);
	}


	/**
	 * Calls a SPARQL expression and returns the result, using some initial bindings.
	 *
	 * @param expression     the expression to execute (must contain absolute URIs)
	 * @param initialBinding the initial bindings for the unbound variables
	 * @param dataset        the query Dataset or null for default
	 * @return the result or null
	 */
	public static Node invokeExpression(String expression, QuerySolution initialBinding, Dataset dataset) {
	    if (dataset == null) {
	        dataset = ARQFactory.get().getDataset(ModelFactory.createDefaultModel());
	    }
	    Query query = ARQFactory.get().createExpressionQuery(expression);
	    try(QueryExecution qexec = ARQFactory.get().createQueryExecution(query, dataset, initialBinding)) {
	        ResultSet rs = qexec.execSelect();
	        Node result = null;
	        if (rs.hasNext()) {
	            QuerySolution qs = rs.next();
	            String firstVarName = rs.getResultVars().get(0);
	            RDFNode rdfNode = qs.get(firstVarName);
	            if (rdfNode != null) {
	                result = rdfNode.asNode();
	            }
	        }
	        return result;
	    }
	}


	/**
	 * Calls a given SPARQL function with no arguments.
	 *
	 * @param function the URI resource of the function to call
	 * @param dataset  the Dataset to operate on or null for default
	 * @return the result of the function call
	 */
	public static Node invokeFunction0(Resource function, Dataset dataset) {
		ExprList args = new ExprList();
		return invokeFunction(function, args, dataset);
	}


	/**
	 * Calls a given SPARQL function with one argument.
	 *
	 * @param function the URI resource of the function to call
	 * @param argument the first argument
	 * @param dataset  the Dataset to operate on or null for default
	 * @return the result of the function call
	 */
	public static Node invokeFunction1(Resource function, RDFNode argument, Dataset dataset) {
		ExprList args = new ExprList();
		args.add(argument != null ? NodeValue.makeNode(argument.asNode()) : new ExprVar("arg1"));
		return invokeFunction(function, args, dataset);
	}


	public static Node invokeFunction1(Resource function, Node argument, Dataset dataset) {
		return invokeFunction1(function, toRDFNode(argument), dataset);
	}


	/**
	 * Calls a given SPARQL function with two arguments.
	 *
	 * @param function  the URI resource of the function to call
	 * @param argument1 the first argument
	 * @param argument2 the second argument
	 * @param dataset   the Dataset to operate on or null for default
	 * @return the result of the function call
	 */
	public static Node invokeFunction2(Resource function, RDFNode argument1, RDFNode argument2, Dataset dataset) {
		ExprList args = new ExprList();
		args.add(argument1 != null ? NodeValue.makeNode(argument1.asNode()) : new ExprVar("arg1"));
		args.add(argument2 != null ? NodeValue.makeNode(argument2.asNode()) : new ExprVar("arg2"));
		return invokeFunction(function, args, dataset);
	}


	public static Node invokeFunction2(Resource function, Node argument1, Node argument2, Dataset dataset) {
		return invokeFunction2(function, toRDFNode(argument1), toRDFNode(argument2), dataset);
	}


	public static Node invokeFunction3(Resource function, RDFNode argument1, RDFNode argument2, RDFNode argument3, Dataset dataset) {
		ExprList args = new ExprList();
		args.add(argument1 != null ? NodeValue.makeNode(argument1.asNode()) : new ExprVar("arg1"));
		args.add(argument2 != null ? NodeValue.makeNode(argument2.asNode()) : new ExprVar("arg2"));
		args.add(argument3 != null ? NodeValue.makeNode(argument3.asNode()) : new ExprVar("arg3"));
		return invokeFunction(function, args, dataset);
	}
	
	
	private static Node invokeFunction(Resource function, ExprList args, Dataset dataset) {

		if (dataset == null) {
	        dataset = ARQFactory.get().getDataset(ModelFactory.createDefaultModel());
	    }
		
		E_Function expr = new E_Function(function.getURI(), args);
		DatasetGraph dsg = dataset.asDatasetGraph();
		Context cxt = ARQ.getContext().copy();
		cxt.set(ARQConstants.sysCurrentTime, NodeFactoryExtra.nowAsDateTime());
		FunctionEnv env = new ExecutionContext(cxt, dsg.getDefaultGraph(), dsg, null);
		try {
			NodeValue r = expr.eval(BindingRoot.create(), env);
			if(r != null) {
				return r.asNode();
			}
		}
		catch(ExprEvalException ex) {
		}
		return null;
	}


	public static Node invokeFunction3(Resource function, Node argument1, Node argument2, Node argument3, Dataset dataset) {
		return invokeFunction3(function, toRDFNode(argument1), toRDFNode(argument2), toRDFNode(argument3), dataset);
	}


	/**
	 * Temp patch for a bug in Jena's syntaxtransform, also applying substitutions on
	 * HAVING clauses.
	 * @param query  the Query to transform
	 * @param substitutions  the variable bindings
	 * @return a new Query with the bindings applied
	 */
	public static Query queryWithSubstitutions(Query query, final Map<Var, Node> substitutions) {
		Query result = QueryTransformOps.transform(query, substitutions);
		
		// TODO: Replace this hack once there is a Jena patch
		if(result.hasHaving()) {
			NodeTransform nodeTransform = new NodeTransform() {
			    @Override
			    public Node apply(Node node) {
			        Node n = substitutions.get(node) ;
			        if ( n == null ) {
			            return node ;
			        }
			        return n ;
			    }
			};
	        ElementTransform eltrans = new ElementTransformSubst(substitutions) ;
	        ExprTransform exprTrans = new ExprTransformNodeElement(nodeTransform, eltrans) ;
			List<Expr> havingExprs = result.getHavingExprs();
			for(int i = 0; i < havingExprs.size(); i++) {
				Expr old = havingExprs.get(i);
	            Expr neo = ExprTransformer.transform(exprTrans, old) ;
	            if ( neo != old ) {
	            	havingExprs.set(i, neo);
	            }
			}
		}
		return result;
	}
	
	
	public static void sort(List<Resource> nodes) {
		Collections.sort(nodes, new Comparator<Resource>() {
			@Override
			public int compare(Resource o1, Resource o2) {
		        return NodeUtils.compareRDFTerms(o1.asNode(), o2.asNode());
			}
		});
	}


	public static RDFNode toRDFNode(Node node) {
		if(node != null) {
			return dummyModel.asRDFNode(node);
		}
		else {
			return null;
		}
	}
	
	
	public static String withImports(String uri) {
		if(!uri.startsWith(WITH_IMPORTS_PREFIX)) {
			return WITH_IMPORTS_PREFIX + uri;
		}
		else {
			return uri;
		}
	}
	
	
	public static String withoutImports(String uri) {
		if(uri.startsWith(WITH_IMPORTS_PREFIX)) {
			return uri.substring(WITH_IMPORTS_PREFIX.length());
		}
		else {
			return uri;
		}
	}
}