/*
 * 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 com.intellij.openapi.util.Getter;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

import static com.intellij.reference.SoftReference.deref;

/**
 * Concurrent map with weak keys and soft values.
 * Null keys are NOT allowed
 * Null values are NOT allowed
 * @deprecated Use {@link ContainerUtil#createConcurrentWeakKeySoftValueMap(int, float, int, gnu.trove.TObjectHashingStrategy)} instead
 */
public class ConcurrentWeakKeySoftValueHashMap<K, V> implements ConcurrentMap<K, V> {
  private final ConcurrentMap<KeyReference<K,V>, ValueReference<K,V>> myMap;
  final ReferenceQueue<K> myKeyQueue = new ReferenceQueue<K>();
  final ReferenceQueue<V> myValueQueue = new ReferenceQueue<V>();
  @NotNull final TObjectHashingStrategy<K> myHashingStrategy;

  public ConcurrentWeakKeySoftValueHashMap(int initialCapacity,
                                              float loadFactor,
                                              int concurrencyLevel,
                                              @NotNull final TObjectHashingStrategy<K> hashingStrategy) {
    myHashingStrategy = hashingStrategy;
    myMap = ContainerUtil.newConcurrentMap(initialCapacity, loadFactor, concurrencyLevel, ContainerUtil.<KeyReference<K,V>>canonicalStrategy());
  }

  public interface KeyReference<K, V> extends Getter<K> {
    @Override
    K get();

    @NotNull
    ValueReference<K,V> getValueReference(); // no strong references

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

    @Override
    int hashCode();
  }

  public interface ValueReference<K, V> extends Getter<V> {
    @NotNull
    KeyReference<K,V> getKeyReference(); // no strong references

    @Override
    V get();
  }

  static class WeakKey<K, V> extends WeakReference<K> implements KeyReference<K, V> {
    private final int myHash; // Hash code of the key, stored here since the key may be tossed by the GC
    private final TObjectHashingStrategy<K> myStrategy;
    @NotNull private final ValueReference<K, V> myValueReference;

    WeakKey(@NotNull K k,
            @NotNull ValueReference<K, V> valueReference,
            @NotNull TObjectHashingStrategy<K> strategy,
            @NotNull ReferenceQueue<K> queue) {
      super(k, queue);
      myValueReference = valueReference;
      myHash = strategy.computeHashCode(k);
      myStrategy = strategy;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof KeyReference)) return false;
      K t = get();
      K other = ((KeyReference<K,V>)o).get();
      if (t == null || other == null) return false;
      if (t == other) return true;
      return myHash == o.hashCode() && myStrategy.equals(t, other);
    }
    @Override
    public int hashCode() {
      return myHash;
    }

    @NotNull
    @Override
    public ValueReference<K, V> getValueReference() {
      return myValueReference;
    }
  }

  private static class SoftValue<K, V> extends SoftReference<V> implements ValueReference<K,V> {
   @NotNull private volatile KeyReference<K, V> myKeyReference; // can't make it final because of circular dependency of KeyReference to ValueReference
   private SoftValue(@NotNull V value, @NotNull ReferenceQueue<V> queue) {
     super(value, queue);
   }

   // When referent is collected, equality should be identity-based (for the processQueues() remove this very same SoftValue)
   // otherwise it's just canonical equals on referents for replace(K,V,V) to work
   @Override
   public final boolean equals(final Object o) {
     if (this == o) return true;
     if (o == null) return false;

     V v = get();
     Object thatV = ((ValueReference)o).get();
     return v != null && thatV != null && v.equals(thatV);
   }

   @NotNull
   @Override
   public KeyReference<K, V> getKeyReference() {
     return myKeyReference;
   }
 }

  @NotNull
  protected KeyReference<K,V> createKeyReference(@NotNull K k, @NotNull final V v) {
    final ValueReference<K, V> valueReference = createValueReference(v, myValueQueue);
    WeakKey<K, V> keyReference = new WeakKey<K, V>(k, valueReference, myHashingStrategy, myKeyQueue);
    if (valueReference instanceof SoftValue) {
      ((SoftValue)valueReference).myKeyReference = keyReference;
    }
    return keyReference;
  }

  @NotNull
  protected ValueReference<K, V> createValueReference(@NotNull V value, @NotNull ReferenceQueue<V> queue) {
    return new SoftValue<K, V>(value, queue);
  }

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

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

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

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

    private void set(@NotNull K key, int hash) {
      myKey = key;
      myHash = hash;
    }

    private void clear() {
      myKey = null;
    }

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

    @Override
    public boolean equals(Object o) {
      return o.equals(this); // see WeakKey.equals()
    }

    @Override
    public int hashCode() {
      return myHash;
    }

    @NotNull
    @Override
    public ValueReference<K, V> getValueReference() {
      throw new UnsupportedOperationException();
    }
  }

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

  @NotNull
  private HardKey<K,V> createHardKey(Object o) {
    @SuppressWarnings("unchecked") K key = (K)o;
    @SuppressWarnings("unchecked") HardKey<K,V> hardKey = HARD_KEY.get();
    hardKey.set(key, myHashingStrategy.computeHashCode(key));
    return hardKey;
  }

