/*
 * Copyright 2000-2015 JetBrains s.r.o.
 *
 * 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.
 */

/*
 * Created by IntelliJ IDEA.
 * User: Alexey
 * Date: 18.12.2006
 * Time: 20:18:31
 */
package com.intellij.util.containers;

import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.lang.ref.ReferenceQueue;
import java.util.*;
import java.util.concurrent.ConcurrentMap;

/**
 * Base class for concurrent (soft/weak) key:K -> strong value:V map
 * Null keys are allowed
 * Null values are NOT allowed
 */
abstract class ConcurrentRefHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, TObjectHashingStrategy<K> {
  protected final ReferenceQueue<K> myReferenceQueue = new ReferenceQueue<K>();
  private final ConcurrentMap<KeyReference<K, V>, V> myMap; // hashing strategy must be canonical, we compute corresponding hash codes using our own myHashingStrategy
  @NotNull
  private final TObjectHashingStrategy<K> myHashingStrategy;

  interface KeyReference<K, V> {
    K get();

    @NotNull
    V getValue();

    // MUST work even with gced references for the code in processQueue to work
    boolean equals(Object o);

    int hashCode();
  }

  protected abstract KeyReference<K, V> createKeyReference(@NotNull K key, @NotNull V value, @NotNull TObjectHashingStrategy<K> hashingStrategy);

  private static final HardKey NULL_KEY = new HardKey() {
    @Override
    public Object get() {
      return null;
    }

    @Override
    void setKey(Object key, int hash) {
    }
  };

  @NotNull
  private KeyReference<K, V> createKeyReference(@Nullable K key, @NotNull V value) {
    if (key == null) {
      //noinspection unchecked
      return NULL_KEY;
    }
    return createKeyReference(key, value, myHashingStrategy);
  }

  // returns true if some keys were processed
  boolean processQueue() {
    KeyReference<K, V> wk;
    boolean processed = false;
    while ((wk = (KeyReference)myReferenceQueue.poll()) != null) {
      V value = wk.getValue();
      myMap.remove(wk, value);
      processed = true;
    }
    return processed;
  }

  public ConcurrentRefHashMap(@NotNull Map<? extends K, ? extends V> t) {
    this(Math.max(2 * t.size(), 11), ConcurrentHashMap.LOAD_FACTOR);
    putAll(t);
  }

  public ConcurrentRefHashMap() {
    this(ConcurrentHashMap.DEFAULT_CAPACITY);
  }

  public ConcurrentRefHashMap(int initialCapacity) {
    this(initialCapacity, ConcurrentHashMap.LOAD_FACTOR);
  }

