package org.ontoware.rdf2go.impl.jena;

import static org.ontoware.rdf2go.impl.jena.ModelImplJena.getJenaLang;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.io.input.ReaderInputStream;
import org.apache.commons.io.output.WriterOutputStream;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.RiotException;
import org.apache.jena.riot.RiotWriter;
import org.ontoware.aifbcommons.collection.ClosableIterable;
import org.ontoware.aifbcommons.collection.ClosableIterator;
import org.ontoware.rdf2go.Reasoning;
import org.ontoware.rdf2go.exception.LockException;
import org.ontoware.rdf2go.exception.MalformedQueryException;
import org.ontoware.rdf2go.exception.ModelRuntimeException;
import org.ontoware.rdf2go.exception.QueryLanguageNotSupportedException;
import org.ontoware.rdf2go.exception.SyntaxNotSupportedException;
import org.ontoware.rdf2go.model.Model;
import org.ontoware.rdf2go.model.ModelSet;
import org.ontoware.rdf2go.model.QuadPattern;
import org.ontoware.rdf2go.model.QueryResultTable;
import org.ontoware.rdf2go.model.Statement;
import org.ontoware.rdf2go.model.Syntax;
import org.ontoware.rdf2go.model.impl.AbstractModelSetImpl;
import org.ontoware.rdf2go.model.node.Node;
import org.ontoware.rdf2go.model.node.NodeOrVariable;
import org.ontoware.rdf2go.model.node.Resource;
import org.ontoware.rdf2go.model.node.ResourceOrVariable;
import org.ontoware.rdf2go.model.node.URI;
import org.ontoware.rdf2go.model.node.UriOrVariable;
import org.ontoware.rdf2go.model.node.Variable;
import org.ontoware.rdf2go.model.node.impl.URIImpl;

import com.hp.hpl.jena.query.Query;
import com.hp.hpl.jena.query.QueryExecution;
import com.hp.hpl.jena.query.QueryExecutionFactory;
import com.hp.hpl.jena.query.QueryFactory;


/**
 * A ModelSet implementation for Jena. It relies on the Jena
 * {@linkplain com.hp.hpl.jena.query.Dataset}.
 * 
 * @since 4.8.1
 * 
 * @author Roland Stühmer
 * 
 * @version $Revision$
 * 
 */
public class ModelSetImplJena extends AbstractModelSetImpl {

	private static final long serialVersionUID = 9211877052180956697L;
	
	private final com.hp.hpl.jena.query.Dataset dataset;
	private final com.hp.hpl.jena.shared.Lock lock;
	private Query countStatementsQuery;
	private boolean open = true;
	
	private static class ContextIterator implements ClosableIterator<URI> {

		private final Iterator<String> underlying;

		public ContextIterator(Iterator<String> idIterator) {
			this.underlying = idIterator;
		}

		@Override
		public void close() {
		}

		@Override
		public boolean hasNext() {
			return this.underlying.hasNext();
		}

		@Override
		public URI next() {
			return new URIImpl(this.underlying.next());
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}

	private class ModelIterator implements ClosableIterator<Model> {

		private final ClosableIterator<URI> underlying;

		private URI lastURI;

		public ModelIterator(ClosableIterator<URI> contextIterator) {
			this.underlying = contextIterator;
		}

		@Override
		public void close() {
			this.underlying.close();
		}

		@Override
		public boolean hasNext() {
			return this.underlying.hasNext();
		}

		@Override
		public Model next() {
			URI uri = this.underlying.next();

			Model model = getModel(uri);

			this.lastURI = uri;
			return model;
		}

		@Override
		public void remove() {
			// only possible when next() has been invoked at least once
			if (this.lastURI == null) {
				throw new IllegalStateException();
			}

			removeModel(lastURI);

			this.lastURI = null;
		}

	}

	private class JenaQuadPattern implements QuadPattern {

		private static final long serialVersionUID = -2397218722246644188L;

		private final UriOrVariable context;

		private final NodeOrVariable object;

		private final UriOrVariable predicate;

		private final ResourceOrVariable subject;

