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

import java.util.AbstractSet;
import java.util.BitSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.io.Serializable;
import java.lang.reflect.Modifier;
import org.opengis.util.CodeList;
import org.apache.sis.util.NullArgumentException;
import org.apache.sis.util.internal.CodeLists;
import org.apache.sis.util.internal.CheckedArrayList;
import org.apache.sis.util.resources.Errors;


/**
 * A specialized {@code Set} implementation for use with {@link CodeList} values.
 * All elements in a {@code CodeListSet} are of the same {@code CodeList} class,
 * which must be final. Iterators traverse the elements in the order in which the
 * code list constants are declared.
 *
 * <h2>Implementation note</h2>
 * {@code CodeListSet} is implemented internally by bit vectors for compact and efficient storage.
 * All bulk operations ({@code addAll}, {@code removeAll}, {@code containsAll}) are very quick if
 * their argument is also a {@code CodeListSet} instance.
 *
 * <h2>Usage example</h2>
 * The following example creates a set of {@link org.opengis.referencing.cs.AxisDirection}s
 * for a (<var>x</var>,<var>y</var>,<var>z</var>) coordinate system:
 *
 * {@snippet lang="java" :
 *   CodeListSet<AxisDirection> codes = new CodeListSet<>(AxisDirection.class);
 *   Collections.addAll(codes, AxisDirection.EAST, AxisDirection.NORTH, AxisDirection.UP),
 *   }
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.4
 *
 * @param <E>  the type of code list elements in the set.
 *
 * @see java.util.EnumSet
 *
 * @since 0.3
 */
