package eu.wdaqua.qanary.commons.triplestoreconnectors;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.stream.Collectors;

import org.apache.jena.query.ParameterizedSparqlString;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryFactory;
import org.apache.jena.query.QuerySolutionMap;
import org.apache.jena.query.ResultSet;
import org.apache.jena.rdf.model.ResourceFactory;
import org.apache.jena.update.UpdateFactory;
import org.apache.jena.update.UpdateRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import eu.wdaqua.qanary.exceptions.SparqlQueryFailed;

public abstract class QanaryTripleStoreConnector {
	private static final Logger logger = LoggerFactory.getLogger(QanaryTripleStoreConnector.class);

	public abstract void connect(); // TODO: add exception

	public abstract ResultSet select(String sparql) throws SparqlQueryFailed;

	public abstract boolean ask(String sparql) throws SparqlQueryFailed;

	public abstract void update(String sparql, URI graph) throws SparqlQueryFailed;

	public abstract void update(String sparql) throws SparqlQueryFailed;

	/**
	 * return a readable description of the triplestore endpoint
	 * 
	 * @return
	 */
	public abstract String getFullEndpointDescription();

	/**
	 * get current time in milliseconds
	 */
	protected static long getTime() {
		return System.currentTimeMillis();
	}

	/**
	 *
	 * @param description
	 * @param duration
	 */
	protected void logTime(long duration, String description) {
		logger.info("runtime measurement: {} ms for {}", duration, description);
	}

	/**
	 * 
	 * @param duration
	 * @param description
	 */
	protected void logTime(long duration, String description, String endpoint) {
		logger.info("runtime measurement: {} ms on {} for {}", duration, endpoint, description);
	}

	protected static Logger getLogger() {
		return logger;
	}

	/**
	 * read SPARQL query from files in resources folder
	 * 
	 * @param filenameWithRelativePath
	 * @return
	 * @throws IOException
	 */
	public static String readFileFromResources(String filenameWithRelativePath) throws IOException {
		InputStream in = QanaryTripleStoreConnector.class.getResourceAsStream(filenameWithRelativePath);
		getLogger().info("filenameWithRelativePath: {}, {}", filenameWithRelativePath, in);

		if (in == null) {
			return null;
		} else {
			BufferedReader reader = new BufferedReader(new InputStreamReader(in));
			return reader.lines().collect(Collectors.joining("\n"));
		}
	}

	/**
	 * get SELECT query to count the number of triples in a graph
	 * 
	 * @param graph
	 * @return
	 * @throws IOException
	 */
	public static String getCountAllTriplesInGraph(URI graph) throws IOException {
		String sparqlQueryString = readFileFromResources("/queries/select_count_all_triples.rq");

		QuerySolutionMap bindings = new QuerySolutionMap();
		bindings.add("graph", ResourceFactory.createResource(graph.toASCIIString()));

		ParameterizedSparqlString pq = new ParameterizedSparqlString(sparqlQueryString, bindings);
		Query query = QueryFactory.create(pq.toString());
		logger.info("generated query:\n{}", query.toString());

		return query.toString();
	}

	/**
	 * get SELECT query to count the number AnnotationOfAnswer in a graph
	 * 
	 * @param graph
	 * @return
	 * @throws IOException
	 */
	public static String getAllAnnotationOfAnswerInGraph(URI graph) throws IOException {
		String sparqlQueryString = readFileFromResources("/queries/select_all_AnnotationOfAnswerJson.rq");

		QuerySolutionMap bindings = new QuerySolutionMap();
		bindings.add("graph", ResourceFactory.createResource(graph.toASCIIString()));

		ParameterizedSparqlString pq = new ParameterizedSparqlString(sparqlQueryString, bindings);
		Query query = QueryFactory.create(pq.toString());
		logger.info("generated query:\n{}", query.toString());

		return query.toString();
	}

