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

import java.util.ArrayList ;
import java.util.Collection ;
import java.util.Iterator ;
import java.util.List ;
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.query.ARQ ;
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.QueryIterRoot ;
import org.apache.jena.sparql.path.P_NegPropSet ;
import org.apache.jena.sparql.path.Path ;
import org.apache.jena.sparql.path.eval.PathEvaluator.FilterExclude ;
import org.apache.jena.sparql.pfunction.* ;
import org.apache.jena.sparql.util.Context ;
import org.apache.jena.sparql.util.graph.GraphContainerUtils ;
import org.apache.jena.sparql.util.graph.GraphList ;
import org.apache.jena.sparql.vocabulary.ListPFunction ;
import org.apache.jena.vocabulary.RDFS ;

abstract public class PathEngine
{
    private final boolean doingRDFSmember ;
    private final boolean doingListMember ;
    private final Graph   graph ;
    private final Context context ;
    private final PropertyFunctionRegistry registry ; 

    protected PathEngine(Graph graph, Context context) {
        boolean doingRDFSmember$ = false ;
        boolean doingListMember$ = false ;
        PropertyFunctionRegistry registry$ = null ;

        if ( context == null || context.isTrueOrUndef(ARQ.propertyFunctions) ) {
            registry$ = PropertyFunctionRegistry.chooseRegistry(context) ;
            if ( registry$ != null ) {
                doingRDFSmember$ = ( registry$.get(RDFSmember.getURI()) != null ) ;
                doingListMember$ = ( registry$.get(ListMember.getURI()) != null ) ;
            }
        }
        
        this.registry = registry$ ;
        this.doingRDFSmember = doingRDFSmember$ ;
        this.doingListMember = doingListMember$ ;
        this.graph = graph ;
        this.context = context ;
    }
    
    protected final Iter<Node> eval(Path path, Node node) {
        return PathEval.eval$(graph, node, path, this) ;
    }

    protected final void eval(Path path, Node node, Collection<Node> output) {
        PathEval.eval$(graph, node, path, this, output) ;
    }

    protected abstract void flipDirection() ;

    protected abstract boolean direction() ;

    // protected abstract void doOne(Path pathStep, Node node, Collection<Node> output) ;

    // Because for SP? or ?PO, no duplicates occur, so works for both strategies.
    protected final Iterator<Node> doOne(Node node, Node property) {
        Iterator<Node> iter2 = null ;
        if ( direction() ) {
            Iter<Triple> iter1 = Iter.iter(graphFind(node, property, Node.ANY)) ;
            iter2 = iter1.map(Triple::getObject) ;
        } else {
            Iter<Triple> iter1 = Iter.iter(graphFind(Node.ANY, property, node)) ;
            iter2 = iter1.map(Triple::getSubject) ;
        }

        return iter2 ;
    }

    protected abstract void doSeq(Path pathStepLeft, Path pathStepRight, Node node, Collection<Node> output) ;

    protected abstract void doAlt(Path pathStepLeft, Path pathStepRight, Node node, Collection<Node> output) ;

    // path*
    protected abstract void doZeroOrMore(Path pathStep, Node node, Collection<Node> output) ;

    // path+
    protected abstract void doOneOrMore(Path pathStep, Node node, Collection<Node> output) ;

    // path?
    protected abstract void doZeroOrOne(Path pathStep, Node node, Collection<Node> output) ;

    protected abstract void doNegatedPropertySet(P_NegPropSet pathNotOneOf, Node node, Collection<Node> output) ;

    // path{*} : possibly counting (if the engine supports that): default implementation
    protected void doZeroOrMoreN(Path pathStep, Node node, Collection<Node> output) {
        doZeroOrMore(pathStep, node, output) ;
    }

    // path{+} : possibly counting (if the engine supports that): default implementation
    protected void doOneOrMoreN(Path pathStep, Node node, Collection<Node> output) {
        doOneOrMore(pathStep, node, output) ;
    }

    protected abstract void doZero(Path path, Node node, Collection<Node> output) ;

    // {N,M} and variations

    protected abstract void doFixedLengthPath(Path pathStep, Node node, long fixedLength, Collection<Node> output) ;

    protected abstract void doMultiLengthPath(Path pathStep, Node node, long min, long max, Collection<Node> output) ;

    protected final void fill(Iterator<Node> iter, Collection<Node> output) {
        for (; iter.hasNext();)
            output.add(iter.next()) ;
    }

