package ru.yandex.qe.util.collect;

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.annotation.Nonnull;

import com.google.common.collect.ImmutableSet;

/**
 * Immutable map containing entries with different keys all having the same value.
 * <p>
 * This is useful when calling complex APIs taking a map (with entry values typically corresponding to a configuration option).
 * <br>
 * E.g. you can write a one-liner to specify a single retry strategy for all runnables submitted to a retrying executor:
 * <blockquote>
 * <pre>
 * retryingExecutor.submitAll(OneValueMap.of(RetryStrategy.EXPONENTIAL_BACKOFF, run1, run2));
 * </pre>
 * </blockquote>
 *
 * @param <K> the type of keys maintained by this map
 * @param <V> the type of mapped values
 *
 * @author entropia
 */
public final class OneValueMap<K, V> extends AbstractMap<K, V> {
  private final Entries entrySet;

  /**
   * Returns an immutable map associating all keys with a specified value.
   * <p>
   * The resulting map's {@link java.util.Map#keySet() key set} will have the same elements as the {@code keys}
   * array, in order, excluding duplicates.
   * <p>
   * <em>Note:</em> like {@code ImmutableSet.copyOf()}, this method takes a <em>snapshot</em> of the {@code keys}
   * array; changes to the array do not write through to the returned {@code Map} instance.
   *
   * @param value value to associate all keys with
   * @param keys keys
   *
   * @throws NullPointerException {@code value == null || keys == null || Arrays.asList(keys).contains(null)}
   */
  @SafeVarargs
  public static <K, V> Map<K, V> of(@Nonnull final V value, @Nonnull final K... keys) {
    return of(value, Arrays.asList(keys));
  }

  /**
   * Returns an immutable map associating all keys with a specified value.
   * <p>
   * The resulting map's {@link java.util.Map#keySet() key set} will have the same elements as the {@code keys}
   * iterable, in order, excluding duplicates.
   * <p>
   * <em>Note:</em> like {@code ImmutableSet.copyOf()}, this method takes a <em>snapshot</em> of the {@code keys}
   * iterable; values added or removed from the iterable after map creation will not be reflected in the returned
   * {@code Map} instance.
   *
   * @param value value to associate all keys with
   * @param keys keys
   *
   * @throws NullPointerException {@code value == null || keys == null || Iterables.contains(keys, null)}
   */
  public static <K, V> Map<K, V> of(@Nonnull final V value, @Nonnull final Iterable</*@Nonnull*/ ? extends K> keys) {
    return new OneValueMap<>(value, ImmutableSet.copyOf(keys));
  }

  private OneValueMap(@Nonnull final V value, @Nonnull final ImmutableSet<K> keys) {
    this.entrySet = new Entries(keys, Objects.requireNonNull(value, "value"));
  }

  @Override
  public boolean containsKey(Object key) {
    return entrySet.keySet.contains(key);
  }

  @Override
  public boolean containsValue(Object value) {
    return Objects.equals(entrySet.value, value);
  }

  @Override
  public V get(Object key) {
    return containsKey(key) ? entrySet.value : null;
  }

  @Override
  @Nonnull
  public Set<Entry<K, V>> entrySet() {
    return entrySet;
  }

  private final class Entries extends AbstractSet<Entry<K, V>> {
    private final Set<K> keySet;
    private final V value;

    private Entries(Set<K> keySet, V value) {
      this.keySet = keySet;
      this.value = value;
    }

    @Override
    @Nonnull
    public Iterator<Entry<K, V>> iterator() {
      return new EntryIterator();
    }

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

    private final class EntryIterator implements Iterator<Entry<K, V>> {
      private final Iterator<K> keyIterator = keySet.iterator();

      @Override
      public boolean hasNext() {
        return keyIterator.hasNext();
      }

      @Override
      public Entry<K, V> next() {
        return new SimpleImmutableEntry<>(keyIterator.next(), value);
      }

      @Override
      public void remove() {
        keyIterator.remove();
      }
    }
  }
}
