/*
 * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import org.testng.annotations.Test;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

/**
 * @test
 * @run testng IteratorDefaults
 * @summary test extension methods on Iterator
 */
@Test
public class IteratorDefaults {

    private static interface Callback {
        void call(List<Integer> list);
    }

    // call the callback for each recursive subList
    private void trimmedSubList(final List<Integer> list, final Callback callback) {
        int size = list.size();
        if (size > 1) {
            // trim 1 element from both ends
            final List<Integer> subList = list.subList(1, size - 1);
            callback.call(subList);
            trimmedSubList(subList, callback);
        }
    }

    public void testRemoveUnsupported() {
        final Iterator iterator = new Iterator() {
            @Override
            public boolean hasNext() {
                return false;
            }
            @Override
            public Object next() {
                return null;
            }
        };

        try {
            iterator.remove();
            fail("expected UnsupportedOperationException from remove not thrown");
        } catch (UnsupportedOperationException ignore) {
        }
    }

    public void testRemoveOverride() {
        final IteratorWithRemove iterator = new IteratorWithRemove();
        iterator.remove();
        assertTrue(iterator.removed);
    }

    public void testForEach() throws Exception {
        final Integer[] data = new Integer[1000];
        for (int i=0; i < data.length; i++) {
            data[i] = i;
        }
        final List<Integer> source = Arrays.asList(data);

        final String[] iterableCollectionClasses = {
                "java.util.ArrayDeque",
                "java.util.ArrayList",
                "java.util.HashSet",
                "java.util.LinkedHashSet",
                "java.util.LinkedList",
                "java.util.PriorityQueue",
                "java.util.TreeSet",
                "java.util.Vector",
                "java.util.concurrent.ConcurrentLinkedDeque",
                "java.util.concurrent.ConcurrentLinkedQueue",
                "java.util.concurrent.ConcurrentSkipListSet",
                "java.util.concurrent.CopyOnWriteArrayList",
                "java.util.concurrent.CopyOnWriteArraySet",
                "java.util.concurrent.LinkedBlockingDeque",
                "java.util.concurrent.LinkedBlockingQueue",
                "java.util.concurrent.LinkedTransferQueue",
                "java.util.concurrent.PriorityBlockingQueue"
        };

        for (final String iterableClass : iterableCollectionClasses) {
            final Iterable<Integer> iterable =
                    (Iterable<Integer>) Class.forName(iterableClass).newInstance();
            ((Collection<Integer>) iterable).addAll(source);
            final Iterator<Integer> iterator = iterable.iterator();
            final List<Integer> target = new ArrayList<>(source.size());
            iterator.forEachRemaining(target::add);
            if ("java.util.HashSet".equals(iterableClass)) {
                target.sort((x, y) -> x - y);
                assertEquals(target, source);
            } else {
                assertEquals(target, source);
            }

            // verify that for an iterator that has been advanced via next(),
            // forEach starts from the current location, not zero
            final int OFFSET = 5;
            final List<Integer> reference2 = new ArrayList<>(source).subList(OFFSET, source.size());
            final List<Integer> removed2 = new ArrayList<>(OFFSET);
            final Iterator<Integer> iterator2 = iterable.iterator();
            for (int i=0; i < OFFSET; i++) {
                // advance the iterator by OFFSET, saving iterated elements
                removed2.add(iterator2.next());
            }
            final List<Integer> target2 = new ArrayList<>(reference2.size());
            iterator2.forEachRemaining(target2::add);
            if ("java.util.HashSet".equals(iterableClass)) {
                assertEquals(target2.size(), reference2.size());
                target2.addAll(removed2);
                target2.sort((x, y) -> x - y);
                assertEquals(target2, source);
                assertEquals(target2.subList(OFFSET, source.size()), reference2);
            } else {
                assertEquals(target2, reference2);
            }
        }
    }

