/*
 * Copyright (C) 2007 The Guava Authors
 *
 * 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.glassfish.jersey.internal.guava;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Supplier;

import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull;

/**
 * Provides static methods acting on or generating a {@code Multimap}.
 * <p>
 * <p>See the Guava User Guide article on <a href=
 * "http://code.google.com/p/guava-libraries/wiki/CollectionUtilitiesExplained#Multimaps">
 * {@code Multimaps}</a>.
 *
 * @author Jared Levy
 * @author Robert Konigsberg
 * @author Mike Bostock
 * @author Louis Wasserman
 * @since 2.0 (imported from Google Collections Library)
 */
public final class Multimaps {
    private Multimaps() {
    }

    /**
     * Creates a new {@code ListMultimap} that uses the provided map and factory.
     * It can generate a multimap based on arbitrary {@link Map} and {@link List}
     * classes.
     * <p>
     * <p>The {@code factory}-generated and {@code map} classes determine the
     * multimap iteration order. They also specify the behavior of the
     * {@code equals}, {@code hashCode}, and {@code toString} methods for the
     * multimap and its returned views. The multimap's {@code get}, {@code
     * removeAll}, and {@code replaceValues} methods return {@code RandomAccess}
     * lists if the factory does. However, the multimap's {@code get} method
     * returns instances of a different class than does {@code factory.get()}.
     * <p>
     * <p>The multimap is serializable if {@code map}, {@code factory}, the
     * lists generated by {@code factory}, and the multimap contents are all
     * serializable.
     * <p>
     * <p>The multimap is not threadsafe when any concurrent operations update the
     * multimap, even if {@code map} and the instances generated by
     * {@code factory} are. Concurrent read operations will work correctly. To
     * allow concurrent update operations, wrap the multimap with a call to
     * {@link #synchronizedListMultimap}.
     * <p>
     * <p>Call this method only when the simpler methods
     * {@link ArrayListMultimap#create()} and {@link LinkedListMultimap#create()}
     * won't suffice.
     * <p>
     * <p>Note: the multimap assumes complete ownership over of {@code map} and
     * the lists returned by {@code factory}. Those objects should not be manually
     * updated, they should be empty when provided, and they should not use soft,
     * weak, or phantom references.
     *
     * @param map     place to store the mapping from each key to its corresponding
     *                values
     * @param factory supplier of new, empty lists that will each hold all values
     *                for a given key
     * @throws IllegalArgumentException if {@code map} is not empty
     */
    public static <K, V> ListMultimap<K, V> newListMultimap(
            Map<K, Collection<V>> map, final Supplier<? extends List<V>> factory) {
        return new CustomListMultimap<K, V>(map, factory);
    }

    static boolean equalsImpl(Multimap<?, ?> multimap, Object object) {
        if (object == multimap) {
            return true;
        }
        if (object instanceof Multimap) {
            Multimap<?, ?> that = (Multimap<?, ?>) object;
            return multimap.asMap().equals(that.asMap());
        }
        return false;
    }

    private static class CustomListMultimap<K, V>
            extends AbstractListMultimap<K, V> {
        private static final long serialVersionUID = 0;
        transient Supplier<? extends List<V>> factory;

        CustomListMultimap(Map<K, Collection<V>> map,
                           Supplier<? extends List<V>> factory) {
            super(map);
            this.factory = checkNotNull(factory);
        }

        @Override
        protected List<V> createCollection() {
            return factory.get();
        }

        /**
         * @serialData the factory and the backing map
         */
        private void writeObject(ObjectOutputStream stream) throws IOException {
            stream.defaultWriteObject();
            stream.writeObject(factory);
            stream.writeObject(backingMap());
        }

        @SuppressWarnings("unchecked") // reading data stored by writeObject
        private void readObject(ObjectInputStream stream)
                throws IOException, ClassNotFoundException {
            stream.defaultReadObject();
            factory = (Supplier<? extends List<V>>) stream.readObject();
            Map<K, Collection<V>> map = (Map<K, Collection<V>>) stream.readObject();
            setMap(map);
        }
    }

    /**
     * A skeleton implementation of {@link Multimap#entries()}.
     */
    abstract static class Entries<K, V> extends
            AbstractCollection<Entry<K, V>> {
        abstract Multimap<K, V> multimap();

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

        @Override
        public boolean contains(Object o) {
            if (o instanceof Map.Entry) {
                Entry<?, ?> entry = (Entry<?, ?>) o;
                return multimap().containsEntry(entry.getKey(), entry.getValue());
            }
            return false;
        }

        @Override
        public boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Entry<?, ?> entry = (Entry<?, ?>) o;
                return multimap().remove(entry.getKey(), entry.getValue());
            }
            return false;
        }

        @Override
        public void clear() {
            multimap().clear();
        }
    }

    // TODO(jlevy): Create methods that filter a SortedSetMultimap.
}
