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

import java.io.Serializable;

/**
 * An EnumSet is a specialized Set to be used with enums as keys.
 */
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
        implements Cloneable, Serializable {
    private static final long serialVersionUID = 1009687484059888093L;

    final Class<E> elementClass;

    EnumSet(Class<E> cls) {
        elementClass = cls;
    }

    /**
     * Creates an empty enum set. The permitted elements are of type
     * Class&lt;E&gt;.
     *
     * @param elementType
     *            the class object for the elements contained.
     * @return an empty enum set, with permitted elements of type {@code
     *         elementType}.
     * @throws ClassCastException
     *             if the specified element type is not and enum type.
     */
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        if (!elementType.isEnum()) {
            throw new ClassCastException(elementType.getClass().getName() + " is not an Enum");
        }
        E[] enums = Enum.getSharedConstants(elementType);
        if (enums.length <= 64) {
            return new MiniEnumSet<E>(elementType, enums);
        }
        return new HugeEnumSet<E>(elementType, enums);
    }

    /**
     * Creates an enum set filled with all the enum elements of the specified
     * {@code elementType}.
     *
     * @param elementType
     *            the class object for the elements contained.
     * @return an enum set with elements solely from the specified element type.
     * @throws ClassCastException
     *             if the specified element type is not and enum type.
     */
    public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {
        EnumSet<E> set = noneOf(elementType);
        set.complement();
        return set;
    }

    /**
     * Creates an enum set. All the contained elements are of type
     * Class&lt;E&gt;, and the contained elements are the same as those
     * contained in {@code s}.
     *
     * @param s
     *            the enum set from which to copy.
     * @return an enum set with all the elements from the specified enum set.
     * @throws ClassCastException
     *             if the specified element type is not and enum type.
     */
    public static <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s) {
        EnumSet<E> set = EnumSet.noneOf(s.elementClass);
        set.addAll(s);
        return set;
    }

    /**
     * Creates an enum set. The contained elements are the same as those
     * contained in collection {@code c}. If c is an enum set, invoking this
     * method is the same as invoking {@link #copyOf(EnumSet)}.
     *
     * @param c
     *            the collection from which to copy. if it is not an enum set,
     *            it must not be empty.
     * @return an enum set with all the elements from the specified collection.
     * @throws IllegalArgumentException
     *             if c is not an enum set and contains no elements at all.
     * @throws NullPointerException
     *             if {@code c} is {@code null}.
     */
    public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) {
        if (c instanceof EnumSet) {
            return copyOf((EnumSet<E>) c);
        }
        if (c.isEmpty()) {
            throw new IllegalArgumentException("empty collection");
        }
        Iterator<E> iterator = c.iterator();
        E element = iterator.next();
        EnumSet<E> set = EnumSet.noneOf(element.getDeclaringClass());
        set.add(element);
        while (iterator.hasNext()) {
            set.add(iterator.next());
        }
        return set;
    }

    /**
     * Creates an enum set. All the contained elements complement those from the
     * specified enum set.
     *
     * @param s
     *            the specified enum set.
     * @return an enum set with all the elements complementary to those from the
     *         specified enum set.
     * @throws NullPointerException
     *             if {@code s} is {@code null}.
     */
    public static <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s) {
        EnumSet<E> set = EnumSet.noneOf(s.elementClass);
        set.addAll(s);
        set.complement();
        return set;
    }

    abstract void complement();

    /**
     * Creates a new enum set, containing only the specified element. There are
     * six overloadings of the method. They accept from one to five elements
     * respectively. The sixth one receives an arbitrary number of elements, and
     * runs slower than those that only receive a fixed number of elements.
     *
     * @param e
     *            the element to be initially contained.
     * @return an enum set containing the specified element.
     * @throws NullPointerException
     *             if {@code e} is {@code null}.
     */
    public static <E extends Enum<E>> EnumSet<E> of(E e) {
        EnumSet<E> set = EnumSet.noneOf(e.getDeclaringClass());
        set.add(e);
        return set;
    }

    /**
     * Creates a new enum set, containing only the specified elements. There are
     * six overloadings of the method. They accept from one to five elements
     * respectively. The sixth one receives an arbitrary number of elements, and
     * runs slower than those that only receive a fixed number of elements.
     *
     * @param e1
     *            the initially contained element.
     * @param e2
     *            another initially contained element.
     * @return an enum set containing the specified elements.
     * @throws NullPointerException
     *             if any of the specified elements is {@code null}.
     */
    public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) {
        EnumSet<E> set = of(e1);
        set.add(e2);
        return set;
    }

    /**
     * Creates a new enum set, containing only the specified elements. There are
     * six overloadings of the method. They accept from one to five elements
     * respectively. The sixth one receives an arbitrary number of elements, and
     * runs slower than those that only receive a fixed number of elements.
     *
     * @param e1
     *            the initially contained element.
     * @param e2
     *            another initially contained element.
     * @param e3
     *            another initially contained element.
     * @return an enum set containing the specified elements.
     * @throws NullPointerException
     *             if any of the specified elements is {@code null}.
     */
    public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3) {
        EnumSet<E> set = of(e1, e2);
        set.add(e3);
        return set;
    }

    /**
     * Creates a new enum set, containing only the specified elements. There are
     * six overloadings of the method. They accept from one to five elements
     * respectively. The sixth one receives an arbitrary number of elements, and
     * runs slower than those that only receive a fixed number of elements.
     *
     * @param e1
     *            the initially contained element.
     * @param e2
     *            another initially contained element.
     * @param e3
     *            another initially contained element.
     * @param e4
     *            another initially contained element.
     * @return an enum set containing the specified elements.
     * @throws NullPointerException
     *             if any of the specified elements is {@code null}.
     */
    public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4) {
        EnumSet<E> set = of(e1, e2, e3);
        set.add(e4);
        return set;
    }

    /**
     * Creates a new enum set, containing only the specified elements. There are
     * six overloadings of the method. They accept from one to five elements
     * respectively. The sixth one receives an arbitrary number of elements, and
     * runs slower than those that only receive a fixed number of elements.
     *
     * @param e1
     *            the initially contained element.
     * @param e2
     *            another initially contained element.
     * @param e3
     *            another initially contained element.
     * @param e4
     *            another initially contained element.
     * @param e5
     *            another initially contained element.
     * @return an enum set containing the specified elements.
     * @throws NullPointerException
     *             if any of the specified elements is {@code null}.
     */
    public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5) {
        EnumSet<E> set = of(e1, e2, e3, e4);
        set.add(e5);
        return set;
    }

    /**
     * Creates a new enum set, containing only the specified elements. It can
     * receive an arbitrary number of elements, and runs slower than those only
     * receiving a fixed number of elements.
     *
     * @param start
     *            the first initially contained element.
     * @param others
     *            the other initially contained elements.
     * @return an enum set containing the specified elements.
     * @throws NullPointerException
     *             if any of the specified elements is {@code null}.
     */
    @SafeVarargs
    public static <E extends Enum<E>> EnumSet<E> of(E start, E... others) {
        EnumSet<E> set = of(start);
        for (E e : others) {
            set.add(e);
        }
        return set;
    }

    /**
     * Creates an enum set containing all the elements within the range defined
     * by {@code start} and {@code end} (inclusive). All the elements must be in
     * order.
     *
     * @param start
     *            the element used to define the beginning of the range.
     * @param end
     *            the element used to define the end of the range.
     * @return an enum set with elements in the range from start to end.
     * @throws NullPointerException
     *             if any one of {@code start} or {@code end} is {@code null}.
     * @throws IllegalArgumentException
     *             if {@code start} is behind {@code end}.
     */
    public static <E extends Enum<E>> EnumSet<E> range(E start, E end) {
        if (start.compareTo(end) > 0) {
            throw new IllegalArgumentException("start is behind end");
        }
        EnumSet<E> set = EnumSet.noneOf(start.getDeclaringClass());
        set.setRange(start, end);
        return set;
    }

    abstract void setRange(E start, E end);

    /**
     * Creates a new enum set with the same elements as those contained in this
     * enum set.
     *
     * @return a new enum set with the same elements as those contained in this
     *         enum set.
     */
    @SuppressWarnings("unchecked")
    @Override
    public EnumSet<E> clone() {
        try {
            return (EnumSet<E>) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

    boolean isValidType(Class<?> cls) {
        return cls == elementClass || cls.getSuperclass() == elementClass;
    }

    private static class SerializationProxy<E extends Enum<E>> implements
            Serializable {

        private static final long serialVersionUID = 362491234563181265L;

        private Class<E> elementType;

        private E[] elements;

        private Object readResolve() {
            EnumSet<E> set = EnumSet.noneOf(elementType);
            for (E e : elements) {
                set.add(e);
            }
            return set;
        }
    }

    @SuppressWarnings("unchecked")
    Object writeReplace() {
        SerializationProxy proxy = new SerializationProxy();
        proxy.elements = toArray(new Enum[0]);
        proxy.elementType = elementClass;
        return proxy;
    }
}