		public JenaQuadPattern(UriOrVariable context,
				ResourceOrVariable subject, UriOrVariable predicate,
				NodeOrVariable object) {
			this.checkNonNull(context);
			this.checkNonNull(subject);
			this.checkNonNull(predicate);
			this.checkNonNull(object);

			this.context = context;
			this.subject = subject;
			this.predicate = predicate;
			this.object = object;
		}

		@Override
		public UriOrVariable getContext() {
			return this.context;
		}

		@Override
		public NodeOrVariable getObject() {
			return this.object;
		}

		@Override
		public UriOrVariable getPredicate() {
			return this.predicate;
		}

		@Override
		public ResourceOrVariable getSubject() {
			return this.subject;
		}

		@Override
		public boolean matches(Statement statement) {
			return this.matches(statement.getContext(), this.context)
					&& this.matches(statement.getSubject(), this.subject)
					&& this.matches(statement.getPredicate(), this.predicate)
					&& this.matches(statement.getObject(), this.object);
		}

		private void checkNonNull(NodeOrVariable value) {
			if (value == null) {
				throw new NullPointerException();
			}
		}

		private boolean matches(Node node, NodeOrVariable variable) {
			return variable.equals(Variable.ANY) || variable.equals(node);
		}
	}

	private static class StatementIterator implements ClosableIterator<Statement> {
		
		private final Iterator<com.hp.hpl.jena.sparql.core.Quad> underlying;
		
		public StatementIterator(
				Iterator<com.hp.hpl.jena.sparql.core.Quad> jenaQuadIterator) {
			this.underlying = jenaQuadIterator;
		}

		@Override
		public void close() {
		}

		@Override
		public boolean hasNext() {
			return this.underlying.hasNext();
		}

		@Override
		public Statement next() {
			com.hp.hpl.jena.sparql.core.Quad quad = this.underlying
					.next();
			return new StatementJena29Impl(quad.getGraph(), quad.getSubject(),
					quad.getPredicate(), quad.getObject());
		}

		@Override
		public void remove() {
			this.underlying.remove();
		}
	}

	public ModelSetImplJena(com.hp.hpl.jena.query.Dataset dataset) {
		this.dataset = dataset;
		this.lock = this.dataset.getLock();
		org.apache.jena.riot.RIOT.init(); //wires RIOT readers/writers into Jena
	}

	@Override
	public ModelSet open() {
		return this;
	}

	@Override
	public boolean isOpen() {
		return this.open;
	}

	@Override
	public void close() {
		this.open = false;
	}

	@Override
	public long size() throws ModelRuntimeException {

		// Start with the size of the default graph
		long size = this.dataset.getDefaultModel().size();

		// Loop and add the sizes of all contained graphs
		Iterator<String> it = this.dataset.listNames();
		while (it.hasNext()) {
			size += this.dataset.getNamedModel(it.next()).size();
		}

		return size;
	}

	@Override
	public Model getModel(URI contextURI) {
		com.hp.hpl.jena.rdf.model.Model jenaModel = dataset
				.getNamedModel(contextURI.toString());

		if (jenaModel == null) {
			jenaModel = com.hp.hpl.jena.rdf.model.ModelFactory
					.createDefaultModel();
		}
		
		// return opened model
		return new ModelImplJena(contextURI, jenaModel).open();
	}

	@Override
	public boolean removeModel(URI contextURI) {
		this.dataset.removeNamedModel(contextURI.toString());
		return true;
	}

	@Override
	public boolean containsModel(URI contextURI) {
		return this.dataset.containsNamedModel(contextURI.toString());
	}

	@Override
	public Model getDefaultModel() {
		com.hp.hpl.jena.rdf.model.Model jenaModel = this.dataset
				.getDefaultModel();
		// return opened model
		return new ModelImplJena(jenaModel).open();
	}

	/**
	 * Return all <i>named</i> models. This does not currently return the
	 * default model because there is no usable iterator in Jena for this. Also
	 * the default Model in Jena has no usable context URI which causes problems
	 * later when iterating over the models e.g., using
	 * {@linkplain ClosableIterator#remove()}
	 */
	@Override
	public ClosableIterator<Model> getModels() {
		return new ModelIterator(this.getModelURIs());
	}

	@Override
	public ClosableIterator<URI> getModelURIs() {
		return new ContextIterator(this.dataset.listNames());
	}

