/*
 * 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.ItemManager;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.session.SessionOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.NodeIterator;

import java.util.NoSuchElementException;

/**
 * Implements a {@link javax.jcr.NodeIterator} returned by
 * {@link javax.jcr.query.QueryResult#getNodes()}.
 */
class NodeIteratorImpl implements NodeIterator {

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

    /**
     * Component context of the current session
     */
    protected final SessionContext sessionContext;

    /** The node ids of the nodes in the result set with their score value */
    protected final ScoreNodeIterator scoreNodes;

    /** The index for the default selector within {@link #scoreNodes} */
    private final int selectorIndex;

    /** Number of invalid nodes */
    protected int invalid = 0;

    /** Reference to the next node instance */
    private NodeImpl next;

    /**
     * Whether this iterator had been initialized.
     */
    private boolean initialized;

    /**
     * Creates a new <code>NodeIteratorImpl</code> instance.
     *
     * @param sessionContext component context of the current session
     * @param scoreNodes    iterator over score nodes.
     * @param selectorIndex the index for the default selector within
     *                      <code>scoreNodes</code>.
     */
    NodeIteratorImpl(
            SessionContext sessionContext, ScoreNodeIterator scoreNodes,
            int selectorIndex) {
        this.sessionContext = sessionContext;
        this.scoreNodes = scoreNodes;
        this.selectorIndex = selectorIndex;
    }

    /**
     * Returns the next <code>Node</code> in the result set.
     * @return the next <code>Node</code> in the result set.
     * @throws NoSuchElementException if iteration has no more
     *   <code>Node</code>s.
     */
    public Node nextNode() throws NoSuchElementException {
        initialize();
        if (next == null) {
            throw new NoSuchElementException();
        }
        NodeImpl n = next;
        fetchNext();
        return n;
    }

    /**
     * Returns the next <code>Node</code> in the result set.
     * @return the next <code>Node</code> in the result set.
     * @throws NoSuchElementException if iteration has no more
     *   <code>Node</code>s.
     */
    public Object next() throws NoSuchElementException {
        initialize();
        return nextNode();
    }

    /**
     * Skip a number of <code>Node</code>s in this iterator.
     * @param skipNum the non-negative number of <code>Node</code>s to skip
     * @throws NoSuchElementException
     *          if skipped past the last <code>Node</code> in this iterator.
     */
    public void skip(long skipNum) throws NoSuchElementException {
        initialize();
        if (skipNum > 0) {
            scoreNodes.skip(skipNum - 1);
            fetchNext();
        }
    }

    /**
     * Returns the number of nodes in this iterator.
     * </p>
     * Note: The number returned by this method may differ from the number
     * of nodes actually returned by calls to hasNext() / getNextNode()! This
     * is because this iterator works on a lazy instantiation basis and while
     * iterating over the nodes some of them might have been deleted in the
     * meantime. Those will not be returned by getNextNode(). As soon as an
     * invalid node is detected, the size of this iterator is adjusted.
     *
     * @return the number of node in this iterator.
     */
    public long getSize() {
        long size = scoreNodes.getSize();
        if (size == -1) {
            return size;
        } else {
            return size - invalid;
        }
    }

    /**
     * Returns the current position in this <code>NodeIterator</code>.
     * @return the current position in this <code>NodeIterator</code>.
     */
    public long getPosition() {
        initialize();
        long position = scoreNodes.getPosition() - invalid;
        // scoreNode.getPosition() is one ahead
        // if there is a prefetched node
        if (next != null) {
            position--;
        }
        return position;
    }

    /**
     * Returns <code>true</code> if there is another <code>Node</code>
     * available; <code>false</code> otherwise.
     * @return <code>true</code> if there is another <code>Node</code>
     *  available; <code>false</code> otherwise.
     */
    public boolean hasNext() {
        initialize();
        return next != null;
    }

    /**
     * @throws UnsupportedOperationException always.
     */
    public void remove() {
        throw new UnsupportedOperationException("remove");
    }

    /**
     * Clears {@link #next} and tries to fetch the next Node instance.
     * When this method returns {@link #next} refers to the next available
     * node instance in this iterator. If {@link #next} is null when this
     * method returns, then there are no more valid element in this iterator.
     */
    protected void fetchNext() {
        try {
            sessionContext.getSessionState().perform(new FetchNext());
        } catch (RepositoryException e) {
            log.warn("Failed to fetch next node", e);
        }
    }

    private class FetchNext implements SessionOperation<Object> {

        public Object perform(SessionContext context) {
            next = null; // reset

            ItemManager itemMgr = context.getItemManager();
            while (next == null && scoreNodes.hasNext()) {
                ScoreNode[] sn = scoreNodes.nextScoreNodes();
                try {
                    next = (NodeImpl) itemMgr.getItem(
                            sn[selectorIndex].getNodeId());
                } catch (RepositoryException e) {
                    log.warn("Failed to retrieve query result node "
                            + sn[selectorIndex].getNodeId(), e);
                    // try next
                    invalid++;
                }
            }

            return this;
        }

    }

    protected void initialize() {
        if (!initialized) {
            fetchNext();
            initialized = true;
        }
    }
}
