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

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

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.jena.ontology.Individual;
import org.apache.jena.ontology.OntClass;
import org.apache.jena.ontology.OntModel;
import org.apache.jena.ontology.OntModelSpec;
import org.apache.jena.query.Dataset;
import org.apache.jena.query.DatasetFactory;
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.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
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.shared.Lock;
import org.apache.jena.vocabulary.OWL;
import org.apache.jena.vocabulary.RDF;
import org.apache.jena.vocabulary.RDFS;

import edu.cornell.mannlib.vitro.webapp.beans.VClass;
import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary;
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactoryConfig;

public class JenaModelUtils {
    
    private static final Log log = LogFactory.getLog(JenaModelUtils.class.getName());

    private static final Set<String>  nonIndividualTypeURIs ;
    
    static {
        nonIndividualTypeURIs = new HashSet<String>();
        nonIndividualTypeURIs.add(OWL.Class.getURI());
        nonIndividualTypeURIs.add(OWL.Restriction.getURI());
        nonIndividualTypeURIs.add(OWL.ObjectProperty.getURI());
        nonIndividualTypeURIs.add(OWL.DatatypeProperty.getURI());
        nonIndividualTypeURIs.add(OWL.AnnotationProperty.getURI());
        nonIndividualTypeURIs.add(OWL.Ontology.getURI());
        nonIndividualTypeURIs.add(RDFS.Class.getURI());
        nonIndividualTypeURIs.add(RDF.Property.getURI());
    }
    
    /**
     * Creates a set of vitro:ClassGroup resources for each root class in
     * an ontology.  Also creates annotations to place each root class and all 
     * of its children in the appropriate groups.  In the case of multiple 
     * inheritance, classgroup assignment will be arbitrary.
     * @param wadf DAO Factory
     * @param tboxModel containing ontology classes
     * @return resultArray of OntModels, where resultArray[0] is the model containing
     * the triples about the classgroups, and resultArray[1] is the model containing
     * annotation triples assigning ontology classes to classgroups.
     */
    public synchronized static OntModel[] makeClassGroupsFromRootClasses(
            WebappDaoFactory wadf, Model tboxModel) {
        
        OntModel ontModel = ModelFactory.createOntologyModel(
                OntModelSpec.OWL_DL_MEM, tboxModel);
        OntModel modelForClassgroups = ModelFactory.createOntologyModel(
                OntModelSpec.OWL_DL_MEM);
        OntModel modelForClassgroupAnnotations = ModelFactory.createOntologyModel(
                OntModelSpec.OWL_DL_MEM);
        SimpleOntModelSelector oms = new SimpleOntModelSelector();
        oms.setTBoxModel(ontModel);
        oms.setApplicationMetadataModel(modelForClassgroups);
        WebappDaoFactoryConfig config = new WebappDaoFactoryConfig();
        config.setDefaultNamespace(wadf.getDefaultNamespace());
        WebappDaoFactory myWebappDaoFactory = new WebappDaoFactoryJena(
                new SimpleOntModelSelector(ontModel), config, null);
        
        Resource classGroupClass = ResourceFactory.createResource(
                VitroVocabulary.CLASSGROUP);
        Property inClassGroupProperty = ResourceFactory.createProperty(
                VitroVocabulary.IN_CLASSGROUP);
        
        ontModel.enterCriticalSection(Lock.READ);
        try { 
            try {                    
                List<VClass> rootClasses = myWebappDaoFactory.getVClassDao()
                        .getRootClasses();
                for (VClass rootClass : rootClasses) {
                    Individual classGroup = modelForClassgroups.createIndividual(
                            wadf.getDefaultNamespace() + "vitroClassGroup" +
                                    rootClass.getLocalName(), classGroupClass);
                    classGroup.setLabel(rootClass.getName(), null);

                    Resource rootClassRes = modelForClassgroupAnnotations.getResource(
                            rootClass.getURI());
                    modelForClassgroupAnnotations.add(
                            rootClassRes, inClassGroupProperty, classGroup);
                    for (String childURI : myWebappDaoFactory.getVClassDao()
                            .getAllSubClassURIs(rootClass.getURI())) {
                        Resource childClass = modelForClassgroupAnnotations
                                .getResource(childURI);
                        if (!modelForClassgroupAnnotations.contains(
                                childClass, inClassGroupProperty, (RDFNode) null)) {
                            childClass.addProperty(inClassGroupProperty, classGroup);
                        }
                    }
                }
            } catch (Exception e) {
                String errMsg = "Unable to create class groups automatically " +
                        "based on class hierarchy";
                log.error(errMsg, e);
                throw new RuntimeException(errMsg, e);
            }
        } finally {
            ontModel.leaveCriticalSection();
        }
        OntModel[] resultArray = new OntModel[2];
        resultArray[0] = modelForClassgroups;
        resultArray[1] = modelForClassgroupAnnotations;
        return resultArray;
    }
    
    private final OntModelSpec DEFAULT_ONT_MODEL_SPEC = OntModelSpec.OWL_MEM;
    private final boolean NORMAL = false;
    private final boolean AGGRESSIVE = true;


