/*
 * 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.jackrabbit.oak.plugins.index.aggregate;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.jackrabbit.oak.query.fulltext.FullTextAnd;
import org.apache.jackrabbit.oak.query.fulltext.FullTextContains;
import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
import org.apache.jackrabbit.oak.query.fulltext.FullTextOr;
import org.apache.jackrabbit.oak.query.fulltext.FullTextTerm;
import org.apache.jackrabbit.oak.query.fulltext.FullTextVisitor;
import org.apache.jackrabbit.oak.query.index.FilterImpl;
import org.apache.jackrabbit.oak.spi.query.Cursor;
import org.apache.jackrabbit.oak.spi.query.Cursors;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.collect.Lists;

import static org.apache.jackrabbit.oak.spi.query.QueryIndex.AdvanceFulltextQueryIndex;

/**
 * A virtual full-text that can aggregate nodes based on aggregate definitions.
 * Internally, it uses another full-text index.
 */
public class AggregateIndex implements AdvanceFulltextQueryIndex {

    private static final Logger LOG = LoggerFactory.getLogger(AggregateIndex.class);    

    private final AdvanceFulltextQueryIndex baseIndex;

    public AggregateIndex(AdvanceFulltextQueryIndex baseIndex) {
        this.baseIndex = baseIndex;
    }

    @Override
    public double getMinimumCost() {
        return baseIndex.getMinimumCost();
    }

    @Override
    public double getCost(Filter filter, NodeState rootState) {
        throw new UnsupportedOperationException("Not supported as implementing AdvancedQueryIndex");
    }

    @Override
    public Cursor query(Filter filter, NodeState rootState) {
        throw new UnsupportedOperationException("Not supported as implementing AdvancedQueryIndex");
    }

    @Override
    public List<IndexPlan> getPlans(Filter filter, List<OrderEntry> sortOrder, NodeState rootState) {
        if (baseIndex == null) {
            return Collections.emptyList();
        }
        FullTextExpression e = filter.getFullTextConstraint();
        if (getNodeAggregator() == null || e == null) {
            // no aggregation: path-though
            return baseIndex.getPlans(filter, sortOrder, rootState);
        }
        if (!hasCompositeExpression(e)) {
            // path-though, but without node type restriction
            return baseIndex.getPlans(newAggregationFilter(filter, null), sortOrder, rootState);
        }
        AggregateIndexPlan plan = new AggregateIndexPlan(filter);
        collectCombinedPlan(e, filter, sortOrder, rootState, plan, "");
        if (plan.containsPathWithoutPlan()) {
            // the full-text index didn't return a plan
            LOG.debug("Full-text index without plan: " + e);
            return Collections.emptyList();
        }
        return Collections.singletonList((IndexPlan) plan);
    }

    @Override
    public Cursor query(IndexPlan plan, NodeState rootState) {
        if (getNodeAggregator() == null) {
            // path-though
            return baseIndex.query(plan, rootState);
        } else if (!(plan instanceof AggregateIndexPlan)) {
            // remove the node type restriction
            plan.setFilter(newAggregationFilter(plan.getFilter(), null));
            return newAggregationCursor(plan, rootState);
        }
        Filter filter = plan.getFilter();
        AggregateIndexPlan aggPlan = (AggregateIndexPlan) plan;
        FullTextExpression constraint = filter.getFullTextConstraint();
        return flatten(constraint, aggPlan, filter, rootState, "");
    }

    private static boolean hasCompositeExpression(FullTextExpression ft) {
        if (ft == null) {
            return false;
        }
        final AtomicReference<Boolean> composite = new AtomicReference<Boolean>();
        composite.set(false);

        ft.accept(new FullTextVisitor() {
            
            @Override
            public boolean visit(FullTextContains contains) {
                return contains.getBase().accept(this);
            }

            @Override
            public boolean visit(FullTextTerm term) {
                return true;
            }

            @Override
            public boolean visit(FullTextAnd and) {
                composite.set(true);
                return true;
            }

            @Override
            public boolean visit(FullTextOr or) {
                composite.set(true);
                return true;
            }
        });
        return composite.get() && !hasNegativeContains(ft);
    }

    private static boolean hasNegativeContains(FullTextExpression ft) {
        if (ft == null) {
            return false;
        }
        final AtomicReference<Boolean> hasNegative = new AtomicReference<Boolean>();
        hasNegative.set(false);

        ft.accept(new FullTextVisitor.FullTextVisitorBase() {

            @Override
            public boolean visit(FullTextTerm term) {
                if (term.isNot()) {
                    hasNegative.set(true);
                }
                return true;
            }

        });
        return hasNegative.get();
    }

