/*
 * 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.commons.collections4.iterators;

import java.util.ListIterator;
import java.util.NoSuchElementException;

import org.apache.commons.collections4.Predicate;

/**
 * Decorates another {@link ListIterator} using a predicate to filter elements.
 * <p>
 * This iterator decorates the underlying iterator, only allowing through
 * those elements that match the specified {@link Predicate Predicate}.
 *
 * @param <E> the type of elements returned by this iterator.
 * @since 2.0
 */
public class FilterListIterator<E> implements ListIterator<E> {

    /** The iterator being used */
    private ListIterator<? extends E> iterator;

    /** The predicate being used */
    private Predicate<? super E> predicate;

    /**
     * The value of the next (matching) object, when
     * {@link #nextObjectSet} is true.
     */
    private E nextObject;

    /**
     * Whether or not the {@link #nextObject} has been set
     * (possibly to {@code null}).
     */
    private boolean nextObjectSet;

    /**
     * The value of the previous (matching) object, when
     * {@link #previousObjectSet} is true.
     */
    private E previousObject;

    /**
     * Whether or not the {@link #previousObject} has been set
     * (possibly to {@code null}).
     */
    private boolean previousObjectSet;

    /**
     * The index of the element that would be returned by {@link #next}.
     */
    private int nextIndex;

    /**
     * Constructs a new {@code FilterListIterator} that will not function
     * until {@link #setListIterator(ListIterator) setListIterator}
     * and {@link #setPredicate(Predicate) setPredicate} are invoked.
     */
    public FilterListIterator() {
    }

    /**
     * Constructs a new {@code FilterListIterator} that will not
     * function until {@link #setPredicate(Predicate) setPredicate} is invoked.
     *
     * @param iterator  the iterator to use
     */
    public FilterListIterator(final ListIterator<? extends E> iterator ) {
        this.iterator = iterator;
    }

    /**
     * Constructs a new {@code FilterListIterator}.
     *
     * @param iterator  the iterator to use
     * @param predicate  the predicate to use
     */
    public FilterListIterator(final ListIterator<? extends E> iterator, final Predicate<? super E> predicate) {
        this.iterator = iterator;
        this.predicate = predicate;
    }

    /**
     * Constructs a new {@code FilterListIterator} that will not function
     * until {@link #setListIterator(ListIterator) setListIterator} is invoked.
     *
     * @param predicate  the predicate to use.
     */
    public FilterListIterator(final Predicate<? super E> predicate) {
        this.predicate = predicate;
    }

    /**
     * Not supported.
     * @param o the element to insert
     */
    @Override
    public void add(final E o) {
        throw new UnsupportedOperationException("FilterListIterator.add(Object) is not supported.");
    }

    private void clearNextObject() {
        nextObject = null;
        nextObjectSet = false;
    }

    private void clearPreviousObject() {
        previousObject = null;
        previousObjectSet = false;
    }

    /**
     * Gets the iterator this iterator is using.
     *
     * @return the iterator.
     */
    public ListIterator<? extends E> getListIterator() {
        return iterator;
    }

    /**
     * Gets the predicate this iterator is using.
     *
     * @return the predicate.
     */
    public Predicate<? super E> getPredicate() {
        return predicate;
    }

    @Override
    public boolean hasNext() {
        return nextObjectSet || setNextObject();
    }

    @Override
    public boolean hasPrevious() {
        return previousObjectSet || setPreviousObject();
    }

    @Override
    public E next() {
        if (!nextObjectSet && !setNextObject()) {
            throw new NoSuchElementException();
        }
        nextIndex++;
        final E temp = nextObject;
        clearNextObject();
        return temp;
    }

    @Override
    public int nextIndex() {
        return nextIndex;
    }

    @Override
    public E previous() {
        if (!previousObjectSet && !setPreviousObject()) {
            throw new NoSuchElementException();
        }
        nextIndex--;
        final E temp = previousObject;
        clearPreviousObject();
        return temp;
    }

    @Override
    public int previousIndex() {
        return nextIndex - 1;
    }

    /** Not supported. */
    @Override
    public void remove() {
        throw new UnsupportedOperationException("FilterListIterator.remove() is not supported.");
    }

    /**
     * Not supported.
     * @param o the element with which to replace the last element returned by
     *          {@code next} or {@code previous}
     */
    @Override
    public void set(final E o) {
        throw new UnsupportedOperationException("FilterListIterator.set(Object) is not supported.");
    }

    /**
     * Sets the iterator for this iterator to use.
     * If iteration has started, this effectively resets the iterator.
     *
     * @param iterator  the iterator to use
     */
    public void setListIterator(final ListIterator<? extends E> iterator) {
        this.iterator = iterator;
    }

    private boolean setNextObject() {
        // if previousObjectSet,
        // then we've walked back one step in the
        // underlying list (due to a hasPrevious() call)
        // so skip ahead one matching object
        if (previousObjectSet) {
            clearPreviousObject();
            if (!setNextObject()) {
                return false;
            }
            clearNextObject();
        }

        if (iterator == null) {
            return false;
        }
        while (iterator.hasNext()) {
            final E object = iterator.next();
            if (predicate.evaluate(object)) {
                nextObject = object;
                nextObjectSet = true;
                return true;
            }
        }
        return false;
    }

    /**
     * Sets the predicate this the iterator to use.
     *
     * @param predicate  the transformer to use
     */
    public void setPredicate(final Predicate<? super E> predicate) {
        this.predicate = predicate;
    }

    private boolean setPreviousObject() {
        // if nextObjectSet,
        // then we've walked back one step in the
        // underlying list (due to a hasNext() call)
        // so skip ahead one matching object
        if (nextObjectSet) {
            clearNextObject();
            if (!setPreviousObject()) {
                return false;
            }
            clearPreviousObject();
        }

        if (iterator == null) {
            return false;
        }
        while (iterator.hasPrevious()) {
            final E object = iterator.previous();
            if (predicate.evaluate(object)) {
                previousObject = object;
                previousObjectSet = true;
                return true;
            }
        }
        return false;
    }

}
