/*
 *  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.tools.ant.types.resources;

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

import org.apache.tools.ant.types.Resource;

/**
 * Resource collection which load underlying resource collection only on demand
 * with support for caching
 */
public class LazyResourceCollectionWrapper extends
        AbstractResourceCollectionWrapper {

    /** List of cached resources */
    private final List<Resource> cachedResources = new ArrayList<>();

    private Iterator<Resource> filteringIterator;

    private final Supplier<Iterator<Resource>> filteringIteratorSupplier =
        () -> new FilteringIterator(getResourceCollection().iterator());

    @Override
    protected Iterator<Resource> createIterator() {
        if (isCache()) {
            if (filteringIterator == null) {
                // no worry of thread safety here, see function's contract
                filteringIterator = filteringIteratorSupplier.get();
            }
            return new CachedIterator(filteringIterator);
        }
        return filteringIteratorSupplier.get();
    }

    @Override
    protected int getSize() {
        // to compute the size, just iterate: the iterator will take care of
        // caching
        final Iterator<Resource> it = createIterator();
        int size = 0;
        while (it.hasNext()) {
            it.next();
            size++;
        }
        return size;
    }

    /**
     * Specify if the resource should be filtered or not. This function should
     * be overridden in order to define the filtering algorithm
     *
     * @param r resource considered for filtration
     * @return whether the resource should be filtered or not
     */
    protected boolean filterResource(final Resource r) {
        return false;
    }

    private class FilteringIterator implements Iterator<Resource> {

        Resource next = null;

        boolean ended = false;

        protected final Iterator<Resource> it;

        FilteringIterator(final Iterator<Resource> it) {
            this.it = it;
        }

        @Override
        public boolean hasNext() {
            if (ended) {
                return false;
            }
            while (next == null) {
                if (!it.hasNext()) {
                    ended = true;
                    return false;
                }
                next = it.next();
                if (filterResource(next)) {
                    next = null;
                }
            }
            return true;
        }

        @Override
        public Resource next() {
            if (!hasNext()) {
                throw new UnsupportedOperationException();
            }
            final Resource r = next;
            next = null;
            return r;
        }

    }

    /**
     * Iterator that will put in the shared cache array list the selected
     * resources
     */
    private class CachedIterator implements Iterator<Resource> {

        int cursor = 0;

        private final Iterator<Resource> it;

        /**
         * Default constructor
         *
         * @param it
         *            the iterator which will provide the resources to put in
         *            cache
         */
        public CachedIterator(final Iterator<Resource> it) {
            this.it = it;
        }

        @Override
        public boolean hasNext() {
            synchronized (cachedResources) {
                // have we already cached the next entry ?
                if (cachedResources.size() > cursor) {
                    return true;
                }
                // does the wrapped iterator any more resource ?
                if (!it.hasNext()) {
                    return false;
                }
                // put in cache the next resource
                final Resource r = it.next();
                cachedResources.add(r);
            }
            return true;
        }

        @Override
        public Resource next() {
            // first check that we have some to deliver
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            synchronized (cachedResources) {
                // return the cached entry as hasNext should have put one for
                // this iterator
                return cachedResources.get(cursor++);
            }
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}