///////////////////////////////////

  @Override
  public V get(@NotNull Object key) {
    HardKey<K,V> hardKey = createHardKey(key);
    ValueReference<K, V> valueReference = myMap.get(hardKey);
    V v = deref(valueReference);
    hardKey.clear();
    return v;
  }

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

  @Override
  public boolean containsValue(Object value) {
    throw new UnsupportedOperationException();
  }

  @Override
  public V remove(Object key) {
    processQueues();
    HardKey<K,V> hardKey = createHardKey(key);
    ValueReference<K, V> valueReference = myMap.remove(hardKey);
    V v = deref(valueReference);
    hardKey.clear();
    return v;
  }

  @Override
  public void putAll(@NotNull Map<? extends K, ? extends V> m) {
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
      put(e.getKey(), e.getValue());
    }
  }

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

    KeyReference<K, V> keyReference = createKeyReference(key, value);
    ValueReference<K,V> valueReference = keyReference.getValueReference();
    ValueReference<K, V> prevValReference = myMap.put(keyReference, valueReference);

    return deref(prevValReference);
  }

  boolean processQueues() {
    boolean removed = false;
    KeyReference<K,V> keyReference;
    while ((keyReference = (KeyReference<K, V>)myKeyQueue.poll()) != null) {
      ValueReference<K, V> valueReference = keyReference.getValueReference();
      removed |= myMap.remove(keyReference, valueReference);
    }

    ValueReference<K,V> valueReference;
    while ((valueReference = (ValueReference<K, V>)myValueQueue.poll()) != null) {
      keyReference = valueReference.getKeyReference();
      removed |= myMap.remove(keyReference, valueReference);
    }

    return removed;
  }

  @NotNull
  @Override
  public Set<K> keySet() {
    throw new UnsupportedOperationException();
  }

  @NotNull
  @Override
  public Collection<V> values() {
    throw new UnsupportedOperationException();
  }

  @NotNull
  @Override
  public Set<Entry<K, V>> entrySet() {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean remove(@NotNull Object key, @NotNull Object value) {
    processQueues();

    HardKey<K, V> hardKey = createHardKey(key);
    ValueReference<K, V> valueReference = myMap.get(hardKey);
    V v = deref(valueReference);

    boolean result = value.equals(v) && myMap.remove(hardKey, valueReference);
    hardKey.clear();
    return result;
  }

  @Override
  public V putIfAbsent(@NotNull K key, @NotNull V value) {
    processQueues();
    KeyReference<K, V> keyReference = createKeyReference(key, value);
    ValueReference<K, V> valueReference = keyReference.getValueReference();

    ValueReference<K, V> result = myMap.putIfAbsent(keyReference, valueReference);
    return deref(result);
  }

  @Override
  public boolean replace(@NotNull K key, @NotNull V oldValue, @NotNull V newValue) {
    processQueues();
    KeyReference<K, V> oldKeyReference = createKeyReference(key, oldValue);
    ValueReference<K, V> oldValueReference = oldKeyReference.getValueReference();
    KeyReference<K, V> newKeyReference = createKeyReference(key, newValue);
    ValueReference<K, V> newValueReference = newKeyReference.getValueReference();

    return myMap.replace(oldKeyReference, oldValueReference, newValueReference);
  }

  @Override
  public V replace(@NotNull K key, @NotNull V value) {
    processQueues();
    KeyReference<K, V> keyReference = createKeyReference(key, value);
    ValueReference<K, V> valueReference = keyReference.getValueReference();
    ValueReference<K, V> result = myMap.replace(keyReference, valueReference);
    return deref(result);
  }
}
