/* $This file is distributed under the terms of the license in LICENSE$ */

package edu.cornell.mannlib.vitro.webapp.dao.jena;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import edu.cornell.mannlib.vitro.webapp.rdfservice.ResultSetConsumer;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;

import org.apache.jena.ontology.OntClass;
import org.apache.jena.ontology.OntModel;
import org.apache.jena.ontology.UnionClass;
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.QueryFactory;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.query.ResultSet;
import org.apache.jena.rdf.model.AnonId;
import org.apache.jena.rdf.model.Literal;
import org.apache.jena.rdf.model.Property;
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.rdf.model.StmtIterator;
import org.apache.jena.shared.Lock;
import org.apache.jena.vocabulary.RDFS;

import edu.cornell.mannlib.vitro.webapp.beans.Individual;
import edu.cornell.mannlib.vitro.webapp.beans.IndividualImpl;
import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatement;
import edu.cornell.mannlib.vitro.webapp.beans.VClass;
import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary;
import edu.cornell.mannlib.vitro.webapp.dao.jena.IndividualSDB.IndividualNotFoundException;
import edu.cornell.mannlib.vitro.webapp.dao.jena.WebappDaoFactorySDB.SDBDatasetMode;
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;

public class IndividualDaoSDB extends IndividualDaoJena {

	private DatasetWrapperFactory dwf;
    private SDBDatasetMode datasetMode;
    private WebappDaoFactorySDB wadf;

    public IndividualDaoSDB(DatasetWrapperFactory dwf,
                            SDBDatasetMode datasetMode,
                            WebappDaoFactorySDB wadf) {
        super(wadf);
        this.dwf = dwf;
        this.datasetMode = datasetMode;
        this.wadf = wadf;
    }

    protected DatasetWrapper getDatasetWrapper() {
    	return dwf.getDatasetWrapper();
    }

    protected Individual makeIndividual(String individualURI) {
        try {
            return new IndividualSDB(individualURI,
            	                     this.dwf,
            	                     datasetMode,
            	                     wadf);
        } catch (IndividualNotFoundException e) {
            // If the individual does not exist, return null.
            return null;
        } catch(Exception ex) {
        	//Should some other error occur, please log it here
        	log.error("An error occurred trying to make an individual ", ex);
        	if(StringUtils.isNotEmpty(individualURI)) {
        		log.error("IndividualURI equals " + individualURI);
        	} else {
        		log.error("IndividualURI is null or empty");
        	}
        	return null;
        }
    }

    private static final Log log = LogFactory.getLog(
    		IndividualDaoSDB.class.getName());

    @Override
    protected OntModel getOntModel() {
    	return getOntModelSelector().getABoxModel();
    }

    private static final boolean SKIP_INITIALIZATION = true;

    @Override
    public List getIndividualsByVClassURI(String vclassURI,
    		                              int offset,
    		                              int quantity ) {

    	if (vclassURI==null) {
            return null;
        }

        List<Individual> ents = new ArrayList<Individual>();

        Resource theClass = (vclassURI.indexOf(PSEUDO_BNODE_NS) == 0)
            ? getOntModel().createResource(new AnonId(vclassURI.split("#")[1]))
            : ResourceFactory.createResource(vclassURI);

        if (theClass.isAnon() && theClass.canAs(UnionClass.class)) {
        	UnionClass u = theClass.as(UnionClass.class);
        	for (OntClass operand : u.listOperands().toList()) {
        		VClass vc = new VClassJena(operand, getWebappDaoFactory());
        		ents.addAll(getIndividualsByVClass(vc));
        	}
        } else {

        	List<Individual> individualList;

        	// Check if there is a graph filter.
        	// If so, we will use it in a slightly strange way.  Unfortunately,
        	// performance is quite bad if we add several graph variables in
        	// order to account for the fact that an individual's type
        	// declaration may be in a different graph from its label.
        	// Thus, we will run two queries: one with a single
        	// graph variable to get the list of URIs, and a second against
        	// the union graph to get individuals with their labels.
        	// We will then toss out any individual in the second
        	// list that is not also in the first list.
        	// Annoying, yes, but better than the alternative.
        	// Note that both queries need to sort identically or
        	// the results may be very strange.
        	String[] graphVars = {"?g"};
        	String filterStr = WebappDaoFactorySDB.getFilterBlock(
        			graphVars, datasetMode);
        	if (!StringUtils.isEmpty(filterStr)) {
        		List<Individual> graphFilteredIndividualList =
        			    getGraphFilteredIndividualList(theClass, filterStr);
        		List<Individual> unfilteredIndividualList = getIndividualList(
        				theClass);
        		Iterator<Individual> unfilteredIt  = unfilteredIndividualList
        													.iterator();
        		for (Individual filt : graphFilteredIndividualList) {
        			Individual unfilt = unfilteredIt.next();
        			while (!unfilt.getURI().equals(filt.getURI())) {
        				unfilt = unfilteredIt.next();
        			}
        			ents.add(unfilt);
        		}
        	} else {
        		ents = getIndividualList(theClass);
        	}
        }

        java.util.Collections.sort(ents);

        if (quantity > 0 && offset > 0) {
            List<Individual> sublist = new ArrayList<Individual>();
            for (int i = offset - 1; i < ((offset - 1) + quantity); i++) {
                sublist.add(ents.get(i));
            }
            return sublist;
        }

        return ents;

    }

