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

import static org.apache.jena.sparql.util.StringUtils.printAbbrev ;

import java.util.ArrayList ;
import java.util.List ;
import java.util.stream.Collectors;

import org.apache.jena.atlas.iterator.Iter ;
import org.apache.jena.atlas.lib.StrUtils ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.Triple ;
import org.apache.jena.sparql.ARQException ;
import org.apache.jena.sparql.core.BasicPattern ;
import org.apache.jena.sparql.core.Var ;
import org.apache.jena.sparql.sse.Item ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/** Machinery.
 * This code implements the connectiveness assumed by execution based on substitution (index joins).
 * i.e. if <code>{ ?x :p ?v . ?x :q ?w }</code> then <code>?x</code> is <code>TERM</code>
 * at the second triple. 
 */
public abstract class ReorderTransformationSubstitution implements ReorderTransformation
{
    static public final Logger log = LoggerFactory.getLogger(ReorderTransformationSubstitution.class) ;
    static private final boolean DEBUG = false ;  
    
    public ReorderTransformationSubstitution() {}
    
    @Override
    public BasicPattern reorder(BasicPattern pattern) {
        return reorderIndexes(pattern).reorder(pattern) ;
    }

    @Override
    public final ReorderProc reorderIndexes(BasicPattern pattern) {
        if ( pattern.size() <= 1 )
            return ReorderLib.identityProc() ;

        List<Triple> triples = pattern.getList() ;

        // Could merge into the conversion step to do the rewrite WRT a Binding.
        // Or done here as a second pass mutate of PatternTriples

        // Convert to a mutable form (that allows things like "TERM")
        List<PatternTriple> components = Iter.toList(Iter.map(triples.iterator(), PatternTriple::new)) ;

        // Allow subclasses to get in (e.g. static reordering).
        components = modifyComponents(components) ;
        ReorderProc proc = reorder(triples, components) ;
        return proc ;
    }

    protected List<PatternTriple> modifyComponents(List<PatternTriple> components) {
        return components ;
    }

    private String formatted(List<PatternTriple> components) {
        return components.stream().map(c->"(" + printAbbrev(c.toString()) + ")").collect(Collectors.joining(" "));
    }
    
    protected ReorderProc reorder(List<Triple> triples, List<PatternTriple> components) {
        int N = components.size() ;
        int numReorder = N ;        // Maybe choose 4, say, and copy over the rest.
        int indexes[] = new int[N] ;

        if ( DEBUG )
            log.debug("Reorder: " + formatted(components));

        int idx = 0 ;
        for ( ; idx < numReorder ; idx++ ) {
            int j = chooseNext(components) ;
            if ( j < 0 )
                break ;
            Triple triple = triples.get(j) ;
            indexes[idx] = j ;
            update(triple, components) ;
            components.set(j, null) ;
        }

        // Copy over the remainder (if any)
        for ( int i = 0 ; i < components.size() ; i++ ) {
            if ( components.get(i) != null )
                indexes[idx++] = i ;
        }
        if ( triples.size() != idx )
            throw new ARQException(String.format("Inconsistency: number of triples (%d) does not equal to number of indexes processed (%d)",
                                                 triples.size(), idx)) ;

        ReorderProc proc = new ReorderProcIndexes(indexes) ;

        return proc ;
    }    
    
    /** Return index of next pattern triple */
    protected int chooseNext(List<PatternTriple> pTriples) {
        if ( DEBUG ) {
            int i = -1 ;
            StringBuilder buff = new StringBuilder() ;
            for ( PatternTriple pt : pTriples ) {
                i++ ;
                if ( pt == null ) {
                    buff.append(String.format("    %d          : null\n", i)) ;
                    continue ;
                }
                double w2 = weight(pt) ;
                buff.append(String.format("    %d %8.0f : %s\n", i, w2, printAbbrev(pt))) ;
            }
            String x = StrUtils.noNewlineEnding(buff.toString()) ;
            log.debug(">> Input\n" + x) ;
        }

        int idx = processPTriples(pTriples, null) ;

        if ( DEBUG ) {
            String x = printAbbrev(pTriples.get(idx)) ;
            x = StrUtils.noNewlineEnding(x) ;
            log.debug("<< Output\n    " + x) ;
        }
        return idx ;
    }

