UnmodifiableList.java

/*
 * Copyright (C) 2018 Stein Eldar Johnsen
 *
 * Licensed 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 net.morimekta.collect;

import net.morimekta.collect.collectors.UnmodifiableListCollector;

import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.RandomAccess;
import java.util.function.Predicate;
import java.util.stream.Collector;

import static java.lang.Math.max;
import static java.util.Objects.requireNonNull;

/**
 * Simple unmodifiable list implementation backed by an object array.
 *
 * @param <E> The list item type.
 */
public final class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E>, RandomAccess {
    // -------- Collectors --------

    /**
     * @param <E> The list item type.
     * @return Collector for making an unmodifiable list.
     */
    public static <E> Collector<E, ?, UnmodifiableList<E>> toList() {
        return new UnmodifiableListCollector<>();
    }

    // -------- Constructors --------

    /**
     * @param elements Elements to make unmodifiable list of.
     * @param <E>      The list item type.
     * @return The unmodifiable list.
     */
    @SafeVarargs
    public static <E> UnmodifiableList<E> asList(E... elements) {
        checkNotNull(elements);
        if (elements.length == 0) {
            return listOf();
        }
        return new UnmodifiableList<>(Arrays.copyOf(elements, elements.length));
    }

    /**
     * @param iterable Iterable to make unmodifiable list of.
     * @param <E>      The list item type.
     * @return The unmodifiable list.
     */
    @SuppressWarnings("unchecked")
    public static <E> UnmodifiableList<E> asList(Iterable<? extends E> iterable) {
        requireNonNull(iterable, "iterable == null");
        if (iterable instanceof UnmodifiableCollection) {
            return ((UnmodifiableCollection<E>) iterable).asList();
        } else if (iterable instanceof Collection) {
            return asList((Collection<E>) iterable);
        }
        return asList(iterable.iterator());
    }

    /**
     * @param iterator Iterator to make unmodifiable list of.
     * @param <E>      The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> asList(Iterator<? extends E> iterator) {
        requireNonNull(iterator, "iterator == null");
        if (!iterator.hasNext()) {
            return listOf();
        }
        return UnmodifiableList.<E>newBuilder().addAll(iterator).build();
    }

    /**
     * @param enumeration Enumeration to make unmodifiable list of.
     * @param <E>         The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> asList(Enumeration<? extends E> enumeration) {
        requireNonNull(enumeration, "enumeration == null");
        if (!enumeration.hasMoreElements()) {
            return listOf();
        }
        return UnmodifiableList.<E>newBuilder().addAll(enumeration).build();
    }

    /**
     * @param collection Collection to make unmodifiable list of.
     * @param <E>        The list item type.
     * @return The unmodifiable list.
     */
    @SuppressWarnings("unchecked")
    public static <E> UnmodifiableList<E> asList(Collection<? extends E> collection) {
        checkNotNull(collection);
        if (collection.size() == 0) {
            return listOf();
        }
        if (collection instanceof UnmodifiableCollection) {
            return ((UnmodifiableCollection<E>) collection).asList();
        }
        return construct((E[]) collection.toArray());
    }

    /**
     * @param <E> The list item type.
     * @return The unmodifiable empty list.
     */
    @SuppressWarnings("unchecked")
    public static <E> UnmodifiableList<E> listOf() {
        return (UnmodifiableList<E>) EMPTY;
    }

    /**
     * @param e   The list item.
     * @param <E> The list item type.
     * @return The unmodifiable singleton list.
     */
    public static <E> UnmodifiableList<E> listOf(E e) {
        return construct(e);
    }