	@Override
	public Object getUnderlyingModelSetImplementation() {
		return this.dataset;
	}

	@Override
	public ClosableIterable<Statement> queryConstruct(String query,
			String querylanguage) throws QueryLanguageNotSupportedException,
			MalformedQueryException, ModelRuntimeException {

		Query jenaQuery = QueryFactory.create(query);
		QueryExecution qexec = QueryExecutionFactory.create(jenaQuery,
				this.dataset);

		if (jenaQuery.isConstructType()) {
			com.hp.hpl.jena.rdf.model.Model m = qexec.execConstruct();
			Model resultModel = new ModelImplJena(null, m, Reasoning.none);
			resultModel.open();
			return resultModel;
		} else {
			throw new RuntimeException(
					"Cannot handle this type of query! Please use CONSTRUCT.");
		}
	}

	@Override
	public QueryResultTable querySelect(String query, String querylanguage)
			throws QueryLanguageNotSupportedException, MalformedQueryException,
			ModelRuntimeException {
		com.hp.hpl.jena.query.Syntax syntax = com.hp.hpl.jena.query.Syntax
				.lookup(querylanguage);
		if (syntax == null) {
			// delegate to super
			throw new QueryLanguageNotSupportedException(
					"Unsupported query language: " + querylanguage);
		}
		Query jenaQuery = QueryFactory.create(query, syntax);
		return new QueryResultTableImpl(jenaQuery,
				this.dataset);
	}

	@Override
	public ClosableIterable<Statement> sparqlConstruct(String query)
			throws ModelRuntimeException, MalformedQueryException {
		Query jenaQuery = QueryFactory.create(query);
		QueryExecution qexec = QueryExecutionFactory.create(jenaQuery,
				this.dataset);

		if (jenaQuery.isConstructType()) {
			com.hp.hpl.jena.rdf.model.Model m = qexec.execConstruct();
			Model resultModel = new ModelImplJena(null, m, Reasoning.none);
			resultModel.open();
			return resultModel;
		} else {
			throw new RuntimeException(
					"Cannot handle this type of query! Please use CONSTRUCT.");
		}

	}

	@Override
	public ClosableIterable<Statement> sparqlDescribe(String query)
			throws ModelRuntimeException {
		Query jenaQuery = QueryFactory.create(query);
		QueryExecution qexec = QueryExecutionFactory.create(jenaQuery,
				this.dataset);

		if (jenaQuery.isDescribeType()) {
			com.hp.hpl.jena.rdf.model.Model m = qexec.execDescribe();
			Model resultModel = new ModelImplJena(null, m, Reasoning.none);
			resultModel.open();
			return resultModel;
		} else {
			throw new RuntimeException(
					"Cannot handle this type of query! Please use DESCRIBE.");
		}
	}

	@Override
	public QueryResultTable sparqlSelect(String queryString)
			throws MalformedQueryException, ModelRuntimeException {
		Query jenaQuery = QueryFactory.create(queryString);
		return new QueryResultTableImpl(jenaQuery,
				this.dataset);
	}

	@Override
	public boolean sparqlAsk(String query) throws ModelRuntimeException,
			MalformedQueryException {
		Query jenaQuery = QueryFactory.create(query);
		QueryExecution qexec = QueryExecutionFactory.create(jenaQuery,
				this.dataset);

		if (jenaQuery.isAskType()) {
			return qexec.execAsk();
		} else {
			throw new RuntimeException(
					"Cannot handle this type of query! Please use ASK.");
		}
	}

	/**
	 * Read data from an {@linkplain Reader} with RDF syntax {@link Syntax#Trix}.
	 */
	@Override
	public void readFrom(Reader in) throws IOException, ModelRuntimeException {
		readFrom(in, Syntax.Trix, null);
	}

	@Override
	public void readFrom(Reader in, Syntax syntax) throws IOException,
			ModelRuntimeException, SyntaxNotSupportedException {
		readFrom(in, syntax, null);
	}

	@Override
	public void readFrom(Reader in, Syntax syntax, String baseURI)
			throws IOException, ModelRuntimeException,
			SyntaxNotSupportedException {
		ReaderInputStream is = new ReaderInputStream(in, StandardCharsets.UTF_8);
		readFrom(is, syntax, baseURI);
	}

