/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.index.search.nested;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Weight;
import org.apache.lucene.search.join.BitSetProducer;
import org.apache.lucene.util.BitSet;

import java.io.IOException;
import java.util.Collection;
import java.util.Set;

/**
 * A special query that accepts a top level parent matching query, and returns the nested docs of the matching parent
 * doc as well. This is handy when deleting by query, don't use it for other purposes.
 */
public class IncludeNestedDocsQuery extends Query {

    private final BitSetProducer parentFilter;
    private final Query parentQuery;

    // If we are rewritten, this is the original childQuery we
    // were passed; we use this for .equals() and
    // .hashCode().  This makes rewritten query equal the
    // original, so that user does not have to .rewrite() their
    // query before searching:
    private final Query origParentQuery;


    public IncludeNestedDocsQuery(Query parentQuery, BitSetProducer parentFilter) {
        this.origParentQuery = parentQuery;
        this.parentQuery = parentQuery;
        this.parentFilter = parentFilter;
    }

    // For rewriting
    IncludeNestedDocsQuery(Query rewrite, Query originalQuery, IncludeNestedDocsQuery previousInstance) {
        this.origParentQuery = originalQuery;
        this.parentQuery = rewrite;
        this.parentFilter = previousInstance.parentFilter;
        //setBoost(previousInstance.getBoost());
    }

    // For cloning
    IncludeNestedDocsQuery(Query originalQuery, IncludeNestedDocsQuery previousInstance) {
        this.origParentQuery = originalQuery;
        this.parentQuery = originalQuery;
        this.parentFilter = previousInstance.parentFilter;
    }

    @Override
    public Weight createWeight(IndexSearcher searcher, boolean needsScores, float boost) throws IOException {
        return new IncludeNestedDocsWeight(this, parentQuery, parentQuery.createWeight(searcher, needsScores, boost), parentFilter);
    }

    static class IncludeNestedDocsWeight extends Weight {

        private final Query parentQuery;
        private final Weight parentWeight;
        private final BitSetProducer parentsFilter;

        IncludeNestedDocsWeight(Query query, Query parentQuery, Weight parentWeight, BitSetProducer parentsFilter) {
            super(query);
            this.parentQuery = parentQuery;
            this.parentWeight = parentWeight;
            this.parentsFilter = parentsFilter;
        }

        @Override
        public void extractTerms(Set<Term> terms) {
            parentWeight.extractTerms(terms);
        }

        /*
        @Override
        public void normalize(float norm, float topLevelBoost) {
            parentWeight.normalize(norm, topLevelBoost);
        }

        @Override
        public float getValueForNormalization() throws IOException {
            return parentWeight.getValueForNormalization(); // this query is never boosted so just delegate...
        }
        */
        
        @Override
        public Scorer scorer(LeafReaderContext context) throws IOException {
            final Scorer parentScorer = parentWeight.scorer(context);

            // no matches
            if (parentScorer == null) {
                return null;
            }

            BitSet parents = parentsFilter.getBitSet(context);
            if (parents == null) {
                // No matches
                return null;
            }

            int firstParentDoc = parentScorer.iterator().nextDoc();
            if (firstParentDoc == DocIdSetIterator.NO_MORE_DOCS) {
                // No matches
                return null;
            }
            return new IncludeNestedDocsScorer(this, parentScorer, parents, firstParentDoc);
        }

        @Override
        public Explanation explain(LeafReaderContext context, int doc) throws IOException {
            return null; //Query is used internally and not by users, so explain can be empty
        }

        @Override
        public boolean isCacheable(LeafReaderContext ctx) {
            return false;
        }
    }

    static class IncludeNestedDocsScorer extends Scorer {

        final Scorer parentScorer;
        final BitSet parentBits;

        int currentChildPointer = -1;
        int currentParentPointer = -1;
        int currentDoc = -1;