public class CodeListSet<E extends CodeList<E>> extends AbstractSet<E>
        implements CheckedContainer<E>, Cloneable, Serializable
{
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = -6328082298556260980L;

    /**
     * A pool of code list arrays. When many {@code CodeListSet} instances are for the
     * same code list type, this allows those instances to share the same arrays.
     */
    @SuppressWarnings("rawtypes")
    private static final WeakHashSet<CodeList[]> POOL = new WeakHashSet<>(CodeList[].class);

    /**
     * The type of code list elements.
     *
     * @see #getElementType()
     */
    private final Class<E> elementType;

    /**
     * A bitmask of code list values present in this map.
     */
    private long values;

    /**
     * The bit set for supplementary values beyond the {@code values} mask, or {@code null}
     * if none. This is very rarely needed, but we need this field in case a code list has
     * more than 64 elements.
     *
     * <h4>Implementation note</h4>
     * The standard {@link java.util.EnumSet} class uses different implementations depending on whether
     * the enumeration contains more or less than 64 elements. We cannot apply the same strategy for
     * {@code CodeListSet}, because new code list elements can be created at runtime. Consequently, this
     * implementation needs to be able to growth its capacity.
     */
    private BitSet supplementary;

    /**
     * All possible code list elements, fetched when first needed.
     * Note that this array may need to be fetched more than once,
     * because code list elements can be dynamically added.
     *
     * @see #valueOf(int)
     */
    private transient E[] codes;

    /**
     * Creates an initially empty set for code lists of the given type.
     * The given {@code CodeList} type shall be final.
     *
     * @param  elementType  the type of code list elements to be included in this set.
     * @throws IllegalArgumentException if the given class is not final.
     */
    public CodeListSet(final Class<E> elementType) throws IllegalArgumentException {
        if (!Modifier.isFinal(elementType.getModifiers())) {
            throw new IllegalArgumentException(Errors.format(Errors.Keys.ClassNotFinal_1, elementType));
        }
        this.elementType = elementType;
    }

    /**
     * Creates set for code lists of the given type. If the {@code fill} argument is {@code false},
     * then the new set will be initially empty. Otherwise the new set will be filled with all code
     * list elements of the given type that are known at construction time. Note that if new code
     * list elements are created after the invocation of this {@code CodeListSet} constructor, then
     * those new elements will <em>not</em> be in this set.
     *
     * @param  elementType  the type of code list elements to be included in this set.
     * @param  fill {@code true} for filling the set with all known elements of the given type,
     *         or {@code false} for leaving the set empty.
     * @throws IllegalArgumentException if the given class is not final.
     */
    public CodeListSet(final Class<E> elementType, final boolean fill) throws IllegalArgumentException {
        this(elementType);
        if (fill) {
            codes = POOL.unique(CodeLists.values(elementType));
            int n = codes.length;
            if (n < Long.SIZE) {
                values = (1L << n) - 1;
            } else {
                values = -1;
                if ((n -= Long.SIZE) != 0) {
                    supplementary = new BitSet(n);
                    supplementary.set(0, n);
                }
            }
        }
    }

    /**
     * Returns the type of code list elements in this set.
     *
     * @return the type of code list elements in this set.
     */
    @Override
    public Class<E> getElementType() {
        return elementType;
    }

    /**
     * Returns the code list for the given ordinal value. This methods depends
     * only on the code list type; it does not depend on the content of this set.
     */
    final E valueOf(final int ordinal) {
        E[] array = codes;
        if (array == null || ordinal >= array.length) {
            codes = array = POOL.unique(CodeLists.values(elementType));
        }
        return array[ordinal];
    }

    /**
     * Removes all elements from this set.
     */
    @Override
    public void clear() {
        values = 0;
        final BitSet s = supplementary;
        if (s != null) {
            s.clear();
        }
    }

    /**
     * Returns {@code true} if this set does not contains any element.
     *
     * @return {@code true} if this set is empty.
     */
    @Override
    public boolean isEmpty() {
        final BitSet s;
        return values == 0 && ((s = supplementary) == null || s.isEmpty());
    }

    /**
     * Returns the number of elements in this set.
     *
     * @return the number of elements in this set.
     */
    @Override
    public int size() {
        int n = Long.bitCount(values);
        final BitSet s = supplementary;
        if (s != null) {
            n += s.cardinality();
        }
        return n;
    }

    /**
     * Adds the specified code list element in this set.
     *
     * @param  element  the code list element to add in this set.
     * @return {@code true} if this set has been modified as a consequence of this method call.
     */
    @Override
    public boolean add(final E element) {
        if (element == null) {
            final String message = CheckedArrayList.illegalElement(this, element, elementType);
            if (message == null) {
                /*
                 * If a unmarshalling process is under way, silently discard null element.
                 * This case happen when a codeListValue attribute in a XML file is empty.
                 * See https://issues.apache.org/jira/browse/SIS-157
                 */
                return false;
            }
            throw new NullArgumentException(message);
        }
        int ordinal = element.ordinal();
        if (ordinal < Long.SIZE) {
            return values != (values |= (1L << ordinal));
        }
        /*
         * The above code is sufficient in the vast majority of cases. In the rare cases where
         * there is more than 64 elements, create a BitSet for storing the supplementary values.
         */
        BitSet s = supplementary;
        if (s == null) {
            supplementary = s = new BitSet();
        }
        if (s.get(ordinal -= Long.SIZE)) {
            return false;
        }
        s.set(ordinal);
        return true;
    }

    /**
     * Removes the specified code list element from this set.
     * This methods does nothing if the given argument is {@code null} or is
     * not an instance of the code list class specified at construction time.
     *
     * @param  object  the code list element to remove from this set.
     * @return {@code true} if this set has been modified as a consequence of this method call.
     */
    @Override
    public boolean remove(final Object object) {
        if (elementType.isInstance(object)) {
            return clear(((CodeList<?>) object).ordinal());
        }
        return false;
    }

    /**
     * Clears the bit at the given ordinal value. This method is invoked by
     * {@link #remove(Object)} or by {@link Iter#remove()}.
     */
    final boolean clear(int ordinal) {
        if (ordinal < Long.SIZE) {
            return values != (values &= ~(1L << ordinal));
        }
        // Rare cases where there is more than 64 elements.
        final BitSet s = supplementary;
        if (s != null && s.get(ordinal -= Long.SIZE)) {
            s.clear(ordinal);
            return true;
        }
        return false;
    }

    /**
     * Returns {@code true} if this set contains the given element.
     * This methods returns {@code false} if the given argument is {@code null} or
     * is not an instance of the code list class specified at construction time.
     *
     * @param  object  the element to test for presence in this set.
     * @return {@code true} if the given object is contained in this set.
     */
    @Override
    public boolean contains(final Object object) {
        if (elementType.isInstance(object)) {
            int ordinal = ((CodeList<?>) object).ordinal();
            if (ordinal < Long.SIZE) {
                return (values & (1L << ordinal)) != 0;
            }
            // Rare cases where there is more than 64 elements.
            final BitSet s = supplementary;
            if (s != null) {
                return s.get(ordinal - Long.SIZE);
            }
        }
        return false;
    }

    /**
     * Returns {@code true} if this set contains all the elements of the given collection.
     *
     * @param  c  the collection to be checked for containment in this set.
     * @return {@code true} if this set contains all elements of the given collection.
     */
    @Override
    public boolean containsAll(final Collection<?> c) {
        if (c instanceof CodeListSet) {
            final CodeListSet<?> o = (CodeListSet<?>) c;
            if (elementType == o.elementType) {
                if (values == (values | o.values)) {
                    /*
                     * Code below this point checks for the rare cases
                     * where there is more than 64 code list elements.
                     */
                    final BitSet s = supplementary;
                    final BitSet os = o.supplementary;
                    if (( s == null ||  s.isEmpty()) &&
                        (os == null || os.isEmpty()))
                    {
                        return true;
                    }
                    if (s != null && os != null) {
                        final BitSet tmp = (BitSet) os.clone();
                        tmp.andNot(s);
                        return tmp.isEmpty();
                    }
                }
            }
            return false;
        }
        return super.containsAll(c);
    }

    /**
     * Adds all elements of the given collection to this set.
     *
     * @param  c  the collection containing elements to be added to this set.
     * @return {@code true} if this set changed as a result of this method call.
     */
    @Override
    public boolean addAll(final Collection<? extends E> c) throws IllegalArgumentException {
        if (c instanceof CodeListSet) {
            final CodeListSet<?> o = (CodeListSet<?>) c;
            /*
             * Following assertion should be ensured by parameterized types.
             */
            assert elementType.isAssignableFrom(o.elementType);
            boolean changed = (values != (values |= o.values));
            /*
             * Code below this point is for the rare cases
             * where there is more than 64 code list elements.
             */
            final BitSet os = o.supplementary;
            if (os != null) {
                final BitSet s = supplementary;
                if (s == null) {
                    if (!os.isEmpty()) {
                        supplementary = (BitSet) os.clone();
                        changed = true;
                    }
                } else if (changed) {
                    // Avoid the cost of computing cardinality.
                    s.or(os);
                } else {
                    final int cardinality = s.cardinality();
                    s.or(os);
                    changed = (cardinality != s.cardinality());
                }
            }
            return changed;
        }
        return super.addAll(c);
    }

    /**
     * Returns the bitmask to use for a bulk operation with another set of code lists.
     */
    private long mask(final CodeListSet<?> other) {
        return (elementType == other.elementType) ? other.values : 0;
    }

    /**
     * Adds all elements of the given collection from this set.
     *
     * @param  c  the collection containing elements to be removed from this set.
     * @return {@code true} if this set changed as a result of this method call.
     */
    @Override
    public boolean removeAll(final Collection<?> c) {
        if (c instanceof CodeListSet) {
            boolean changed = (values != (values &= ~mask((CodeListSet<?>) c)));
            /*
             * Code below this point is for the rare cases
             * where there is more than 64 code list elements.
             */
            final BitSet s = supplementary;
            if (s != null) {
                final BitSet os = ((CodeListSet<?>) c).supplementary;
                if (os != null) {
                    if (changed) {
                        // Avoid the cost of computing cardinality.
                        s.andNot(os);
                    } else {
                        final int cardinality = s.cardinality();
                        s.andNot(os);
                        changed = (cardinality != s.cardinality());
                    }
                }
            }
            return changed;
        }
        return super.removeAll(c);
    }

    /**
     * Retains only the elements of the given collection in this set.
     *
     * @param  c  the collection containing elements to retain in this set.
     * @return {@code true} if this set changed as a result of this method call.
     */
    @Override
    public boolean retainAll(final Collection<?> c) {
        if (c instanceof CodeListSet) {
            boolean changed = (values != (values &= mask((CodeListSet<?>) c)));
            /*
             * Code below this point is for the rare cases
             * where there is more than 64 code list elements.
             */
            final BitSet s = supplementary;
            if (s != null) {
                final BitSet os = ((CodeListSet<?>) c).supplementary;
                if (os == null) {
                    changed |= !s.isEmpty();
                    s.clear();
                } else if (changed) {
                    // Avoid the cost of computing cardinality.
                    s.and(os);
                } else {
                    final int cardinality = s.cardinality();
                    s.and(os);
                    changed = (cardinality != s.cardinality());
                }
            }
            return changed;
        }
        return super.retainAll(c);
    }

    /**
     * Returns an iterator over the elements in this set.
     * The instance returned by this implementation will iterate over a snapshot of this
     * {@code CodeListSet} content at the time this method has been invoked. Changes in
     * this {@code CodeListSet} made after this method call will not affect the values
     * returned by the iterator.
     *
     * @return an iterator over the elements in this set.
     */
    @Override
    public Iterator<E> iterator() {
        return new Iter(values, supplementary);
    }

    /**
     * The iterator returned by {@link CodeListSet#iterator()}.
     */
    private final class Iter implements Iterator<E> {
        /**
         * Initialized to {@link CodeListSet#values}, then the bits are cleared as we
         * progress in the iteration. This value become 0 when the iteration is done.
         */
        private long remaining;

        /**
         * Initialized to a clone of {@link CodeListSet#supplementary}, then the bits are cleared
         * as we progress in the iteration. The bit set become empty when the iteration is done.
         */
        private final BitSet more;

        /**
         * Ordinal value of the last element returned by {@link #next()}, or -1 if none.
         */
        private int last;

        /**
         * Creates a new iterator initialized to the given values.
         */
        Iter(final long values, final BitSet supplementary) {
            remaining = values;
            more = (supplementary != null) ? (BitSet) supplementary.clone() : null;
            last = -1;
        }

        /**
         * Returns {@code true} if there is more elements to return.
         */
        @Override
        public boolean hasNext() {
            return remaining != 0 || (more != null && !more.isEmpty());
        }

        /**
         * Returns the next element.
         */
        @Override
        public E next() {
            int ordinal = Long.numberOfTrailingZeros(remaining);
            if (ordinal >= Long.SIZE) {
                // Rare case when we have more than 64 elements.
                if (more == null || (ordinal = more.nextSetBit(0)) < 0) {
                    throw new NoSuchElementException();
                }
                more.clear(ordinal);
                ordinal += Long.SIZE;
            }
            last = ordinal;
            remaining &= ~(1L << ordinal);
            return valueOf(ordinal);
        }

        /**
         * Removes the last element returned by this iterator.
         */
        @Override
        public void remove() {
            if (last < 0) {
                throw new IllegalStateException();
            }
            clear(last);
            last = -1;
        }
    }

    /**
     * Returns a new set of the same class containing the same elements than this set.
     *
     * @return a clone of this set.
     */
    @Override
    @SuppressWarnings("unchecked")
    public CodeListSet<E> clone() {
        final CodeListSet<E> clone;
        try {
            clone = (CodeListSet<E>) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);                // Should never happen, since we are cloneable.
        }
        final BitSet s = supplementary;
        if (s != null) {
            clone.supplementary = (BitSet) s.clone();
        }
        return clone;
    }
}
