/*
 *  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.shacl.arq;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.jena.graph.Node;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryParseException;
import org.apache.jena.rdf.model.Model;
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.sparql.path.P_Alt;
import org.apache.jena.sparql.path.P_Inverse;
import org.apache.jena.sparql.path.P_Link;
import org.apache.jena.sparql.path.P_OneOrMore1;
import org.apache.jena.sparql.path.P_OneOrMoreN;
import org.apache.jena.sparql.path.P_Path1;
import org.apache.jena.sparql.path.P_Seq;
import org.apache.jena.sparql.path.P_ZeroOrMore1;
import org.apache.jena.sparql.path.P_ZeroOrMoreN;
import org.apache.jena.sparql.path.P_ZeroOrOne;
import org.apache.jena.sparql.path.Path;
import org.apache.jena.sparql.path.eval.PathEval;
import org.apache.jena.sparql.syntax.Element;
import org.apache.jena.sparql.syntax.ElementGroup;
import org.apache.jena.sparql.syntax.ElementPathBlock;
import org.apache.jena.sparql.syntax.ElementTriplesBlock;
import org.apache.jena.sparql.util.Context;
import org.apache.jena.sparql.util.FmtUtils;
import org.apache.jena.vocabulary.RDF;
import org.topbraid.jenax.util.ARQFactory;
import org.topbraid.shacl.vocabulary.SH;

/**
 * Utilties to manage the conversion between SHACL paths and SPARQL 1.1 property paths.
 * 
 * @author Holger Knublauch
 */
public class SHACLPaths {
	
	private final static String ALTERNATIVE_PATH_SEPARATOR = "|";
	