    private List<Individual> getIndividualList(Resource theClass) {
    	final List<Individual> ents = new ArrayList<Individual>();
   	    DatasetWrapper w = getDatasetWrapper();
	    Dataset dataset = w.getDataset();
    	dataset.getLock().enterCriticalSection(Lock.READ);
    	try {
    		String query =
	    		"SELECT DISTINCT ?ind ?label " +
	    		"WHERE " +
	    		 "{ \n" +
                    "{   ?ind a <" + theClass.getURI() + "> } \n" +
	    		 	"UNION { \n" +
                    "    ?ind a <" + theClass.getURI() + "> . \n" +
	    		 	"    ?ind  <" + RDFS.label.getURI() + "> ?label \n" +
	    		 	"} \n" +
	    		 "} ORDER BY ?ind ?label";

            RDFService rdfService = wadf.getRDFService();
            try {
                rdfService.sparqlSelectQuery(
                        query, new ResultSetConsumer() {
                            String uri = null;
                            String label = null;

                            @Override
                            protected void processQuerySolution(QuerySolution qs) {
                                Resource currRes = qs.getResource("ind");
                                if (currRes.isAnon()) {
                                    return;
                                }

                                if (uri != null && !uri.equals(currRes.getURI())) {
                                    try {
                                        ents.add(makeIndividual(uri, label));
                                    } catch (IndividualNotFoundException e) {
                                        // don't add
                                    }
                                    uri = currRes.getURI();
                                    label = null;
                                } else if (uri == null) {
                                    uri = currRes.getURI();
                                }
                                Literal labelLit = qs.getLiteral("label");
                                if (labelLit != null) {
                                    label = labelLit.getLexicalForm();
                                }
                            }

                            @Override
                            protected void endProcessing() {
                                if (uri != null) {
                                    try {
                                        ents.add(makeIndividual(uri, label));
                                    } catch (IndividualNotFoundException e) {
                                        // don't add
                                    }
                                }
                            }
                        });
            } catch (RDFServiceException e) {
                log.debug(e,e);
                throw new RuntimeException(e);
            }
    	} finally {
    		dataset.getLock().leaveCriticalSection();
    		w.close();
    	}
        return ents;
    }

    private List<Individual> getGraphFilteredIndividualList(Resource theClass,
    		                                                String filterStr) {
		final List<Individual> filteredIndividualList = new ArrayList<Individual>();
		DatasetWrapper w = getDatasetWrapper();
   	    Dataset dataset = w.getDataset();
       	dataset.getLock().enterCriticalSection(Lock.READ);
       	try {
    		String query =
    			"SELECT DISTINCT ?ind " +
    			"WHERE " +
    			"{ GRAPH ?g { \n" +
                	"{   ?ind a <" + theClass.getURI() + "> } \n" +
                "  } \n" + filterStr +
                "} ORDER BY ?ind";
    		RDFService rdfService = wadf.getRDFService();
    		try {
    		    rdfService.sparqlSelectQuery(
                        query, new ResultSetConsumer() {
                            @Override
                            protected void processQuerySolution(QuerySolution qs) {
                                Resource currRes = qs.getResource("ind");
                                if (!currRes.isAnon()) {
                                    try {
                                        filteredIndividualList.add(
                                                makeIndividual(currRes.getURI(), null));
                                    } catch (IndividualNotFoundException e) {
                                        // don't add
                                    }
                                }
                            }
                        });
    		} catch (RDFServiceException e) {
    		    log.debug(e,e);
    		    throw new RuntimeException(e);
    		}
       	} finally {
    		dataset.getLock().leaveCriticalSection();
    		w.close();
		}
       	return filteredIndividualList;
    }

