/*
 *  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.validation.sparql;

import java.util.HashMap;
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.Function;

import org.apache.jena.graph.Node;
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.rdf.model.Literal;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.rdf.model.ResourceFactory;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.shared.impl.PrefixMappingImpl;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.vocabulary.OWL;
import org.apache.jena.vocabulary.RDFS;
import org.topbraid.jenax.util.ARQFactory;
import org.topbraid.jenax.util.JenaUtil;
import org.topbraid.jenax.util.RDFLabels;
import org.topbraid.shacl.validation.SHACLException;
import org.topbraid.shacl.vocabulary.SH;
import org.topbraid.shacl.vocabulary.TOSH;

/**
 * Collects various helper algorithms currently used by the SPARQL execution language.
 *
 * @author Holger Knublauch
 */
public class SPARQLSubstitutions {
	
	// Flag to bypass sh:prefixes and instead use all prefixes in the Jena object of the shapes graph.
	public static boolean useGraphPrefixes = false;

	// Currently switched to old setInitialBinding solution
	private static boolean USE_TRANSFORM = false;
	
	
	public static void addMessageVarNames(String labelTemplate, Set<String> results) {
		for(int i = 0; i < labelTemplate.length(); i++) {
			if(i < labelTemplate.length() - 3 && labelTemplate.charAt(i) == '{' && labelTemplate.charAt(i + 1) == '?') {
				int varEnd = i + 2;
				while(varEnd < labelTemplate.length()) {
					if(labelTemplate.charAt(varEnd) == '}') {
						String varName = labelTemplate.substring(i + 2, varEnd);
						results.add(varName);
						break;
					}
					else {
						varEnd++;
					}
				}
				i = varEnd;
			}
		}
	}
	
	
	public static QueryExecution createQueryExecution(Query query, Dataset dataset, QuerySolution bindings) {
		if(USE_TRANSFORM && bindings != null) {
			Map<Var,Node> substitutions = new HashMap<Var,Node>();
			Iterator<String> varNames = bindings.varNames();
			while(varNames.hasNext()) {
				String varName = varNames.next();
				substitutions.put(Var.alloc(varName), bindings.get(varName).asNode());
			}
			Query newQuery = JenaUtil.queryWithSubstitutions(query, substitutions);
			return ARQFactory.get().createQueryExecution(newQuery, dataset);
		}
		else {
			return ARQFactory.get().createQueryExecution(query, dataset, bindings);
		}
	}
	
	
	public static Query substitutePaths(Query query, String pathString, Model model) {
		// TODO: This is a bad algorithm - should be operating on syntax tree, not string
		String str = query.toString().replaceAll(" \\?" + SH.PATHVar.getVarName() + " ", pathString);
		return ARQFactory.get().createQuery(model, str);
	}

	
	public static Literal withSubstitutions(Literal template, QuerySolution bindings, Function<RDFNode,String> labelFunction) {
		StringBuffer buffer = new StringBuffer();
		String labelTemplate = template.getLexicalForm();
		for(int i = 0; i < labelTemplate.length(); i++) {
			if(i < labelTemplate.length() - 3 && labelTemplate.charAt(i) == '{' && (labelTemplate.charAt(i + 1) == '?' || labelTemplate.charAt(i + 1) == '$')) {
				int varEnd = i + 2;
				while(varEnd < labelTemplate.length()) {
					if(labelTemplate.charAt(varEnd) == '}') {
						String varName = labelTemplate.substring(i + 2, varEnd);
						RDFNode varValue = bindings.get(varName);
						if(varValue != null) {
							if(labelFunction != null) {
								buffer.append(labelFunction.apply(varValue));
							}
							else if(varValue instanceof Resource) {
								buffer.append(RDFLabels.get().getLabel((Resource)varValue));
							}
							else if(varValue instanceof Literal) {
								buffer.append(varValue.asNode().getLiteralLexicalForm());
							}
						}
						break;
					}
					else {
						varEnd++;
					}
				}
				i = varEnd;
			}
			else {
				buffer.append(labelTemplate.charAt(i));
			}
		}
		if(template.getLanguage().isEmpty()) {
			return ResourceFactory.createTypedLiteral(buffer.toString());
		}
		else {
			return ResourceFactory.createLangLiteral(buffer.toString(), template.getLanguage());
		}
	}
	
	
	static void appendTargets(StringBuffer sb, Resource shape, Dataset dataset) {
		
		List<String> targets = new LinkedList<String>();
		
		if(shape.getModel().contains(shape, SH.targetNode, (RDFNode)null)) {
			targets.add("        GRAPH " + SH.JS_SHAPES_VAR + " { $" + SH.currentShapeVar.getName() + " <" + SH.targetNode + "> ?this } .\n");
		}
		
		if(JenaUtil.hasIndirectType(shape, RDFS.Class)) {
			String varName = "?CLASS_VAR";
			targets.add("        " + varName + " <" + RDFS.subClassOf + ">* $" + SH.currentShapeVar.getName() + " .\n            ?this a " + varName + " .\n");
		}
		
		for(Resource cls : JenaUtil.getResourceProperties(shape, SH.targetClass)) {
			String varName = "?SHAPE_CLASS_VAR";
			targets.add("        " + varName + " <" + RDFS.subClassOf + ">* <" + cls + "> .\n            ?this a " + varName + " .\n");
		}

		int index = 0;
		for(Resource property : JenaUtil.getResourceProperties(shape, SH.targetSubjectsOf)) {
			targets.add("        ?this <" + property + "> ?ANY_VALUE_" + index++ + " .\n");
		}
		for(Resource property : JenaUtil.getResourceProperties(shape, SH.targetObjectsOf)) {
			targets.add("        ?ANY_VALUE_" + index++ + "  <" + property + "> ?this .\n");
		}
		
		if(shape.hasProperty(SH.target)) {
			targets.add(createTargets(shape));
		}
		
		if(targets.isEmpty()) {
			throw new SHACLException("Shape without target " + shape);
		}
		else if(targets.size() == 1) {
			sb.append(targets.get(0));
		}
		else {
			for(int i = 0; i < targets.size(); i++) {
				sb.append("        {");
				sb.append(targets.get(i));
				sb.append("        }");
				if(i < targets.size() - 1) {
					sb.append("        UNION\n");
				}
			}
		}
	}
	
	
	private static String createTargets(Resource shape) {
		String targetVar = "?trgt_" + (int)(Math.random() * 10000);
		return  "        GRAPH $" + SH.shapesGraphVar.getName() + " { $" + SH.currentShapeVar.getName() + " <" + SH.target + "> " + targetVar + "} .\n" +
				"        (" + targetVar + " $" + SH.shapesGraphVar.getName() + ") <" + TOSH.targetContains + "> ?this .\n";
	}
	
	
	/**
	 * Gets a parsable SPARQL string based on a fragment and prefix declarations.
	 * Depending on the setting of the flag useGraphPrefixes, this either uses the
	 * prefixes from the Jena graph of the given executable, or strictly uses sh:prefixes.
	 * @param str  the query fragment (e.g. starting with SELECT)
	 * @param executable  the sh:SPARQLExecutable potentially holding the sh:prefixes
	 * @return the parsable SPARQL string
	 */
	public static String withPrefixes(String str, Resource executable) {
		if(useGraphPrefixes) {
			return ARQFactory.get().createPrefixDeclarations(executable.getModel()) + str;
		}
		else {
			StringBuffer sb = new StringBuffer();
			PrefixMapping pm = new PrefixMappingImpl();
			Set<Resource> reached = new HashSet<Resource>();
			for(Resource ontology : JenaUtil.getResourceProperties(executable, SH.prefixes)) {
				String duplicate = collectPrefixes(ontology, pm, reached);
				if(duplicate != null) {
					throw new SHACLException("Duplicate prefix declaration for prefix " + duplicate);
				}
			}
			for(String prefix : pm.getNsPrefixMap().keySet()) {
				sb.append("PREFIX ");
				sb.append(prefix);
				sb.append(": <");
				sb.append(pm.getNsPrefixURI(prefix));
				sb.append(">\n");
			}
			sb.append(str);
			return sb.toString();
		}
	}
	
	
	// Returns the duplicate prefix, if any
	private static String collectPrefixes(Resource ontology, PrefixMapping pm, Set<Resource> reached) {
		
		reached.add(ontology);
		
		for(Resource decl : JenaUtil.getResourceProperties(ontology, SH.declare)) {
			String prefix = JenaUtil.getStringProperty(decl, SH.prefix);
			String ns = JenaUtil.getStringProperty(decl, SH.namespace);
			if(prefix != null && ns != null) {
				String oldNS = pm.getNsPrefixURI(prefix);
				if(oldNS != null && !oldNS.equals(ns)) {
					return prefix;
				}
				pm.setNsPrefix(prefix, ns);
			}
		}
		
		for(Resource imp : JenaUtil.getResourceProperties(ontology, OWL.imports)) {
			if(!reached.contains(imp)) {
				String duplicate = collectPrefixes(imp, pm, reached);
				if(duplicate != null) {
					return duplicate;
				}
			}
		}
		
		return null;
	}
}