/*
 * $Id: ConcurrentSoftHashMap.java 3888 2008-07-27 14:45:45Z uckelman $
 *
 * Copyright (c) 2007 by Joel Uckelman
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */

package VASSAL.tools;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * A memory-sensitive {@link ConcurrentMap} which stores the values
 * in {@link SoftReference}s. This <code>ConcurrentMap</code> grows
 * without bound, but when the JVM is under memory pressure, values
 * held by it may be garbage collected.
 *
 * <p>All methods except {@link #get} cause the <code>Map</code> to
 * be cleared of key-value pairs for which the value has been garbage
 * collected. Processing key-value pairs with dead values is <em>not</em>
 * an atomic operation. Thus, it is possible, though unlikely, that more
 * values will be garbage collected between the removal of dead key-value
 * pairs and the return of the method in which this takes place.</p>
 *
 * <p>This implementation does not permit <code>null</code> keys or
 * values.</p>
 *
 * @since 3.1.0
 * @author Joel Uckelman
 */
public class ConcurrentSoftHashMap<K,V> extends AbstractMap<K,V>
                                        implements ConcurrentMap<K,V> {

  private static final class SoftValue<K,V> extends SoftReference<V> {
    private final K key;

    private SoftValue(K key, V value, ReferenceQueue<V> queue) {
      super(value, queue);
      this.key = key;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || !(o instanceof SoftValue)) return false;
      final SoftValue sv = (SoftValue) o;
      return key.equals(sv.key) &&
             get() == null ? sv.get() == null : get().equals(sv.get());
    }

    @Override
    public int hashCode() {
      return get() == null ? 0 : get().hashCode(); 
    }
  } 

  private final ConcurrentMap<K,SoftValue<K,V>> map =
    new ConcurrentHashMap<K,SoftValue<K,V>>();

  private final ReferenceQueue<V> queue = new ReferenceQueue<V>();

  @SuppressWarnings("unchecked")
  private void processQueue() { 
    SoftValue<K,V> sv;
    // The ReferenceQueue API is broken. ReferenceQueue<T>.poll()
    // returns a Reference<? extends T>. WTF? How could you ever use
    // this without having to cast back to the kind of Reference
    // you put in?
    while ((sv = (SoftValue<K,V>) queue.poll()) != null) {
      map.remove(sv.key, sv);
//      System.out.println("Hasta la vista, " + sv.key + ".");
    }
//    System.out.println("Cache size = " + map.size());
  }

  // Query Operations

  /** {@inheritDoc} */
  @Override
  public int size() {
    processQueue();
    return map.size();
  }

  /** {@inheritDoc} */
  @Override 
  public boolean containsKey(Object key) {
    if (key == null)
      throw new NullPointerException();

    processQueue();
    return map.containsKey(key);
  }

  /** {@inheritDoc} */
  @Override
  public V get(Object key) {
    if (key == null)
      throw new NullPointerException();

    final SoftValue<K,V> sv = map.get(key);
    if (sv != null) {
      final V value = sv.get();
      if (value == null) {
        map.remove(key, sv);
      }
      return value;
    }
    return null;
  }

  // Modification Operations

  /** {@inheritDoc} */
  @Override
  public V put(K key, V value) {
    if (key == null)
      throw new NullPointerException();
    if (value == null)
      throw new NullPointerException();

    processQueue();
    final SoftValue<K,V> oldSV =
      map.put(key, new SoftValue<K,V>(key, value, queue));
    return oldSV == null ? null : oldSV.get();
  }

  /** {@inheritDoc} */
  @Override
  public V remove(Object key) {
    if (key == null)
      throw new NullPointerException();

    processQueue();
    final SoftValue<K,V> oldSV = map.remove(key);
    return oldSV == null ? null : oldSV.get();
  }

  // Bulk Operations
 
  /** {@inheritDoc} */
  @Override
  public void clear() {
    map.clear();
    while (queue.poll() != null);
  }

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

  /** 
   * An implementation of {@link Map.Entry}. Remove this and use
   * {@link AbstractMap.SimpleEntry} with 1.6+. 
   */ 
  public static class SimpleEntry<K,V> implements Entry<K,V> {
    private final K key;
    private V value;

    public SimpleEntry(K key, V value) {
      this.key   = key;
      this.value = value;
    }

    public SimpleEntry(Entry<? extends K, ? extends V> entry) {
      this.key   = entry.getKey();
      this.value = entry.getValue();
    }

    public K getKey() {
      return key;
    }

    public V getValue() {
      return value;
    }

    public V setValue(V value) {
      V oldValue = this.value;
      this.value = value;
      return oldValue;
    }

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

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

    public String toString() {
      return key + "=" + value;
    }

    private static boolean eq(Object o1, Object o2) {
        return o1 == null ? o2 == null : o1.equals(o2);
    }
  }
 
  /** {@inheritDoc} */
  public Set<Map.Entry<K, V>> entrySet() {
    processQueue();

    if (entrySet == null) {
      entrySet = new AbstractSet<Map.Entry<K,V>>() {
        public Iterator<Map.Entry<K,V>> iterator() {
          return new Iterator<Map.Entry<K,V>>() {
            private final Iterator<Map.Entry<K,SoftValue<K,V>>> i =
              map.entrySet().iterator();

            public boolean hasNext() {
              return i.hasNext();
            }

            public Map.Entry<K,V> next() {
              final Map.Entry<K,SoftValue<K,V>> e = i.next();
              return new SimpleEntry<K,V>(e.getKey(), e.getValue().get()); 
            }

            public void remove() {
              i.remove();
            }
          };
        }

        public int size() {
          return ConcurrentSoftHashMap.this.size();
        }

        public boolean contains(Object v) {
          return ConcurrentSoftHashMap.this.containsValue(v);
        }  
      };
    }

    return entrySet;
  }
  
  // Concurrent Operations

  /** {@inheritDoc} */
  public V putIfAbsent(K key, V value) {
    if (key == null)
      throw new NullPointerException();
    if (value == null)
      throw new NullPointerException();

    processQueue();
    final SoftValue<K,V> oldSV =
      map.putIfAbsent(key, new SoftValue<K,V>(key, value, queue));
    return oldSV == null ? null : oldSV.get();
  }

  /** {@inheritDoc} */
  public boolean remove(Object key, Object value) {
    if (key == null)
      throw new NullPointerException();
    if (value == null)
      throw new NullPointerException();

    processQueue();
    return map.remove(key, new SoftValue<Object,Object>(key, value, null));
  }

  /** {@inheritDoc} */
  public boolean replace(K key, V oldValue, V newValue) {
    if (key == null)
      throw new NullPointerException();
    if (oldValue == null)
      throw new NullPointerException();
    if (newValue == null)
      throw new NullPointerException();

    processQueue();
    return map.replace(key,
                       new SoftValue<K,V>(key, oldValue, null),
                       new SoftValue<K,V>(key, newValue, queue));
  }

  /** {@inheritDoc} */
  public V replace(K key, V value) {
    if (key == null)
      throw new NullPointerException();
    if (value == null)
      throw new NullPointerException();

    processQueue();
    final SoftValue<K,V> oldSV =
      map.replace(key, new SoftValue<K,V>(key, value, queue));
    return oldSV == null ? null : oldSV.get(); 
  }
}
