/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 *    KStarCache.java
 *    Copyright (C) 1995-2012 University of Waikato
 *    Java port to Weka by Abdelaziz Mahoui (am14@cs.waikato.ac.nz).
 *
 */


package weka.classifiers.lazy.kstar;

import java.io.Serializable;

import weka.core.RevisionHandler;
import weka.core.RevisionUtils;

/**
 * A class representing the caching system used to keep track of each attribute
 * value and its corresponding scale factor or stop parameter.
 *
 * @author Len Trigg (len@reeltwo.com)
 * @author Abdelaziz Mahoui (am14@cs.waikato.ac.nz)
 * @version $Revision: 8034 $
 */
public class KStarCache
  implements Serializable, RevisionHandler {

  /** for serialization */
  private static final long serialVersionUID = -7693632394267140678L;
  
  /**
   * cache table
   */
  CacheTable m_Cache = new CacheTable();
  
  /**
   * Stores the specified values in the cahce table for easy retrieval.
   *
   * @param key attribute value used key to lookup the cache table.
   * @param value cache parameter: attribute scale/stop parameter.
   * @param pmiss cache parameter: transformation probability to 
   * attribute with missing value.
   */
  public void store(double key, double value, double pmiss) {
    if ( !m_Cache.containsKey(key) ) {
      m_Cache.insert(key, value, pmiss);
    }
  }
  
  /**
   * Checks if the specified key maps with an entry in the cache table
   *
   * @param key the key to map with an entry in the hashtable.
   */
  public boolean containsKey(double key) {
    if ( m_Cache.containsKey(key) ) {
      return true;
    }
    return false;
  }
  
  /**
   * Returns the values in the cache mapped by the specified key
   *
   * @param key the key used to retrieve the table entry.
   */
  public TableEntry getCacheValues( double key ) {
    if ( m_Cache.containsKey(key) ) {
      return m_Cache.getEntry(key);
    }
    return null;
  }

  /**
   * A custom hashtable class to support the caching system.
   *
   */
  public class CacheTable
    implements Serializable, RevisionHandler {

    /** for serialization */
    private static final long serialVersionUID = -8086106452588253423L;

    /** The hash table data. */
    private TableEntry [] m_Table;

    /** The total number of entries in the hash table. */
    private int m_Count;

    /** Rehashes the table when count exceeds this threshold. */
    private int m_Threshold;

    /** The load factor for the hashtable. */
    private float m_LoadFactor;

    /** The default size of the hashtable */
    private final int DEFAULT_TABLE_SIZE = 101;

    /** The default load factor for the hashtable */
    private final float DEFAULT_LOAD_FACTOR = 0.75f;

    //    private final float DEFAULT_LOAD_FACTOR = 0.5f;
    /** Accuracy value for equality */
    private final double EPSILON = 1.0E-5;
    
    /**
     * Constructs a new hashtable with a default capacity and load factor.
     */
    public CacheTable(int size, float loadFactor) {
      m_Table = new TableEntry[size];
      m_LoadFactor = loadFactor;
      m_Threshold = (int)(size * loadFactor);
      m_Count = 0;
    }
    
    /**
     * Constructs a new hashtable with a default capacity and load factor.
     */
    public CacheTable() {
      this(101, 0.75f);
    }
    
    /**
     * Tests if the specified double is a key in this hashtable.
     */
    public boolean containsKey(double key) {
      TableEntry [] table = m_Table;
      int hash = hashCode(key);
      int index = (hash & 0x7FFFFFFF) % table.length;
      for (TableEntry e = table[index] ; e != null ; e = e.next) {
	if ((e.hash == hash) && (Math.abs(e.key - key) < EPSILON)) {
	  return true;
	}
      }
      return false;
    }
    
    /**
     * Inserts a new entry in the hashtable using the specified key. 
     * If the key already exist in the hashtable, do nothing.
     */
    public void insert(double key, double value, double pmiss) {
      // Makes sure the key is not already in the hashtable.
      TableEntry e, ne;
      TableEntry [] table = m_Table;
      int hash = hashCode(key);
      int index = (hash & 0x7FFFFFFF) % table.length;
      // start looking along the chain
      for (e = table[index] ; e != null ; e = e.next) {
	if ((e.hash == hash) && (Math.abs(e.key - key) < EPSILON)) {
	  return;
	}
      }
      // At this point, key is not in table.
      // Creates a new entry.
      ne = new TableEntry( hash, key, value, pmiss, table[index] );
      // Put entry at the head of the chain.
      table[index] = ne;
      m_Count++;
      // Rehash the table if the threshold is exceeded
      if (m_Count >= m_Threshold) {
	rehash();
      }
    }
    
    /**
     * Returns the table entry to which the specified key is mapped in 
     * this hashtable.
     * @return a table entry.
     */
    public TableEntry getEntry(double key) {
      TableEntry [] table = m_Table;
      int hash = hashCode(key);
      int index = (hash & 0x7FFFFFFF) % table.length;
      for (TableEntry e = table[index] ; e != null ; e = e.next) {
	if ((e.hash == hash) && (Math.abs(e.key - key) < EPSILON)) {
	  return e;
	}
      }
      return null;
    }
    
    /**
     * Returns the number of keys in this hashtable.
     * @return the number of keys in this hashtable.
     */
    public int size() {
      return m_Count;
    }
    
    /**
     * Tests if this hashtable maps no keys to values.
     * @return true if this hastable maps no keys to values.
     */
    public boolean isEmpty() {
      return m_Count == 0;
    }
    
    /**
     * Clears this hashtable so that it contains no keys.
     */
    public void clear() {
      TableEntry table[] = m_Table;
      for (int index = table.length; --index >= 0; ) {
	table[index] = null;
      }
      m_Count = 0;
    }
    
    /**
     * Rehashes the contents of the hashtable into a hashtable with a 
     * larger capacity. This method is called automatically when the 
     * number of keys in the hashtable exceeds this hashtable's capacity 
     * and load factor. 
     */
    private void rehash() {
      int oldCapacity = m_Table.length;
      TableEntry [] oldTable = m_Table;    
      int newCapacity = oldCapacity * 2 + 1;
      TableEntry [] newTable = new TableEntry[newCapacity];
      m_Threshold = (int)(newCapacity * m_LoadFactor);
      m_Table = newTable;
      TableEntry e, old;
      for (int i = oldCapacity ; i-- > 0 ;) {
	for (old = oldTable[i] ; old != null ; ) {
	  e = old;
	  old = old.next;
	  int index = (e.hash & 0x7FFFFFFF) % newCapacity;
	  e.next = newTable[index];
	  newTable[index] = e;
	}
      }
    }
    
    /**
     * Returns the hash code of the specified double.
     * @return the hash code of the specified double.
     */
    private int hashCode(double key) {
      long bits = Double.doubleToLongBits(key);
      return (int)(bits ^ (bits >> 32));
    }
    
    /**
     * Returns the revision string.
     * 
     * @return		the revision
     */
    public String getRevision() {
      return RevisionUtils.extract("$Revision: 8034 $");
    }
  } // CacheTable
  
  /**
   * Hashtable collision list.
   */
  public class TableEntry
    implements Serializable, RevisionHandler {

    /** for serialization */
    private static final long serialVersionUID = 4057602386766259138L;

    /** attribute value hash code */
    public int hash;

    /** attribute value */
    public double key;

    /** scale factor or stop parameter */
    public double value;

    /** transformation probability to missing value */
    public double pmiss;

    /** next table entry (separate chaining) */
    public TableEntry next = null;

    /** Constructor */
    public TableEntry(int hash, double key, double value, 
		      double pmiss, TableEntry next) {
      this.hash  = hash;
      this.key   = key;
      this.value = value;
      this.pmiss = pmiss;
      this.next  = next;
    }
    
    /**
     * Returns the revision string.
     * 
     * @return		the revision
     */
    public String getRevision() {
      return RevisionUtils.extract("$Revision: 8034 $");
    }
  }  // TableEntry
  
  /**
   * Returns the revision string.
   * 
   * @return		the revision
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 8034 $");
  }
  
} // Cache
