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

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

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.RangeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.version.Version;
import javax.jcr.version.VersionIterator;

import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry;
import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager;
import org.apache.jackrabbit.spi.ItemId;
import org.apache.jackrabbit.spi.NodeId;
import org.apache.jackrabbit.spi.PropertyId;
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()} always returns -1.
 */
public class LazyItemIterator implements NodeIterator, PropertyIterator, VersionIterator {

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

    private static final long UNDEFINED_SIZE = -1;

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

    /** Iterator over HierarchyEntry elements */
    private final Iterator<? extends HierarchyEntry> iter;

    /**
     * The number of items.
     * Note, that the size may change over the time due to the lazy behaviour
     * of this iterator that may only upon iteration found out, that a
     * hierarchy entry has been invalidated or removed in the mean time.
     */
    private long size;
    /** 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 itemMgr item manager
     * @param hierarchyEntryIterator Iterator over HierarchyEntries
     */
    public LazyItemIterator(ItemManager itemMgr, Iterator<? extends HierarchyEntry> hierarchyEntryIterator) {
        this.itemMgr = itemMgr;
        this.iter = hierarchyEntryIterator;
        if (hierarchyEntryIterator instanceof RangeIterator) {
            size = ((RangeIterator) hierarchyEntryIterator).getSize();
        } else {
            size = UNDEFINED_SIZE;
        }
        pos = 0;
        // fetch first item
        next = prefetchNext();
    }

    /**
     * Creates a new <code>LazyItemIterator</code> instance.
     *
     * @param itemMgr
     * @param hierarchyMgr
     * @param itemIds
     */
    public LazyItemIterator(ItemManager itemMgr, HierarchyManager hierarchyMgr,
                            Iterator<? extends ItemId> itemIds)
        throws ItemNotFoundException, RepositoryException {
        this.itemMgr = itemMgr;
        List<HierarchyEntry> entries = new ArrayList<HierarchyEntry>();
        while (itemIds.hasNext()) {
            ItemId id = itemIds.next();
            HierarchyEntry entry;
            if (id.denotesNode()) {
                entry = hierarchyMgr.getNodeEntry((NodeId) id);
            } else {
                entry = hierarchyMgr.getPropertyEntry((PropertyId) id);
            }
            entries.add(entry);
        }
        iter = entries.iterator();
        size = entries.size();
        pos = 0;
        // fetch first item
        next = 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 Item prefetchNext() {
        Item nextItem = null;
        while (nextItem == null && iter.hasNext()) {
            HierarchyEntry entry = iter.next();
            try {
                nextItem = itemMgr.getItem(entry);
            } catch (RepositoryException e) {
                log.warn("Failed to fetch item " + entry.getName() + ", skipping.", e.getMessage());
                // reduce the size... and try the next one
                size--;
            }
        }
        return nextItem;
    }

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

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

    //----------------------------------------------------< VersionIterator >---
    /**
     * {@inheritDoc}
     * @see VersionIterator#nextVersion()
     */
    public Version nextVersion() {
        return (Version) next();
    }

    //------------------------------------------------------< RangeIterator >---
    /**
     * {@inheritDoc}
     * @see javax.jcr.RangeIterator#getPosition()
     */
    public long getPosition() {
        return pos;
    }

    /**
     * Returns the number of <code>Item</code>s in this iterator or -1 if the
     * size is unknown.
     * </p>
     * Note: The number returned by this method may differ from the number
     * of <code>Item</code>s actually returned by calls to hasNext() / getNextNode().
     * This is caused by the lazy instantiation behaviour of this iterator,
     * that may detect only upon iteration that an Item has been invalidated
     * or removed in the mean time. As soon as an invalid <code>Item</code> is
     * detected, the size of this iterator is adjusted.
     *
     * @return the number of <code>Item</code>s in this iterator.
     * @see RangeIterator#getSize()
     */
    public long getSize() {
        return size;
    }

    /**
     * {@inheritDoc}
     * @see RangeIterator#skip(long)
     */
    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();
        }

        // skip the first (skipNum - 1) items without actually retrieving them
        while (--skipNum > 0) {
            pos++;
            HierarchyEntry entry = iter.next();
            // check if item exists but don't build Item instance.
            while (!itemMgr.itemExists(entry)) {
                log.debug("Ignoring nonexistent item " + entry);
                entry = iter.next();
            }
        }
        // fetch final item (the one to be returned on next())
        pos++;
        next = prefetchNext();
    }

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

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

    /**
     * {@inheritDoc}
     * @see Iterator#remove()
     *
     * @throws UnsupportedOperationException always since removal is not implemented.
     */
    public void remove() {
        throw new UnsupportedOperationException("remove");
    }
}