    public void testForEachSubList() throws Exception {
        final Integer[] data = new Integer[100];
        for (int i = 0; i < data.length; i++) {
            data[i] = i;
        }
        final List<Integer> source = Arrays.asList(data);
        final String[] listClasses = {
                "java.util.ArrayList",
                "java.util.LinkedList",
                "java.util.Vector",
                "java.util.concurrent.CopyOnWriteArrayList"
        };
        for (final String listClass : listClasses) {
            final List<Integer> list =
                    (List<Integer>) Class.forName(listClass).newInstance();
            list.addAll(source);
            trimmedSubList(list, new Callback() {
                @Override
                public void call(final List<Integer> list) {
                    if (list.size() < 1) {
                        return;
                    }
                    final List<Integer> target = new ArrayList<>(list.size());
                    final ListIterator<Integer> iterator = list.listIterator();
                    assertTrue(iterator.hasNext());
                    assertFalse(iterator.hasPrevious());
                    assertEquals(iterator.nextIndex(), 0);
                    assertEquals(iterator.previousIndex(), -1);

                    iterator.forEachRemaining(target::add);
                    assertEquals(target, list);

                    assertFalse(iterator.hasNext());
                    assertTrue(iterator.hasPrevious());
                    assertEquals(iterator.nextIndex(), list.size());
                    assertEquals(iterator.previousIndex(), list.size() - 1);

                    try {
                        iterator.next();
                        fail(listClass + " iterator advanced beyond end");
                    } catch (NoSuchElementException ignore) {
                    }
                }
            });
        }
    }

    public void testOptimizedForEach() throws Exception {
        final Integer[] data = new Integer[1000 * 1000];
        for (int i=0; i < data.length; i++) {
            data[i] = i;
        }
        final List<Integer> source = Arrays.asList(data);

        final String[] listClasses = {
                "java.util.ArrayList",
                "java.util.LinkedList",
                "java.util.Vector",
                "java.util.concurrent.CopyOnWriteArrayList"
        };

        final int OFFSET = 3;
        final List<Integer> target = new ArrayList<>(source);
        for (final String listClass : listClasses) {
            final List<Integer> list =
                    (List<Integer>) Class.forName(listClass).newInstance();
            list.addAll(source);
            final ListIterator<Integer> iterator = list.listIterator();
            assertFalse(iterator.hasPrevious());
            for (int i=0; i < OFFSET; i++) {
                iterator.next();
            }
            assertTrue(iterator.hasNext());
            assertTrue(iterator.hasPrevious());
            assertEquals(iterator.nextIndex(), OFFSET);
            assertEquals(iterator.previousIndex(), OFFSET - 1);

            iterator.forEachRemaining(e -> {
                target.set(e, e + 1);
            });
            for (int i=OFFSET; i < data.length; i++) {
                assertEquals(target.get(i).intValue(), source.get(i)+1);
            }

            assertFalse(iterator.hasNext());
            assertTrue(iterator.hasPrevious());
            assertEquals(iterator.nextIndex(), data.length);
            assertEquals(iterator.previousIndex(), data.length - 1);

            // CopyOnWriteArrayList.listIterator().remove() is unsupported
            if (!"java.util.concurrent.CopyOnWriteArrayList".equals(listClass)) {
                for (int i = data.length - 1; i >= 0; i--) {
                    iterator.remove(); // must not throw
                    if (i > 0) {
                        iterator.previous();
                    }
                }
                assertTrue(list.isEmpty());
            }

            try {
                iterator.next();
                fail(listClass + " iterator advanced beyond end");
            } catch (NoSuchElementException ignore) {
            }
        }
    }