	/**
	 * Read data from an {@linkplain InputStream} with RDF syntax
	 * {@link Syntax#Trix}.
	 * 
	 * <br />
	 * <b>Please note:</b><br />
	 * In this Jena implementation this will fail until a matching
	 * {@linkplain RiotReader} is available. Please use
	 * {@linkplain ModelSetImplJena#readFrom(Reader, Syntax)} with an available
	 * syntax such as {@link Syntax#Nquads} or {@link Syntax#Trig}.
	 */
	@Override
	public void readFrom(InputStream in) throws IOException,
			ModelRuntimeException {
		readFrom(in, Syntax.Trix, null);
	}

	@Override
    public void readFrom(InputStream in, Syntax syntax) throws IOException,
			ModelRuntimeException, SyntaxNotSupportedException {
		readFrom(in, syntax, null);
    }
    
	@Override
	public void readFrom(InputStream in, Syntax syntax, String baseURI)
			throws IOException, ModelRuntimeException,
			SyntaxNotSupportedException {

		RDFDataMgr.read(this.dataset.asDatasetGraph(), in, baseURI, getJenaLang(syntax));
	}

	/**
	 * Write data to a {@linkplain Writer} with RDF syntax
	 * {@link Syntax#Trix}.
	 * 
	 * <br />
	 * <b>Please note:</b><br />
	 * In this Jena implementation this will fail until a matching
	 * {@linkplain RiotWriter} is available. Please use
	 * {@linkplain ModelSetImplJena#writeTo(Writer, Syntax)} with an available
	 * syntax such as {@link Syntax#Nquads}.
	 */
	@Override
	public void writeTo(Writer out) throws IOException, ModelRuntimeException {
		writeTo(out, Syntax.Trix);
	}

	/**
	 * Write data to an {@linkplain OutputStream} with RDF syntax
	 * {@link Syntax#Trix}.
	 * 
	 * <br />
	 * <b>Please note:</b><br />
	 * In this Jena implementation this will fail until a matching
	 * {@linkplain RiotWriter} is available. Please use
	 * {@linkplain ModelSetImplJena#writeTo(OutputStream, Syntax)} with an available
	 * syntax such as {@link Syntax#Nquads}.
	 */
	@Override
	public void writeTo(OutputStream out) throws IOException,
			ModelRuntimeException {
		writeTo(out, Syntax.Trix);
	}

	@Override
    public void writeTo(Writer writer, Syntax syntax) throws IOException,
			ModelRuntimeException, SyntaxNotSupportedException {
		WriterOutputStream stream = new WriterOutputStream(writer, StandardCharsets.UTF_8);
		writeTo(stream, syntax);
	}

	@Override
	public void writeTo(OutputStream out, Syntax syntax) throws IOException,
			ModelRuntimeException, SyntaxNotSupportedException {
		
		if (syntax == null) {
			throw new NullPointerException("syntax may not be null");
		}
		
		Lang jenaLang = getJenaLang(syntax);

//		if (RDFLanguages.isTriples(jenaLang)) {
//			/*
//			 * NB: Writing a ModelSet to a triple serialization loses the
//			 * context of any quads if present.
//			 */
//			Iterator<Model> it = this.getModels();
//			while (it.hasNext()) {
//				Model model = it.next();
//				model.writeTo(out, syntax);
//			}
//			this.getDefaultModel().writeTo(out, syntax);
//		}
// FIXME stuehmer: write unit test to see if this can be removed
//		else {
		try {
			RDFDataMgr.write(out, this.dataset, jenaLang);
		}
		catch (RiotException e) {
			throw new SyntaxNotSupportedException(
					"error writing syntax " + syntax + ": " + e.getMessage());
		}
	}

	@Override
	public QuadPattern createQuadPattern(UriOrVariable context,
			ResourceOrVariable subject, UriOrVariable predicate,
			NodeOrVariable object) {
		return new JenaQuadPattern(context, subject, predicate, object);
	}

	@Override
	public void addStatement(Statement statement) throws ModelRuntimeException {
		addStatement(statement.getContext(), statement.getSubject(),
				statement.getPredicate(), statement.getObject());
	}

