/**
 *  Copyright 2014 Martynas Jusevičius <[email protected]>
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package com.atomgraph.server.provider;

import com.atomgraph.core.util.jena.DataManager;
import org.apache.jena.ontology.OntDocumentManager;
import org.apache.jena.ontology.OntModel;
import org.apache.jena.ontology.OntModelSpec;
import org.apache.jena.ontology.OntResource;
import org.apache.jena.ontology.Ontology;
import org.apache.jena.util.iterator.ExtendedIterator;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.PerRequestTypeInjectableProvider;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.shared.Lock;
import com.atomgraph.processor.exception.OntologyException;
import com.sun.jersey.api.client.ClientHandlerException;
import javax.ws.rs.ext.Providers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Application ontology provider.
 * 
 * @see org.apache.jena.ontology.Ontology
 * @author Martynas Jusevičius {@literal <[email protected]>}
 */
@Provider
public class OntologyProvider extends PerRequestTypeInjectableProvider<Context, Ontology> implements ContextResolver<Ontology>
{
    private static final Logger log = LoggerFactory.getLogger(OntologyProvider.class);
    
    @Context Providers providers;

    private final OntDocumentManager ontDocumentManager;
    private final String ontologyURI;
    
    public OntologyProvider(final OntDocumentManager ontDocumentManager, final String ontologyURI,
            final OntModelSpec materializationSpec, final boolean materialize)
    {
        super(Ontology.class);
        
        if (ontDocumentManager == null) throw new IllegalArgumentException("OntDocumentManager cannot be null");
        if (ontologyURI == null) throw new IllegalArgumentException("URI cannot be null");
        if (materializationSpec == null) throw new IllegalArgumentException("OntModelSpec cannot be null");
        
        this.ontDocumentManager = ontDocumentManager;
        this.ontologyURI = ontologyURI;
        
        // materialize OntModel inferences to avoid invoking rules engine on every request
        if (materialize && materializationSpec.getReasoner() != null)
        {
            OntModel ontModel = getOntModel(ontDocumentManager, ontologyURI, materializationSpec);
            Ontology ontology = ontModel.getOntology(ontologyURI);

            ImportCycleChecker checker = new ImportCycleChecker();
            checker.check(ontology);
            if (checker.getCycleOntology() != null)
            {
                if (log.isErrorEnabled()) log.error("Sitemap contains an ontology which forms an import cycle: {}", checker.getCycleOntology());
                throw new OntologyException("Sitemap contains an ontology which forms an import cycle: " + checker.getCycleOntology().getURI());
            }
            
            OntModel materializedModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM);
            materializedModel.add(ontModel);
            ontDocumentManager.addModel(ontologyURI, materializedModel, true);
        }
    }

    public class ImportCycleChecker
    {
        private final Map<Ontology, Boolean> marked = new HashMap<>(), onStack = new HashMap<>();
        private Ontology cycleOntology = null;

        public void check(Ontology ontology)
        {
            if (ontology == null) throw new IllegalArgumentException("Ontology cannot be null");
            
            marked.put(ontology, Boolean.TRUE);
            onStack.put(ontology, Boolean.TRUE);

            ExtendedIterator<OntResource> it = ontology.listImports();
            try
            {
                while (it.hasNext())
                {
                    OntResource importRes = it.next();
                    if (importRes.canAs(Ontology.class))
                    {
                        Ontology imported = importRes.asOntology();
                        if (marked.get(imported) == null)
                            check(imported);
                        else if (onStack.get(imported))
                        {
                            cycleOntology = imported;
                            return;
                        }
                    }
                }

                onStack.put(ontology, Boolean.FALSE);
            }
            finally
            {
                it.close();
            }
        }
        
        public Ontology getCycleOntology()
        {
            return cycleOntology;
        }
        
    }
    
    @Override
    public Injectable<Ontology> getInjectable(ComponentContext cc, Context context)
    {
        return new Injectable<Ontology>()
        {
            @Override
            public Ontology getValue()
            {
                return getOntology();
            }
        };
    }

    @Override
    public Ontology getContext(Class<?> type)
    {
        return getOntology();
    }
    
    public Ontology getOntology()
    {
        OntModelSpec loadSpec = OntModelSpec.OWL_MEM;
        
        // attempt to use DataManager to retrieve owl:import Models
        if (getOntDocumentManager().getFileManager() instanceof DataManager)
            loadSpec.setImportModelGetter((DataManager)getOntDocumentManager().getFileManager());
        
        return getOntology(loadSpec);
    }
    
    public Ontology getOntology(OntModelSpec loadSpec)
    {   
        return getOntModel(getOntDocumentManager(), getOntologyURI(), loadSpec).getOntology(getOntologyURI());
    }
    
    /**
     * Loads ontology by URI.
     * 
     * @param manager
     * @param ontologyURI ontology location
     * @param ontModelSpec ontology model specification
     * @return ontology model
     */
    public static OntModel getOntModel(OntDocumentManager manager, String ontologyURI, OntModelSpec ontModelSpec)
    {
        if (manager == null) throw new IllegalArgumentException("OntDocumentManager cannot be null");
        if (ontologyURI == null) throw new IllegalArgumentException("URI cannot be null");
        if (ontModelSpec == null) throw new IllegalArgumentException("OntModelSpec cannot be null");
        if (log.isDebugEnabled()) log.debug("Loading sitemap ontology from URI: {}", ontologyURI);

        try
        {
            OntModel ontModel = manager.getOntology(ontologyURI, ontModelSpec);

            // explicitly loading owl:imports -- workaround for Jena bug: https://issues.apache.org/jira/browse/JENA-1210
            ontModel.enterCriticalSection(Lock.WRITE);
            try
            {
                ontModel.loadImports();
            }
            finally
            {
                ontModel.leaveCriticalSection();
            }

            // lock and clone the model to avoid ConcurrentModificationExceptions
            ontModel.enterCriticalSection(Lock.READ);
            try
            {
                return ModelFactory.createOntologyModel(ontModelSpec,
                        ModelFactory.createUnion(ModelFactory.createDefaultModel(), ontModel.getBaseModel()));
            }
            finally
            {
                ontModel.leaveCriticalSection();
            }
        }
        catch (ClientHandlerException ex) // thrown by DataManager
        {
            // remove ontology from cache, so next time it will be reloaded, possibly with fixed imports
            manager.getFileManager().removeCacheModel(ontologyURI);
            
            if (log.isErrorEnabled()) log.error("Could not load ontology '{}' or its imports", ontologyURI);
            throw new OntologyException("Could not load ontology '" + ontologyURI + "' or its imports", ex);
        }
    }

    public OntDocumentManager getOntDocumentManager()
    {
        return ontDocumentManager;
    }
    
    public String getOntologyURI()
    {
        return ontologyURI;
    }
    
    public Providers getProviders()
    {
        return providers;
    }
    
}