    @Test(enabled = false)
    public void compareForEachPerformance() throws Exception {
        final Integer[] data = new Integer[1000 * 100];
        for (int i=0; i < data.length; i++) {
            data[i] = i;
        }
        final List<Integer> source = Arrays.asList(data);

        final String[] iterableCollectionClasses = {
                "java.util.ArrayList", // warmup, results discarded
                "java.util.ArrayDeque",
                "java.util.ArrayList",
                "java.util.HashSet",
                "java.util.LinkedHashSet",
                "java.util.LinkedList",
                "java.util.PriorityQueue",
                "java.util.TreeSet",
                "java.util.Vector",
                "java.util.concurrent.ConcurrentLinkedDeque",
                "java.util.concurrent.ConcurrentLinkedQueue",
                "java.util.concurrent.ConcurrentSkipListSet",
                "java.util.concurrent.CopyOnWriteArrayList",
                "java.util.concurrent.CopyOnWriteArraySet",
                "java.util.concurrent.LinkedBlockingDeque",
                "java.util.concurrent.LinkedBlockingQueue",
                "java.util.concurrent.LinkedTransferQueue",
                "java.util.concurrent.PriorityBlockingQueue"
        };

        boolean warmup = true;
        final int ITERATIONS = 10;
        final Integer[] target = new Integer[source.size()];
        for (final String iterableClass : iterableCollectionClasses) {
            final Class<? extends Collection<Integer>> type =
                    (Class<? extends Collection<Integer>>) Class.forName(iterableClass);
            final Constructor<? extends Collection<Integer>> copyConstructor =
                    type.getConstructor(Collection.class);
            final Iterable<Integer> iterable = copyConstructor.newInstance(source);
            final Iterable<Integer> reference =
                    Collections.unmodifiableCollection((Collection<Integer>) iterable);

            for (int i=0; i < ITERATIONS; i++) {
                final Iterator<Integer> iterator = reference.iterator();
                final long forEachStart = System.nanoTime();
                iterator.forEachRemaining(x -> {target[x.intValue()] = x;});
                final long forEachEnd = System.nanoTime();

                final Iterator<Integer> iterator2 = reference.iterator();
                Integer x;
                final long iteratorStart = System.nanoTime();
                while (iterator2.hasNext()) {
                    x = iterator2.next();
                    target[x.intValue()] = x;
                }
                final long iteratorEnd = System.nanoTime();

                if (warmup) { continue; } // warmup, discard results
                final long forEachTime = forEachEnd - forEachStart;
                final long iteratorTime = iteratorEnd - iteratorStart;
                final long speedup = iteratorTime - forEachTime;
                System.out.print(iterableClass);
                System.out.print(" iterator: ");
                System.out.print(iteratorTime);
                System.out.print(", forEach: ");
                System.out.print(forEachTime);
                System.out.print(", speedup: ");
                System.out.print(speedup);
                System.out.print(" (");
                System.out.print((speedup * 100) / iteratorTime);
                System.out.print("%)\n");
            }
            if (warmup) { warmup = false; }
            System.out.println();
        }
    }

    @Test(enabled = false)
    public void compareSubListForEachPerformance() throws Exception {
        final Integer[] data = new Integer[1000 * 100];
        for (int i = 0; i < data.length; i++) {
            data[i] = i;
        }
        final List<Integer> source = Arrays.asList(data);

        final String[] listClasses = {
                "java.util.ArrayList", // warmup, results discarded
                "java.util.ArrayList",
                "java.util.LinkedList",
                "java.util.Vector",
                "java.util.concurrent.CopyOnWriteArrayList"
        };

        boolean warmup = true;
        final int ITERATIONS = 10;
        final Integer[] target = new Integer[source.size()];
        for (final String listClass : listClasses) {
            final Class<? extends List<Integer >> type =
                    (Class<? extends List<Integer>>) Class.forName(listClass);
            final Constructor<? extends List<Integer >> copyConstructor =
                    type.getConstructor(Collection.class);
            final List<Integer> iterable = copyConstructor.newInstance(source);
            final List<Integer> reference = Collections.unmodifiableList(iterable);

            for (int i = 0; i < ITERATIONS; i++) {
                final Iterator<Integer> iterator = reference.subList(42, reference.size() - 37).iterator();
                final long forEachStart = System.nanoTime();
                iterator.forEachRemaining(x -> {
                    target[x.intValue()] = x;
                });
                final long forEachEnd = System.nanoTime();

                final Iterator<Integer> iterator2 = reference.iterator();
                Integer x;
                final long iteratorStart = System.nanoTime();
                while (iterator2.hasNext()) {
                    x = iterator2.next();
                    target[x.intValue()] = x;
                }
                final long iteratorEnd = System.nanoTime();

                if (warmup) { continue; } // warmup, discard results
                final long forEachTime = forEachEnd - forEachStart;
                final long iteratorTime = iteratorEnd - iteratorStart;
                final long speedup = iteratorTime - forEachTime;
                System.out.print(listClass);
                System.out.print(" iterator: ");
                System.out.print(iteratorTime);
                System.out.print(", forEach: ");
                System.out.print(forEachTime);
                System.out.print(", speedup: ");
                System.out.print(speedup);
                System.out.print(" (");
                System.out.print((speedup * 100) / iteratorTime);
                System.out.print("%)\n");
            }
            if (warmup) { warmup = false; }
            System.out.println();
        }
    }

    static class IteratorWithRemove implements Iterator {

        public boolean removed;

        IteratorWithRemove() {
            removed = false;
        }

        @Override
        public boolean hasNext() {
            return false;
        }

        @Override
        public Object next() {
            return null;
        }

        @Override
        public void remove() {
            removed = true;
        }
    }
}
