/*
 * 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.core.query.lucene;

import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.Similarity;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.Weight;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import java.io.IOException;
import java.util.BitSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Implements a lucene <code>Query</code> which filters a sub query by checking
 * whether the nodes selected by that sub query are descendants or self of
 * nodes selected by a context query.
 */
@SuppressWarnings("serial")
class DescendantSelfAxisQuery extends Query implements JackrabbitQuery {

    /**
     * The logger instance for this class.
     */
    private static final Logger log = LoggerFactory.getLogger(DescendantSelfAxisQuery.class);

    /**
     * The context query
     */
    private final Query contextQuery;

    /**
     * The scorer of the context query
     */
    private Scorer contextScorer;

    /**
     * The sub query to filter
     */
    private final Query subQuery;

    /**
     * The minimal levels required between context and sub nodes for a sub node
     * to match.
     */
    private final int minLevels;

    /**
     * The scorer of the sub query to filter
     */
    private Scorer subScorer;

    /**
     * Creates a new <code>DescendantSelfAxisQuery</code> based on a
     * <code>context</code> and matches all descendants of the context nodes.
     * Whether the context nodes match as well is controlled by
     * <code>includeSelf</code>.
     *
     * @param context     the context for this query.
     * @param includeSelf if <code>true</code> this query acts like a
     *                    descendant-or-self axis. If <code>false</code> this
     *                    query acts like a descendant axis.
     */
    public DescendantSelfAxisQuery(Query context, boolean includeSelf) {
        this(context, new MatchAllDocsQuery(), includeSelf);
    }

    /**
     * Creates a new <code>DescendantSelfAxisQuery</code> based on a
     * <code>context</code> query and filtering the <code>sub</code> query.
     *
     * @param context the context for this query.
     * @param sub     the sub query.
     */
    public DescendantSelfAxisQuery(Query context, Query sub) {
        this(context, sub, true);
    }

    /**
     * Creates a new <code>DescendantSelfAxisQuery</code> based on a
     * <code>context</code> query and filtering the <code>sub</code> query.
     *
     * @param context     the context for this query.
     * @param sub         the sub query.
     * @param includeSelf if <code>true</code> this query acts like a
     *                    descendant-or-self axis. If <code>false</code> this query acts like
     *                    a descendant axis.
     */
    public DescendantSelfAxisQuery(Query context, Query sub, boolean includeSelf) {
        this(context, sub, includeSelf ? 0 : 1);
    }

    /**
     * Creates a new <code>DescendantSelfAxisQuery</code> based on a
     * <code>context</code> query and filtering the <code>sub</code> query.
     *
     * @param context   the context for this query.
     * @param sub       the sub query.
     * @param minLevels the minimal levels required between context and sub
     *                  nodes for a sub node to match.
     */
    public DescendantSelfAxisQuery(Query context, Query sub, int minLevels) {
        this.contextQuery = context;
        this.subQuery = sub;
        this.minLevels = minLevels;
    }

    /**
     * @return the context query of this <code>DescendantSelfAxisQuery</code>.
     */
    Query getContextQuery() {
        return contextQuery;
    }

    /**
     * @return <code>true</code> if the sub query of this <code>DescendantSelfAxisQuery</code>
     *         matches all nodes.
     */
    boolean subQueryMatchesAll() {
        return subQuery instanceof MatchAllDocsQuery;
    }

    /**
     * Returns the minimal levels required between context and sub nodes for a
     * sub node to match.
     * <ul>
     * <li><code>0</code>: a sub node <code>S</code> matches if it is a context
     * node or one of the ancestors of <code>S</code> is a context node.</li>
     * <li><code>1</code>: a sub node <code>S</code> matches if one of the
     * ancestors of <code>S</code> is a context node.</li>
     * <li><code>n</code>: a sub node <code>S</code> matches if
     * <code>S.getAncestor(S.getDepth() - n)</code> is a context node.</li>
     * </ul>
     *
     * @return the minimal levels required between context and sub nodes for a
     *         sub node to match.
     */
    int getMinLevels() {
        return minLevels;
    }

    /**
     * Creates a <code>Weight</code> instance for this query.
     *
     * @param searcher the <code>Searcher</code> instance to use.
     * @return a <code>DescendantSelfAxisWeight</code>.
     */
    public Weight createWeight(Searcher searcher) {
        return new DescendantSelfAxisWeight(searcher);
    }

