/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (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.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.util.cache;

import java.lang.ref.*;
import java.util.*;

/**
 * A special kind of <CODE>Map</CODE> that acts as a cache of data items.  The <CODE>CacheMap</CODE>
 * uses <CODE>SoftReferences</CODE>, so its data values will be shed if the virtual machine needs
 * more memory; also, if too many entries are added to the map, certain entries will be removed in
 * accordance with the supplied or default <CODE>CacheMapStrategy</CODE> object.
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 * @see CacheMapStrategy
 * @see java.util.Map
 */
public class CacheMap implements Map
{
  /*--------------------------------------------------------------------------------
   * Internal class used to do comparisons for cache shrinkage
   *--------------------------------------------------------------------------------
   */

  static final class CacheOrdering implements Comparator
  {
    private CacheMapStrategy strategy;   // CacheMap's strategy object
    private long tick;                   // when the sort operation started

    CacheOrdering(CacheMapStrategy strategy)
    {
      this.strategy = strategy;
      this.tick = System.currentTimeMillis();

    } // end constructor

    public int compare(Object o1, Object o2)
    {
      long figm1 = strategy.getEntryValue((CacheMapEntry)o1,tick);
      long figm2 = strategy.getEntryValue((CacheMapEntry)o2,tick);
      return (int)(figm1 - figm2);  // we want the largest figures of merit to go first

    } // end compare

    public boolean equals(Object o)
    {
      return (o instanceof CacheOrdering);

    } // end equals

  } // end class CacheOrdering

  /*--------------------------------------------------------------------------------
   * Internal class implementing a default cache ordering strategy
   *--------------------------------------------------------------------------------
   */

  static final class DefaultStrategy implements CacheMapStrategy
  {
    private static final long SCALING_FACTOR = 5000;

    DefaultStrategy()
    { // do nothing
    } // end constructor

    public long getEntryValue(CacheMapEntry entry, long tick)
    {
      return (entry.getHits() * SCALING_FACTOR) - entry.getAge(tick);

    } // end getEntryValue

  } // end class DefaultStrategy

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static final DefaultStrategy default_strategy_singleton = new DefaultStrategy();

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private int capacity;                // capacity of the CacheMap
  private int shrink_percentage;       // what percentage we shrink by when full
  private CacheMapStrategy strategy;   // strategy routine to use to purge entries
  private HashMap base_map;            // maps keys to CacheMapEntry values
  private ArrayList element_list;      // the actual elements
  private ReferenceQueue rq;           // holds references that the garbage collector has cleared

  /*--------------------------------------------------------------------------------
   * Constructors
   *--------------------------------------------------------------------------------
   */

  /**
   * Constructs a new <CODE>CacheMap</CODE>.
   *
   * @param capacity The maximum number of entries this map can contain.
   * @param shrink_percentage The percentage of entries which will be removed from the cache
   *                          whenever it needs to shrink itself.
   * @param strategy The strategy object which is used to determine which elements to remove.
   * @exception java.lang.IllegalArgumentException If the specified capacity is negative or zero, or
   *            the shrink percentage is not in the range [1..100].
   * @exception java.lang.NullPointerException If the strategy object reference is <CODE>null</CODE>.
   */
  public CacheMap(int capacity, int shrink_percentage, CacheMapStrategy strategy)
  {
    if (capacity<=0)
      throw new IllegalArgumentException("capacity must be greater than 0");
    if ((shrink_percentage<=0) || (shrink_percentage>100))
      throw new IllegalArgumentException("shrink_percentage must be in [1, 100]");
    if (strategy==null)
      throw new NullPointerException("no strategy passed to CacheMap");

    this.capacity = capacity;
    this.shrink_percentage = shrink_percentage;
    this.strategy = strategy;
    this.base_map = new HashMap(10);
    this.element_list = new ArrayList(10);
    this.rq = new ReferenceQueue();

  } // end constructor

  /**
   * Constructs a new <CODE>CacheMap</CODE> with a default strategy.
   *
   * @param capacity The maximum number of entries this map can contain.
   * @param shrink_percentage The percentage of entries which will be removed from the cache
   *                          whenever it needs to shrink itself.
   * @exception java.lang.IllegalArgumentException If the specified capacity is negative or zero, or
   *            the shrink percentage is not in the range [1..100].
   */
  public CacheMap(int capacity, int shrink_percentage)
  {
    this(capacity,shrink_percentage,default_strategy_singleton);

  } // end constructor

  /**
   * Constructs a new <CODE>CacheMap</CODE> with a default strategy and shrink percentage.
   *
   * @param capacity The maximum number of entries this map can contain.
   * @exception java.lang.IllegalArgumentException If the specified capacity is negative or zero.
   */
  public CacheMap(int capacity)
  {
    this(capacity,10,default_strategy_singleton);

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  private void doSweep()
  {
    Reference r = rq.poll();              // the reference that's been cleared
    ArrayList ditch = new ArrayList();    // a list of entries to ditch
    Iterator it;                          // current iterator

    while (r!=null)
    { // look for the dead reference in the element list
      it = element_list.iterator();
      while (it.hasNext())
      { // check each cache map entry in return
	CacheMapEntry ntry = (CacheMapEntry)(it.next());
	if (ntry.matchReference(r))
	{ // remove the offending entry and save it in the temporary list
	  it.remove();
	  ditch.add(ntry);
	  break;

	} // end if

      } // end while

      r = rq.poll();  // get next dead reference

    } // end while

    if (ditch.isEmpty())
      return;  // nothing to prune

    it = ditch.iterator();
    while (it.hasNext())
    { // clear all entries from the base hashmap as well
      CacheMapEntry ntry = (CacheMapEntry)(it.next());
      base_map.remove(ntry.getKey());
      ntry.discard();

    } // end while

  } // end doSweep

  public synchronized void doShrink(int num_remove)
  {
    // Sort the element list to figure out which elements to remove.
    Collections.sort(element_list,new CacheOrdering(strategy));
    
    // The elements we want to remove are at the end of the array, so start from there.
    for (int i=0; i<num_remove; i++)
    { // remove the "removed" entries from the hash map
      CacheMapEntry cme = (CacheMapEntry)(element_list.remove(element_list.size() - 1));
      base_map.remove(cme.getKey());

    } // end for

  } // end doShrink

  /*--------------------------------------------------------------------------------
   * Implementations from interface Map
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns the number of key-value mappings in this map. If the map contains more than
   * <CODE>Integer.MAX_VALUE</CODE> elements, returns <CODE>Integer.MAX_VALUE</CODE>.
   *
   * @return The number of key-value mappings in this map.
   */
  public int size()
  {
    doSweep();
    return base_map.size();

  } // end size

  /**
   * Returns <CODE>true</CODE> if this map contains no key-value mappings.
   *
   * @return <CODE>true</CODE> if this map contains no key-value mappings.
   */
  public boolean isEmpty()
  {
    doSweep();
    return base_map.isEmpty();

  } // end isEmpty

  /**
   * Returns <CODE>true</CODE> if this map contains a mapping for the specified key.
   *
   * @param key Key whose presence in this map is to be tested.
   * @return <CODE>true</CODE> if this map contains a mapping for the specified key.
   * @exception java.lang.ClassCastException If the key is of an inappropriate type for this map.
   * @exception java.lang.NullPointerException If the key is <CODE>null</CODE>.
   */
  public boolean containsKey(Object key)
  {
    doSweep();
    return base_map.containsKey(key);

  } // end containsKey

  /**
   * Returns <CODE>true</CODE> if this map maps one or more keys to the specified value.
   *
   * @param value Value whose presence in this map is to be tested.
   * @return <CODE>true</CODE> if this map maps one or more keys to the specified value.
   */
  public boolean containsValue(Object value)
  {
    doSweep();
    Iterator it = element_list.iterator();
    while (it.hasNext())
    { // look at all the CacheMapEntry values we have
      CacheMapEntry cme = (CacheMapEntry)(it.next());
      Object my_val = cme.getValue();
      if (my_val==null)
      { // test for also null
	if (value==null)
	  return true;

      } // end if
      else
      { // make sure the other value is non-null before we test equality
	if ((value!=null) && my_val.equals(value))
	  return true;

      } // end else

    } // end while

    return false;  // nope, sorry

  } // end containsValue

  /**
   * Returns the value to which this map maps the specified key. Returns <CODE>null</CODE> if the map
   * contains no mapping for this key.
   *
   * @param key Key whose associated value is to be returned.
   * @return The value to which this map maps the specified key, or <CODE>null</CODE> if the map contains
   *         no mapping for this key.
   * @exception java.lang.ClassCastException If the key is of an inappropriate type for this map.
   * @exception java.lang.NullPointerException If the key is <CODE>null</CODE>.
   * @see #containsKey(java.lang.Object)
   */
  public Object get(Object key)
  {
    doSweep();
    CacheMapEntry cme = (CacheMapEntry)(base_map.get(key));
    if (cme==null)
      return null;
    cme.touch();
    return cme.getValue();

  } // end get

  /**
   * Associates the specified value with the specified key in this map. If the map previously contained
   * a mapping for this key, the old value is replaced.
   *
   * @param key Key with which the specified value is to be associated.
   * @param value Value to be associated with the specified key.
   * @return The previous value associated with the specified key, or <CODE>null</CODE> if there was no
   *         mapping for the key.
   * @exception java.lang.ClassCastException If the class of the specified key or value prevents it from
   *            being stored in this map.
   * @exception java.lang.IllegalArgumentException If some aspect of this key or value prevents it from
   *            being stored in this map.
   * @exception java.lang.NullPointerException If the specified key or value is <CODE>null</CODE>.
   */
  public Object put(Object key, Object value)
  {
    doSweep();
    Object rc = null;
    CacheMapEntry cme = (CacheMapEntry)(base_map.get(key));
    if (cme==null)
    { // create a new CacheMapEntry for this key
      cme = new CacheMapEntry(key,value,rq);

      synchronized (this)
      { // insert it into the basic object
	if (base_map.size()==capacity)
	  doShrink((element_list.size() * shrink_percentage) / 100);
	element_list.add(cme);
	base_map.put(cme.getKey(),cme);

      } // end synchronized block

    } // end if
    else
    { // we have an old value - replace it and touch the entry
      cme.touch();
      rc = cme.setValue(value,rq);

    } // end else

    return rc;

  } // end put

  /**
   * Removes the mapping for this key from this map if present.
   *
   * @param key Key whose mapping is to be removed from the map.
   * @return The previous value associated with the specified key, or <CODE>null</CODE> if there was no
   *         mapping for the key.
   */
  public Object remove(Object key)
  {
    doSweep();
    Object rc = null;
    CacheMapEntry cme = (CacheMapEntry)(base_map.get(key));
    if (cme!=null)
    { // save the mapped value before we remove it
      rc = cme.getValue();

      synchronized (this)
      { // remove the values
	base_map.remove(key);
	element_list.remove(cme);

      } // end synchronized block

      cme.discard();  // zap the reference

    } // end if

    return rc;

  } // end remove

  /**
   * Copies all of the mappings from the specified map to this map. These mappings will replace any mappings
   * that this map had for any of the keys currently in the specified map.
   *
   * @param map Mappings to be stored in this map.
   * @exception java.lang.ClassCastException If the class of a key or value in the specified map prevents
   *            it from being stored in this map.
   * @exception java.lang.IllegalArgumentException If some aspect of a key or value in the specified map
   *            prevents it from being stored in this map.
   * @exception java.lang.NullPointerException If the specified key or value is <CODE>null</CODE>.
   */
  public void putAll(Map map)
  {
    doSweep();
    synchronized (this)
    { // make sure we have enough space in the CacheMap for all the new elements!
      int nover = (map.size() + base_map.size()) - capacity;
      if (nover>0)
	doShrink(nover);

    } // end synchronized block

    Iterator it = map.entrySet().iterator();
    while (it.hasNext())
    { // add each element in turn
      Map.Entry me = (Map.Entry)(it.next());
      put(me.getKey(),me.getValue());

    } // end while

  } // end putAll

  /**
   * Removes all mappings from this map.
   */
  public synchronized void clear()
  {
    base_map.clear();
    Iterator it = element_list.iterator();
    while (it.hasNext())
    { // discard all entries we have
      CacheMapEntry cme = (CacheMapEntry)(it.next());
      cme.discard();

    } // end while

    element_list.clear();

  } // end clear

  /**
   * Returns a set view of the keys contained in this map.
   *
   * @return A set view of the keys contained in this map.
   */
  public Set keySet()
  {
    return base_map.keySet();

  } // end keySet

  /**
   * Returns a collection view of the values contained in this map.
   *
   * @return A collection view of the values contained in this map.
   * @exception java.lang.UnsupportedOperationException This map does not support this operation.
   */
  public Collection values()
  {
    throw new UnsupportedOperationException("CacheMap.values() is not implemented");

  } // end values

  /**
   * Returns a set view of the mappings contained in this map.
   *
   * @return A set view of the mappings contained in this map.
   * @exception java.lang.UnsupportedOperationException This map does not support this operation.
   */
  public Set entrySet()
  {
    throw new UnsupportedOperationException("CacheMap.entrySet() is not implemented");

  } // end entrySet

  /**
   * Compares the specified object with this map for equality. Returns <CODE>true</CODE> if the given object
   * is also a map and the two Maps represent the same mappings.
   *
   * @param o Object to be compared for equality with this map.
   * @return <CODE>true</CODE> if the specified object is equal to this map.
   */
  public boolean equals(Object o)
  {
    if ((o==null) || !(o instanceof Map))
      return false;  // not a map
    doSweep();
    Map other = (Map)o;
    if (other.size()!=base_map.size())
      return false;  // size does matter!
    Iterator it = base_map.values().iterator();
    while (it.hasNext())
    { // get each of the entries out and use that to do a key-value comparison
      CacheMapEntry cme = (CacheMapEntry)(it.next());
      Object o1 = cme.getValue();
      Object o2 = other.get(cme.getKey());
      if (o1==null)
      { // must have a matching null
	if (o2!=null)
	  return false;

      } // end if
      else
      { // make sure we have a matching object (not null)
	if ((o2==null) || !(o2.equals(o1)))
	  return false;

      } // end else

    } // end while

    return true;  // all OK!

  } // end equals

  /**
   * Returns the hash code value for this map. The hash code of a map is defined to be the sum of the
   * hash codes of each entry in the map's entrySet view.
   *
   * @return The hash code value for this map.
   * @see #equals(java.lang.Object)
   * @see CacheMapEntry#hashCode()
   */
  public int hashCode()
  {
    doSweep();
    int rc = 0;
    Iterator it = base_map.values().iterator();
    while (it.hasNext())
    { // add up the hash codes and return them
      CacheMapEntry cme = (CacheMapEntry)(it.next());
      rc += cme.hashCode();

    } // end while

    return rc;

  } // end hashCode

  /*--------------------------------------------------------------------------------
   * External getters/setters
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns the capacity of this cache map.
   *
   * @return The capacity of this cache map.
   */
  public int getCapacity()
  {
    return capacity;

  } // end getCapacity

  /**
   * Sets the capacity of this cache map.
   *
   * @param c The new capacity for this cache map.
   * @exception java.lang.IllegalArgumentException If the specified capacity is negative or zero.
   */
  public void setCapacity(int c)
  {
    if (c<=0)
      throw new IllegalArgumentException("capacity must be greater than 0");
    capacity = c;

  } // end setCapacity

  /**
   * Returns the shrink percentage of this cache map.
   *
   * @return The shrink percentage of this cache map.
   */
  public int getShrinkPercentage()
  {
    return shrink_percentage;

  } // end getShrinkPercentage

  /**
   * Sets the shrink percentage of this cache map.
   *
   * @param p The new shrink percentage for this cache map.
   * @exception java.lang.IllegalArgumentException If the specified shrink percentage is not in the
   *            range [1..100].
   */
  public void setShrinkPercentage(int p)
  {
    if ((p<=0) || (p>100))
      throw new IllegalArgumentException("shrink_percentage must be in [1, 100]");
    shrink_percentage = p;

  } // end setShrinkPercentage

  /**
   * Returns the strategy object associated with the cache map.
   *
   * @return The strategy object associated with the cache map.
   */
  public CacheMapStrategy getStrategy()
  {
    return strategy;

  } // end getStrategy

  /**
   * Sets the strategy object associated with this cache map.
   *
   * @param s The new strategy object to be associated with the cache map.
   * @exception java.lang.NullPointerException If the strategy object reference is <CODE>null</CODE>.
   */
  public void setStrategy(CacheMapStrategy s)
  {
    if (s==null)
      throw new NullPointerException("no strategy passed to CacheMap");
    strategy = s;

  } // end setStrategy

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Causes the current size of the cache map to shrink by at least the shrink percentage specified
   * in the constructor or in <CODE>setShrinkPercentage</CODE>.  Cached items already reclaimed by the
   * garbage collector are stripped out first.
   */
  public synchronized void shrink()
  {
    // Figure out how many elements to remove.
    int num_remove = (element_list.size() * shrink_percentage) / 100;

    // Try a sweep first.
    int n1 = base_map.size();
    doSweep();
    n1 -= base_map.size();
    if (n1<num_remove)
      doShrink(num_remove - n1);

  } // end shrink

} // end class CacheMap