	@Override
	public void addStatement(URI context, Resource subject, URI predicate,
			Node object) throws ModelRuntimeException {
		com.hp.hpl.jena.rdf.model.Model jenaModel;
		
		if (context != null) {
			jenaModel = this.dataset.getNamedModel(context.toString());
		} else {
			jenaModel = this.dataset.getDefaultModel();
		}
		jenaModel.getGraph().add(
				new com.hp.hpl.jena.graph.Triple(TypeConversion.toJenaNode(
						subject, jenaModel), TypeConversion.toJenaNode(
						predicate, jenaModel), TypeConversion.toJenaNode(
						object, jenaModel)));
	}

	@Override
	public void removeAll() throws ModelRuntimeException {
		// Empty the default graph
		this.dataset.getDefaultModel().removeAll();
		// Remove all named graphs
		Iterator<String> it = this.dataset.listNames();
		while (it.hasNext()) {
			it.next();
			it.remove();
		}
	}

	@Override
    public void removeStatements(QuadPattern quadPattern)
			throws ModelRuntimeException {
		removeStatements(quadPattern.getContext(), quadPattern.getSubject(),
				quadPattern.getPredicate(), quadPattern.getObject());
	}

	@Override
    public void removeStatements(UriOrVariable context,
			ResourceOrVariable subject, UriOrVariable predicate,
			NodeOrVariable object) throws ModelRuntimeException {
		this.dataset.asDatasetGraph().deleteAny(
				(context != null) ? TypeConversion.toJenaNode(context)
						: com.hp.hpl.jena.graph.Node.ANY,
				TypeConversion.toJenaNode(subject),
				TypeConversion.toJenaNode(predicate),
				TypeConversion.toJenaNode(object));
	}
	
	@Override
	public void removeStatement(Statement statement)
			throws ModelRuntimeException {
		removeStatement(statement.getContext(), statement.getSubject(),
				statement.getPredicate(), statement.getObject());
	}

	@Override
	public void removeStatement(URI context, Resource subject, URI predicate,
			Node object) throws ModelRuntimeException {
		
		if (context == null) {
			this.dataset
					.getDefaultModel()
					.getGraph()
					.delete(new com.hp.hpl.jena.graph.Triple(TypeConversion
							.toJenaNode(subject), TypeConversion
							.toJenaNode(predicate), TypeConversion
							.toJenaNode(object)));
		} else {
			this.dataset.asDatasetGraph().delete(
					TypeConversion.toJenaNode(context),
					TypeConversion.toJenaNode(subject),
					TypeConversion.toJenaNode(predicate),
					TypeConversion.toJenaNode(object));
		}
	}

	@Override
    public long countStatements(QuadPattern pattern) {
		String context = (pattern.getContext() != null) ? pattern.getContext()
				.toString() : com.hp.hpl.jena.graph.Node.ANY.getName();
		String subject = pattern.getSubject().toString();
		String predicate = pattern.getPredicate().toString();
		String object = pattern.getObject().toString();

		com.hp.hpl.jena.query.QuerySolutionMap initialBindings =
				new com.hp.hpl.jena.query.QuerySolutionMap();
		if (pattern.getContext() != null) {
			initialBindings.add("c", this.dataset.getDefaultModel()
					.createResource(context));
		}
		initialBindings.add("s",
				this.dataset.getDefaultModel().createResource(subject));
		initialBindings.add("p",
				this.dataset.getDefaultModel().createResource(predicate));
		initialBindings.add("o",
				this.dataset.getDefaultModel().createResource(object));

		String query = "SELECT (count(*) AS ?count) WHERE { GRAPH ?c { ?s ?p ?o } }";

		// Initialize the reusable query
		if (this.countStatementsQuery == null) {
			this.countStatementsQuery = QueryFactory.create(query,
					com.hp.hpl.jena.query.Syntax.syntaxARQ);
		}

		QueryExecution exec = QueryExecutionFactory.create(
				this.countStatementsQuery, this.dataset, initialBindings);
		QueryResultTable result = new QueryResultTableImpl(exec);
		return Long
				.parseLong(result.iterator().next().getLiteralValue("count"));
	}