	/**
	 * get SELECT query to get the answer with the highest score in a graph
	 *
	 * @param graph
	 * @return
	 * @throws IOException
	 */
	public static String getHighestScoreAnnotationOfAnswerInGraph(URI graph) throws IOException {
		String sparqlQueryString = readFileFromResources("/queries/select_highestScore_AnnotationOfAnswerSPARQL.rq");

		QuerySolutionMap bindings = new QuerySolutionMap();
		bindings.add("graph", ResourceFactory.createResource(graph.toASCIIString()));

		ParameterizedSparqlString pq = new ParameterizedSparqlString(sparqlQueryString, bindings);
		Query query = QueryFactory.create(pq.toString());
		logger.info("generated query:\n{}", query.toString());

		return query.toString();
	}

	/**
	 * add AnnotationAnswer and AnnotationOfAnswerType to allow annotating typed literals
	 * as it may be required by Qanary QueryBuilder components
	 *
	 * @param bindings
	 * @return
	 * @throws IOException
	 */
	public static String insertAnnotationOfTypedLiteral(QuerySolutionMap bindings) throws IOException {
		return readFileFromResourcesWithMap("/queries/insert_one_AnnotationOfTypedLiteral.rq", bindings);
	}

	/**
	 * add AnnotationOfAnswerSPARQL as it is done typically in Qanary QueryBuilder
	 * components
	 *
	 * @param bindings
	 * @return
	 * @throws IOException
	 */
	public static String insertAnnotationOfAnswerSPARQL(QuerySolutionMap bindings) throws IOException {
		return readFileFromResourcesWithMap("/queries/insert_one_AnnotationOfAnswerSPARQL.rq", bindings);
	}

	/**
	 * add AnnotationOfAnswerJson as it is done typically in Qanary QueryExecutor
	 * components
	 *
	 * @param bindings
	 * @return
	 * @throws IOException
	 */
	public static String insertAnnotationOfAnswerJson(QuerySolutionMap bindings) throws IOException {
		return readFileFromResourcesWithMap("/queries/insert_one_AnnotationOfAnswerJson.rq", bindings);
	}

	public static String getAnnotationOfAnswerSPARQL(QuerySolutionMap bindings) throws IOException {
		return readFileFromResourcesWithMap("/queries/select_all_AnnotationOfAnswerSPARQL.rq", bindings);
	}

	/**
	 * read query from file and apply bindings
	 *
	 * @param filenameWithRelativePath
	 * @param bindings
	 * @return
	 * @throws IOException
	 */
	public static String readFileFromResourcesWithMap(String filenameWithRelativePath, QuerySolutionMap bindings)
			throws IOException {
		String sparqlQueryString = readFileFromResources(filenameWithRelativePath);
		ParameterizedSparqlString pq = new ParameterizedSparqlString(sparqlQueryString, bindings);

		logger.debug("readFileFromResourcesWithMap sparqlQueryString: {}", sparqlQueryString);

		if ((sparqlQueryString).contains("\nSELECT ") || sparqlQueryString.startsWith("SELECT")) {
			Query query = QueryFactory.create(pq.toString());
			logger.info("generated SELECT query:\n{}", query.toString());
			return query.toString();
		} else if (sparqlQueryString.contains("\nASK ") || sparqlQueryString.startsWith("ASK")) {
			Query query = QueryFactory.create(pq.toString());
			logger.info("generated ASK query:\n{}", query.toString());
			return query.toString();
		} else {
			UpdateRequest query = UpdateFactory.create(pq.toString());
			logger.info("generated UPDATE query:\n{}", query.toString());
			query.toString();
			return query.toString();
		}

	}

	/**
	 * ensures that files exists in the resources and are non-empty
	 * 
	 * e.g., useful for component constructors to ensure that SPRARQL query template
	 * files (*.rq) are valid
	 * 
	 * @param filenameInResources
	 */
	public static void guardNonEmptyFileFromResources(String filenameInResources) {
		String message = null;
		try {
			String readFileContent = readFileFromResources(filenameInResources);

			if (readFileContent == null) {
				message = "file content was null (does the file exist?): " + filenameInResources;
			} else if (readFileContent.isBlank()) {
				message = "no content: " + filenameInResources;
			} else {
				return; // ok
			}
			logger.error(message);
			throw new RuntimeException(message);

		} catch (IOException e) {
			// should not happen as readFileContent should always be readable (as null)
			message = "not available: " + filenameInResources;
			logger.error(message);
			throw new RuntimeException(message);
		}
	}

}