    private Individual makeIndividual(String uri, String label) throws IndividualNotFoundException {
        Individual ent = new IndividualSDB(uri,
                this.dwf, datasetMode, wadf,
                SKIP_INITIALIZATION);
        ent.setName(label);
		ent.setRdfsLabel(label);
        return ent;
    }

    @Override
    public Individual getIndividualByURI(String entityURI) {
        if( entityURI == null || entityURI.length() == 0 ) {
            return null;
        } else {
        	return makeIndividual(entityURI);
        }
    }

    /**
     * fills in the Individual objects needed for any ObjectPropertyStatements
     * attached to the specified individual.
     * @param entity An individual
     */
    private void fillIndividualsForObjectPropertyStatements(Individual entity){
        getOntModel().enterCriticalSection(Lock.READ);
        try {
            for (ObjectPropertyStatement e2e : entity.getObjectPropertyStatements()) {
                e2e.setSubject(makeIndividual(e2e.getSubjectURI()));
                e2e.setObject(makeIndividual(e2e.getObjectURI()));
            }
        } finally {
            getOntModel().leaveCriticalSection();
        }
    }

    /**
     * In Jena it can be difficult to get an object with a given dataproperty if
     * you do not care about the datatype or lang of the literal.  Use this
     * method if you would like to ignore the lang and datatype.
     *
     * Note: this method doesn't require that a property be declared in the
     * ontology as a data property -- only that it behaves as one.
     */
    @Override
    public List<Individual> getIndividualsByDataProperty(String dataPropertyUri,
    	                                                 String value){
    	OntModel fullModel = getOntModelSelector().getFullModel();

        Property prop = null;
        if( RDFS.label.getURI().equals( dataPropertyUri )){
            prop = RDFS.label;
        }else{
            prop = fullModel.getProperty(dataPropertyUri);
        }

        if( prop == null ) {
            log.debug("Could not getIndividualsByDataProperty() " +
                    "because " + dataPropertyUri + "was not found in model.");
            return Collections.emptyList();
        }

        if( value == null ){
            log.debug("Could not getIndividualsByDataProperty() " +
                    "because value was null");
            return Collections.emptyList();
        }

        Literal litv1 = fullModel.createLiteral(value);
        Literal litv2 = fullModel.createTypedLiteral(value);

        //warning: this assumes that any language tags will be EN
        Literal litv3 = fullModel.createLiteral(value,"EN");

        HashMap<String,Individual> individualsMap =
        		new HashMap<String, Individual>();

        fullModel.enterCriticalSection(Lock.READ);
        int count = 0;
        try{
            StmtIterator stmts
                = fullModel.listStatements((Resource)null, prop, litv1);
            while(stmts.hasNext()){
                count++;
                Statement stmt = stmts.nextStatement();

                RDFNode sub = stmt.getSubject();
                if( sub == null || sub.isAnon() || sub.isLiteral() )
                    continue;

                RDFNode obj = stmt.getObject();
                if( obj == null || !obj.isLiteral() )
                    continue;

                Literal literal = (Literal)obj;
                Object v = literal.getValue();
                if( v == null )
                    continue;

                String subUri = ((Resource)sub).getURI();
                if( ! individualsMap.containsKey(subUri)){
                    individualsMap.put(subUri,makeIndividual(subUri));
                }
            }

            stmts = fullModel.listStatements((Resource)null, prop, litv2);
            while(stmts.hasNext()){
                count++;
                Statement stmt = stmts.nextStatement();

                RDFNode sub = stmt.getSubject();
                if( sub == null || sub.isAnon() || sub.isLiteral() )
                    continue;

                RDFNode obj = stmt.getObject();
                if( obj == null || !obj.isLiteral() )
                    continue;

                Literal literal = (Literal)obj;
                Object v = literal.getValue();
                if( v == null )
                    continue;

                String subUri = ((Resource)sub).getURI();
                if( ! individualsMap.containsKey(subUri)){
                    individualsMap.put(subUri, makeIndividual(subUri));
                }
            }

            stmts = fullModel.listStatements((Resource)null, prop, litv3);
            while(stmts.hasNext()){
                count++;
                Statement stmt = stmts.nextStatement();

                RDFNode sub = stmt.getSubject();
                if( sub == null || sub.isAnon() || sub.isLiteral() )
                    continue;

                RDFNode obj = stmt.getObject();
                if( obj == null || !obj.isLiteral() )
                    continue;

                Literal literal = (Literal)obj;
                Object v = literal.getValue();
                if( v == null )
                    continue;

                String subUri = ((Resource)sub).getURI();
                if( ! individualsMap.containsKey(subUri)){
                    individualsMap.put(subUri, makeIndividual(subUri));
                }
            }
        } finally {
            fullModel.leaveCriticalSection();
        }

        List<Individual> rv = new ArrayList(individualsMap.size());
        rv.addAll(individualsMap.values());
        return rv;
    }

