/*
 * 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.sparql.path;

import java.util.ArrayList ;
import java.util.Iterator ;
import java.util.List ;
import java.util.Objects;
import java.util.function.Predicate;

import org.apache.jena.atlas.iterator.Iter ;
import org.apache.jena.graph.Graph ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.Triple ;
import org.apache.jena.sparql.ARQInternalErrorException ;
import org.apache.jena.sparql.algebra.Op ;
import org.apache.jena.sparql.algebra.op.OpBGP ;
import org.apache.jena.sparql.algebra.op.OpPath ;
import org.apache.jena.sparql.algebra.op.OpSequence ;
import org.apache.jena.sparql.core.BasicPattern ;
import org.apache.jena.sparql.core.PathBlock ;
import org.apache.jena.sparql.core.TriplePath ;
import org.apache.jena.sparql.core.Var ;
import org.apache.jena.sparql.engine.ExecutionContext ;
import org.apache.jena.sparql.engine.QueryIterator ;
import org.apache.jena.sparql.engine.binding.Binding ;
import org.apache.jena.sparql.engine.binding.BindingFactory ;
import org.apache.jena.sparql.engine.iterator.QueryIterConcat ;
import org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper ;
import org.apache.jena.sparql.engine.iterator.QueryIterYieldN ;
import org.apache.jena.sparql.mgt.Explain ;
import org.apache.jena.sparql.path.eval.PathEval ;
import org.apache.jena.sparql.pfunction.PropertyFunctionFactory ;
import org.apache.jena.sparql.pfunction.PropertyFunctionRegistry ;
import org.apache.jena.sparql.util.Context;
import org.apache.jena.sparql.util.graph.GraphUtils ;

public class PathLib
{
    /** Convert any paths of exactly one predicate to a triple pattern */
    public static Op pathToTriples(PathBlock pattern) {
        BasicPattern bp = null;
        Op op = null;

        for ( TriplePath tp : pattern ) {
            if ( tp.isTriple() ) {
                if ( bp == null )
                    bp = new BasicPattern();
                bp.add(tp.asTriple());
                continue;
            }
            // Path form.
            op = flush(bp, op);
            bp = null;

            OpPath opPath2 = new OpPath(tp);
            op = OpSequence.create(op, opPath2);
            continue;
        }

        // End. Finish off any outstanding BGP.
        op = flush(bp, op);
        return op;
    }
    
    static private Op flush(BasicPattern bp, Op op) {
        if ( bp == null || bp.isEmpty() )
            return op;

        OpBGP opBGP = new OpBGP(bp);
        op = OpSequence.create(op, opBGP);
        return op;
    }

    /** Install a path as a property function in the global property function registry */
    public static void install(String uri, Path path)
    { install(uri, path, PropertyFunctionRegistry.get()) ; }

    /** Install a path as a property function in a given registry */
    public static void install(String uri, final Path path, PropertyFunctionRegistry registry) {
        PropertyFunctionFactory pathPropFuncFactory = (u) -> new PathPropertyFunction(path) ;
        registry.put(uri, pathPropFuncFactory) ;
    }

    public static QueryIterator execTriplePath(Binding binding, TriplePath triplePath, ExecutionContext execCxt) {
        if ( triplePath.isTriple() ) {
            // Fake it. This happens only for API constructed situations.
            Path path = new P_Link(triplePath.getPredicate());
            triplePath = new TriplePath(triplePath.getSubject(), path, triplePath.getObject());
        }
        
        return execTriplePath(binding, 
                              triplePath.getSubject(),
                              triplePath.getPath(),
                              triplePath.getObject(),
                              execCxt) ;
    }
    
    public static QueryIterator execTriplePath(Binding binding, Node s, Path path, Node o, ExecutionContext execCxt) {
        Explain.explain(s, path, o, execCxt.getContext()) ;
        s = Var.lookup(binding, s) ;
        o = Var.lookup(binding, o) ;
        Iterator<Node> iter = null ;
        Node endNode = null ;
        Graph graph = execCxt.getActiveGraph() ;

        // Both variables.
        if ( Var.isVar(s) && Var.isVar(o) ) {
            if ( s.equals(o) )
                return execUngroundedPathSameVar(binding, graph, Var.alloc(s), path, execCxt);
            else
                return execUngroundedPath(binding, graph, Var.alloc(s), path, Var.alloc(o), execCxt);
        }

        // Both constants.
        if ( !Var.isVar(s) && !Var.isVar(o) )
            return evalGroundedPath(binding, graph, s, path, o, execCxt);

        // One variable, one constant
        if ( Var.isVar(s) ) {
            // Var subject, concrete object - do backwards.
            iter = PathEval.evalReverse(graph, o, path, execCxt.getContext());
            endNode = s;
        } else {
            iter = PathEval.eval(graph, s, path, execCxt.getContext());
            endNode = o;
        }
        return evalGroundedOneEnd(binding, iter, endNode, execCxt);
    }
    
    private static QueryIterator evalGroundedOneEnd(Binding binding, Iterator<Node> iter, Node endNode, ExecutionContext execCxt) {
        List<Binding> results = new ArrayList<>() ;
        
        if (! Var.isVar(endNode))
            throw new ARQInternalErrorException("Non-variable endnode in _execTriplePath") ;
        
        Var var = Var.alloc(endNode) ;
        // Assign.
        for (; iter.hasNext();) {
            Node n = iter.next() ;
            results.add(BindingFactory.binding(binding, var, n)) ;
        }
        return QueryIterPlainWrapper.create(results.iterator(), execCxt) ;
    }

    // Subject and object are nodes.
    private static QueryIterator evalGroundedPath(Binding binding, 
                                                  Graph graph, Node subject, Path path, Node object,
                                                  ExecutionContext execCxt) {
        Iterator<Node> iter = PathEval.eval(graph, subject, path, execCxt.getContext()) ;
        // Now count the number of matches.
        
        int count = 0 ;
        for ( ; iter.hasNext() ; ) {
            Node n = iter.next() ;
            if ( n.sameValueAs(object) )
                count++ ;
        }
        
        return new QueryIterYieldN(count, binding, execCxt) ;
    }

    // Brute force evaluation of a TriplePath where neither subject nor object are bound 
    private static QueryIterator execUngroundedPath(Binding binding, Graph graph, Var sVar, Path path, Var oVar, ExecutionContext execCxt) {
        // Starting points.
        Iterator<Node> iter = determineUngroundedStartingSet(graph, path, execCxt) ;
        QueryIterConcat qIterCat = new QueryIterConcat(execCxt) ;
        
        for ( ; iter.hasNext() ; )
        {
            Node n = iter.next() ;
            Binding b2 = BindingFactory.binding(binding, sVar, n) ;
            Iterator<Node> pathIter = PathEval.eval(graph, n, path, execCxt.getContext()) ;
            QueryIterator qIter = evalGroundedOneEnd(b2, pathIter, oVar, execCxt) ;
            qIterCat.add(qIter) ;
        }
        return qIterCat ;
    }
    
    private static QueryIterator execUngroundedPathSameVar(Binding binding, Graph graph, Var var, Path path, ExecutionContext execCxt) {
        // Try each end, ungrounded.
        // Slightly more efficient would be to add a per-engine to do this.
        Iterator<Node> iter = determineUngroundedStartingSet(graph, path, execCxt) ;
        QueryIterConcat qIterCat = new QueryIterConcat(execCxt) ;
        
        for ( ; iter.hasNext() ; )
        {
            Node n = iter.next() ;
            Binding b2 = BindingFactory.binding(binding, var, n) ;
            int x = existsPath(graph, n, path, n, execCxt) ;
            if ( x > 0 )
            {
                QueryIterator qIter = new QueryIterYieldN(x, b2, execCxt) ;
                qIterCat.add(qIter) ;
            }
        }
        return qIterCat ; 
    }
    
    private static Iterator<Node> determineUngroundedStartingSet(Graph graph, Path path, ExecutionContext execCxt) {
        // Find a better set of seed values than "everything"
        //    (:p+) and (^:p)+
        //  :p* need everything because it is always the case that "<x> :p* <x>" 
        if ( path instanceof P_OneOrMore1 || path instanceof P_OneOrMoreN ) {
            Path subPath = ((P_Path1)path).getSubPath() ;
            if ( subPath instanceof P_Link ) {
                // :predicate+
                // If a property functions, 
                P_Link link = (P_Link)subPath ;
                if ( ! isPropertyFunction(link.getNode(), execCxt.getContext()) ) {
                    Iterator<Triple> sIter = graph.find(null, link.getNode(), null) ;
                    return Iter.iter(sIter).distinctAdjacent().map(Triple::getSubject).distinct() ;
                }
            } else {
                if ( subPath instanceof P_Inverse ) {
                    P_Inverse pInv = (P_Inverse)subPath ;
                    if ( pInv.getSubPath() instanceof P_Link ) {
                        //  (^:predicate)+
                        P_Link link = (P_Link)(pInv.getSubPath()) ;
                        if ( ! isPropertyFunction(link.getNode(), execCxt.getContext()) ) {
                            Iterator<Triple> sIter = graph.find(null, link.getNode(), null) ;
                            return Iter.iter(sIter).distinctAdjacent().map(Triple::getObject).distinct() ;
                        }
                    }
                }
            }
        }
        // No idea - everything.
        return GraphUtils.allNodes(graph) ;
    }
    
    private static boolean isPropertyFunction(Node node, Context context) {
        if ( ! node.isURI() )
            return false ;
        return PropertyFunctionRegistry.chooseRegistry(context).isRegistered(node.getURI());
    }

    private static int existsPath(Graph graph, Node subject, Path path, final Node object, ExecutionContext execCxt) {
        if ( ! subject.isConcrete() || !object.isConcrete() )
            throw new ARQInternalErrorException("Non concrete node for existsPath evaluation") ;
        Iterator<Node> iter = PathEval.eval(graph, subject, path, execCxt.getContext()) ;
        Predicate<Node> filter = node -> Objects.equals(node,  object); 
        // See if we got to the node we're interested in finishing at.
        iter = Iter.filter(iter, filter) ;
        long x = Iter.count(iter) ; 
        return (int)x ;
    }
}
