/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

import java.util.AbstractList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;


/**
 * {@link List} that filters another {@link List} instance by a type.
 * 
 * <p>
 * This filtered list provides a live view of another list. It is
 * useful when you want to look for objects of a particular class
 * inside a heterogeneous list.
 * 
 * <p>
 * The filtered list is "live" in the sense that any modification done
 * to this list will be immediately propagated to the underlying list,
 * and any modification done on the underlying list will be immediately
 * visible through this list.
 * In fact, this filter doesn't actually store objects by itself,
 * but rather delegate all the work to the underlying list.
 * 
 * <p>
 * Note that the performance characteristic of this {@link List} is
 * closer to that of {@link java.util.LinkedList}. It's not suited
 * for random access by index (but iteration works nicely.)
 * 
 * @author
 *     Kohsuke Kawaguchi (kohsuke.kawaguchi@sun.com)
 */
public final class FilterList extends AbstractList {
    
    /**
     * Instances of this class will be visible through this list.
     */
    private final Class type;
    
    /**
     * The underlying {@link List} that this object works on.
     */
    private final List core;
    
    /**
     * We use an iterator from the core to detect modification to it.
     * The assumption here is that the iterator is fail-fast,
     * and it tells us so when the underlying list is modified
     * without our knowledge.
     * 
     * <p>
     * Note that the List interface doesn't mandate the fail-fast
     * behavior, so there's a risk of this doesn't work correctly.
     * But I think the risk is minimal in practice, given that
     * all the JDK classes and {@link AbstractList} implements
     * the semantics - KK.
     */
    private ListIterator itr;
    
    /**
     * Cached value of the {@link #size()}.
     * 
     * The above method could be called very frequently, and
     * it involves the whole re-scanning of the list, so we
     * cache the result and use it.
     */
    private int size = -1;
    
    /**
     * Creates a new filtered list that filters objects in
     * the spcified {@link List}.
     * 
     * @param core
     *      {@link List} to be filtered.
     * @param type
     *      Out of all the objects in the <tt>core</tt> list,
     *      only objects of this type will be visible through
     *      this list.
     */
    public FilterList( List core, Class type ) {
        this.core = core;
        this.type = type;
        itr = core.listIterator();
    }
    
    /**
     * Returns true if the core is modified.
     */
    private boolean isCoreModified() {
        try {
            itr.next();
            itr.previous();
            return false;
        } catch( NoSuchElementException e ) {
            ; // this is cool. 
            return false;
        } catch( ConcurrentModificationException e ) {
            itr = core.listIterator();
            return true;
        }
    }
    
    /**
     * Converts the index of this collection to that of the underlying list.
     * 
     * @param allowEnd
     *      If true, the index maps idx==this.size() to core.size().
     *      Otherwise it results in {@link IndexOutOfBoundsException}.
     */
    private int toCoreIndex(int idx,boolean allowEnd) {
        int i=0;
        final int index = idx;  // keep the original value
        
        for (Iterator itr = core.iterator(); itr.hasNext();i++) {
            if(type.isInstance(itr.next())) {
                if(idx==0)  return i;
                idx--;
            }
        }
        
        if(allowEnd && idx==0)
            return i;
        else
            throw new IndexOutOfBoundsException(Integer.toString(index));
    }
    
    public void add(int index, Object element) {
        core.add(toCoreIndex(index,true),element);
    }
    
    public Iterator iterator() {
        return listIterator();
    }
    
    public ListIterator listIterator(final int index) {
        return new ListIterator() {
            /**
             * Index of the current item in this list (not the core list).
             * This will be the one returned from the next {@link #next()}.
             * 
             * When the iterator hits the end of the list, this equals to
             * the size of this filtered list.
             * 
             * Note that just because the iterator is positioned at the
             * beginning of the list doesn't mean its thisIndex==0.
             */
            private int thisIndex = index;
            
            /**
             * Index of the current item in the core list (not this list).
             * 
             * When the iterator hits the end of the list, this equals to
             * the size of the core list.
             */
            private int coreIndex = toCoreIndex(index,true);

            /**
             * Index of element returned by most recent call to next or
             * previous.  Reset to -1 if this element is deleted by a call
             * to remove.
             */
            private int lastRet = -1;
            
            public int nextIndex() {
                return thisIndex;
            }
            public int previousIndex() {
                return thisIndex-1;
            }
            public void remove() {
                if (lastRet == -1)
                    throw new IllegalStateException();

                try {
                    core.remove(lastRet);
                    if (lastRet<coreIndex) {
                        coreIndex--;
                        thisIndex--;
                    }
                    lastRet = -1;
                } catch (IndexOutOfBoundsException e) {
                    throw new ConcurrentModificationException();
                }
            }
            public boolean hasNext() {
                return coreIndex<core.size();
            }
            public Object next() {
                int coreSize = core.size();
                
                if(coreIndex>=coreSize)
                    throw new NoSuchElementException();
                lastRet = coreIndex;
                
                // move forward
                thisIndex++;
                do {
                    coreIndex++;
                } while( !match(core.get(coreIndex)) && coreIndex<coreSize );
                    
                return core.get(lastRet);
            }
            public boolean hasPrevious() {
                // TODO: this could be made bit more efficient
                
                // look for the previous item
                int idx = coreIndex-1;
                while(idx>=0 && !match(core.get(idx)))
                    idx--;
                
                return idx>=0;
            }
            public Object previous() {
                // TODO: this could be made bit more efficient
                
                // look for the previous item
                int idx = coreIndex-1;
                while(idx>=0 && !match(core.get(idx)))
                    idx--;
                
                if(idx<0)
                    throw new NoSuchElementException();
                lastRet=idx;
                
                thisIndex--;
                coreIndex=idx;
                
                return core.get(lastRet);
            }
            public void add(Object o) {
                try {
                    core.add(coreIndex,o);
                    lastRet = -1;
                } catch (IndexOutOfBoundsException e) {
                    throw new ConcurrentModificationException();
                }
            }
            public void set(Object o) {
                if (lastRet == -1)
                    throw new IllegalStateException();

                try {
                    core.set(lastRet,o);
                } catch (IndexOutOfBoundsException e) {
                    throw new ConcurrentModificationException();
                }
            }
        };
    }
    
    public Object set(int index, Object element) {
        return core.set(toCoreIndex(index,false),element);
    }

    /**
     * Remove the object at the specified offset.
     *
     * @param index The offset of the object.
     * @return The Object which was removed.
     */
    public Object remove(int index) {
        return core.remove(toCoreIndex(index,false));
    }
    
    public Object get(int index) {
        return core.get(toCoreIndex(index,false));
    }

    public int size() {
        if(isCoreModified() || size==-1) {
            size=0;
            for (Iterator itr = core.iterator(); itr.hasNext();) {
                if(match(itr.next()))
                    size++;
            }
        }
        
        return size;
    }
    
    /**
     * Returns true if the object matches the filtering criteria.
     */
    private boolean match(Object o) {
        return type.isInstance(o);
    }
}
