/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr.query.engine;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.jcr.query.qom.Constraint;
import org.modeshape.common.logging.Logger;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.JcrI18n;
import org.modeshape.jcr.query.NodeSequence;
import org.modeshape.jcr.query.QueryContext;
import org.modeshape.jcr.query.QueryEngine;
import org.modeshape.jcr.query.QueryResults.Columns;
import org.modeshape.jcr.query.model.QueryCommand;
import org.modeshape.jcr.query.model.SelectorName;
import org.modeshape.jcr.query.optimize.AddIndexes;
import org.modeshape.jcr.query.optimize.Optimizer;
import org.modeshape.jcr.query.optimize.OptimizerRule;
import org.modeshape.jcr.query.optimize.RuleBasedOptimizer;
import org.modeshape.jcr.query.plan.PlanHints;
import org.modeshape.jcr.query.plan.PlanNode;
import org.modeshape.jcr.query.plan.Planner;
import org.modeshape.jcr.spi.index.IndexCollector;
import org.modeshape.jcr.spi.index.IndexDefinition;
import org.modeshape.jcr.spi.index.IndexManager;
import org.modeshape.jcr.spi.index.provider.Index;
import org.modeshape.jcr.spi.index.provider.IndexPlanner;
import org.modeshape.jcr.spi.index.provider.IndexProvider;

/**
 * A {@link QueryEngine} implementation that uses available indexes to more quickly produce query results.
 * <p>
 * This query engine is capable of producing results for a query by scanning all nodes in the workspace(s) and filtering out any
 * node that does not satisfy the criteria. This scanning is not very efficient and can result in slow queries, especially when
 * the repository is quite large or when the number of nodes that satisfies the query's criteria is a small fraction of all
 * possible nodes in the workspace(s).
 * </p>
 * <p>
 * This engine can use indexes for certain properties so that it can more quickly identify the nodes that have property values
 * satisfying the constraints of a query. It is possible to index all properties, but this requires significant space and
 * evaluating all constraints using indexes may actually not be the most efficient and fastest approach. This query engine
 * attempts to use a minimum number of indexes that will most quickly produce the set of nodes closest to the actual expected
 * results.
 * </p>
 * <p>
 * Indexes are access from the repository's {@link IndexProvider} instances.
 */
public class IndexQueryEngine extends ScanningQueryEngine {

    /** We don't use the standard logging convention here; we want clients to easily configure logging for the indexes */
    protected static final Logger LOGGER = Logger.getLogger("org.modeshape.jcr.query");
    protected static final boolean TRACE = LOGGER.isTraceEnabled();
    protected static final boolean DEBUG = LOGGER.isDebugEnabled();

    public static class Builder extends ScanningQueryEngine.Builder {

        @Override
        public QueryEngine build() {
            // Determine the names of the query index providers ...
            Set<String> providerNames = indexManager().getProviderNames();
            if (providerNames.isEmpty()) {
                // There are no providers and thus we're not using any explicit indexes. There's no reason to use this engine ...
                return super.build();
            }

            // Get the planner for each provider ...
            Map<String, IndexPlanner> plannersByProviderName = new HashMap<>();
            for (String providerName : indexManager().getProviderNames()) {
                IndexProvider provider = indexManager().getProvider(providerName);
                if (provider != null) {
                    IndexPlanner planner = provider.getIndexPlanner();
                    if (planner == null) {
                        throw new IllegalStateException(JcrI18n.indexProviderMissingPlanner.text(providerName, repositoryName()));
                    }
                    plannersByProviderName.put(providerName, planner);
                }
            }

            // Create the optimizer for the query engine ...
            IndexPlanners indexPlanners = IndexPlanners.withProviders(plannersByProviderName);
            Optimizer optimizer = optimizer();
            if (optimizer == null) {
                // Create a single indexing rule that will use the index planner from all the providers ...
                final OptimizerRule indexingRule = AddIndexes.with(indexPlanners);
                // Create the optimizer that will add the providers' indexes using the same IndexingRule instance
                optimizer = new RuleBasedOptimizer() {
                    @Override
                    protected void populateIndexingRules( LinkedList<OptimizerRule> ruleStack,
                                                          PlanHints hints ) {
                        super.populateIndexingRules(ruleStack, hints);
                        ruleStack.addLast(indexingRule);
                    }
                };
            }
            // Finally create the query engine ...
            return new IndexQueryEngine(context(), repositoryName(), planner(), optimizer, indexManager());
        }