    /**
     * {@inheritDoc}
     */
    public String toString(String field) {
        StringBuffer sb = new StringBuffer();
        sb.append("DescendantSelfAxisQuery(");
        sb.append(contextQuery);
        sb.append(", ");
        sb.append(subQuery);
        sb.append(", ");
        sb.append(minLevels);
        sb.append(")");
        return sb.toString();
    }

    /**
     * {@inheritDoc}
     */
    public void extractTerms(Set<Term> terms) {
        contextQuery.extractTerms(terms);
        subQuery.extractTerms(terms);
    }

    /**
     * {@inheritDoc}
     */
    public Query rewrite(IndexReader reader) throws IOException {
        Query cQuery = contextQuery.rewrite(reader);
        Query sQuery = subQuery.rewrite(reader);
        if (contextQuery instanceof DescendantSelfAxisQuery) {
            DescendantSelfAxisQuery dsaq = (DescendantSelfAxisQuery) contextQuery;
            if (dsaq.subQueryMatchesAll()) {
                return new DescendantSelfAxisQuery(dsaq.getContextQuery(),
                        sQuery, dsaq.getMinLevels() + getMinLevels()).rewrite(reader);
            }
        }
        if (cQuery == contextQuery && sQuery == subQuery) {
            return this;
        } else {
            return new DescendantSelfAxisQuery(cQuery, sQuery, minLevels);
        }
    }

    //------------------------< JackrabbitQuery >-------------------------------

    /**
     * {@inheritDoc}
     */
    public QueryHits execute(final JackrabbitIndexSearcher searcher,
                             final SessionImpl session,
                             final Sort sort) throws IOException {
        if (sort.getSort().length == 0 && subQueryMatchesAll()) {
            // maps path String to ScoreNode
            Map<String, ScoreNode> startingPoints = new TreeMap<String, ScoreNode>();
            QueryHits result = searcher.evaluate(getContextQuery());
            try {
                // minLevels 0 and 1 are handled with a series of
                // NodeTraversingQueryHits directly on result. For minLevels >= 2
                // intermediate ChildNodesQueryHits are required.
                for (int i = 2; i <= getMinLevels(); i++) {
                    result = new ChildNodesQueryHits(result, session);
                }

                ScoreNode sn;
                while ((sn = result.nextScoreNode()) != null) {
                    NodeId id = sn.getNodeId();
                    try {
                        Node node = session.getNodeById(id);
                        startingPoints.put(node.getPath(), sn);
                    } catch (ItemNotFoundException e) {
                        // JCR-3001 access denied to score node, will just skip it
                        log.warn("Access denied to node id {}.", id);
                    } catch (RepositoryException e) {
                        throw Util.createIOException(e);
                    }
                }
            } finally {
                result.close();
            }

            // prune overlapping starting points
            String previousPath = null;
            for (Iterator<String> it = startingPoints.keySet().iterator(); it.hasNext(); ) {
                String path = it.next();
                // if the previous path is a prefix of this path then the
                // current path is obsolete
                if (previousPath != null && path.startsWith(previousPath)) {
                    it.remove();
                } else {
                    previousPath = path;
                }
            }

            final Iterator<ScoreNode> scoreNodes = startingPoints.values().iterator();
            return new AbstractQueryHits() {

                private NodeTraversingQueryHits currentTraversal;

                {
                    fetchNextTraversal();
                }

                public void close() throws IOException {
                    if (currentTraversal != null) {
                        currentTraversal.close();
                    }
                }

                public ScoreNode nextScoreNode() throws IOException {
                    while (currentTraversal != null) {
                        ScoreNode sn = currentTraversal.nextScoreNode();
                        if (sn != null) {
                            return sn;
                        } else {
                            fetchNextTraversal();
                        }
                    }
                    // if we get here there are no more score nodes
                    return null;
                }

                private void fetchNextTraversal() throws IOException {
                    if (currentTraversal != null) {
                        currentTraversal.close();
                    }
                    currentTraversal = null;
                    // We only need one node, but because of the acls, we'll
                    // iterate until we find a good one
                    while (scoreNodes.hasNext()) {
                        ScoreNode sn = scoreNodes.next();
                        NodeId id = sn.getNodeId();
                        try {
                            Node node = session.getNodeById(id);
                            currentTraversal = new NodeTraversingQueryHits(
                                    node, getMinLevels() == 0);
                            break;
                        } catch (ItemNotFoundException e) {
                            // JCR-3001 node access denied, will just skip it
                            log.warn("Access denied to node id {}.", id);
                        } catch (RepositoryException e) {
                            throw Util.createIOException(e);
                        }
                    }
                }
            };
        } else {
            return null;
        }
    }