        IncludeNestedDocsScorer(Weight weight, Scorer parentScorer, BitSet parentBits, int currentParentPointer) {
            super(weight);
            this.parentScorer = parentScorer;
            this.parentBits = parentBits;
            this.currentParentPointer = currentParentPointer;
            if (currentParentPointer == 0) {
                currentChildPointer = 0;
            } else {
                this.currentChildPointer = this.parentBits.prevSetBit(currentParentPointer - 1);
                if (currentChildPointer == -1) {
                    // no previous set parent, we delete from doc 0
                    currentChildPointer = 0;
                } else {
                    currentChildPointer++; // we only care about children
                }
            }

            currentDoc = currentChildPointer;
        }

        @Override
        public Collection<ChildScorer> getChildren() throws IOException {
            return parentScorer.getChildren();
        }

        @Override
        public DocIdSetIterator iterator() {
            final DocIdSetIterator parentIterator = parentScorer.iterator();
            return new DocIdSetIterator() {
                @Override
                public int docID() {
                    return currentDoc;
                }

                @Override
                public int nextDoc() throws IOException {
                    if (currentParentPointer == NO_MORE_DOCS) {
                        return (currentDoc = NO_MORE_DOCS);
                    }

                    if (currentChildPointer == currentParentPointer) {
                        // we need to return the current parent as well, but prepare to return
                        // the next set of children
                        currentDoc = currentParentPointer;
                        currentParentPointer = parentIterator.nextDoc();
                        if (currentParentPointer != NO_MORE_DOCS) {
                            currentChildPointer = parentBits.prevSetBit(currentParentPointer - 1);
                            if (currentChildPointer == -1) {
                                // no previous set parent, just set the child to the current parent
                                currentChildPointer = currentParentPointer;
                            } else {
                                currentChildPointer++; // we only care about children
                            }
                        }
                    } else {
                        currentDoc = currentChildPointer++;
                    }

                    assert currentDoc != -1;
                    return currentDoc;
                }

                @Override
                public int advance(int target) throws IOException {
                    if (target == NO_MORE_DOCS) {
                        return (currentDoc = NO_MORE_DOCS);
                    }

                    if (target == 0) {
                        return nextDoc();
                    }

                    if (target < currentParentPointer) {
                        currentDoc = currentParentPointer = parentIterator.advance(target);
                        if (currentParentPointer == NO_MORE_DOCS) {
                            return (currentDoc = NO_MORE_DOCS);
                        }
                        if (currentParentPointer == 0) {
                            currentChildPointer = 0;
                        } else {
                            currentChildPointer = parentBits.prevSetBit(currentParentPointer - 1);
                            if (currentChildPointer == -1) {
                                // no previous set parent, just set the child to 0 to delete all up to the parent
                                currentChildPointer = 0;
                            } else {
                                currentChildPointer++; // we only care about children
                            }
                        }
                    } else {
                        currentDoc = currentChildPointer++;
                    }

                    return currentDoc;
                }

                @Override
                public long cost() {
                    return parentIterator.cost();
                }
            };
        }

        @Override
        public float score() throws IOException {
            return parentScorer.score();
        }

        /*
        @Override
        public int freq() throws IOException {
            return parentScorer.freq();
        }
        */
        
        @Override
        public int docID() {
            return currentDoc;
        }
    }

    @Override
    public Query rewrite(IndexReader reader) throws IOException {
        final Query parentRewrite = parentQuery.rewrite(reader);
        if (parentRewrite != parentQuery) {
            return new IncludeNestedDocsQuery(parentRewrite, parentQuery, this);
        } else {
            return this;
        }
    }

    @Override
    public String toString(String field) {
        return "IncludeNestedDocsQuery (" + parentQuery.toString() + ")";
    }

    @Override
    public boolean equals(Object _other) {
        if (_other instanceof IncludeNestedDocsQuery) {
            final IncludeNestedDocsQuery other = (IncludeNestedDocsQuery) _other;
            return origParentQuery.equals(other.origParentQuery) && parentFilter.equals(other.parentFilter);
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int hash = 1;
        hash = prime * hash + origParentQuery.hashCode();
        hash = prime * hash + parentFilter.hashCode();
        return hash;
    }

    /*
    @Override
    public Query clone() {
        Query clonedQuery = origParentQuery.clone();
        return new IncludeNestedDocsQuery(clonedQuery, this);
    }
    */
}
