/*
 * 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;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

import javax.jcr.AccessDeniedException;
import javax.jcr.Item;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;

import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.state.ItemStateManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>LazyItemIterator</code> is an id-based iterator that instantiates
 * the <code>Item</code>s only when they are requested.
 * <p/>
 * <strong>Important:</strong> <code>Item</code>s that appear to be nonexistent
 * for some reason (e.g. because of insufficient access rights or because they
 * have been removed since the iterator has been retrieved) are silently
 * skipped. As a result the size of the iterator as reported by
 * {@link #getSize()} might appear to be shrinking while iterating over the
 * items.
 * todo should getSize() better always return -1?
 *
 * @see #getSize()
 */
public class LazyItemIterator implements NodeIterator, PropertyIterator {

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

    /**
     * The session context used to access the repository.
     */
    private final SessionContext sessionContext;

    /** the item manager that is used to lazily fetch the items */
    private final ItemManager itemMgr;

    /** the list of item ids */
    private final List<ItemId> idList;

    /** parent node id (when returning children nodes) or <code>null</code> */
    private final NodeId parentId;

    /** the position of the next item */
    private int pos;

    /** prefetched item to be returned on <code>{@link #next()}</code> */
    private Item next;

    /**
     * Creates a new <code>LazyItemIterator</code> instance.
     *
     * @param sessionContext session context
     * @param idList  list of item id's
     */
    public LazyItemIterator(SessionContext sessionContext, List< ? extends ItemId> idList) {
        this(sessionContext, idList, null);
    }

    /**
     * Creates a new <code>LazyItemIterator</code> instance, additionally taking
     * a parent id as parameter. This version should be invoked to strictly return
     * children nodes of a node.
     *
     * @param sessionContext session context
     * @param idList  list of item id's
     * @param parentId parent id.
     */
    public LazyItemIterator(SessionContext sessionContext, List< ? extends ItemId> idList, NodeId parentId) {
        this.sessionContext = sessionContext;
        this.itemMgr = sessionContext.getSessionImpl().getItemManager();
        this.idList = new ArrayList<ItemId>(idList);
        this.parentId = parentId;
        // prefetch first item
        pos = 0;
        prefetchNext();
    }

    /**
     * Prefetches next item.
     * <p/>
     * {@link #next} is set to the next available item in this iterator or to
     * <code>null</code> in case there are no more items.
     */
    private void prefetchNext() {
        // reset
        next = null;
        while (next == null && pos < idList.size()) {
            ItemId id = idList.get(pos);
            try {
                if (parentId != null) {
                    next = itemMgr.getNode((NodeId) id, parentId);
                } else {
                    next = itemMgr.getItem(id);
                }
            } catch (ItemNotFoundException e) {
                log.debug("ignoring nonexistent item " + id);
                // remove invalid id
                idList.remove(pos);

                // maybe fix the root cause
                if (parentId != null && sessionContext.getSessionImpl().autoFixCorruptions()) {
                    try {
                        // it might be an access right problem
                        // we need to check if the item doesn't exist in the ism
                        ItemStateManager ism = sessionContext.getItemStateManager();
                        if (!ism.hasItemState(id)) {
                            NodeImpl p = (NodeImpl) itemMgr.getItem(parentId);
                            p.removeChildNode((NodeId) id);
                            p.save();
                        }
                    } catch (RepositoryException e2) {
                        log.error("could not fix repository inconsistency", e);
                        // ignore
                    }
                }

                // try next
            } catch (AccessDeniedException e) {
                log.debug("ignoring nonexistent item " + id);
                // remove invalid id
                idList.remove(pos);
                // try next
            } catch (RepositoryException e) {
                log.error("failed to fetch item " + id + ", skipping...", e);
                // remove invalid id
                idList.remove(pos);
                // try next
            }
        }
    }

    //---------------------------------------------------------< NodeIterator >
    /**
     * {@inheritDoc}
     */
    public Node nextNode() {
        return (Node) next();
    }

    //-----------------------------------------------------< PropertyIterator >
    /**
     * {@inheritDoc}
     */
    public Property nextProperty() {
        return (Property) next();
    }

    //--------------------------------------------------------< RangeIterator >
    /**
     * {@inheritDoc}
     */
    public long getPosition() {
        return pos;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Note that the size of the iterator as reported by {@link #getSize()}
     * might appear to be shrinking while iterating because items that for
     * some reason cannot be retrieved through this iterator are silently
     * skipped, thus reducing the size of this iterator.
     *
     * todo better to always return -1?
     */
    public long getSize() {
        return idList.size();
    }

    /**
     * {@inheritDoc}
     */
    public void skip(long skipNum) {
        if (skipNum < 0) {
            throw new IllegalArgumentException("skipNum must not be negative");
        }
        if (skipNum == 0) {
            return;
        }
        if (next == null) {
            throw new NoSuchElementException();
        }

        // reset
        next = null;
        // skip the first (skipNum - 1) items without actually retrieving them
        while (--skipNum > 0) {
            pos++;
            if (pos >= idList.size()) {
                // skipped past last item
                throw new NoSuchElementException();
            }
            ItemId id = idList.get(pos);
            // eliminate invalid items from this iterator
            while (!itemMgr.itemExists(id)) {
                log.debug("ignoring nonexistent item " + id);
                // remove invalid id
                idList.remove(pos);
                if (pos >= idList.size()) {
                    // skipped past last item
                    throw new NoSuchElementException();
                }
                id = idList.get(pos);
            }
        }
        // prefetch final item (the one to be returned on next())
        pos++;
        prefetchNext();
    }

    //-------------------------------------------------------------< Iterator >
    /**
     * {@inheritDoc}
     */
    public boolean hasNext() {
        return next != null;
    }

    /**
     * {@inheritDoc}
     */
    public Object next() {
        if (next == null) {
            throw new NoSuchElementException();
        }
        Item item = next;
        pos++;
        prefetchNext();
        return item;
    }

    /**
     * {@inheritDoc}
     *
     * @throws UnsupportedOperationException always since not implemented
     */
    public void remove() {
        throw new UnsupportedOperationException("remove");
    }
}