	private final static String SEQUENCE_PATH_SEPARATOR = "/";
	
	
	public static void addValueNodes(RDFNode focusNode, Path path, Collection<RDFNode> results) {
		Set<Node> seen = new HashSet<>();
		Iterator<Node> it = PathEval.eval(focusNode.getModel().getGraph(), focusNode.asNode(), path, Context.emptyContext);
		while(it.hasNext()) {
			Node node = it.next();
			if(!seen.contains(node)) {
				seen.add(node);
				results.add(focusNode.getModel().asRDFNode(node));
			}
		}
	}
	
	
	public static void addValueNodes(RDFNode focusNode, Property predicate, Collection<RDFNode> results) {
		if(focusNode instanceof Resource) {
			StmtIterator it = ((Resource)focusNode).listProperties(predicate);
			while(it.hasNext()) {
				results.add(it.next().getObject());
			}
		}
	}

	
	/**
	 * Renders a given path into a given StringBuffer, using the prefixes supplied by the
	 * Path's Model.
	 * @param sb  the StringBuffer to write into
	 * @param path  the path resource
	 */
	public static void appendPath(StringBuffer sb, Resource path) {
		if(path.isURIResource()) {
			sb.append(FmtUtils.stringForNode(path.asNode(), path.getModel()));
		}
		else {
			appendPathBlankNode(sb, path, SEQUENCE_PATH_SEPARATOR);
		}
	}
	
	
	private static void appendNestedPath(StringBuffer sb, Resource path, String separator) {
		if(path.isURIResource()) {
			sb.append(FmtUtils.stringForNode(path.asNode(), path.getModel()));
		}
		else {
			sb.append("(");
			appendPathBlankNode(sb, path, separator);
			sb.append(")");
		}
	}
	
	
	private static void appendPathBlankNode(StringBuffer sb, Resource path, String separator) {
		if(path.hasProperty(RDF.first)) {
			Iterator<RDFNode> it = path.as(RDFList.class).iterator();
			while(it.hasNext()) {
				Resource item = (Resource) it.next();
				appendNestedPath(sb, item, SEQUENCE_PATH_SEPARATOR);
				if(it.hasNext()) {
					sb.append(" ");
					sb.append(separator);
					sb.append(" ");
				}
			}
		}
		else if(path.hasProperty(SH.inversePath)) {
			sb.append("^");
			if(path.getProperty(SH.inversePath).getObject().isAnon()) {
				sb.append("(");
				appendPath(sb, path.getPropertyResourceValue(SH.inversePath));
				sb.append(")");
			}
			else {
				appendPath(sb, path.getPropertyResourceValue(SH.inversePath));
			}
		}
		else if(path.hasProperty(SH.alternativePath)) {
			appendNestedPath(sb, path.getPropertyResourceValue(SH.alternativePath), ALTERNATIVE_PATH_SEPARATOR);
		}
		else if(path.hasProperty(SH.zeroOrMorePath)) {
			appendNestedPath(sb, path.getPropertyResourceValue(SH.zeroOrMorePath), SEQUENCE_PATH_SEPARATOR);
			sb.append("*");
		}
		else if(path.hasProperty(SH.oneOrMorePath)) {
			appendNestedPath(sb, path.getPropertyResourceValue(SH.oneOrMorePath), SEQUENCE_PATH_SEPARATOR);
			sb.append("+");
		}
		else if(path.hasProperty(SH.zeroOrOnePath)) {
			appendNestedPath(sb, path.getPropertyResourceValue(SH.zeroOrOnePath), SEQUENCE_PATH_SEPARATOR);
			sb.append("?");
		}
	}
	
	
	public static Resource clonePath(Resource path, Model targetModel) {
		if(path.isURIResource()) {
			return path.inModel(targetModel);
		}
		else {
			Resource clone = targetModel.createResource();
			for(Statement s : path.listProperties().toList()) {
				if(s.getSubject().hasProperty(RDF.first)) {
					if(RDF.first.equals(s.getPredicate()) || RDF.rest.equals(s.getPredicate())) {
						Resource newObject = clonePath(s.getResource(), targetModel);
						clone.addProperty(s.getPredicate(), newObject);
					}
				}
				else {
					Resource newObject = clonePath(s.getResource(), targetModel);
					clone.addProperty(s.getPredicate(), newObject);
				}
			}
			return clone;
		}
	}
	
	
	/**
	 * Creates SHACL RDF triples for a given Jena Path (which may have been created using getJenaPath).
	 * @param path  the Jena Path
	 * @param model  the Model to create the triples in
	 * @return the (root) Resource of the SHACL path
	 */
	public static Resource createPath(Path path, Model model) {
		if(path instanceof P_Alt) {
			Resource result = model.createResource();
			RDFList list = model.createList(Arrays.asList(new RDFNode[] {
				createPath(((P_Alt) path).getLeft(), model),
				createPath(((P_Alt) path).getRight(), model)
			}).iterator());
			result.addProperty(SH.alternativePath, list);
			return result;
		}
		else if(path instanceof P_Inverse) {
			Resource result = model.createResource();
			result.addProperty(SH.inversePath, createPath(((P_Inverse) path).getSubPath(), model));
			return result;
		}
		else if(path instanceof P_Link) {
			return (Resource) model.asRDFNode(((P_Link) path).getNode());
		}
		else if(path instanceof P_OneOrMore1 || path instanceof P_OneOrMoreN) {
			Resource result = model.createResource();
			result.addProperty(SH.oneOrMorePath, createPath(((P_Path1)path).getSubPath(), model));
			return result;
		}
		if(path instanceof P_Seq) {
			return model.createList(Arrays.asList(new RDFNode[] {
				createPath(((P_Seq) path).getLeft(), model),
				createPath(((P_Seq) path).getRight(), model)
			}).iterator());
		}
		else if(path instanceof P_ZeroOrMore1 || path instanceof P_ZeroOrMoreN) {
			Resource result = model.createResource();
			result.addProperty(SH.zeroOrMorePath, createPath(((P_Path1)path).getSubPath(), model));
			return result;
		}
		else if(path instanceof P_ZeroOrOne) {
			Resource result = model.createResource();
			result.addProperty(SH.zeroOrOnePath, createPath(((P_Path1)path).getSubPath(), model));
			return result;
		}
		else {
			throw new IllegalArgumentException("Path element not supported by SHACL syntax: " + path);
		}
	}
	
	
	public static Object getJenaPath(Resource path) throws QueryParseException {
		if(path.isURIResource()) {
			return path;
		}
		else {
			String pathString = SHACLPaths.getPathString(path);
			return SHACLPaths.getJenaPath(pathString, path.getModel());
		}
	}

	
	/**
	 * Attempts to parse a given string into a Jena Path.
	 * Throws an Exception if the string cannot be parsed.
	 * @param string  the string to parse
	 * @param model  the Model to operate on (for prefixes)
	 * @return a Path or a Resource if this is a URI
	 */
	public static Object getJenaPath(String string, Model model) throws QueryParseException {
		Query query = ARQFactory.get().createQuery(model, "ASK { ?a \n" + string + "\n ?b }");
		Element element = query.getQueryPattern();
		if(element instanceof ElementGroup) {
			Element e = ((ElementGroup)element).getElements().get(0);
			if(e instanceof ElementPathBlock) {
				Path path = ((ElementPathBlock) e).getPattern().get(0).getPath();
				if(path instanceof P_Link && ((P_Link)path).isForward()) {
					return model.asRDFNode(((P_Link)path).getNode());
				}
				else {
					return path;
				}
			}
			else if(e instanceof ElementTriplesBlock) {
				return model.asRDFNode(((ElementTriplesBlock) e).getPattern().get(0).getPredicate());
			}
		}
		throw new QueryParseException("Not a SPARQL 1.1 Path expression", 2, 1);
	}

	
	public static String getPathString(Resource path) {
		StringBuffer sb = new StringBuffer();
		appendPath(sb, path);
		return sb.toString();
	}
}