package cz.muni.fi.lasaris.sbms.semantics.logic;

import java.io.ByteArrayOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.jena.ontology.OntModelSpec;
import org.apache.jena.query.Dataset;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryExecution;
import org.apache.jena.query.QueryExecutionFactory;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.query.ReadWrite;
import org.apache.jena.query.ResultSet;
import org.apache.jena.query.ResultSetFormatter;
import org.apache.jena.rdf.model.InfModel;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.reasoner.Reasoner;
import org.apache.jena.reasoner.ReasonerRegistry;
import org.apache.jena.tdb.TDBFactory;
import org.apache.log4j.Logger;

public class TdbConnector {
	
	private static String dataPath;
	private static InfModel model;
	private static Dataset dataset;
	
	private static AtomicBoolean dirty = new AtomicBoolean(false);
	
	final static Logger logger = Logger.getLogger(TdbConnector.class);
	
	public static void init(Properties props) {
		TdbConnector.dataPath = (props != null && props.getProperty("tdb.path") != null) ? props.getProperty("tdb.path") : "./sbmstdb";
		dataset = TDBFactory.createDataset(dataPath) ;
	}
	
	
	public static void open() {
		
		if(dataPath == null || dataset == null) {
			throw new IllegalStateException("Path not set");
		}
		
		if(model != null) {
			model.close();
		}
		
		
		Path currentRelativePath = Paths.get("");
		String s = currentRelativePath.toAbsolutePath().toString();
		logger.debug("Current relative path is: " + s);
		logger.debug("User dir is: " + System.getProperty("user.dir"));
		
		  GregorianCalendar d = new GregorianCalendar();
		  dataset.begin(ReadWrite.READ);
		  // Get model inside the transaction
		  /*
		  if(false) {
			  model = dataset.getDefaultModel();
			  return;
		  }
		  */
		  
		  
		  /*
		   * RDFSReasoner + OntModel with RDFS does not work with certain queries (can't return individuals from other ontologies)
		   * OWLMicro + OWL_MEM - slow, memory consuming, some queries are not returned due to complexity
		   */
		  
		  Reasoner r = ReasonerRegistry.getOWLMicroReasoner();
		  
		  //OntModelSpec oms = new OntModelSpec(OntModelSpec.OWL_MEM);
		  OntModelSpec oms = new OntModelSpec(OntModelSpec.OWL_LITE_MEM_RDFS_INF);
		  
		  Model schema = ModelFactory.createOntologyModel(oms);
		  
		  schema.read(NS.sbms.replace("#", ""));
		  
		  logger.debug("schema loaded");
		  
		  Model data = ModelFactory.createOntologyModel(oms, dataset.getDefaultModel());
		  
		  logger.debug("data loaded from " + dataPath);
		  
		  model = ModelFactory.createInfModel(r, schema, data);
		 
		  logger.debug("ontoModel created");
		  
		  dataset.end();
		  
		  /*
		  ValidityReport v = model.validate();
		  if(!v.isValid()) {
			  logger.debug("Model is invalid: ");
			  while(v.getReports().hasNext()) {
				  Report report = v.getReports().next();
				  logger.debug(report);
			  }
		  }
		  */
		  logger.debug("Total time creating models in ms: " + ((new GregorianCalendar()).getTimeInMillis() - d.getTimeInMillis()));
		  
	}
	
	public static Model getModel()  {
		return getModel(true);
	}
	
	private static Model getModel(boolean checkDirty) {
		if(model == null) {
			throw new IllegalStateException("Tdb not opened");
		}
		
		if(checkDirty && dirty.get()) {
			logger.debug("cached model is dirty - reopening");
			// we will open clean model
			dirty.set(false);
			open();
			// cleanup will set it to dirty
			ModelUpdater.cleanup();
			// setting back to true
			dirty.set(false);
			// opening clean model
			open();
			
		}
		
		return model;
	}
	
	public static Model getWritableModel() {
		if(dataPath == null || dataset == null) {
			throw new IllegalStateException("Path not set");
		}
		logger.debug("Opening writable model...");
		dataset.begin(ReadWrite.WRITE);
		return dataset.getDefaultModel();
	}
	
	public static void commit() {
		endWriting(true);
	}
	
	public static void discardChanges() {
		endWriting(false);
	}
	
	public static void endWriting(boolean commit) {
		logger.debug("Ending writing");
		if(commit) {
			dataset.commit();
			dirty.set(true);
			logger.debug("Commiting changes - Readable model is dirty");
		} else {
			logger.debug("Discarding changes"); 
		}
		dataset.end();
		
	}
	
	public static void close() {
		model.close();
		model = null;
	}
	/*
	public static ResultSet executeSPARQLResults(Query q) {
		try (QueryExecution qexec = QueryExecutionFactory.create(q, getModel())) {
			return qexec.execSelect();
		} catch (Exception e) {
			logger.debug(e);
			return null;
		}
	}
	*/
	
	public static List<QuerySolution> executeSPARQLResults(Query q) {
		List<QuerySolution> l = new ArrayList<QuerySolution>();
		try (QueryExecution qexec = QueryExecutionFactory.create(q, getModel())) {
			ResultSet results = qexec.execSelect();
			for ( ; results.hasNext() ; )
			{
				l.add(results.next());
			}
		} catch (Exception e) {
			logger.debug(e);
		}
		return l;
	}
	
	public static String executeSPARQLFormatted(String sparql, String format) {
		String result = null;
		logger.debug("SPARQL query:\n" + sparql);
		Model m = TdbConnector.getModel();
		logger.debug("model opened");
		ByteArrayOutputStream s = new ByteArrayOutputStream();
		try (QueryExecution qexec = QueryExecutionFactory.create(sparql, m)) {
		    ResultSet results = qexec.execSelect();
		    logger.debug("query executed");
		    
		    if(format != null && format.equals("csv")) {
		    	ResultSetFormatter.outputAsCSV(s, results);
		    } else {
		    	ResultSetFormatter.outputAsJSON(s, results);
		    }
		    
		    logger.debug("query returned");
		    result = s.toString("UTF-8");
		    s.close(); 
		  } catch (Exception e) {
			  logger.debug(e);
			  result = "Unexpected error";
		  }
		  return result;
	}
	
	public static List<RDFNode> executeSPARQLList(String queryString) {
		return executeSPARQLList(queryString, false);
	}
	
	public static List<RDFNode> executeSPARQLList(String queryString, boolean dirtyIsOK) {
		logger.debug("GetList Query: " + queryString);
		List<RDFNode> result = new ArrayList<RDFNode>();
		try (QueryExecution qexec = QueryExecutionFactory.create(queryString, TdbConnector.getModel(!dirtyIsOK))) {
			ResultSet results = qexec.execSelect();
			for ( ; results.hasNext() ; )
			{
				RDFNode r = results.next().get("member");
				result.add(r);
			}
		} catch (Exception e) {
			logger.debug(e);
		} finally {
			logger.debug("Query finished.");
		}
		return Collections.unmodifiableList(result);
	}
}