	@Override
    public ClosableIterator<Statement> findStatements(QuadPattern pattern)
			throws ModelRuntimeException {
		return this.findStatements(pattern.getContext(),
				pattern.getSubject(), pattern.getPredicate(),
				pattern.getObject());
	}
	
	@Override
    public ClosableIterator<Statement> findStatements(UriOrVariable contextURI,
			ResourceOrVariable subject, UriOrVariable predicate,
			NodeOrVariable object) throws ModelRuntimeException {
		return new StatementIterator(this.dataset.asDatasetGraph().find(
				(contextURI != null) ? TypeConversion.toJenaNode(contextURI)
						: com.hp.hpl.jena.graph.Node.ANY,
				TypeConversion.toJenaNode(subject),
				TypeConversion.toJenaNode(predicate),
				TypeConversion.toJenaNode(object)));
	}

	@Override
	public ClosableIterator<Statement> iterator() {
		return new StatementIterator(this.dataset.asDatasetGraph().find());
	}
	
	@Override
	public void commit() throws ModelRuntimeException {
	}

	@Override
	public void setAutocommit(boolean autocommit) {
	}

	@Override
	public String getNamespace(String prefix) {
		// We use the default model because there is no prefix mapping on
		// Dataset itself:
		return this.dataset.getDefaultModel().getNsPrefixURI(prefix);
	}

	@Override
	public Map<String, String> getNamespaces() {
		// We use the default model because there is no prefix mapping on
		// Dataset itself:
		return this.dataset.getDefaultModel().getNsPrefixMap();
	}

	/**
	 * Remove the specified namespace from all {@linkplain Model}s
	 * in this {@linkplain ModelSet} including the default graph.
	 */
	@Override
	public void removeNamespace(String prefix) {
		this.dataset.getDefaultModel().removeNsPrefix(prefix);
		Iterator<Model> it = this.getModels();
		while (it.hasNext()) {
			it.next().removeNamespace(prefix);
		}
	}

	/**
	 * Set the specified namespace for all {@linkplain Model}s
	 * in this {@linkplain ModelSet} including the default graph.
	 */
	@Override
	public void setNamespace(String prefix, String namespaceURI)
			throws IllegalArgumentException {
		this.dataset.getDefaultModel().setNsPrefix(prefix, namespaceURI);
		Iterator<Model> it = this.getModels();
		while (it.hasNext()) {
			it.next().setNamespace(prefix, namespaceURI);
		}
	}

	@Override
	public boolean isLocked() {
		return super.isLocked();
	}

	@Override
	public void lock() throws LockException {
		if (isLocked()) {
			throw new LockException("Already locked");
		}
		else {
			super.lock();
			this.lock.enterCriticalSection(com.hp.hpl.jena.shared.Lock.WRITE);
		}
	}

	@Override
	public void unlock() {
		if (isLocked()) {
			this.lock.leaveCriticalSection();
			super.unlock();
		}
	}

	@Override
	public boolean isEmpty() {
		return this.dataset.asDatasetGraph().isEmpty();
	}

	@Override
	public String toString() {
		return this.dataset.asDatasetGraph().toString();
	}

	@Override
	public boolean addModel(Model model) {
		for (String prefix : model.getNamespaces().keySet()) {
			this.dataset.getDefaultModel().setNsPrefix(prefix, model.getNamespace(prefix));
		}
			
		if (model instanceof ModelImplJena) {
			this.dataset.getNamedModel(model.getContextURI().toString()).add(
					((com.hp.hpl.jena.rdf.model.Model) model
							.getUnderlyingModelImplementation()));
			return true;
		}
		else {
			return super.addModel(model);
		}
	}

	@Override
	public void addModel(Model model, URI contextURI) {
		for (String prefix : model.getNamespaces().keySet()) {
			this.dataset.getDefaultModel().setNsPrefix(prefix, model.getNamespace(prefix));
		}
		
		if (model instanceof ModelImplJena) {
			this.dataset.getNamedModel(contextURI.toString()).add(
					((com.hp.hpl.jena.rdf.model.Model) model
							.getUnderlyingModelImplementation()));
		}
		else {
			super.addModel(model, contextURI);
		}
	}
}