/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.stanbol.enhancer.servicesapi.helper;

import static java.util.Collections.singleton;
import static org.apache.stanbol.enhancer.servicesapi.rdf.Properties.DC_LANGUAGE;
import static org.apache.stanbol.enhancer.servicesapi.rdf.Properties.RDF_TYPE;
import static org.apache.stanbol.enhancer.servicesapi.rdf.TechnicalClasses.ENHANCER_TEXTANNOTATION;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;

import org.apache.clerezza.rdf.core.Literal;
import org.apache.clerezza.rdf.core.LiteralFactory;
import org.apache.clerezza.rdf.core.MGraph;
import org.apache.clerezza.rdf.core.NonLiteral;
import org.apache.clerezza.rdf.core.Resource;
import org.apache.clerezza.rdf.core.Triple;
import org.apache.clerezza.rdf.core.TripleCollection;
import org.apache.clerezza.rdf.core.TypedLiteral;
import org.apache.clerezza.rdf.core.UriRef;
import org.apache.clerezza.rdf.core.impl.TripleImpl;
import org.apache.stanbol.enhancer.servicesapi.Chain;
import org.apache.stanbol.enhancer.servicesapi.ContentItem;
import org.apache.stanbol.enhancer.servicesapi.EnhancementEngine;
import org.apache.stanbol.enhancer.servicesapi.ServiceProperties;
import org.apache.stanbol.enhancer.servicesapi.rdf.Properties;
import org.apache.stanbol.enhancer.servicesapi.rdf.TechnicalClasses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class EnhancementEngineHelper {

    protected final static Random rng = new Random();

    private final static Logger log = LoggerFactory.getLogger(EnhancementEngineHelper.class);

    private final static LiteralFactory lf = LiteralFactory.getInstance();

    public static void setSeed(long seed) {
        rng.setSeed(seed);
    }

    /**
     * Create a new instance with the types enhancer:Enhancement and
     * enhancer:TextAnnotation in the metadata-graph of the content
     * item along with default properties (dc:creator and dc:created) and return
     * the UriRef of the extraction so that engines can further add.
     *
     * @param ci the ContentItem being under analysis
     * @param engine the Engine performing the analysis
     *
     * @return the URI of the new enhancement instance
     */
    public static UriRef createTextEnhancement(ContentItem ci,
            EnhancementEngine engine){
        return createTextEnhancement(ci.getMetadata(), engine, new UriRef(ci.getUri().getUnicodeString()));
    }
    /**
     * Create a new instance with the types enhancer:Enhancement and
     * enhancer:TextAnnotation in the parsed graph along with default properties
     * (dc:creator, dc:created and enhancer:extracted-form) and return
     * the UriRef of the extraction so that engines can further add.
     *
     * @param metadata the graph
     * @param engine the engine
     * @param contentItemId the id
     *
     * @return the URI of the new enhancement instance
     */
    public static UriRef createTextEnhancement(MGraph metadata,
                EnhancementEngine engine, UriRef contentItemId){
        UriRef enhancement = createEnhancement(metadata, engine,contentItemId);
        //add the Text Annotation Type
        metadata.add(new TripleImpl(enhancement, Properties.RDF_TYPE,
                TechnicalClasses.ENHANCER_TEXTANNOTATION));
        return enhancement;
    }
    /**
     * Create a new instance with the types enhancer:Enhancement and
     * enhancer:EntityAnnotation in the metadata-graph of the content
     * item along with default properties (dc:creator and dc:created) and return
     * the UriRef of the extraction so that engines can further add
     *
     * @param ci the ContentItem being under analysis
     * @param engine the Engine performing the analysis
     * @return the URI of the new enhancement instance
     */
    public static UriRef createEntityEnhancement(ContentItem ci,
            EnhancementEngine engine){
        return createEntityEnhancement(ci.getMetadata(), engine, new UriRef(ci.getUri().getUnicodeString()));
    }
    /**
     * Create a new instance with the types enhancer:Enhancement and
     * enhancer:EntityAnnotation in the parsed graph along with default properties
     * (dc:creator, dc:created and enhancer:extracted-form) and return
     * the UriRef of the extraction so that engines can further add.
     *
     * @param metadata the graph
     * @param engine the engine
     * @param contentItemId the id
     *
     * @return the URI of the new enhancement instance
     */
    public static UriRef createEntityEnhancement(MGraph metadata,
                EnhancementEngine engine, UriRef contentItemId){
        UriRef enhancement = createEnhancement(metadata, engine, contentItemId);
        metadata.add(new TripleImpl(enhancement, Properties.RDF_TYPE,
                TechnicalClasses.ENHANCER_ENTITYANNOTATION));
        return enhancement;
    }
    /**
     * Create a new instance with the types enhancer:Enhancement and
     * enhancer:TopicAnnotation in the parsed graph along with default properties
     * (dc:creator, dc:created and enhancer:extracted-form) and return
     * the UriRef of the extraction so that engines can further add.
     *
     * @param metadata the graph
     * @param engine the engine
     * @param contentItemId the id
     *
     * @return the URI of the new enhancement instance
     */
    public static UriRef createTopicEnhancement(MGraph metadata,
                 EnhancementEngine engine, UriRef contentItemId){
         UriRef enhancement = createEnhancement(metadata, engine, contentItemId);
         metadata.add(new TripleImpl(enhancement, Properties.RDF_TYPE,
                 TechnicalClasses.ENHANCER_TOPICANNOTATION));
         return enhancement;
     }
    /**
     * Create a new instance with the types enhancer:Enhancement and
     * enhancer:TopicAnnotation in the metadata-graph of the content
     * item along with default properties (dc:creator and dc:created) and return
     * the UriRef of the extraction so that engines can further add
     *
     * @param ci the ContentItem being under analysis
     * @param engine the Engine performing the analysis
     * @return the URI of the new enhancement instance
     */
    public static UriRef createTopicEnhancement(ContentItem ci,
            EnhancementEngine engine){
        return createTopicEnhancement(ci.getMetadata(), engine, new UriRef(ci.getUri().getUnicodeString()));
    }
    /**
     * Create a new enhancement instance in the metadata-graph of the content
     * item along with default properties (dc:creator and dc:created) and return
     * the UriRef of the extraction so that engines can further add.
     *
     * @param ci the ContentItem being under analysis
     * @param engine the Engine performing the analysis
     *
     * @return the URI of the new enhancement instance
     */
    protected static UriRef createEnhancement(MGraph metadata,
            EnhancementEngine engine, UriRef contentItemId){
        LiteralFactory literalFactory = LiteralFactory.getInstance();

        UriRef enhancement = new UriRef("urn:enhancement-"
                + EnhancementEngineHelper.randomUUID());
        //add the Enhancement Type
        metadata.add(new TripleImpl(enhancement, Properties.RDF_TYPE,
                TechnicalClasses.ENHANCER_ENHANCEMENT));
        //add the extracted from content item
        metadata.add(new TripleImpl(enhancement,
                Properties.ENHANCER_EXTRACTED_FROM, contentItemId));
        // creation date
        metadata.add(new TripleImpl(enhancement, Properties.DC_CREATED,
                literalFactory.createTypedLiteral(new Date())));

        // the engines that extracted the data
        // TODO: add some kind of versioning info for the extractor?
        // TODO: use a public dereferencing URI instead? that would allow for
        // explicit versioning too
        /* NOTE (Rupert Westenthaler 2010-05-26):
         * The Idea is to use the  ComponentContext in the activate() method of
         * an Enhancer to get the bundle name/version and use that as an
         * URI for the creator.
         * We would need to add getEnhancerID() method to the enhancer interface
         * to access this information
          */
        metadata.add(new TripleImpl(enhancement, Properties.DC_CREATOR,
                literalFactory.createTypedLiteral(engine.getClass().getName())));
        return enhancement;
    }
    /**
     * Adds the parsed {@link EnhancementEngine} as dc:contributer to the
     * enhancement and also sets the dc:modified property accordingly
     * @param metadata the {@link ContentItem#getMetadata()}
     * @param enhancement the enhancement
     * @param engine the engine
     */
    public static void addContributingEngine(MGraph metadata, UriRef enhancement,
                                             EnhancementEngine engine){
        LiteralFactory literalFactory = LiteralFactory.getInstance();
        // TODO: use a public dereferencing URI instead?
        metadata.add(new TripleImpl(enhancement, Properties.DC_CONTRIBUTOR,
            literalFactory.createTypedLiteral(engine.getClass().getName())));
        //set the modification date to the current date.
        set(metadata,enhancement,Properties.DC_MODIFIED,new Date(),literalFactory);
    }
    
    /**
     * Create a new extraction instance in the metadata-graph of the content
     * item along with default properties (dc:creator and dc:created) and return
     * the UriRef of the extraction so that engines can further add
     *
     * @param ci the ContentItem being under analysis
     * @param engine the Engine performing the analysis
     * @return the URI of the new extraction instance
     * @deprecated
     * @see EnhancementEngineHelper#createEntityEnhancement(ContentItem, EnhancementEngine)
     * @see EnhancementEngineHelper#createTextEnhancement(ContentItem, EnhancementEngine)
     */
    @Deprecated
    public static UriRef createNewExtraction(ContentItem ci,
            EnhancementEngine engine) {
        LiteralFactory literalFactory = LiteralFactory.getInstance();

        MGraph metadata = ci.getMetadata();
        UriRef extraction = new UriRef("urn:extraction-"
                + EnhancementEngineHelper.randomUUID());

        metadata.add(new TripleImpl(extraction, Properties.RDF_TYPE,
                TechnicalClasses.ENHANCER_EXTRACTION));

        // relate the extraction to the content item
        metadata.add(new TripleImpl(extraction,
                Properties.ENHANCER_RELATED_CONTENT_ITEM, new UriRef(ci.getUri().getUnicodeString())));

        // creation date
        metadata.add(new TripleImpl(extraction, Properties.DC_CREATED,
                literalFactory.createTypedLiteral(new Date())));

        // the engines that extracted the data
        // TODO: add some kind of versioning info for the extractor?
        // TODO: use a public dereferencing URI instead? that would allow for
        // explicit versioning too
        metadata.add(new TripleImpl(extraction, Properties.DC_CREATOR,
                literalFactory.createTypedLiteral(engine.getClass().getName())));

        return extraction;
    }

    /**
     * Random UUID generator with re-seedable RNG for the tests.
     *
     * @return a new Random UUID
     */
    public static UUID randomUUID() {
        return new UUID(rng.nextLong(), rng.nextLong());
    }

    /**
     * Getter for the first typed literal value of the property for a resource.
     *
     * @param <T> the java class the literal value needs to be converted to.
     * Note that the parsed LiteralFactory needs to support this conversion
     * @param graph the graph used to query for the property value
     * @param resource the resource
     * @param property the property
     * @param type the type the literal needs to be converted to
     * @param literalFactory the literalFactory
     * @return the value
     */
    public static <T> T get(TripleCollection graph, NonLiteral resource, UriRef property, Class<T> type,
            LiteralFactory literalFactory){
        Iterator<Triple> results = graph.filter(resource, property, null);
        if(results.hasNext()){
            while(results.hasNext()){
                Triple result = results.next();
                if(result.getObject() instanceof TypedLiteral){
                    return literalFactory.createObject(type, (TypedLiteral)result.getObject());
                } else {
                    log.debug("Triple {} does not have a TypedLiteral as object! -> ignore",result);
                }
            }
            log.info("No value for {} and property {} had the requested Type {} -> return null",
                new Object[]{resource,property,type});
            return null;
        } else {
            log.debug("No Triple found for {} and property {}! -> return null",resource,property);
            return null;
        }
    }
    /**
     * Replaces all current values of the property for the resource
     * with the parsed value
     * @param graph the graph
     * @param resource the resource
     * @param property the property
     * @param value the value
     */
    public static void set(MGraph graph, NonLiteral resource, UriRef property, Resource value){
        set(graph,resource,property,value == null ? null : singleton(value),null);
    }
    /**
     * Replaces all current values of the property for the resource
     * with the parsed values
     * @param graph the graph
     * @param resource the resource
     * @param property the property
     * @param value the value
     */
    public static void set(MGraph graph, NonLiteral resource, UriRef property, Collection<Resource> values){
        set(graph,resource,property,values,null);
    }

    /**
     * Replaces all current values of the property for the resource
     * with the parsed value
     * @param graph the graph
     * @param resource the resource
     * @param property the property
     * @param value the value. In case it is an instance of {@link Resource} it
     * is directly added to the graph. Otherwise the parsed {@link LiteralFactory}
     * is used to create a {@link TypedLiteral} for the parsed value.
     * @param literalFactory the {@link LiteralFactory} used in case the parsed
     * value is not an {@link Resource}
     */
    public static void set(MGraph graph, NonLiteral resource, UriRef property,
                           Object value, LiteralFactory literalFactory){
        set(graph,resource,property,value == null ? null : singleton(value),literalFactory);
    }
    /**
     * Replaces all current values of the property for the resource
     * with the parsed values
     * @param graph the graph
     * @param resource the resource
     * @param property the property
     * @param value the value. In case it is an instance of {@link Resource} it
     * is directly added to the graph. Otherwise the parsed {@link LiteralFactory}
     * is used to create a {@link TypedLiteral} for the parsed value.
     * @param literalFactory the {@link LiteralFactory} used in case the parsed
     * value is not an {@link Resource}
     */
    public static void set(MGraph graph, NonLiteral resource, UriRef property,
                               Collection<?> values, LiteralFactory literalFactory){
        Iterator<Triple> currentValues = graph.filter(resource, property, null);
        while(currentValues.hasNext()){
            currentValues.next();
            currentValues.remove();
        }
        if(values != null){
            for(Object value : values){
                if(value instanceof Resource){
                    graph.add(new TripleImpl(resource, property, (Resource) value));
                } else if (value != null){
                    graph.add(new TripleImpl(resource, property, 
                        literalFactory.createTypedLiteral(value)));
                }
            }
        }
    }
    
    /**
     * Getter for the typed literal values of the property for a resource
     * @param <T> the java class the literal value needs to be converted to.
     * Note that the parsed LiteralFactory needs to support this conversion
     * @param graph the graph used to query for the property value
     * @param resource the resource
     * @param property the property
     * @param type the type the literal needs to be converted to
     * @param literalFactory the literalFactory
     * @return the value
     */
    public static <T> Iterator<T> getValues(TripleCollection graph, NonLiteral resource,
            UriRef property, final Class<T> type, final  LiteralFactory literalFactory){
        final Iterator<Triple> results = graph.filter(resource, property, null);
        return new Iterator<T>() {
            //TODO: dose not check if the object of the triple is of type UriRef
            @Override
            public boolean hasNext() {    return results.hasNext(); }
            @Override
            public T next() {
                return literalFactory.createObject(type, (TypedLiteral)results.next().getObject());
            }
            @Override
            public void remove() { results.remove(); }
        };
    }
    /**
     * Getter for the first String literal value the property for a resource
     * @param graph the graph used to query for the property value
     * @param resource the resource
     * @param property the property
     * @return the value
     */
    public static String getString(TripleCollection graph, NonLiteral resource, UriRef property){
        Iterator<Triple> results = graph.filter(resource, property, null);
        if(results.hasNext()){
            while (results.hasNext()){
                Triple result = results.next();
                if(result.getObject() instanceof Literal){
                    return ((Literal)result.getObject()).getLexicalForm();
                } else {
                    log.debug("Triple {} does not have a literal as object! -> ignore",result);
                }
            }
            log.info("No Literal value for {} and property {} -> return null",
                resource,property);
            return null;
        } else {
            log.debug("No Triple found for "+resource+" and property "+property+"! -> return null");
            return null;
        }
    }
    /**
     * Getter for the string literal values the property for a resource
     * @param graph the graph used to query for the property value
     * @param resource the resource
     * @param property the property
     * @return the value
     */
    public static Iterator<String> getStrings(TripleCollection graph, NonLiteral resource, UriRef property){
        final Iterator<Triple> results = graph.filter(resource, property, null);
        return new Iterator<String>() {
            //TODO: dose not check if the object of the triple is of type UriRef
            @Override
            public boolean hasNext() { return results.hasNext(); }
            @Override
            public String next() {
                return ((Literal)results.next().getObject()).getLexicalForm();
            }
            @Override
            public void remove() { results.remove(); }
        };
    }
    /**
     * Getter for the first value of the data type property for a resource
     * @param graph the graph used to query for the property value
     * @param resource the resource
     * @param property the property
     * @return the value
     */
    public static UriRef getReference(TripleCollection graph, NonLiteral resource, UriRef property){
        Iterator<Triple> results = graph.filter(resource, property, null);
        if(results.hasNext()){
            while(results.hasNext()){
            Triple result = results.next();
                if(result.getObject() instanceof UriRef){
                    return (UriRef)result.getObject();
                } else {
                    log.debug("Triple "+result+" does not have a UriRef as object! -> ignore");
                }
            }
            log.info("No UriRef value for {} and property {} -> return null",resource,property);
            return null;
        } else {
            log.debug("No Triple found for {} and property {}! -> return null",resource,property);
            return null;
        }
    }
    /**
     * Getter for the values of the data type property for a resource.
     *
     * @param graph the graph used to query for the property value
     * @param resource the resource
     * @param property the property
     * @return The iterator over all the values (
     */
    public static Iterator<UriRef> getReferences(TripleCollection graph, NonLiteral resource, UriRef property){
        final Iterator<Triple> results = graph.filter(resource, property, null);
        return new Iterator<UriRef>() {
            //TODO: dose not check if the object of the triple is of type UriRef
            @Override
            public boolean hasNext() { return results.hasNext(); }
            @Override
            public UriRef next() { return (UriRef)results.next().getObject(); }
            @Override
            public void remove() { results.remove(); }
        };
    }
    
    /**
     * Comparator that allows to sort a list/array of {@link EnhancementEngine}s
     * based on there {@link ServiceProperties#ENHANCEMENT_ENGINE_ORDERING}.
     */
    public static final Comparator<EnhancementEngine> EXECUTION_ORDER_COMPARATOR = new Comparator<EnhancementEngine>() {

        @Override
        public int compare(EnhancementEngine engine1, EnhancementEngine engine2) {
            Integer order1 = getEngineOrder(engine1);
            Integer order2 = getEngineOrder(engine2);
            //start with the highest number finish with the lowest ...
            return order1 == order2?0:order1<order2?1:-1;
        }

    };
    /**
     * Gets the {@link ServiceProperties#ENHANCEMENT_ENGINE_ORDERING} value
     * for the parsed EnhancementEngine. If the Engine does not implement the
     * {@link ServiceProperties} interface or does not provide the
     * {@link ServiceProperties#ENHANCEMENT_ENGINE_ORDERING} the 
     * {@link ServiceProperties#ORDERING_DEFAULT} is returned <p>
     * This method is guaranteed to NOT return <code>null</code>.
     * @param engine the engine
     * @return the ordering
     */
    public static Integer getEngineOrder(EnhancementEngine engine){
        log.debug("getOrder "+engine);
        if (engine instanceof ServiceProperties){
            log.debug(" ... implements ServiceProperties");
            Object value = ((ServiceProperties)engine).getServiceProperties().get(ServiceProperties.ENHANCEMENT_ENGINE_ORDERING);
            log.debug("   > value = "+value +" "+value.getClass());
            if (value !=null && value instanceof Integer){
                return (Integer)value;
            }
        }
        return ServiceProperties.ORDERING_DEFAULT;
    }
    
    /**
     * Getter for the Resources of fise:TextAnnotations that do have a value 
     * of the dc:language property. The returned list is sorted by 'fise:confidence'.
     * Annotations with missing confidence are ranked last.<p>
     * NOTE that the returned list will likely contain annotations for the same language
     * if multiple language identification are used in the same {@link Chain}.
     * @param graph the graph with the enhancement. 
     * Typically {@link ContentItem#getMetadata()}
     * @return the sorted list of language annotations or an empty list if none.
     * @throws IllegalArgumentException if <code>null</code> is parsed as graph
     */
    public static List<NonLiteral> getLanguageAnnotations(TripleCollection graph){
        if(graph == null){
            throw new IllegalArgumentException("The parsed graph MUST NOT be NULL!");
        }
        // I do not use SPARQL, because I do not want to instantiate a QueryEngine
        final Map<NonLiteral,Double> confidences = new HashMap<NonLiteral,Double>();
        List<NonLiteral> langAnnotations = new ArrayList<NonLiteral>();
        Iterator<Triple> textAnnoataions = graph.filter(null, RDF_TYPE, ENHANCER_TEXTANNOTATION);
        while(textAnnoataions.hasNext()){
            NonLiteral textAnnotation = textAnnoataions.next().getSubject();
            String language = getString(graph, textAnnotation, DC_LANGUAGE);
            if(language != null){
                Double confidence = get(graph, textAnnotation, Properties.ENHANCER_CONFIDENCE, Double.class, lf);
                confidences.put(textAnnotation,confidence);
                langAnnotations.add(textAnnotation);
            }
        }
        if(langAnnotations.size() > 1){
            Collections.sort(langAnnotations,new Comparator<NonLiteral>() {
                @Override
                public int compare(NonLiteral o1, NonLiteral o2) {
                    Double c1 = confidences.get(o1);
                    Double c2 = confidences.get(o2);
                    //decrising order (values without confidence last)
                    if(c1 == null){
                        return c2 == null ? 0 : 1;
                    } else if(c2 == null){
                        return -1;
                    } else {
                        return c2.compareTo(c1);
                    }
                }
            });
        }
        return langAnnotations;
    }
    /**
     * Getter for language identified for (extracted-from) the parsed
     * ContentItem. The returned value is the Annotation with the highest
     * 'fise:confidence' value - or if no annotations are present - the
     * 'dc-terms:language' value of the {@link ContentItem#getUri()}.<p>
     * Users that want to obtain all language annotations should use
     * {@link #getLanguageAnnotations(TripleCollection)} instead.<p>
     * This method ensures a write lock on the {@link ContentItem}.
     * @param ci the contentItem
     * @return the identified language of the parsed {@link ContentItem}.
     * <code>null</code> if not available.
     * @throws IllegalArgumentException if <code>null</code> is parsed as content item
     * @see #getLanguageAnnotations(TripleCollection)
     */
    public static String getLanguage(ContentItem ci){
        if(ci == null){
            throw new IllegalArgumentException("The parsed ContentItem MUST NOT be NULL!");
        }
        ci.getLock().readLock().lock();
        try {
            List<NonLiteral> langAnnotations = getLanguageAnnotations(ci.getMetadata());
            if(langAnnotations.isEmpty()){ //fallback
                return getString(ci.getMetadata(), ci.getUri(), DC_LANGUAGE);
            } else {
                return getString(ci.getMetadata(), langAnnotations.get(0), DC_LANGUAGE);
            }
        } finally {
            ci.getLock().readLock().unlock();
        }
    }
}
