/*
 * 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.commons.collections4.list;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;

import org.apache.commons.collections4.ListUtils;
import org.apache.commons.collections4.iterators.AbstractIteratorDecorator;
import org.apache.commons.collections4.iterators.AbstractListIteratorDecorator;
import org.apache.commons.collections4.set.UnmodifiableSet;

/**
 * Decorates a {@code List} to ensure that no duplicates are present much
 * like a {@code Set}.
 * <p>
 * The {@code List} interface makes certain assumptions/requirements. This
 * implementation breaks these in certain ways, but this is merely the result of
 * rejecting duplicates. Each violation is explained in the method, but it
 * should not affect you. Bear in mind that Sets require immutable objects to
 * function correctly.
 * </p>
 * <p>
 * The {@link org.apache.commons.collections4.set.ListOrderedSet ListOrderedSet}
 * class provides an alternative approach, by wrapping an existing Set and
 * retaining insertion order in the iterator.
 * </p>
 * <p>
 * This class is Serializable from Commons Collections 3.1.
 * </p>
 *
 * @param <E> the type of the elements in the list.
 * @since 3.0
 */
public class SetUniqueList<E> extends AbstractSerializableListDecorator<E> {

    /**
     * Inner class iterator.
     */
    static class SetListIterator<E> extends AbstractIteratorDecorator<E> {

        private final Set<E> set;
        private E last;

        protected SetListIterator(final Iterator<E> it, final Set<E> set) {
            super(it);
            this.set = set;
        }

        @Override
        public E next() {
            last = super.next();
            return last;
        }

        @Override
        public void remove() {
            super.remove();
            set.remove(last);
            last = null;
        }
    }

    /**
     * Inner class iterator.
     */
    static class SetListListIterator<E> extends
            AbstractListIteratorDecorator<E> {

        private final Set<E> set;
        private E last;

        protected SetListListIterator(final ListIterator<E> it, final Set<E> set) {
            super(it);
            this.set = set;
        }

        @Override
        public void add(final E object) {
            if (!set.contains(object)) {
                super.add(object);
                set.add(object);
            }
        }

        @Override
        public E next() {
            last = super.next();
            return last;
        }

        @Override
        public E previous() {
            last = super.previous();
            return last;
        }

        @Override
        public void remove() {
            super.remove();
            set.remove(last);
            last = null;
        }

        @Override
        public void set(final E object) {
            throw new UnsupportedOperationException("ListIterator does not support set");
        }
    }

    /** Serialization version. */
    private static final long serialVersionUID = 7196982186153478694L;

    /**
     * Factory method to create a SetList using the supplied list to retain order.
     * <p>
     * If the list contains duplicates, these are removed (first indexed one
     * kept). A {@code HashSet} is used for the set behavior.
     *
     * @param <E>  the element type
     * @param list  the list to decorate, must not be null
     * @return a new {@link SetUniqueList}
     * @throws NullPointerException if list is null
     * @since 4.0
     */
    public static <E> SetUniqueList<E> setUniqueList(final List<E> list) {
        Objects.requireNonNull(list, "list");
        if (list.isEmpty()) {
            return new SetUniqueList<>(list, new HashSet<>());
        }
        final List<E> temp = new ArrayList<>(list);
        list.clear();
        final SetUniqueList<E> sl = new SetUniqueList<>(list, new HashSet<>());
        sl.addAll(temp);
        return sl;
    }

    /** Internal Set to maintain uniqueness. */
    private final Set<E> set;

    /**
     * Constructor that wraps (not copies) the List and specifies the set to use.
     * <p>
     * The set and list must both be correctly initialized to the same elements.
     *
     * @param set  the set to decorate, must not be null
     * @param list  the list to decorate, must not be null
     * @throws NullPointerException if set or list is null
     */
    protected SetUniqueList(final List<E> list, final Set<E> set) {
        super(list);
        this.set = Objects.requireNonNull(set, "set");
    }

    /**
     * Adds an element to the list if it is not already present.
     * <p>
     * <em>(Violation)</em> The {@code List} interface requires that this
     * method returns {@code true} always. However, this class may return
     * {@code false} because of the {@code Set} behavior.
     *
     * @param object  the object to add
     * @return true if object was added
     */
    @Override
    public boolean add(final E object) {
        // gets initial size
        final int sizeBefore = size();

        // adds element if unique
        add(size(), object);

        // compares sizes to detect if collection changed
        return sizeBefore != size();
    }

    /**
     * Adds an element to a specific index in the list if it is not already
     * present.
     * <p>
     * <em>(Violation)</em> The {@code List} interface makes the assumption
     * that the element is always inserted. This may not happen with this
     * implementation.
     *
     * @param index  the index to insert at
     * @param object  the object to add
     */
    @Override
    public void add(final int index, final E object) {
        // adds element if it is not contained already
        if (!set.contains(object)) {
            set.add(object);
            super.add(index, object);
        }
    }

    /**
     * Adds a collection of objects to the end of the list avoiding duplicates.
     * <p>
     * Only elements that are not already in this list will be added, and
     * duplicates from the specified collection will be ignored.
     * <p>
     * <em>(Violation)</em> The {@code List} interface makes the assumption
     * that the elements are always inserted. This may not happen with this
     * implementation.
     *
     * @param coll  the collection to add in iterator order
     * @return true if this collection changed
     */
    @Override
    public boolean addAll(final Collection<? extends E> coll) {
        return addAll(size(), coll);
    }

