package org.infinispan.commons.util;

import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.function.IntFunction;

/**
 * Base for classes that implement hash map by storing keys in one array and values in another.
 * It assumes that all keys that are in the array are contained in the map and that values
 * are on corresponding indices in the map.
 *
 * Does not support null keys nor values.
 *
 * Forces implementation of methods {@link #get(Object)}, {@link #put(Object, Object)}, {@link #remove(Object)}
 */
public abstract class ArrayMap<K, V> extends java.util.AbstractMap<K, V> {
   protected int size;
   protected Object[] keys;
   protected Object[] values;
   protected int modCount;
   private Set<K> keySet;
   private Collection<V> valueCollection;
   private Set<Entry<K, V>> entrySet;

   @Override
   public abstract V get(Object key);

   @Override
   public abstract V put(K key, V value);

   @Override
   public abstract V remove(Object key);

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

   @Override
   public boolean containsValue(Object value) {
      Objects.requireNonNull(value);
      for (int i = 0; i < values.length; ++i) {
         Object v = values[i];
         if (v != null && v.equals(value)) return true;
      }
      return false;
   }

   @Override
   public Set<K> keySet() {
      if (keySet == null) {
         keySet = new KeySet();
      }
      return keySet;
   }

   @Override
   public Collection<V> values() {
      if (valueCollection == null) {
         valueCollection = new Values();
      }
      return valueCollection;
   }

   @Override
   public Set<Entry<K, V>> entrySet() {
      if (entrySet == null) {
         entrySet = new EntrySet();
      }
      return entrySet;
   }

   @Override
   public boolean containsKey(Object key) {
      return get(key) != null;
   }

   @Override
   public void clear() {
      size = 0;
      ++modCount;
      Arrays.fill(keys, null);
      Arrays.fill(values, null);
   }

   private class KeySet extends AbstractSet<K> {
      @Override
      public Iterator<K> iterator() {
         return new It<>(i -> (K) keys[i], keys.length, modCount);
      }

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

      @Override
      public void clear() {
         ArrayMap.this.clear();
      }

      @Override
      public boolean contains(Object o) {
         return containsKey(o);
      }

      @Override
      public boolean remove(Object o) {
         return ArrayMap.this.remove(o) != null;
      }
   }

   private class Values extends AbstractCollection<V> {
      @Override
      public Iterator<V> iterator() {
         return new It<>(i -> (V) values[i], values.length, modCount);
      }

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

      @Override
      public void clear() {
         ArrayMap.this.clear();
      }
   }

   private class EntrySet extends AbstractSet<Entry<K, V>> {
      @Override
      public Iterator<Entry<K, V>> iterator() {
         return new It<>(i -> {
            Object key = keys[i];
            return key == null ? null : new SimpleEntry<K, V>((K) key, (V) values[i]);
         }, keys.length, modCount);
      }

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

      @Override
      public void clear() {
         ArrayMap.this.clear();
      }
   }

   private class It<T> implements Iterator<T> {
      private final IntFunction<T> retriever;
      private final int length;
      private final int initialModCount;
      private int last = -1, pos = 0;
      private T next;

      public It(IntFunction<T> retriever, int length, int initialModCount) {
         this.retriever = retriever;
         this.length = length;
         this.initialModCount = initialModCount;
      }

      @Override
      public boolean hasNext() {
         if (initialModCount != modCount) {
            throw new ConcurrentModificationException();
         }
         if (next != null) {
            return true;
         }
         while (pos < length) {
            next = retriever.apply(pos);
            ++pos;
            if (next != null) {
               last = pos -1;
               return true;
            }
         }
         return false;
      }

      @Override
      public T next() {
         if (hasNext()) {
            T tmp = next;
            next = null;
            return tmp;
         } else {
            throw new NoSuchElementException();
         }
      }

      @Override
      public void remove() {
         if (initialModCount != modCount) {
            throw new ConcurrentModificationException();
         }
         keys[last] = null;
         values[last] = null;
      }
   }
}
