/*
 * JBoss, Home of Professional Open Source
 * Copyright 2014, Red Hat, Inc., and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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 org.jboss.weld.util.collections;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

import org.jboss.weld.util.Preconditions;

/**
 * Weld's immutable {@link List} implementations. Based on the size of the data, methods of this class may return {@link List} instances using various storage
 * strategies in order to achieve the best performance / memory consumption balance.
 *
 * @author Jozef Hartinger
 * @see WELD-1753
 *
 * @param <E> the element type
 */
public abstract class ImmutableList<E> extends AbstractImmutableList<E> {

    ImmutableList() {
    }

    /**
     * Creates an immutable list that consists of the given elements. This method should only be used in the varargs form. If there is a need to create
     * an immutable list of an array of elements, {@link #copyOf(Object[])} should be used instead.
     *
     * @param elements the given elements
     * @return an immutable list
     */
    @SafeVarargs
    public static <T> List<T> of(T... elements) {
        Preconditions.checkNotNull(elements);
        return ofInternal(elements);
    }

    /**
     * Creates an immutable list that consists of the elements in the given array. A copy of the given array is used which means
     * that any modifications to the given array will not affect the immutable list.
     *
     * @param elements the given array of elements
     * @return an immutable list
     */
    public static <T> List<T> copyOf(T[] elements) {
        Preconditions.checkNotNull(elements);
        return ofInternal(elements.clone());
    }

    /**
     * Creates an immutable list that consists of the elements in the given collection. If the given collection is already an immutable list,
     * it is returned directly.
     *
     * @param source the given collection
     * @return an immutable list
     */
    public static <T> List<T> copyOf(Collection<T> source) {
        Preconditions.checkNotNull(source);
        if (source instanceof ImmutableList<?>) {
            return (ImmutableList<T>) source;
        }
        if (source.isEmpty()) {
            return Collections.emptyList();
        }
        return ofInternal(source.toArray());
    }

    public static <T> List<T> copyOf(Iterable<T> source) {
        Preconditions.checkNotNull(source);
        if (source instanceof Collection<?>) {
            return copyOf((Collection<T>) source);
        }
        return ImmutableList.<T>builder().addAll(source).build();
    }

    @SuppressWarnings("unchecked")
    private static <T> List<T> ofInternal(Object[] elements) {
        switch (elements.length) {
            case 0:
                return Collections.emptyList();
            case 1:
                return new ImmutableTinyList.Singleton<T>((T) elements[0]);
            default:
                return new ImmutableArrayList<T>(checkElementsNotNull(elements));
        }
    }

    private static Object[] checkElementsNotNull(Object[] objects) {
        for (Object object : objects) {
            Preconditions.checkNotNull(object);
        }
        return objects;
    }

    /**
     * Returns a collector that can be used to collect items of a stream into an immutable list.
     *
     * @return collector
     */
    @SuppressWarnings("unchecked")
    public static <T> ImmutableListCollector<T> collector() {
        return (ImmutableListCollector<T>) ImmutableListCollector.INSTANCE;
    }

    /**
     * Creates a new empty builder for building immutable lists.
     *
     * @return a new empty builder
     */
    public static <T> Builder<T> builder() {
        return new BuilderImpl<T>();
    }

    public interface Builder<T> {

        Builder<T> add(T item);

        Builder<T> addAll(Iterable<? extends T> items);

        Builder<T> addAll(@SuppressWarnings("unchecked") T... items);

        List<T> build();
    }

    private static class BuilderImpl<T> implements Builder<T> {

        private static final int DEFAULT_CAPACITY = 10;

        private List<T> list;

        private BuilderImpl() {
            this.list = new ArrayList<>(DEFAULT_CAPACITY);
        }

        @Override
        public Builder<T> add(T item) {
            if (item == null) {
                throw new IllegalArgumentException("This collection does not support null values");
            }
            list.add(item);
            return this;
        }

        @Override
        public Builder<T> addAll(@SuppressWarnings("unchecked") T... items) {
            for (T item : items) {
                add(item);
            }
            return this;
        }

        @Override
        public Builder<T> addAll(Iterable<? extends T> items) {
            for (T item : items) {
                add(item);
            }
            return this;
        }

        BuilderImpl<T> addAll(BuilderImpl<T> items) {
            addAll(items.list);
            return this;
        }

        @Override
        public List<T> build() {
            return ImmutableList.ofInternal(list.toArray());
        }
    }

    private static class ImmutableListCollector<T> implements Collector<T, BuilderImpl<T>, List<T>> {

        private static final ImmutableListCollector<Object> INSTANCE = new ImmutableListCollector<>();
        private static final Set<Characteristics> CHARACTERISTICS = ImmutableSet.of();

        @Override
        public Supplier<BuilderImpl<T>> supplier() {
            return BuilderImpl::new;
        }

        @Override
        public BiConsumer<BuilderImpl<T>, T> accumulator() {
            return BuilderImpl::add;
        }

        @Override
        public BinaryOperator<BuilderImpl<T>> combiner() {
            return (builder1, builder2) -> builder1.addAll(builder2);
        }

        @Override
        public Function<BuilderImpl<T>, List<T>> finisher() {
            return BuilderImpl::build;
        }

        @Override
        public Set<java.util.stream.Collector.Characteristics> characteristics() {
            return CHARACTERISTICS;
        }
    }
}