    protected static long dec(long x) {
        return (x <= 0) ? x : x - 1 ;
    }

    protected Iterator<Node> stepExcludeForwards(Node node, List<Node> excludedNodes) {
        Iter<Triple> iter1 = forwardLinks(node, excludedNodes) ;
        Iter<Node> r1 = iter1.map(Triple::getObject) ;
        return r1 ;
    }

    protected Iterator<Node> stepExcludeBackwards(Node node, List<Node> excludedNodes) {
        Iter<Triple> iter1 = backwardLinks(node, excludedNodes) ;
        Iter<Node> r1 = iter1.map(Triple::getSubject) ;
        return r1 ;
    }

    protected Iter<Triple> forwardLinks(Node x, Collection<Node> excludeProperties) {
        Iter<Triple> iter1 = Iter.iter(graphFind(x, Node.ANY, Node.ANY)) ;
        if ( excludeProperties != null )
            iter1 = iter1.filter(new FilterExclude(excludeProperties)) ;
        return iter1 ;
    }

    protected Iter<Triple> backwardLinks(Node x, Collection<Node> excludeProperties) {
        Iter<Triple> iter1 = Iter.iter(graphFind(Node.ANY, Node.ANY, x)) ;
        if ( excludeProperties != null )
            iter1 = iter1.filter(new FilterExclude(excludeProperties)) ;
        return iter1 ;
    }

    protected Iterator<Triple> graphFind(Node s, Node p, Node o) {
        return graphFind(graph, s, p, o, context) ;
    }

    private static Binding binding = BindingFactory.binding() ;
    private static Node RDFSmember = RDFS.Nodes.member ;
    private static Node ListMember = ListPFunction.nListMember ;
    
    private /*static*/ Iterator<Triple> graphFind(Graph graph, Node s, Node p, Node o, Context context) {
        // This is the only place this is called.
        // It means we can add property functions here.

        // Fast-path common cases.
        if ( doingRDFSmember && RDFSmember.equals(p) )
            return GraphContainerUtils.rdfsMember(graph, s, o) ;
        if ( doingListMember && ListMember.equals(p) )
            return GraphList.listMember(graph, s, o) ;
        // Potentially just allow the cases above.
        //return graph.find(s, p, o) ;
        return graphFind2(graph, s, p, o, context) ;
    }

    /* As general as possible property function inclusion */ 
    private Iterator<Triple> graphFind2(Graph graph, Node s, Node p, Node o, Context context) {
        // Not all property functions make sense in property path
        // For example, ones taking list arguments only make sense at
        // the start or finish, and then only in simple paths
        // (e.g. ?x .../propertyFunction ?z) 
        // which would have been packaged by the optimizer.
        if ( p != null && p.isURI() && registry != null ) {
            PropertyFunctionFactory f = registry.get(p.getURI()) ;
            if ( f != null )
                return graphFindWorker(graph, s, f, p, o, context) ;
        }

        return graph.find(s, p, o) ;
    }

    private Iterator<Triple> graphFindWorker(Graph graph, Node s, PropertyFunctionFactory f, Node p, Node o, Context context) {
        // Expensive?
        PropertyFunction pf = f.create(p.getURI()) ;
        PropFuncArg sv = arg(s, "S") ;
        PropFuncArg ov = arg(o, "O") ;
        QueryIterator r = QueryIterRoot.create(new ExecutionContext(context, graph, null, null)) ;
        QueryIterator qIter = pf.exec(r, sv, p, ov, new ExecutionContext(ARQ.getContext(), graph, null, null)) ;
        if ( ! qIter.hasNext() )
            return Iter.nullIterator() ;
        List<Triple> array = new ArrayList<>() ;
        for ( ; qIter.hasNext() ; ) {
            Binding b = qIter.next() ;
            Node st = value(sv, b) ;
            Node ot = value(ov, b) ;
            array.add(Triple.create(st, p, ot)) ;
        }
        // Materialise so the inner QueryIterators are used up. 
        return array.iterator() ; 
    }

    private static PropFuncArg arg(Node x, String name) {
        if ( x == null || Node.ANY.equals(x) ) 
        { return new PropFuncArg(Var.alloc(name)) ; }
        return new PropFuncArg(x) ;
    }

    private static Node value(PropFuncArg arg, Binding b) {
        Node x = arg.getArg() ;
        if ( !Var.isVar(x) )
            return x ;
        return b.get(Var.alloc(x)) ;
    }
}