    /**
     * @param e1  The first list item.
     * @param e2  The second list item.
     * @param <E> The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> listOf(E e1, E e2) {
        return construct(e1, e2);
    }

    /**
     * @param e1  The first list item.
     * @param e2  The second list item.
     * @param e3  The third list item.
     * @param <E> The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> listOf(E e1, E e2, E e3) {
        return construct(e1, e2, e3);
    }

    /**
     * @param e1  The first list item.
     * @param e2  The second list item.
     * @param e3  The third list item.
     * @param e4  The fourth list item.
     * @param <E> The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> listOf(E e1, E e2, E e3, E e4) {
        return construct(e1, e2, e3, e4);
    }

    /**
     * @param e1  The first list item.
     * @param e2  The second list item.
     * @param e3  The third list item.
     * @param e4  The fourth list item.
     * @param e5  The fifth list item.
     * @param <E> The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> listOf(E e1, E e2, E e3, E e4, E e5) {
        return construct(e1, e2, e3, e4, e5);
    }

    /**
     * @param e1  The first list item.
     * @param e2  The second list item.
     * @param e3  The third list item.
     * @param e4  The fourth list item.
     * @param e5  The fifth list item.
     * @param e6  The sixth list item.
     * @param <E> The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> listOf(E e1, E e2, E e3, E e4, E e5, E e6) {
        return construct(e1, e2, e3, e4, e5, e6);
    }

    /**
     * @param e1  The first list item.
     * @param e2  The second list item.
     * @param e3  The third list item.
     * @param e4  The fourth list item.
     * @param e5  The fifth list item.
     * @param e6  The sixth list item.
     * @param e7  The seventh list item.
     * @param <E> The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> listOf(E e1, E e2, E e3, E e4, E e5, E e6, E e7) {
        return construct(e1, e2, e3, e4, e5, e6, e7);
    }

    /**
     * @param e1  The first list item.
     * @param e2  The second list item.
     * @param e3  The third list item.
     * @param e4  The fourth list item.
     * @param e5  The fifth list item.
     * @param e6  The sixth list item.
     * @param e7  The seventh list item.
     * @param e8  The eighth list item.
     * @param <E> The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> listOf(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) {
        return construct(e1, e2, e3, e4, e5, e6, e7, e8);
    }

    /**
     * @param e1  The first list item.
     * @param e2  The second list item.
     * @param e3  The third list item.
     * @param e4  The fourth list item.
     * @param e5  The fifth list item.
     * @param e6  The sixth list item.
     * @param e7  The seventh list item.
     * @param e8  The eighth list item.
     * @param e9  The ninth list item.
     * @param <E> The list item type.
     * @return The unmodifiable list.
     */
    public static <E> UnmodifiableList<E> listOf(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9) {
        return construct(e1, e2, e3, e4, e5, e6, e7, e8, e9);
    }

    /**
     * @param e1   The first list item.
     * @param e2   The second list item.
     * @param e3   The third list item.
     * @param e4   The fourth list item.
     * @param e5   The fifth list item.
     * @param e6   The sixth list item.
     * @param e7   The seventh list item.
     * @param e8   The eighth list item.
     * @param e9   The ninth list item.
     * @param e10  The tenth list item.
     * @param more Most list items.
     * @param <E>  The list item type.
     * @return The unmodifiable list.
     */
    @SafeVarargs
    public static <E> UnmodifiableList<E> listOf(
            E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10, E... more) {
        Object[] array = new Object[10 + more.length];
        array[0] = e1;
        array[1] = e2;
        array[2] = e3;
        array[3] = e4;
        array[4] = e5;
        array[5] = e6;
        array[6] = e7;
        array[7] = e8;
        array[8] = e9;
        array[9] = e10;
        System.arraycopy(more, 0, array, 10, more.length);
        checkNotNull(array);
        return new UnmodifiableList<>(array);
    }

    /**
     * @param <E> The list item type.
     * @return Builder to make an unmodifiable list.
     */
    public static <E> Builder<E> newBuilder() {
        return new Builder<>(4);
    }

    /**
     * @param initialCapacity Initial capacity of the builder.
     * @param <E>             The list item type.
     * @return Builder to make an unmodifiable list.
     */
    public static <E> Builder<E> newBuilder(int initialCapacity) {
        return new Builder<>(max(1, initialCapacity));
    }

    // -------- Object --------

