/*
 * 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.calcite.util;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

/**
 * An immutable list that may contain null values.
 *
 * <p>If the list cannot contain null values, use
 * {@link com.google.common.collect.ImmutableList}.</p>
 *
 * @param <E> Element type
 */
public class ImmutableNullableList<E> extends AbstractList<E> {
  private static final List SINGLETON_NULL = Collections.singletonList(null);

  private final E[] elements;

  // private - does not copy array
  private ImmutableNullableList(E[] elements) {
    this.elements = elements;
  }

  /**
   * Returns an immutable list containing the given elements, in order.
   *
   * <p>Behavior as
   * {@link com.google.common.collect.ImmutableList#copyOf(java.util.Collection)}
   * except that this list allows nulls.</p>
   */
  public static <E> List<E> copyOf(Collection<? extends E> elements) {
    if (elements instanceof ImmutableNullableList
        || elements instanceof ImmutableList
        || elements == SINGLETON_NULL) {
      //noinspection unchecked
      return (List<E>) elements;
    }
    // If there are no nulls, ImmutableList is better.
    for (E object : elements) {
      if (object == null) {
        final Object[] objects = elements.toArray();
        //noinspection unchecked
        return new ImmutableNullableList<E>((E[]) objects);
      }
    }
    return ImmutableList.copyOf(elements);
  }

  /**
   * Returns an immutable list containing the given elements, in order.
   *
   * <p>Behavior as
   * {@link com.google.common.collect.ImmutableList#copyOf(Iterable)}
   * except that this list allows nulls.
   */
  public static <E> List<E> copyOf(Iterable<? extends E> elements) {
    if (elements instanceof ImmutableNullableList
        || elements instanceof ImmutableList
        || elements == SINGLETON_NULL) {
      //noinspection unchecked
      return (List<E>) elements;
    }
    if (elements instanceof Collection) {
      //noinspection unchecked
      return copyOf((Collection) elements);
    }
    // If there are no nulls, ImmutableList is better.
    final List<E> list = new ArrayList<>();
    Iterables.addAll(list, elements);
    if (list.contains(null)) {
      return ImmutableNullableList.copyOf(list);
    }
    return ImmutableList.copyOf(elements);
  }

  /**
   * Returns an immutable list containing the given elements, in order.
   *
   * <p>Behavior as
   * {@link com.google.common.collect.ImmutableList#copyOf(Object[])}
   * except that this list allows nulls.</p>
   */
  public static <E> List<E> copyOf(E[] elements) {
    // Check for nulls.
    for (E object : elements) {
      if (object == null) {
        //noinspection unchecked
        return new ImmutableNullableList<E>(elements.clone());
      }
    }
    // There are no nulls. ImmutableList is better.
    return ImmutableList.copyOf(elements);
  }

  /** Creates an immutable list of 1 element. */
  public static <E> List<E> of(E e1) {
    //noinspection unchecked
    return e1 == null ? (List<E>) SINGLETON_NULL : ImmutableList.of(e1);
  }

  /** Creates an immutable list of 2 elements. */
  public static <E> List<E> of(E e1, E e2) {
    // Only we can see the varargs array. Therefore the list is immutable.
    //noinspection unchecked
    return UnmodifiableArrayList.of(e1, e2);
  }

  /** Creates an immutable list of 3 elements. */
  public static <E> List<E> of(E e1, E e2, E e3) {
    // Only we can see the varargs array. Therefore the list is immutable.
    //noinspection unchecked
    return UnmodifiableArrayList.of(e1, e2, e3);
  }

  /** Creates an immutable list of 4 elements. */
  public static <E> List<E> of(E e1, E e2, E e3, E e4) {
    // Only we can see the varargs array. Therefore the list is immutable.
    //noinspection unchecked
    return UnmodifiableArrayList.of(e1, e2, e3, e4);
  }

  /** Creates an immutable list of 5 elements. */
  public static <E> List<E> of(E e1, E e2, E e3, E e4, E e5) {
    // Only we can see the varargs array. Therefore the list is immutable.
    //noinspection unchecked
    return UnmodifiableArrayList.of(e1, e2, e3, e4, e5);
  }

  /** Creates an immutable list of 6 elements. */
  public static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6) {
    // Only we can see the varargs array. Therefore the list is immutable.
    //noinspection unchecked
    return UnmodifiableArrayList.of(e1, e2, e3, e4, e5, e6);
  }

  /** Creates an immutable list of 7 elements. */
  public static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) {
    // Only we can see the varargs array. Therefore the list is immutable.
    //noinspection unchecked
    return UnmodifiableArrayList.of(e1, e2, e3, e4, e5, e6, e7);
  }

  /** Creates an immutable list of 8 or more elements. */
  public static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8,
      E... others) {
    Object[] array = new Object[8 + others.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;
    System.arraycopy(others, 0, array, 8, others.length);
    //noinspection unchecked
    return new ImmutableNullableList<E>((E[]) array);
  }

  @Override public E get(int index) {
    return elements[index];
  }

  @Override public int size() {
    return elements.length;
  }

  /**
   * Returns a new builder. The generated builder is equivalent to the builder
   * created by the {@link Builder} constructor.
   */
  public static <E> Builder<E> builder() {
    return new Builder<E>();
  }

  /**
   * A builder for creating immutable nullable list instances.
   *
   * @param <E> element type
   */
  public static final class Builder<E> {
    private final List<E> contents = Lists.newArrayList();

    /**
     * Creates a new builder. The returned builder is equivalent to the builder
     * generated by
     * {@link org.apache.calcite.util.ImmutableNullableList#builder}.
     */
    public Builder() {}

    /**
     * Adds {@code element} to the {@code ImmutableNullableList}.
     *
     * @param element the element to add
     * @return this {@code Builder} object
     */
    public Builder<E> add(E element) {
      contents.add(element);
      return this;
    }

    /**
     * Adds each element of {@code elements} to the
     * {@code ImmutableNullableList}.
     *
     * @param elements the {@code Iterable} to add to the
     *     {@code ImmutableNullableList}
     * @return this {@code Builder} object
     * @throws NullPointerException if {@code elements} is null
     */
    public Builder<E> addAll(Iterable<? extends E> elements) {
      Iterables.addAll(contents, elements);
      return this;
    }

    /**
     * Adds each element of {@code elements} to the
     * {@code ImmutableNullableList}.
     *
     * @param elements the elements to add to the {@code ImmutableNullableList}
     * @return this {@code Builder} object
     * @throws NullPointerException if {@code elements} is null
     */
    public Builder<E> add(E... elements) {
      for (E element : elements) {
        add(element);
      }
      return this;
    }

    /**
     * Adds each element of {@code elements} to the
     * {@code ImmutableNullableList}.
     *
     * @param elements the elements to add to the {@code ImmutableNullableList}
     * @return this {@code Builder} object
     * @throws NullPointerException if {@code elements} is null
     */
    public Builder<E> addAll(Iterator<? extends E> elements) {
      Iterators.addAll(contents, elements);
      return this;
    }

    /**
     * Returns a newly-created {@code ImmutableNullableList} based on the
     * contents of the {@code Builder}.
     */
    public List<E> build() {
      return copyOf(contents);
    }
  }
}

// End ImmutableNullableList.java