        @Override
        protected final Optimizer defaultOptimizer() {
            return null;
        }
    }

    /**
     * A {@link IndexPlanner} implementation that passes through only those indexes that are owned by the named provider.
     * 
     * @author Randall Hauch (rhauch@redhat.com)
     */
    protected static class ProviderIndexPlanner extends IndexPlanner {
        protected final String providerName;
        private final IndexPlanner providerPlanner;

        protected ProviderIndexPlanner( String providerName,
                                        IndexPlanner providerPlanner ) {
            this.providerName = providerName;
            this.providerPlanner = providerPlanner;
        }

        @Override
        public void applyIndexes( QueryContext context,
                                  SelectorName selector,
                                  List<Constraint> andedConstraints,
                                  Iterable<IndexDefinition> indexesOnSelector,
                                  IndexCollector indexes ) {
            if (indexesOnSelector == null) return;
            final Iterator<IndexDefinition> iterator = indexesOnSelector.iterator();
            if (!iterator.hasNext()) return;
            Iterable<IndexDefinition> filtered = new Iterable<IndexDefinition>() {
                @Override
                public Iterator<IndexDefinition> iterator() {
                    return new Iterator<IndexDefinition>() {
                        private IndexDefinition next;

                        @Override
                        public boolean hasNext() {
                            return moveToNext();
                        }

                        @Override
                        public IndexDefinition next() {
                            if (!moveToNext()) throw new NoSuchElementException();
                            return next;
                        }

                        @Override
                        public void remove() {
                            throw new UnsupportedOperationException();
                        }

                        protected boolean moveToNext() {
                            if (next != null) return true;
                            while (iterator.hasNext()) {
                                next = iterator.next();
                                if (providerName.equals(next.getProviderName())) return true;
                            }
                            next = null;
                            return false;
                        }
                    };
                }
            };
            providerPlanner.applyIndexes(context, selector, andedConstraints, filtered, indexes);
        }

        @Override
        public String toString() {
            return providerPlanner.toString();
        }
    }

    /**
     * Obtain a builder that can be used to create new query engine instances.
     * 
     * @return the builder; never null
     */
    public static Builder builder() {
        return new Builder();
    }

    private final IndexManager indexManager;

    protected IndexQueryEngine( ExecutionContext context,
                                String repositoryName,
                                Planner planner,
                                Optimizer optimizer,
                                IndexManager indexManager ) {
        super(context, repositoryName, planner, optimizer);
        this.indexManager = indexManager;
    }

    @Override
    protected NodeSequence createNodeSequenceForSource( QueryCommand originalQuery,
                                                        QueryContext context,
                                                        PlanNode sourceNode,
                                                        IndexPlan indexPlan,
                                                        Columns columns,
                                                        QuerySources sources ) {
        // First let the supertype try to determine a node sequence. This will find the native indexes ...
        NodeSequence sequence = super.createNodeSequenceForSource(originalQuery, context, sourceNode, indexPlan, columns, sources);
        if (sequence != null) return sequence;

        // Look up the index by name ...
        String providerName = indexPlan.getProviderName();
        IndexProvider provider = indexManager.getProvider(providerName);
        if (provider != null) {
            // Use the index to get a NodeSequence ...
            Index index = provider.getIndex(indexPlan.getName());
            if (index != null) {
                return sources.fromIndex(index, indexPlan.getConstraints(), indexPlan.getParameters(), 100);
            }
        }
        return null;
    }
}