  private static final TObjectHashingStrategy THIS = new TObjectHashingStrategy() {
    @Override
    public int computeHashCode(Object object) {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean equals(Object o1, Object o2) {
      throw new UnsupportedOperationException();
    }
  };
  public ConcurrentRefHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 4, THIS);
  }

  public ConcurrentRefHashMap(@NotNull final TObjectHashingStrategy<K> hashingStrategy) {
    this(ConcurrentHashMap.DEFAULT_CAPACITY, ConcurrentHashMap.LOAD_FACTOR, 2, hashingStrategy);
  }

  public ConcurrentRefHashMap(int initialCapacity,
                              float loadFactor,
                              int concurrencyLevel,
                              @NotNull TObjectHashingStrategy<K> hashingStrategy) {
    myHashingStrategy = hashingStrategy == THIS ? this : hashingStrategy;
    myMap = ContainerUtil.<KeyReference<K, V>, V>newConcurrentMap(initialCapacity, loadFactor, concurrencyLevel, CANONICAL);
  }

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

  @Override
  public boolean isEmpty() {
    return entrySet().isEmpty();
  }

  @Override
  public boolean containsKey(@Nullable Object key) {
    HardKey<K, V> hardKey = createHardKey(key);
    boolean result = myMap.containsKey(hardKey);
    releaseHardKey(hardKey);
    return result;
  }

  private static class HardKey<K, V> implements KeyReference<K, V> {
    private K myKey;
    private int myHash;

    void setKey(K key, final int hash) {
      myKey = key;
      myHash = hash;
    }

    @Override
    public K get() {
      return myKey;
    }

    @NotNull
    @Override
    public V getValue() {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean equals(Object o) {
      return o.equals(this); // see com.intellij.util.containers.ConcurrentSoftHashMap.SoftKey or com.intellij.util.containers.ConcurrentWeakHashMap.WeakKey
    }

    public int hashCode() {
      return myHash;
    }
  }
  private static final ThreadLocal<HardKey> HARD_KEY = new ThreadLocal<HardKey>() {
    @Override
    protected HardKey initialValue() {
      return new HardKey();
    }
  };

  @NotNull
  private HardKey<K, V> createHardKey(@Nullable Object o) {
    if (o == null) {
      //noinspection unchecked
      return NULL_KEY;
    }
    @SuppressWarnings("unchecked") K key = (K)o;
    @SuppressWarnings("unchecked") HardKey<K,V> hardKey = HARD_KEY.get();
    hardKey.setKey(key, myHashingStrategy.computeHashCode(key));
    return hardKey;
  }

  private static void releaseHardKey(@NotNull HardKey<?,?> key) {
    key.setKey(null, 0);
  }

  @Override
  public V get(@Nullable Object key) {
    HardKey<K, V> hardKey = createHardKey(key);
    V result = myMap.get(hardKey);
    releaseHardKey(hardKey);
    return result;
  }

  @Override
  public V put(@Nullable K key, @NotNull V value) {
    processQueue();
    KeyReference<K, V> weakKey = createKeyReference(key, value);
    return myMap.put(weakKey, value);
  }

  @Override
  public V remove(@Nullable Object key) {
    processQueue();

    HardKey hardKey = createHardKey(key);
    V result = myMap.remove(hardKey);
    releaseHardKey(hardKey);
    return result;
  }

  @Override
  public void clear() {
    processQueue();
    myMap.clear();
  }

  private static class RefEntry<K, V> implements Map.Entry<K, V> {
    private final Map.Entry<?, V> ent;
    private final K key; /* Strong reference to key, so that the GC
                                 will leave it alone as long as this Entry
                                 exists */

    RefEntry(@NotNull Map.Entry<?, V> ent, @Nullable K key) {
      this.ent = ent;
      this.key = key;
    }

    @Override
    public K getKey() {
      return key;
    }

    @Override
    public V getValue() {
      return ent.getValue();
    }

    @Override
    public V setValue(@NotNull V value) {
      return ent.setValue(value);
    }

    private static boolean valEquals(Object o1, Object o2) {
      return o1 == null ? o2 == null : o1.equals(o2);
    }

    public boolean equals(Object o) {
      if (!(o instanceof Map.Entry)) return false;
      Map.Entry e = (Map.Entry)o;
      return valEquals(key, e.getKey()) && valEquals(getValue(), e.getValue());
    }

    public int hashCode() {
      Object v;
      return (key == null ? 0 : key.hashCode()) ^ ((v = getValue()) == null ? 0 : v.hashCode());
    }
  }

  /* Internal class for entry sets */
  private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
    Set<Map.Entry<KeyReference<K, V>, V>> hashEntrySet = myMap.entrySet();

    @NotNull
    @Override
    public Iterator<Map.Entry<K, V>> iterator() {
      return new Iterator<Map.Entry<K, V>>() {
        private final Iterator<Map.Entry<KeyReference<K, V>, V>> hashIterator = hashEntrySet.iterator();
        private RefEntry<K, V> next;

        @Override
        public boolean hasNext() {
          while (hashIterator.hasNext()) {
            Map.Entry<KeyReference<K, V>, V> ent = hashIterator.next();
            KeyReference<K, V> wk = ent.getKey();
            K k = null;
            if (wk != null && (k = wk.get()) == null && wk != NULL_KEY) {
              /* Weak key has been cleared by GC */
              continue;
            }
            next = new RefEntry<K, V>(ent, k);
            return true;
          }
          return false;
        }

        @Override
        public Map.Entry<K, V> next() {
          if (next == null && !hasNext()) {
            throw new NoSuchElementException();
          }
          RefEntry<K, V> e = next;
          next = null;
          return e;
        }

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

    @Override
    public boolean isEmpty() {
      return !iterator().hasNext();
    }

    @Override
    public int size() {
      int j = 0;
      for (Iterator i = iterator(); i.hasNext(); i.next()) j++;
      return j;
    }

    @Override
    public boolean remove(Object o) {
      processQueue();
      if (!(o instanceof Map.Entry)) return false;
      Map.Entry<K,V> e = (Map.Entry)o;
      V ev = e.getValue();

      HardKey key = createHardKey(e.getKey());

      V hv = myMap.get(key);
      boolean toRemove = hv == null ? ev == null && myMap.containsKey(key) : hv.equals(ev);
      if (toRemove) {
        myMap.remove(key);
      }

      releaseHardKey(key);
      return toRemove;
    }

    public int hashCode() {
      int h = 0;
      for (Object aHashEntrySet : hashEntrySet) {
        Map.Entry ent = (Map.Entry)aHashEntrySet;
        KeyReference wk = (KeyReference)ent.getKey();
        if (wk == null) continue;
        Object v;
        h += wk.hashCode() ^ ((v = ent.getValue()) == null ? 0 : v.hashCode());
      }
      return h;
    }
  }

  private Set<Map.Entry<K, V>> entrySet;

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

  @Override
  public V putIfAbsent(@Nullable final K key, @NotNull V value) {
    processQueue();
    return myMap.putIfAbsent(createKeyReference(key, value), value);
  }

  @Override
  public boolean remove(@Nullable final Object key, @NotNull Object value) {
    processQueue();
    return myMap.remove(createKeyReference((K)key, (V)value), value);
  }

  @Override
  public boolean replace(@Nullable final K key, @NotNull final V oldValue, @NotNull final V newValue) {
    processQueue();
    return myMap.replace(createKeyReference(key, oldValue), oldValue, newValue);
  }

  @Override
  public V replace(@Nullable final K key, @NotNull final V value) {
    processQueue();
    return myMap.replace(createKeyReference(key, value), value);
  }

  // MAKE SURE IT CONSISTENT WITH com.intellij.util.containers.ConcurrentHashMap
  @Override
  public int computeHashCode(final K object) {
    int h = object.hashCode();
    h += ~(h << 9);
    h ^= (h >>> 14);
    h += (h << 4);
    h ^= (h >>> 10);
    return h;
  }

  @Override
  public boolean equals(final K o1, final K o2) {
    return o1.equals(o2);
  }

  @TestOnly
  int underlyingMapSize() {
    return myMap.size();
  }
}
