/*
 * 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.jena.rdf.model.impl;


import java.util.* ;
import java.util.function.Predicate;

import org.apache.jena.graph.* ;
import org.apache.jena.shared.AlreadyReifiedException ;
import org.apache.jena.shared.CannotReifyException ;
import org.apache.jena.util.iterator.ExtendedIterator ;
import org.apache.jena.util.iterator.NullIterator ;
import org.apache.jena.util.iterator.WrappedIterator ;
import org.apache.jena.vocabulary.RDF ;

/** A Reifier that only supports one style Standard (intercept, no conceal 
 *  -- and intercept is a no-op anyway because all triples 
 *  appear in the underlying graph for storing all triples).
 *  This exists to give reification style "Standard" semantics primarily for legacy reasons. 
 */

public class ReifierStd
{
    private ReifierStd() {}
    
    private final static Node rdfType      = RDF.Nodes.type ;
    private final static Node statement    = RDF.Nodes.Statement ;
    private final static Node subject      = RDF.Nodes.subject ;
    private final static Node predicate    = RDF.Nodes.predicate ;
    private final static Node object       = RDF.Nodes.object ;

    // All the methods of the old Reifier interface, converted to statics. 
    
    /**
    Answer an iterator over the reification triples of this Reifier, or an empty 
    iterator - if showHidden is false, only the exposed triples, otherwise only
    the concealed ones.
     */
    public static ExtendedIterator<Triple> findEither(Graph graph, Triple match, boolean showHidden) {
        if ( showHidden )
            return NullIterator.instance() ;
        else
            return graph.find(match) ;
    }
    
    static Predicate<Triple> filterReif = triple -> triple.getPredicate().equals(subject) ||
                   triple.getPredicate().equals(predicate) ||
                   triple.getPredicate().equals(object) ||
                   ( triple.getPredicate().equals(rdfType) && triple.getObject().equals(statement) ) ;

    /**
        Answer an iterator over all the reification triples that this Reifier exposes
        (ie all if Standard, none otherwise) that match m.
    */
    public static ExtendedIterator<Triple> findExposed(Graph graph, Triple match)
    {
        ExtendedIterator<Triple> it = graph.find(match) ;
        it = it.filterKeep(filterReif) ;
        return WrappedIterator.create(it) ;
    }

    /**
     * Answer the triple associated with the node <code>n</code>.
     * 
     * @param n
     *            the node to use as the key
     * @return the associated triple, or <code>null</code> if none
     */

    public static Triple getTriple(Graph graph, Node n)
    {
        // Must have rdf:type rdf:Statement
        if ( ! graph.contains(n, rdfType, statement) )
            return null ;
        Node s = getObject(graph, n, subject) ;
        if ( s == null ) return null ;
        Node p = getObject(graph, n, predicate) ;
        if ( p == null ) return null ;
        Node o = getObject(graph, n, object) ;
        if ( o == null ) return null ;
        return new Triple(s,p,o) ;
    }
    
    // Get one and only one object
    private static Node getObject(Graph graph, Node n, Node predicate)
    {
        ExtendedIterator<Triple> iter = graph.find(n, predicate, Node.ANY) ;
        try {
            if ( ! iter.hasNext() )
                // None.
                return null ;
            Triple t = iter.next() ;
            if ( iter.hasNext() )
                // Too many.
                return null ;
            return t.getObject() ;
        } finally { iter.close() ; }
    }
    
    /**
    @return true iff there's {@literal >} 0 mappings to this triple
     */
    public static boolean hasTriple(Graph graph, Triple t)
    {
        ExtendedIterator<Node> iter = findNodesForTriple(graph, t, false) ;
        try {
            return iter.hasNext() ;
        } finally { iter.close() ; }
    }

    /**
    true iff _n_ is associated with some triple.
     */
    public static boolean hasTriple(Graph graph, Node node)
    {
        return getTriple(graph, node) != null ;
    }

    /**
     * return an iterator over all the nodes that are reifiying something in the
     * graph
     */
    public static ExtendedIterator<Node> allNodes(Graph graph)
    {
        return allNodes(graph, null) ;
    }