    public OntModel extractTBox( Model inputModel ) {
        return extractTBox(inputModel, null);    
    }
    
    public OntModel extractTBox( Model inputModel, boolean MODE ) {
        Dataset dataset = DatasetFactory.create(inputModel);
        return extractTBox(dataset, null, null, MODE);   
    }
    
    public OntModel extractTBox( Model inputModel, String namespace ) {
        Dataset dataset = DatasetFactory.create(inputModel);
        return extractTBox( dataset, namespace, null, NORMAL );
    }
    
    public OntModel extractTBox( Dataset dataset, String namespace, String graphURI) {
        return extractTBox( dataset, namespace, graphURI, NORMAL);
    }
    
    public OntModel extractTBox( Dataset dataset, String namespace, String graphURI, boolean mode ) {
        OntModel tboxModel = ModelFactory.createOntologyModel(DEFAULT_ONT_MODEL_SPEC);
        
        List<String> queryStrList = new LinkedList<String>();
        
        // Use SPARQL DESCRIBE queries to extract the RDF for named ontology entities
        
        queryStrList.add( makeDescribeQueryStr( OWL.Class.getURI(), namespace, graphURI ) );
        queryStrList.add( makeDescribeQueryStr( OWL.Restriction.getURI(), namespace, graphURI ) );
        queryStrList.add( makeDescribeQueryStr( OWL.ObjectProperty.getURI(), namespace, graphURI ) );
        queryStrList.add( makeDescribeQueryStr( OWL.DatatypeProperty.getURI(), namespace, graphURI ) );
        queryStrList.add( makeDescribeQueryStr( OWL.AnnotationProperty.getURI(), namespace, graphURI ) );
        // if we're using to a hash namespace, the URI of the Ontology resource will be
        // that namespace minus the final hash mark.
        if ( namespace != null && namespace.endsWith("#") ) {
            queryStrList.add( makeDescribeQueryStr( OWL.Ontology.getURI(), namespace.substring(0,namespace.length()-1), graphURI ) );    
        } else {
            queryStrList.add( makeDescribeQueryStr( OWL.Ontology.getURI(), namespace, graphURI ) );
        }
        
        // Perform the SPARQL DESCRIBEs
        for ( String queryStr : queryStrList ) {
            Query tboxSparqlQuery = QueryFactory.create(queryStr);
            QueryExecution qe = QueryExecutionFactory.create(tboxSparqlQuery,dataset);
            try {
                dataset.getLock().enterCriticalSection(Lock.READ);
                qe.execDescribe(tboxModel);
            } finally {
                dataset.getLock().leaveCriticalSection();
            }
        }
        
        // Perform possibly-redundant extraction to try ensure we don't miss 
        // individual axioms floating around.  We still might miss things;
        // this approach isn't perfect.
        if (mode == AGGRESSIVE) {
            tboxModel.add(construct(dataset, namespace, graphURI, RDFS.subClassOf));
            tboxModel.add(construct(dataset, namespace, graphURI, RDFS.subPropertyOf));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.equivalentClass));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.unionOf));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.intersectionOf));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.complementOf));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.onProperty));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.allValuesFrom));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.someValuesFrom));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.hasValue));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.minCardinality));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.maxCardinality));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.cardinality));
            tboxModel.add(construct(dataset, namespace, graphURI, OWL.disjointWith));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.DISPLAY_LIMIT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.DISPLAY_RANK_ANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.IN_CLASSGROUP)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.IN_CLASSGROUP)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_INPROPERTYGROUPANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROHIBITED_FROM_UPDATE_BELOW_ROLE_LEVEL_ANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.HIDDEN_FROM_DISPLAY_BELOW_ROLE_LEVEL_ANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
            		                                              VitroVocabulary.HIDDEN_FROM_PUBLISH_BELOW_ROLE_LEVEL_ANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.DESCRIPTION_ANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.SHORTDEF)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.EXAMPLE_ANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.vitroURI + "extendedLinkedData")));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_OFFERCREATENEWOPTIONANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_COLLATEBYSUBCLASSANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_CUSTOM_LIST_VIEW_ANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_CUSTOMDISPLAYVIEWANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_CUSTOMENTRYFORMANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_CUSTOMSEARCHVIEWANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_CUSTOMSHORTVIEWANNOT)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_ENTITYSORTDIRECTION)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_ENTITYSORTFIELD)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_OBJECTINDIVIDUALSORTPROPERTY)));
            tboxModel.add(construct(dataset, namespace, graphURI, ResourceFactory.createResource(
                                                                  VitroVocabulary.PROPERTY_SELECTFROMEXISTINGANNOT)));
        }
        return tboxModel;
    }
    
    private Model construct(Dataset dataset, 
                            String namespace, 
                            String graphURI, 
                            Resource property) {
        dataset.getLock().enterCriticalSection(Lock.READ);
        try {
            StringBuilder buff = new StringBuilder();
            buff.append("PREFIX afn: <http://jena.apache.org/ARQ/function#> \n")
                    .append("CONSTRUCT { \n").append("  ?res <").append(property.getURI()).append("> ?o } WHERE { \n");
            if (graphURI != null) {
                buff.append("    GRAPH ").append(graphURI).append(" { \n");
            }
            buff.append("      ?res <").append(property.getURI()).append("> ?o \n");
            buff.append(getNamespaceFilter(namespace));
            if (graphURI != null) {
                buff.append("    } \n");
            }
            buff.append("}");
            Query constructProp = QueryFactory.create(buff.toString());
            QueryExecution qe = QueryExecutionFactory.create(constructProp, dataset);
            try {
                return qe.execConstruct();
            } finally {
                qe.close();
            }
        } finally {
            dataset.getLock().leaveCriticalSection();
        }
    }
    
    private String makeDescribeQueryStr( String typeURI, String namespace ) {
        return makeDescribeQueryStr( typeURI, namespace, null );
    }     
        
    private String makeDescribeQueryStr( String typeURI, String namespace, String graphURI ) {
        
        StringBuilder describeQueryStrBuff = new StringBuilder()
            .append("PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n")
            .append("PREFIX afn: <http://jena.apache.org/ARQ/function#> \n")
            .append("DESCRIBE ?res WHERE { \n");
            if (graphURI != null) {
                describeQueryStrBuff.append("GRAPH ").append(graphURI).append("{ \n");
            }
            describeQueryStrBuff
            .append("    ?res rdf:type <").append(typeURI).append("> . \n");
            
            describeQueryStrBuff
            .append("    FILTER (!isBlank(?res)) \n")
    
            .append(getNamespaceFilter(namespace));
    
        if (graphURI != null) {
            describeQueryStrBuff
            .append("} \n");
        }
            
        describeQueryStrBuff.append("} \n");
        
        return describeQueryStrBuff.toString();
        
    }
    
    private String getNamespaceFilter(String namespace) {
        StringBuilder buff = new StringBuilder();
        if (namespace == null) {
            // exclude resources in the Vitro internal namespace or in the 
            // OWL namespace, but allow all others
            buff
            .append("    FILTER (afn:namespace(?res) != \"")
            .append("http://www.w3.org/2002/07/owl#")
            .append("\") \n")
            .append("    FILTER (?res != <")
            .append("http://www.w3.org/2002/07/owl")
            .append(">) \n");
        } else {
            // limit resources to those in the supplied namespace
            buff
            .append("    FILTER (regex(str(?res), \"^")
            .append(namespace)
            .append("\")) \n");  
        }
        return buff.toString();
    }
    
    public Model extractABox(Model inputModel){
        Dataset dataset = DatasetFactory.create(inputModel);
        return extractABox(dataset, null, null);
    }
    
    public Model extractABox( Dataset unionDataset, Dataset baseOrInfDataset, String graphURI ) {
        
        Model aboxModel = ModelFactory.createDefaultModel();

        // iterate through all classes and DESCRIBE each of their instances
        // Note that this could be simplified if we knew that the model was a
        // reasoning model: we could then simply describe all instances of 
        // owl:Thing.

        //OntModel ontModel = ( inputModel instanceof OntModel ) 
        //? (OntModel)inputModel
        //: ModelFactory.createOntologyModel( DEFAULT_ONT_MODEL_SPEC, inputModel );
        OntModel ontModel = extractTBox(unionDataset, null, graphURI);    
    
        try {
            ontModel.enterCriticalSection(Lock.READ);
            Iterator classIt = ontModel.listNamedClasses();
            QueryExecution qe = null;
            while ( classIt.hasNext() ) {
                
                OntClass ontClass = (OntClass) classIt.next();
                //if ( !(ontClass.getNameSpace().startsWith(OWL.getURI()) )  
                // && !(ontClass.getNameSpace().startsWith(VitroVocabulary.vitroURI))    ) {
             if(!(ontClass.getNameSpace().startsWith(OWL.getURI()))){
                    
                    String queryStr = makeDescribeQueryStr( ontClass.getURI(), null, graphURI );
                    
                    Query aboxSparqlQuery = QueryFactory.create(queryStr);
                    if(baseOrInfDataset != null){
                        qe = QueryExecutionFactory.create(aboxSparqlQuery,baseOrInfDataset);
                    }
                    else{
                        qe = QueryExecutionFactory.create(aboxSparqlQuery,unionDataset);
                    }
                    if(baseOrInfDataset != null){
                        try {
                            baseOrInfDataset.getLock().enterCriticalSection(Lock.READ);
                            qe.execDescribe(aboxModel); // puts the statements about each resource into aboxModel.
                        } finally {
                            baseOrInfDataset.getLock().leaveCriticalSection();
                        }
                    }
                    else{
                        try {
                            unionDataset.getLock().enterCriticalSection(Lock.READ);
                            qe.execDescribe(aboxModel); // puts the statements about each resource into aboxModel.
                        } finally {
                            unionDataset.getLock().leaveCriticalSection();
                        }
                    }
                    
                }
            }
        } finally {
            ontModel.leaveCriticalSection();
        }
        
        return aboxModel;
        
    }
    
}