    /**
     * Adds a collection of objects a specific index in the list avoiding
     * duplicates.
     * <p>
     * Only elements that are not already in this list will be added, and
     * duplicates from the specified collection will be ignored.
     * <p>
     * <em>(Violation)</em> The {@code List} interface makes the assumption
     * that the elements are always inserted. This may not happen with this
     * implementation.
     *
     * @param index  the index to insert at
     * @param coll  the collection to add in iterator order
     * @return true if this collection changed
     */
    @Override
    public boolean addAll(final int index, final Collection<? extends E> coll) {
        final List<E> temp = new ArrayList<>();
        for (final E e : coll) {
            if (set.add(e)) {
                temp.add(e);
            }
        }
        return super.addAll(index, temp);
    }

    /**
     * Gets an unmodifiable view as a Set.
     *
     * @return an unmodifiable set view
     */
    public Set<E> asSet() {
        return UnmodifiableSet.unmodifiableSet(set);
    }

    @Override
    public void clear() {
        super.clear();
        set.clear();
    }

    @Override
    public boolean contains(final Object object) {
        return set.contains(object);
    }

    @Override
    public boolean containsAll(final Collection<?> coll) {
        return set.containsAll(coll);
    }

    /**
     * Create a new {@link Set} with the same type as the provided {@code set}
     * and populate it with all elements of {@code list}.
     *
     * @param set  the {@link Set} to be used as return type, must not be null
     * @param list  the {@link List} to populate the {@link Set}
     * @return a new {@link Set} populated with all elements of the provided
     *   {@link List}
     */
    protected Set<E> createSetBasedOnList(final Set<E> set, final List<E> list) {
        Set<E> subSet;
        if (set.getClass().equals(HashSet.class)) {
            subSet = new HashSet<>(list.size());
        } else {
            try {
                subSet = set.getClass().getDeclaredConstructor(set.getClass()).newInstance(set);
            } catch (final InstantiationException
                    | IllegalAccessException
                    | InvocationTargetException
                    | NoSuchMethodException ie) {
                subSet = new HashSet<>();
            }
        }
        subSet.addAll(list);
        return subSet;
    }

    @Override
    public Iterator<E> iterator() {
        return new SetListIterator<>(super.iterator(), set);
    }

    @Override
    public ListIterator<E> listIterator() {
        return new SetListListIterator<>(super.listIterator(), set);
    }

    @Override
    public ListIterator<E> listIterator(final int index) {
        return new SetListListIterator<>(super.listIterator(index), set);
    }

    @Override
    public E remove(final int index) {
        final E result = super.remove(index);
        set.remove(result);
        return result;
    }

    @Override
    public boolean remove(final Object object) {
        final boolean result = set.remove(object);
        if (result) {
            super.remove(object);
        }
        return result;
    }

    @Override
    public boolean removeAll(final Collection<?> coll) {
        boolean result = false;
        for (final Object name : coll) {
            result |= remove(name);
        }
        return result;
    }

    /**
     * @since 4.4
     */
    @Override
    public boolean removeIf(final Predicate<? super E> filter) {
        final boolean result = super.removeIf(filter);
        set.removeIf(filter);
        return result;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This implementation iterates over the elements of this list, checking
     * each element in turn to see if it's contained in {@code coll}.
     * If it's not contained, it's removed from this list. As a consequence,
     * it is advised to use a collection type for {@code coll} that provides
     * a fast (for example O(1)) implementation of {@link Collection#contains(Object)}.
     */
    @Override
    public boolean retainAll(final Collection<?> coll) {
        final boolean result = set.retainAll(coll);
        if (!result) {
            return false;
        }
        if (set.isEmpty()) {
            super.clear();
        } else {
            // use the set as parameter for the call to retainAll to improve performance
            super.retainAll(set);
        }
        return result;
    }

    /**
     * Sets the value at the specified index avoiding duplicates.
     * <p>
     * The object is set into the specified index. Afterwards, any previous
     * duplicate is removed. If the object is not already in the list then a
     * normal set occurs. If it is present, then the old version is removed.
     *
     * @param index  the index to insert at
     * @param object  the object to set
     * @return the previous object
     */
    @Override
    public E set(final int index, final E object) {
        final int pos = indexOf(object);
        final E removed = super.set(index, object);

        if (pos != -1 && pos != index) {
            // the object is already in the unique list
            // (and it hasn't been swapped with itself)
            super.remove(pos); // remove the duplicate by index
        }

        set.remove(removed); // remove the item deleted by the set
        set.add(object); // add the new item to the unique set

        return removed; // return the item deleted by the set
    }

    /**
     * {@inheritDoc}
     * <p>
     * NOTE: from 4.0, an unmodifiable list will be returned, as changes to the
     * subList can invalidate the parent list.
     */
    @Override
    public List<E> subList(final int fromIndex, final int toIndex) {
        final List<E> superSubList = super.subList(fromIndex, toIndex);
        final Set<E> subSet = createSetBasedOnList(set, superSubList);
        return ListUtils.unmodifiableList(new SetUniqueList<>(superSubList, subSet));
    }

}
