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

import java.util.Iterator ;
import java.util.List ;

import org.apache.jena.atlas.logging.Log ;
import org.apache.jena.graph.Node ;
import org.apache.jena.sparql.algebra.op.OpProject ;
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.* ;
import org.apache.jena.sparql.engine.main.OpExecutor ;

/**
 * Execute a projection in the middle of an execution. This requires the outcome
 * of each stage of execution (substitution evaluation assumed) to be merged with
 * the input binding. Bindings should already be compatible, otherwise this code
 * should not be used.
 */
public class QueryIterProjectMerge extends QueryIterRepeatApply {
    private final OpProject  opProject ;
    private final OpExecutor engine ;

    public QueryIterProjectMerge(OpProject opProject, QueryIterator input, OpExecutor engine, ExecutionContext execCxt) {
        super(input, execCxt) ;
        this.opProject = opProject ;
        this.engine = engine ;
    }

    @Override
    protected QueryIterator nextStage(Binding binding) {
        QueryIterator qIter = engine.executeOp(opProject.getSubOp(),
                                               QueryIterSingleton.create(binding, getExecContext())) ;
        qIter = new QueryIterConvert(qIter,
                                     new ProjectEnsureBindingConverter(binding, opProject.getVars()),
                                     getExecContext()) ;
        return qIter ;
    }

    /** Ensure binding from outer binding are present when projecting */
    static class ProjectEnsureBindingConverter implements QueryIterConvert.Converter {

        private final Binding   outerBinding ;
        private final List<Var> projectionVars ;

        public ProjectEnsureBindingConverter(Binding outerBinding, List<Var> vars) {
            this.outerBinding = outerBinding ;
            this.projectionVars = vars ;
        }

        @Override
        public Binding convert(Binding bind) {
            return ensure(projectionVars, outerBinding, bind) ;
            // Effectively, this is:
            //bind = new BindingProject(projectionVars, bind) ;
            // return BindingUtils.merge(outerBinding, bind) ;
        }

        /** Merge two bindings, the outer and inner, projecting the inner with the give variables.
         *  This is what happens in substitutIon execution, with a inner select-project.
         */
        private static Binding ensure(List<Var> vars, Binding outer, Binding inner) {
            // A specialised BindingUtils.merge that does project as well.
            // Reduce small object churn.
            BindingBuilder b2 = Binding.builder(outer) ;
            Iterator<Var> vIter = (vars != null) ? vars.iterator() : inner.vars() ;
            // Add any variables from the RHS
            for ( ; vIter.hasNext() ; ) {
                Var v = vIter.next() ;
                Node n2 = inner.get(v) ;
                if ( n2 == null )
                    continue ;
                if ( ! b2.contains(v) )
                    b2.add(v, inner.get(v)) ;
                else {
                    // Checking!
                    Node n1 = outer.get(v) ;
                    if ( ! n1.equals(n2) )
                        Log.warn(ProjectEnsureBindingConverter.class,  "merge: Mismatch : "+n1+" != "+n2);
                }
            }
            return b2.build() ;
        }
    }
}