    @Override
    public Collection<String> getAllIndividualUris() {
        final List<String> list = new LinkedList<String>();

        // get all labeled resources from any non-tbox and non-metadata graphs,
        // as well as the unnamed graph (first pattern below)
        String query = "SELECT DISTINCT ?ind WHERE { \n" +
                       " { ?ind <" + RDFS.label.getURI() + "> ?label } " +
                       " UNION { " +
                       "  GRAPH ?g { ?ind <" + RDFS.label.getURI() +
                                          "> ?label } \n" +
                       "  FILTER (?g != <" + ModelNames.APPLICATION_METADATA + "> " +
                       "          && !regex(str(?g),\"tbox\")) \n " +
                       " } " +
                       "}";

	    Query q = QueryFactory.create(query);
	    DatasetWrapper w = getDatasetWrapper();
	    Dataset dataset = w.getDataset();
	    dataset.getLock().enterCriticalSection(Lock.READ);
	    QueryExecution qe = QueryExecutionFactory.create(q, dataset);
	    try {
	        ResultSet rs = qe.execSelect();
	        while (rs.hasNext()) {
	        	Resource res = rs.next().getResource("ind");
	        	if (!res.isAnon()) {
	        		list.add(res.getURI());
	        	}
	        }
        } finally {
        	qe.close();
        	dataset.getLock().leaveCriticalSection();
        	w.close();
        }

        return list;
    }

    private Iterator<Individual> getIndividualIterator(
    									final List<String> individualURIs) {
        if (individualURIs.size() >0){
            log.info("Number of individuals from source: "
            		+ individualURIs.size());
            return new Iterator<Individual>(){
                Iterator<String> innerIt = individualURIs.iterator();
                public boolean hasNext() {
                    return innerIt.hasNext();
                }
                public Individual next() {
                    String indURI = innerIt.next();
                    Individual ind = makeIndividual(indURI);
                    if (ind != null) {
                        return ind;
                    } else {
                        return new IndividualImpl(indURI);
                    }
                }
                public void remove() {
                    //not used
                }
            };
        }
        else
            return null;
    }

    @Override
    public Iterator<String> getUpdatedSinceIterator(long updatedSince){
        List<String> individualURIs = new ArrayList<String>();
        Date since = new DateTime(updatedSince).toDate();
        String sinceStr = xsdDateTimeFormat.format(since);
        getOntModel().enterCriticalSection(Lock.READ);
        try {
            String queryStr = "PREFIX vitro: <"+ VitroVocabulary.vitroURI+"> " +
                              "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>"+
                              "SELECT ?ent " +
                              "WHERE { " +
                              "     ?ent vitro:modTime ?modTime ." +
                              "     FILTER (xsd:dateTime(?modTime) >= \""
                              		+ sinceStr + "\"^^xsd:dateTime) " +
                              "}";
            Query query = QueryFactory.create(queryStr);
            QueryExecution qe = QueryExecutionFactory.create(
            		query,getOntModel());
            try {
	            ResultSet results = qe.execSelect();
	            while (results.hasNext()) {
	                QuerySolution qs = results.next();
	                Resource res = (Resource) qs.get("?ent");
	                if (res.getURI() != null) {
	                	individualURIs.add(res.getURI());
	                }
	            }
            } finally {
            	qe.close();
            }
        } finally {
            getOntModel().leaveCriticalSection();
        }
        return individualURIs.iterator();
    }

}