    @Override
    public int hashCode() {
        if (hashCode == null) {
            int hash = Objects.hash(getClass(), length);
            for (int i = 0; i < length; ++i) {
                hash ^= Objects.hash(i, array[offset + i]);
            }
            hashCode = hash;
        }
        return hashCode;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof List)) {
            return false;
        }
        if (length != ((List<?>) obj).size()) {
            return false;
        }
        if (obj instanceof UnmodifiableList) {
            UnmodifiableList<?> other = (UnmodifiableList<?>) obj;
            if (offset == other.offset && array == other.array) {
                return true;
            }
            for (int i = 0; i < length; ++i) {
                if (!array[offset + i].equals(other.array[other.offset + i])) {
                    return false;
                }
            }
            return true;
        } else if (obj instanceof RandomAccess) {
            List<?> other = (List<?>) obj;
            for (int i = 0; i < length; ++i) {
                if (!array[offset + i].equals(other.get(i))) {
                    return false;
                }
            }
            return true;
        } else {
            List<?> other = (List<?>) obj;
            int i = offset;
            for (Object o : other) {
                if (!array[i++].equals(o)) {
                    return false;
                }
            }
            return true;
        }
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("[");
        for (int i = offset; i < (offset + length); ++i) {
            if (i > offset) {
                builder.append(", ");
            }
            builder.append(array[i].toString());
        }
        return builder.append("]").toString();
    }

    // -------- List --------

    @Override
    public boolean addAll(int i, Collection<? extends E> collection) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    @SuppressWarnings("unchecked")
    public E get(int i) {
        return (E) array[offset + i];
    }

    @Override
    public E set(int i, E e) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    public void add(int i, E e) {
        throw new UnsupportedOperationException("Operation not allowed");

    }

    @Override
    public E remove(int i) {
        throw new UnsupportedOperationException("Operation not allowed");
    }

    @Override
    public int indexOf(Object item) {
        requireNonNull(item, "item == null");
        for (int i = offset; i < (offset + length); ++i) {
            if (item.equals(array[i])) {
                return i - offset;
            }
        }
        return -1;
    }

    @Override
    public int lastIndexOf(Object item) {
        requireNonNull(item, "item == null");
        for (int i = (offset + length) - 1; i >= offset; --i) {
            if (item.equals(array[i])) {
                return i - offset;
            }
        }
        return -1;
    }

    @Override
    public ListIterator<E> listIterator() {
        return new IteratorImpl(0);
    }

    @Override
    public ListIterator<E> listIterator(int i) {
        if (i < 0 || i > length) {
            throw new IndexOutOfBoundsException("ListIterator index " + i + " out of bounds for length " + array.length);
        }
        return new IteratorImpl(i);
    }

    @Override
    public List<E> subList(int start, int end) {
        if (start == 0 && end == length) {
            return this;
        }
        if (start < 0 || start > length) {
            throw new IndexOutOfBoundsException("Start index " + start + " out of bounds for length " + length);
        }
        if (end < start || end > length) {
            throw new IndexOutOfBoundsException("End index " + end + " out of bounds for length " + length + ", start " + start);
        }
        int len = end - start;
        if (len == 0) {
            return listOf();
        }
        return new UnmodifiableList<>(offset + start, len, array);
    }

    // -------- UnmodifiableCollection --------

    @Override
    public UnmodifiableList<E> asList() {
        return this;
    }

    /**
     * Get list with only the items matching the predicate.
     *
     * @param predicate The predicate to keep item in response.
     * @return The filtered list.
     */
    @Override
    public UnmodifiableList<E> filtered(Predicate<? super E> predicate) {
        return (UnmodifiableList<E>) super.filtered(predicate);
    }

    @Override
    public UnmodifiableList<E> reversed() {
        if (length < 2) {
            return this;
        }
        return new UnmodifiableList<>(reversedArray());
    }

    // -------- Builder --------

    /**
     * Builder to make an unmodifiable list.
     *
     * @param <E> The list item type.
     */
    public final static class Builder<E>
            implements ListBuilder<E>, UnmodifiableCollectionBuilder<E, UnmodifiableList<E>, Builder<E>> {
        private Object[] array;
        private int      size;

        Builder(int initialCapacity) {
            this.array = new Object[initialCapacity];
            this.size = 0;
        }

        @Override
        public Builder<E> add(E e) {
            requireNonNull(e, "null item");
            ensureCapacity(size + 1);
            array[size++] = e;
            return this;
        }

        @Override
        public Builder<E> addAll(Collection<? extends E> collection) {
            checkNotNull(collection);
            if (collection.isEmpty()) {
                return this;
            }
            ensureCapacity(size + collection.size());
            for (Object o : collection) {
                array[size++] = o;
            }
            return this;
        }

        @Override
        @SafeVarargs
        public final Builder<E> addAll(E... items) {
            checkNotNull(items);
            if (items.length == 0) {
                return this;
            }
            ensureCapacity(size + items.length);
            System.arraycopy(items, 0, array, size, items.length);
            size += items.length;
            return this;
        }

        @Override
        public UnmodifiableList<E> build() {
            if (size == 0) {
                return listOf();
            }
            return new UnmodifiableList<>(0, size, array);
        }

        private void ensureCapacity(int capacity) {
            if (array.length < capacity) {
                Object[] old = array;
                array = new Object[array.length * 2];
                System.arraycopy(old, 0, array, 0, size);
            }
        }
    }

    // -------- Private --------

    UnmodifiableList(Object[] array) {
        super(0, array.length, array);
    }

    UnmodifiableList(int offset, int length, Object[] array) {
        super(offset, length, array);
    }

    @Override
    UnmodifiableList<E> newInstance(Object[] array, int length) {
        if (length == 0) {
            return listOf();
        }
        return new UnmodifiableList<>(0, length, array);
    }

    @SafeVarargs
    static <E> UnmodifiableList<E> construct(E... array) {
        checkNotNull(array);
        return new UnmodifiableList<>(array);
    }

    static final UnmodifiableList<Object> EMPTY = new UnmodifiableList<>(new Object[0]);

}