    private Cursor flatten(FullTextExpression constraint,
            final AggregateIndexPlan plan,
            final Filter filter, final NodeState state,
            final String path) {
        if (constraint == null) {
            return null;
        }
        final AtomicReference<Cursor> result = new AtomicReference<Cursor>();
        constraint.accept(new FullTextVisitor() {
            
            @Override
            public boolean visit(FullTextContains contains) {
                return contains.getBase().accept(this);
            }

            @Override
            public boolean visit(FullTextTerm term) {
                IndexPlan p = plan.getPlan(path);
                result.set(newAggregationCursor(p, state));
                return true;
            }

            @Override
            public boolean visit(FullTextAnd and) {
                Iterator<FullTextExpression> iterator = and.list.iterator();
                int index = 0;
                Cursor c = flatten(iterator.next(), plan, filter, state,
                        path + " and(" + index + ")");
                while (iterator.hasNext()) {
                    index++;
                    FullTextExpression input = iterator.next();
                    Cursor newC = flatten(input, plan, filter, state,
                            path + " and(" + index + ")");
                    c = Cursors.newIntersectionCursor(c, newC,
                            filter.getQueryEngineSettings());
                }
                result.set(c);
                return true;
            }

            @Override
            public boolean visit(FullTextOr or) {
                final int[] index = new int[1];
                List<Cursor> cursors = Lists.transform(or.list,
                        new Function<FullTextExpression, Cursor>() {
                            @Override
                            public Cursor apply(FullTextExpression input) {
                                return flatten(input, plan, filter, state,
                                        path + " or(" + index[0]++ + ")");
                            }
                        });
                result.set(Cursors.newConcatCursor(cursors,
                        filter.getQueryEngineSettings()));
                return true;
            }
        });
        return result.get();
    }

    private void collectCombinedPlan(FullTextExpression constraint,
            final Filter filter,
            final List<OrderEntry> sortOrder, 
            final NodeState state,
            final AggregateIndexPlan target,
            final String path) {
        
        constraint.accept(new FullTextVisitor() {
            
            @Override
            public boolean visit(FullTextContains contains) {
                return contains.getBase().accept(this);
            }

            @Override
            public boolean visit(FullTextTerm term) {
                List<IndexPlan> list = baseIndex.getPlans(
                        newAggregationFilter(filter, term), sortOrder, state);
                target.setPlan(path, list);
                return true;
            }

            @Override
            public boolean visit(FullTextAnd and) {
                int index = 0;
                for (FullTextExpression input : and.list) {
                    collectCombinedPlan(input, filter, sortOrder, state, target, 
                            path + " and(" + index + ")");
                    index++;
                }
                return true;
            }

            @Override
            public boolean visit(FullTextOr or) {
                int index = 0;
                for (FullTextExpression input : or.list) {
                    collectCombinedPlan(input, filter, sortOrder, state, target, 
                            path + " or(" + index + ")");
                    index++;
                }
                return true;
            }
        });
    }

    private Cursor newAggregationCursor(IndexPlan plan, NodeState rootState) {
        Cursor c = baseIndex.query(plan, rootState);
        // we possibly get results from a child,
        // so we need to wrap the cursor to do aggregation
        return new AggregationCursor(c,
                getNodeAggregator(), rootState);         
    }

    private static Filter newAggregationFilter(Filter filter, FullTextExpression exp) {
        FilterImpl f = new FilterImpl(filter);
        // disables node type checks for now
        f.setMatchesAllTypes(true);
        if (exp != null) {
            f.setFullTextConstraint(exp);
        }
        return f;
    }

    @Override
    public String getPlan(Filter filter, NodeState rootState) {
        throw new UnsupportedOperationException("Not supported as implementing AdvancedQueryIndex");
    }

    @Override
    public String getPlanDescription(IndexPlan plan, NodeState root) {
        if (baseIndex == null) {
            return "aggregate no-index";
        }
        StringBuilder buff = new StringBuilder("aggregate ");
        if (getNodeAggregator() == null) {
            // path-though
            buff.append(baseIndex.getPlanDescription(plan, root));
            return buff.toString();
        } else if (!(plan instanceof AggregateIndexPlan)) {
            buff.append(baseIndex.getPlanDescription(plan, root));
            return buff.toString();
        }
        AggregateIndexPlan aggPlan = (AggregateIndexPlan) plan;
        for (IndexPlan p : aggPlan.getPlans()) {
            if (p != null) {
                buff.append(baseIndex.getPlanDescription(p, root));
                buff.append(" ");
            }
        }
        return buff.toString();
    }

    @Override
    public String getIndexName() {
        if (baseIndex == null) {
            return "aggregate no-index";
        }
        return "aggregate " + baseIndex.getIndexName();
    }

    @Override
    public NodeAggregator getNodeAggregator() {
        return baseIndex.getNodeAggregator();
    }

}
