/*
 *  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.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;

import org.apache.jena.query.Query;
import org.apache.jena.query.QueryExecution;
import org.apache.jena.query.QueryParseException;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.query.QuerySolutionMap;
import org.apache.jena.query.ResultSet;
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.rdf.model.Statement;
import org.apache.jena.vocabulary.RDF;
import org.topbraid.jenax.statistics.ExecStatistics;
import org.topbraid.jenax.statistics.ExecStatisticsManager;
import org.topbraid.jenax.util.ARQFactory;
import org.topbraid.jenax.util.JenaDatatypes;
import org.topbraid.jenax.util.JenaUtil;
import org.topbraid.jenax.util.RDFLabels;
import org.topbraid.shacl.arq.SHACLPaths;
import org.topbraid.shacl.arq.functions.HasShapeFunction;
import org.topbraid.shacl.engine.Constraint;
import org.topbraid.shacl.engine.ShapesGraph;
import org.topbraid.shacl.util.FailureLog;
import org.topbraid.shacl.util.SHACLUtil;
import org.topbraid.shacl.validation.ConstraintExecutor;
import org.topbraid.shacl.validation.SHACLException;
import org.topbraid.shacl.validation.ValidationEngine;
import org.topbraid.shacl.vocabulary.DASH;
import org.topbraid.shacl.vocabulary.SH;

public abstract class AbstractSPARQLExecutor implements ConstraintExecutor {
	
	// Flag to generate dash:SuccessResults for all violations.
	public static boolean createSuccessResults = false;
	
	private Query query;
	
	private String queryString;
	
	
	protected AbstractSPARQLExecutor(Constraint constraint) {
		this.queryString = getSPARQL(constraint);
		try {
			this.query = ARQFactory.get().createQuery(queryString);
			Resource path = constraint.getShapeResource().getPath();
			if(path != null && path.isAnon()) {
				String pathString = SHACLPaths.getPathString(constraint.getShapeResource().getPropertyResourceValue(SH.path));
				query = SPARQLSubstitutions.substitutePaths(query, pathString, constraint.getShapeResource().getModel());
			}
		}
		catch(QueryParseException ex) {
			throw new SHACLException("Invalid SPARQL constraint (" + ex.getLocalizedMessage() + "):\n" + queryString);
		}

		if(!query.isSelectType()) {
			throw new IllegalArgumentException("SHACL constraints must be SELECT queries");
		}
	}

	
	@Override
	public void executeConstraint(Constraint constraint, ValidationEngine engine, Collection<RDFNode> focusNodes) {
		
		QuerySolutionMap bindings = new QuerySolutionMap();
		addBindings(constraint, bindings);
		bindings.add(SH.currentShapeVar.getVarName(), constraint.getShapeResource());
		bindings.add(SH.shapesGraphVar.getVarName(), ResourceFactory.createResource(engine.getShapesGraphURI().toString()));
		
		Resource path = constraint.getShapeResource().getPath();
		if(path != null && path.isURIResource()) {
			bindings.add(SH.PATHVar.getName(), path);
		}
		
		URI oldShapesGraphURI = HasShapeFunction.getShapesGraphURI();
		ShapesGraph oldShapesGraph = HasShapeFunction.getShapesGraph();
		if(!engine.getShapesGraphURI().equals(oldShapesGraphURI)) {
			HasShapeFunction.setShapesGraph(engine.getShapesGraph(), engine.getShapesGraphURI());
		}
		
		Model oldNestedResults = HasShapeFunction.getResultsModel();
		Model nestedResults = JenaUtil.createMemoryModel();
		HasShapeFunction.setResultsModel(nestedResults);
		
		try {
			long startTime = System.currentTimeMillis();
			Resource messageHolder = getSPARQLExecutable(constraint);
			for(RDFNode focusNode : focusNodes) {
				bindings.add(SH.thisVar.getVarName(), focusNode); // Overwrite any previous binding
				QueryExecution qexec = SPARQLSubstitutions.createQueryExecution(query, engine.getDataset(), bindings);
				executeSelectQuery(engine, constraint, messageHolder, nestedResults, focusNode, qexec, bindings);
				engine.checkCanceled();
			}			
			if(ExecStatisticsManager.get().isRecording()) {
				long endTime = System.currentTimeMillis();
				long duration = endTime - startTime;
				String label = getLabel(constraint);
				Iterator<String> varNames = bindings.varNames();
				if(varNames.hasNext()) {
					queryString += "\nBindings:";
					while(varNames.hasNext()) {
						String varName = varNames.next();
						queryString += "\n- ?" + varName + ": " + bindings.get(varName);
					}
				}
				ExecStatistics stats = new ExecStatistics(label, queryString, duration, startTime, constraint.getComponent().asNode());
				ExecStatisticsManager.get().add(Collections.singletonList(stats));
			}
		}
		finally {
			HasShapeFunction.setShapesGraph(oldShapesGraph, oldShapesGraphURI);
			HasShapeFunction.setResultsModel(oldNestedResults);
		}
	}

	
	protected abstract void addBindings(Constraint constraint, QuerySolutionMap bindings);
	
	
	protected abstract Resource getSPARQLExecutable(Constraint constraint);
	
	
	protected abstract String getLabel(Constraint constraint);
	
	
	protected Query getQuery() {
		return query;
	}
	
	
	protected abstract String getSPARQL(Constraint constraint);
	

	private void executeSelectQuery(ValidationEngine engine, Constraint constraint, Resource messageHolder, Model nestedResults,
			RDFNode focusNode, QueryExecution qexec, QuerySolution bindings) {
		
		ResultSet rs = qexec.execSelect();
		
		if(!rs.getResultVars().contains("this")) {
			qexec.close();
			throw new IllegalArgumentException("SELECT constraints must return $this");
		}
		
		try {
			if(rs.hasNext()) {
				while(rs.hasNext()) {
					QuerySolution sol = rs.next();
					RDFNode thisValue = sol.get(SH.thisVar.getVarName());
					if(thisValue != null) {
						Resource resultType = SH.ValidationResult;
						RDFNode selectMessage = sol.get(SH.message.getLocalName());
						if(JenaDatatypes.TRUE.equals(sol.get(SH.failureVar.getName()))) {
							resultType = DASH.FailureResult;
							String message = getLabel(constraint);
							message += " has produced ?" + SH.failureVar.getName();
							if(focusNode != null) {
								message += " for focus node ";
								if(focusNode.isLiteral()) {
									message += focusNode;
								}
								else {
									message += RDFLabels.get().getLabel((Resource)focusNode);
								}
							}
							FailureLog.get().logFailure(message);
							selectMessage = ResourceFactory.createTypedLiteral("Validation Failure: Could not validate shape");
						}
						
						Resource result = engine.createResult(resultType, constraint, thisValue);
						if(SH.SPARQLConstraintComponent.equals(constraint.getComponent())) {
							result.addProperty(SH.sourceConstraint, constraint.getParameterValue());
						}
						
						if(selectMessage != null) {
							result.addProperty(SH.resultMessage, selectMessage);
						}
						else if(constraint.getShapeResource().hasProperty(SH.message)) {
							for(Statement s : constraint.getShapeResource().listProperties(SH.message).toList()) {
								result.addProperty(SH.resultMessage, s.getObject());
							}
						}
						else {
							addDefaultMessages(engine, messageHolder, constraint.getComponent(), result, bindings, sol);
						}
						
						RDFNode pathValue = sol.get(SH.pathVar.getVarName());
						if(pathValue != null && pathValue.isURIResource()) {
							result.addProperty(SH.resultPath, pathValue);
						}
						else if(constraint.getShapeResource().isPropertyShape()) {
							Resource basePath = constraint.getShapeResource().getPropertyResourceValue(SH.path);
							result.addProperty(SH.resultPath, SHACLPaths.clonePath(basePath, result.getModel()));
						}
						
						if(!SH.HasValueConstraintComponent.equals(constraint.getComponent())) { // See https://github.com/w3c/data-shapes/issues/111
							RDFNode selectValue = sol.get(SH.valueVar.getVarName());
							if(selectValue != null) {
								result.addProperty(SH.value, selectValue);
							}
							else if(SH.NodeShape.equals(constraint.getContext())) {
								result.addProperty(SH.value, focusNode);
							}
						}
						
						if(engine.getConfiguration().getReportDetails()) {
							addDetails(result, nestedResults);
						}
					}
				}
			}
			else if(createSuccessResults) {
				Resource success = engine.createResult(DASH.SuccessResult, constraint, focusNode);
				if(SH.SPARQLConstraintComponent.equals(constraint.getComponent())) {
					success.addProperty(SH.sourceConstraint, constraint.getParameterValue());
				}
				if(engine.getConfiguration().getReportDetails()) {
					addDetails(success, nestedResults);
				}
			}
		}
		finally {
			qexec.close();
		}
	}

	
	private void addDefaultMessages(ValidationEngine engine, Resource messageHolder, Resource fallback, Resource result, 
				QuerySolution bindings, QuerySolution solution) {
		boolean found = false;
		for(Statement s : messageHolder.listProperties(SH.message).toList()) {
			if(s.getObject().isLiteral()) {
				QuerySolutionMap map = new QuerySolutionMap();
				map.addAll(bindings);
				map.addAll(solution);
				engine.addResultMessage(result, s.getLiteral(), map);
				found = true;
			}
		}
		if(!found && fallback != null) {
			addDefaultMessages(engine, fallback, null, result, bindings, solution);
		}
	}

	
	public static void addDetails(Resource parentResult, Model nestedResults) {
		if(!nestedResults.isEmpty()) {
			parentResult.getModel().add(nestedResults);
			for(Resource type : SHACLUtil.RESULT_TYPES) {
				for(Resource nestedResult : nestedResults.listSubjectsWithProperty(RDF.type, type).toList()) {
					if(!parentResult.getModel().contains(null, SH.detail, nestedResult)) {
						parentResult.addProperty(SH.detail, nestedResult);
					}
				}
			}
		}
	}
}