    /**
     * return an iterator over all the nodes that are reifiying t in the graph
     */
    public static ExtendedIterator<Node> allNodes(Graph graph, Triple t)
    {
        return findNodesForTriple(graph, t, false) ;
    }

    private static ExtendedIterator<Node> findNodesForTriple(Graph graph, Triple t, boolean oneWillDo)
    {
        ExtendedIterator<Triple> iter = graph.find(Node.ANY, rdfType, statement) ;
        List<Node> nodes = new ArrayList<>() ;
        try
        {
            while (iter.hasNext())
            {
                Triple typeTriple = iter.next() ;
                Node n = typeTriple.getSubject() ;
                
                // Check.
                if ( t != null )
                {
                    if ( ! exactlyOne(graph, n, subject, t.getSubject()) )
                        continue ;
                    if ( ! exactlyOne(graph, n, predicate, t.getPredicate()) )
                        continue ;
                    if ( ! exactlyOne(graph, n, object, t.getObject()) )
                        continue ;
                }
                nodes.add(n) ;
                if ( oneWillDo )
                    break ;
            }
        } finally { iter.close() ; }
        return WrappedIterator.createNoRemove(nodes.iterator()) ;
    }
    // ----

    // check whether there is exactly the triple expected, and no others with same S and P but different O. 
    private static boolean exactlyOne(Graph graph, Node n, Node predicate, Node object)
    {
        ExtendedIterator<Triple> iter = graph.find(n, predicate, Node.ANY) ;
        try {
            if ( ! iter.hasNext() )
                return false ;
            
            while (iter.hasNext())
            {
                Node obj = iter.next().getObject() ;
                if  ( ! obj.equals(object) )
                    return false ;
            }
            return true ;
        } finally { iter.close() ; }
    }

    /**
     * note the triple _t_ as reified using _n_ as its representing node. If _n_
     * is already reifying something, a AlreadyReifiedException is thrown.
     */

    public static Node reifyAs(Graph graph, Node node, Triple triple)
    {
        if ( node == null )
            node = NodeFactory.createBlankNode() ;
        else
        {
            Triple t = getTriple(graph, node) ; 

            if ( t != null && ! t.equals(triple) )
                throw new AlreadyReifiedException(node) ;
            if ( t != null )
                // Already there
                return node ;

            // Check it's a well-formed reification by Jena's uniqueness rules
            // No fragments (we checked for exact match by getTriple(node))
            if ( graph.contains(node, subject, Node.ANY) )
                throw new CannotReifyException(node) ;
            if ( graph.contains(node, predicate, Node.ANY) )
                throw new CannotReifyException(node) ;
            if ( graph.contains(node, object, Node.ANY) )
                throw new CannotReifyException(node) ;
        }
        
        graph.add(new Triple(node, rdfType, statement)) ;
        graph.add(new Triple(node, subject, triple.getSubject())) ;
        graph.add(new Triple(node, predicate, triple.getPredicate())) ;
        graph.add(new Triple(node, object, triple.getObject())) ;

        return node ;
    }
    /**
    remove all bindings which map to this triple.
     */
    public static void remove(Graph graph, Triple triple)
    {
        // Materialize the nodes to delete - avoid ConcurrentModificationException.
        for ( Node n : allNodes(graph, triple).toList() )
            remove(graph, n, triple) ;
    }

    /**
     * remove any existing binding for _n_; hasNode(n) will return false and
     * getTriple(n) will return null. This only removes *unique, single*
     * bindings.
     */
    public static void remove(Graph graph, Node node, Triple triple)
    {
        Set<Triple> triples = new HashSet<>();
        triplesToZap(graph, triples, node, rdfType, statement) ;
        triplesToZap(graph, triples, node, subject, triple.getSubject()) ;
        triplesToZap(graph, triples, node, predicate, triple.getPredicate()) ;
        triplesToZap(graph, triples, node, object, triple.getObject()) ;
        for ( Triple t : triples )
            graph.delete(t) ;
    }

    private static void triplesToZap(Graph graph, Collection<Triple> acc, Node s, Node p , Node o)
    {
        ExtendedIterator<Triple> iter = graph.find(s,p,o) ;
        while(iter.hasNext())
            acc.add(iter.next()) ;
    }
}
