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

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream ;

import org.apache.jena.atlas.iterator.Iter;
import org.apache.jena.ext.com.google.common.collect.Iterators;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.Triple;
import org.apache.jena.sparql.core.Quad;
import org.apache.jena.sparql.core.Substitute;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.engine.binding.Binding;
import org.apache.jena.sparql.util.ModelUtils;

public class TemplateLib {
    // See also Substitute -- combine?
    // Or is this specifc enough to CONSTRUCT/Update template processing?

    // TODO We could eliminate some of the duplication in this class by writing
    // generic methods and adding a shared super-interface to Triple and Quad

    /**
     * Take a template, as a list of quad patterns, a default graph, and an
     * iterator of bindings, and produce an iterator of quads that results from
     * applying the template to the bindings.
     */
    public static Iterator<Quad> template(List<Quad> quads, final Node dftGraph, Iterator<Binding> bindings) {
        if ( quads == null || quads.isEmpty() )
            return null;
        quads = remapDefaultGraph(quads, dftGraph);
        return calcQuads(quads, bindings);
    }

    /**
     * Map quads to a different graph if they are in the default graph.
     */
    public static List<Quad> remapDefaultGraph(List<Quad> quads, final Node dftGraph) {
        // The fast path is "no change"
        if ( quads == null || quads.isEmpty() )
            return quads ;
        if ( dftGraph == null || Quad.isDefaultGraph(dftGraph) )
            return quads ;
        Stream<Quad> remappedStream = quads.stream().map(q->
            !q.isDefaultGraph() ? q : new Quad(dftGraph, q.getSubject(), q.getPredicate(), q.getObject())
        ) ;
        return remappedStream.collect(Collectors.toList());
    }

    /** Substitute into triple patterns */
    public static Iterator<Triple> calcTriples(final List<Triple> triples, Iterator<Binding> bindings) {
        return Iterators.concat(Iter.map(bindings, new Function<Binding, Iterator<Triple>>() {
            Map<Node, Node> bNodeMap = new HashMap<>();

            @Override
            public Iterator<Triple> apply(final Binding b) {
                // Iteration is a new mapping of bnodes.
                bNodeMap.clear();

                List<Triple> tripleList = new ArrayList<>(triples.size());
                for ( Triple triple : triples ) {
                    Triple q = subst(triple, b, bNodeMap);
                    if ( !q.isConcrete() || !ModelUtils.isValidAsStatement(q.getSubject(), q.getPredicate(), q.getObject()) ) {
                        // Log.warn(TemplateLib.class, "Unbound quad:
                        // "+FmtUtils.stringForQuad(quad)) ;
                        continue;
                    }
                    tripleList.add(q);
                }
                return tripleList.iterator();
            }
        }));
    }

    /** Substitute into quad patterns */
    public static Iterator<Quad> calcQuads(final List<Quad> quads, Iterator<Binding> bindings) {
        return Iterators.concat(Iter.map(bindings, new Function<Binding, Iterator<Quad>>() {
            Map<Node, Node> bNodeMap = new HashMap<>();

            @Override
            public Iterator<Quad> apply(final Binding b) {
                // Iteration is a new mapping of bnodes.
                bNodeMap.clear();

                List<Quad> quadList = new ArrayList<>(quads.size());
                for ( Quad quad : quads ) {
                    Quad q = subst(quad, b, bNodeMap);
                    if ( !q.isConcrete() ) {
                        // Log.warn(TemplateLib.class, "Unbound quad:"+FmtUtils.stringForQuad(quad)) ;
                        continue;
                    }
                    quadList.add(q);
                }
                return quadList.iterator();
            }
        }));
    }

    /** Substitute into a quad, with rewriting of bNodes */
    public static Quad subst(Quad quad, Binding b, Map<Node, Node> bNodeMap) {
        Node g = quad.getGraph();
        Node s = quad.getSubject();
        Node p = quad.getPredicate();
        Node o = quad.getObject();

        Node g1 = g;
        Node s1 = s;
        Node p1 = p;
        Node o1 = o;

        // replace blank nodes.
        if ( g1.isBlank() || Var.isBlankNodeVar(g1) )
            g1 = newBlank(g1, bNodeMap);

        if ( s1.isBlank() || Var.isBlankNodeVar(s1) )
            s1 = newBlank(s1, bNodeMap);

        if ( p1.isBlank() || Var.isBlankNodeVar(p1) )
            p1 = newBlank(p1, bNodeMap);

        if ( o1.isBlank() || Var.isBlankNodeVar(o1) )
            o1 = newBlank(o1, bNodeMap);

        Quad q = quad;
        if ( s1 != s || p1 != p || o1 != o || g1 != g )
            q = new Quad(g1, s1, p1, o1);

        Quad q2 = Substitute.substitute(q, b);
        return q2;
    }

    /** Substitute into a triple, with rewriting of bNodes */
    public static Triple subst(Triple triple, Binding b, Map<Node, Node> bNodeMap) {
        Node s = triple.getSubject();
        Node p = triple.getPredicate();
        Node o = triple.getObject();

        Node s1 = s;
        Node p1 = p;
        Node o1 = o;

        if ( s1.isBlank() || Var.isBlankNodeVar(s1) )
            s1 = newBlank(s1, bNodeMap);

        if ( p1.isBlank() || Var.isBlankNodeVar(p1) )
            p1 = newBlank(p1, bNodeMap);

        if ( o1.isBlank() || Var.isBlankNodeVar(o1) )
            o1 = newBlank(o1, bNodeMap);

        Triple t = triple;
        if ( s1 != s || p1 != p || o1 != o )
            t = Triple.create(s1, p1, o1);

        Triple t2 = Substitute.substitute(t, b);
        return t2;
    }

    /** generate a blank node consistently */
    private static Node newBlank(Node n, Map<Node, Node> bNodeMap) {
        if ( !bNodeMap.containsKey(n) )
            bNodeMap.put(n, NodeFactory.createBlankNode());
        return bNodeMap.get(n);
    }
}