    //--------------------< DescendantSelfAxisWeight >--------------------------

    /**
     * The <code>Weight</code> implementation for this
     * <code>DescendantSelfAxisWeight</code>.
     */
    private class DescendantSelfAxisWeight extends Weight {

        /**
         * The searcher in use
         */
        private final Searcher searcher;

        /**
         * Creates a new <code>DescendantSelfAxisWeight</code> instance using
         * <code>searcher</code>.
         *
         * @param searcher a <code>Searcher</code> instance.
         */
        private DescendantSelfAxisWeight(Searcher searcher) {
            this.searcher = searcher;
        }

        //-----------------------------< Weight >-------------------------------

        /**
         * Returns this <code>DescendantSelfAxisQuery</code>.
         *
         * @return this <code>DescendantSelfAxisQuery</code>.
         */
        public Query getQuery() {
            return DescendantSelfAxisQuery.this;
        }

        /**
         * {@inheritDoc}
         */
        public float getValue() {
            return 1.0f;
        }

        /**
         * {@inheritDoc}
         */
        public float sumOfSquaredWeights() throws IOException {
            return 1.0f;
        }

        /**
         * {@inheritDoc}
         */
        public void normalize(float norm) {
        }

        /**
         * Creates a scorer for this <code>DescendantSelfAxisScorer</code>.
         *
         * @param reader a reader for accessing the index.
         * @return a <code>DescendantSelfAxisScorer</code>.
         * @throws IOException if an error occurs while reading from the index.
         */
        public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder,
                boolean topScorer) throws IOException {
            contextScorer = contextQuery.weight(searcher).scorer(reader, scoreDocsInOrder, topScorer);
            subScorer = subQuery.weight(searcher).scorer(reader, scoreDocsInOrder, topScorer);
            HierarchyResolver resolver = (HierarchyResolver) reader;
            return new DescendantSelfAxisScorer(searcher.getSimilarity(), reader, resolver);
        }