    /** Return all the indexes of pattern triples of the least weight. */
    protected List<Integer> chooseAll(List<PatternTriple> pTriples) {
        List<Integer> results = new ArrayList<>(pTriples.size()) ;
        processPTriples(pTriples, results) ;
        return results ;
    }

    /**
     * Return the index of the first, least triple; optionally accumulate all indexes of
     * the same least weight
     */
    private int processPTriples(List<PatternTriple> pTriples, List<Integer> results) {
        double min = Double.MAX_VALUE ;     // Current minimum
        int N = pTriples.size() ;
        int idx = -1 ;

        for ( int i = 0 ; i < N ; i++ ) {
            PatternTriple pt = pTriples.get(i) ;
            if ( pt == null )
                continue ;
            double x = weight(pt) ;
            if ( x < 0 ) {
                // ****
                DefaultChoice choice = defaultChoice(pt) ;
                if ( choice != null ) {
                    switch (choice) {
                        case FIRST :
                            // Weight very low so it goes to front.
                            x = 0.01 ;
                            break ;
                        case LAST :
                            // Default action : 
                            break ;
                        case ZERO :
                            x = 0 ;
                            break ;
                        case NUMERIC :
                            x = defaultWeight(pt) ;
                            break ;
                    }
                }
                
                // Not found. No default action.
                // Make sure something is returned but otherwise ignore this pattern (goes
                // last).
                if ( idx == -1 ) {
                    idx = i ;
                    if ( results != null )
                        results.add(i) ;
                }
                // Do nothing. Does not update min so will be not be in results.
                continue ;
            }

            if ( x == min ) {
                if ( results != null )
                    results.add(i) ;
                continue ;
            }

            if ( x < min ) {
                min = x ;
                idx = i ;
                if ( results != null ) {
                    results.clear() ;
                    results.add(i) ;
                }
            }
        }
        return idx ;
    }

    /** Return the weight of the pattern, or -1 if no knowledge for it */
    protected abstract double weight(PatternTriple pt) ;
    
    protected enum DefaultChoice { ZERO, LAST, FIRST , NUMERIC ; }
    /** What to do if the {@link weight} comes back as "not found".
     * Choices are:
     *    ZERO      Assume the weight is zero (the rules were complete over the data so this is a pattern that will not match the data. 
     *    LAST      Place after all explicitly weighted triple patterns
     *    FIRST     Place before all explicitly weighted triple patterns
     *    NUMERIC   Use value returned by {@link defaultWeight}
     * The default, default choice is LAST.   
     */
    protected DefaultChoice defaultChoice(PatternTriple pt) { return null ; } // return DefaultChoice.LAST ; }
    
    protected double defaultWeight(PatternTriple pt) { return -1 ; }

    /** Update components to note any variables from triple */
    protected static void update(Triple triple, List<PatternTriple> components) {
        for ( PatternTriple elt : components )
            if ( elt != null )
                update(triple, elt) ;
    }

    private static void update(Triple triple, PatternTriple tuple) {
        update(triple.getSubject(), tuple) ;
        update(triple.getPredicate(), tuple) ;
        update(triple.getObject(), tuple) ;
    }

    private static void update(Node node, PatternTriple elt) {
        if ( Var.isVar(node) ) {
            if ( node.equals(elt.subject.getNode()) )
                elt.subject = PatternElements.TERM ;
            if ( node.equals(elt.predicate.getNode()) )
                elt.predicate = PatternElements.TERM ;
            if ( node.equals(elt.object.getNode()) )
                elt.object = PatternElements.TERM ;
        }
    }
    
    /** Update based on a variable/value (c.f. Substitute.substitute) */
    protected static void update(Var var, Node value, List<PatternTriple> components) {
        for ( PatternTriple elt : components )
            if ( elt != null )
                update(var, value, elt) ;
    }

    private static void update(Var var, Node value, PatternTriple elt) {
        if ( var.equals(elt.subject.getNode()) )
            elt.subject = Item.createNode(value) ;
        if ( var.equals(elt.predicate.getNode()) )
            elt.predicate = Item.createNode(value) ;
        if ( var.equals(elt.object.getNode()) )
            elt.object = Item.createNode(value) ;
    }
}