        /**
         * {@inheritDoc}
         */
        public Explanation explain(IndexReader reader, int doc) throws IOException {
            return new Explanation();
        }
    }

    //----------------------< DescendantSelfAxisScorer >---------------------------------
    /**
     * Implements a <code>Scorer</code> for this
     * <code>DescendantSelfAxisQuery</code>.
     */
    private class DescendantSelfAxisScorer extends Scorer {

        /**
         * The <code>HierarchyResolver</code> of the index.
         */
        private final HierarchyResolver hResolver;

        /**
         * BitSet storing the id's of selected documents
         */
        private final BitSet contextHits;

        /**
         * Set <code>true</code> once the context hits have been calculated.
         */
        private boolean contextHitsCalculated = false;

        /**
         * Remember document numbers of ancestors during validation
         */
        private int[] ancestorDocs = new int[2];

        /**
         * Reusable array that holds document numbers of parents.
         */
        private int[] pDocs = new int[1];

        /**
         * Reusable array that holds a single document number.
         */
        private final int[] singleDoc = new int[1];

        /**
         * The next document id to be returned
         */
        private int currentDoc = -1;

        /**
         * Creates a new <code>DescendantSelfAxisScorer</code>.
         *
         * @param similarity the <code>Similarity</code> instance to use.
         * @param reader     for index access.
         * @param hResolver  the hierarchy resolver of <code>reader</code>.
         */
        protected DescendantSelfAxisScorer(Similarity similarity,
                                           IndexReader reader,
                                           HierarchyResolver hResolver) {
            super(similarity);
            this.hResolver = hResolver;
            // todo reuse BitSets?
            this.contextHits = new BitSet(reader.maxDoc());
        }

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

            collectContextHits();
            currentDoc = subScorer.nextDoc();
            if (contextHits.isEmpty()) {
                currentDoc = NO_MORE_DOCS;
            }
            while (currentDoc != NO_MORE_DOCS) {
                if (isValid(currentDoc)) {
                    return currentDoc;
                }

                // try next
                currentDoc = subScorer.nextDoc();
            }
            return currentDoc;
        }

        @Override
        public int docID() {
            return currentDoc;
        }

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

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

            // optimize in the case of an advance to finish.
            // see https://issues.apache.org/jira/browse/JCR-3082
            if (target == NO_MORE_DOCS) {
                subScorer.advance(target);
                currentDoc = NO_MORE_DOCS;
                return currentDoc;
            }

            currentDoc = subScorer.nextDoc();
            if (currentDoc == NO_MORE_DOCS) {
                return NO_MORE_DOCS;
            } else {
                collectContextHits();
                return isValid(currentDoc) ? currentDoc : nextDoc();
            }
        }

        private void collectContextHits() throws IOException {
            if (!contextHitsCalculated) {
                long time = System.currentTimeMillis();
                contextScorer.score(new AbstractHitCollector() {
                    @Override
                    protected void collect(int doc, float score) {
                        contextHits.set(doc);
                    }
                }); // find all
                contextHitsCalculated = true;
                time = System.currentTimeMillis() - time;
                if (log.isDebugEnabled()) {
                    log.debug("Collected {} context hits in {} ms for {}",
                            new Object[]{
                                    contextHits.cardinality(),
                                    time,
                                    DescendantSelfAxisQuery.this
                            });
                }
            }
        }

        /**
         * Returns <code>true</code> if <code>doc</code> is a valid match from
         * the sub scorer against the context hits. The caller must ensure
         * that the context hits are calculated before this method is called!
         *
         * @param doc the document number.
         * @return <code>true</code> if <code>doc</code> is valid.
         * @throws IOException if an error occurs while reading from the index.
         */
        private boolean isValid(int doc) throws IOException {
            // check self if necessary
            if (minLevels == 0 && contextHits.get(doc)) {
                return true;
            }

            // check if doc is a descendant of one of the context nodes
            pDocs = hResolver.getParents(doc, pDocs);

            if (pDocs.length == 0) {
                return false;
            }

            int ancestorCount = 0;
            // can only remember one parent doc per level
            ancestorDocs[ancestorCount++] = pDocs[0];

            // traverse
            while (pDocs.length != 0) {
                boolean valid = false;
                for (int pDoc : pDocs) {
                    if (ancestorCount >= minLevels && contextHits.get(pDoc)) {
                        valid = true;
                        break;
                    }
                }
                if (valid) {
                    break;
                } else {
                    // load next level
                    pDocs = getParents(pDocs, singleDoc);
                    // resize array if needed
                    if (ancestorCount == ancestorDocs.length) {
                        // double the size of the new array
                        int[] copy = new int[ancestorDocs.length * 2];
                        System.arraycopy(ancestorDocs, 0, copy, 0, ancestorDocs.length);
                        ancestorDocs = copy;
                    }
                    if (pDocs.length != 0) {
                        // can only remember one parent doc per level
                        ancestorDocs[ancestorCount++] = pDocs[0];
                    }
                }
            }

            if (pDocs.length > 0) {
                // since current parentDocs are descendants of one of the context
                // docs we can promote all ancestorDocs to the context hits
                for (int i = 0; i < ancestorCount; i++) {
                    contextHits.set(ancestorDocs[i]);
                }
                return true;
            }
            return false;
        }

        /**
         * Returns the parent document numbers for the given <code>docs</code>.
         *
         * @param docs  the current document numbers, for which to get the
         *              parents.
         * @param pDocs an array of document numbers for reuse as return value.
         * @return the parent document number for the given <code>docs</code>.
         * @throws IOException if an error occurs while reading from the index.
         */
        private int[] getParents(int[] docs, int[] pDocs) throws IOException {
            // optimize single doc
            if (docs.length == 1) {
                return hResolver.getParents(docs[0], pDocs);
            } else {
                pDocs = new int[0];
                for (int doc : docs) {
                    int[] p = hResolver.getParents(doc, new int[0]);
                    int[] tmp = new int[p.length + pDocs.length];
                    System.arraycopy(pDocs, 0, tmp, 0, pDocs.length);
                    System.arraycopy(p, 0, tmp, pDocs.length, p.length);
                    pDocs = tmp;
                }
                return pDocs;
            }
        }
    }
}
