/*
 * Copyright (c) 2000-2005, Dennis M. Sosnoski. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer. Redistributions in binary
 * form must reproduce the above copyright notice, this list of conditions and
 * the following disclaimer in the documentation and/or other materials provided
 * with the distribution. Neither the name of JiBX nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package vkc.util;

import java.io.Serializable;

/**
 * Hash map using <code>String</code> values as keys mapped to primitive
 * <code>int</code> values. This implementation is unsynchronized in order to
 * provide the best possible performance for typical usage scenarios, so
 * explicit synchronization must be implemented by a wrapper class or directly
 * by the application in cases where instances are modified in a multithreaded
 * environment. The map implementation is not very efficient when resizing, but
 * works well when the size of the map is known in advance.
 * 
 * @author Dennis M. Sosnoski
 * @version 1.1
 */
public class StringIntHashMap implements Serializable
{
    /** Default value returned when key not found in table. */
    public static final int DEFAULT_NOT_FOUND = Integer.MIN_VALUE;

    /** Default fill fraction allowed before growing table. */
    protected static final double DEFAULT_FILL = 0.3d;

    /** Minimum size used for hash table. */
    protected static final int MINIMUM_SIZE = 31;

    /** Fill fraction allowed for this hash table. */
    protected final double m_fillFraction;

    /** Number of entries present in table. */
    protected int m_entryCount;

    /** Entries allowed before growing table. */
    protected int m_entryLimit;

    /** Size of array used for keys. */
    protected int m_arraySize;

    /** Offset added (modulo table size) to slot number on collision. */
    protected int m_hitOffset;

    /** Array of key table slots. */
    protected String[] m_keyTable;

    /** Array of value table slots. */
    protected int[] m_valueTable;

    /** Value returned when key not found in table. */
    protected int m_notFoundValue;

    /**
     * Constructor with full specification.
     * 
     * @param count number of values to assume in initial sizing of table
     * @param fill fraction full allowed for table before growing
     * @param miss value returned when key not found in table
     */
    public StringIntHashMap(int count, double fill, int miss) {

        // check the passed in fill fraction
        if (fill <= 0.0d || fill >= 1.0d) {
            throw new IllegalArgumentException("fill value out of range");
        }
        m_fillFraction = fill;

        // compute initial table size (ensuring odd)
        m_arraySize = Math.max((int)(count / m_fillFraction), MINIMUM_SIZE);
        m_arraySize += (m_arraySize + 1) % 2;

        // initialize the table information
        m_entryLimit = (int)(m_arraySize * m_fillFraction);
        m_hitOffset = m_arraySize / 2;
        m_keyTable = new String[m_arraySize];
        m_valueTable = new int[m_arraySize];
        m_notFoundValue = miss;
    }

    /**
     * Constructor with size and fill fraction specified. Uses default hash
     * technique and value returned when key not found in table.
     * 
     * @param count number of values to assume in initial sizing of table
     * @param fill fraction full allowed for table before growing
     */
    public StringIntHashMap(int count, double fill) {
        this(count, fill, DEFAULT_NOT_FOUND);
    }

    /**
     * Constructor with only size supplied. Uses default hash technique and
     * values for fill fraction and value returned when key not found in table.
     * 
     * @param count number of values to assume in initial sizing of table
     */
    public StringIntHashMap(int count) {
        this(count, DEFAULT_FILL);
    }

    /**
     * Default constructor.
     */
    public StringIntHashMap() {
        this(0, DEFAULT_FILL);
    }

    /**
     * Copy (clone) constructor.
     * 
     * @param base instance being copied
     */
    public StringIntHashMap(StringIntHashMap base) {

        // copy the basic occupancy information
        m_fillFraction = base.m_fillFraction;
        m_entryCount = base.m_entryCount;
        m_entryLimit = base.m_entryLimit;
        m_arraySize = base.m_arraySize;
        m_hitOffset = base.m_hitOffset;
        m_notFoundValue = base.m_notFoundValue;

        // copy table of items
        m_keyTable = new String[m_arraySize];
        System.arraycopy(base.m_keyTable, 0, m_keyTable, 0, m_arraySize);
        m_valueTable = new int[m_arraySize];
        System.arraycopy(base.m_valueTable, 0, m_valueTable, 0, m_arraySize);
    }

    /**
     * Step the slot number for an entry. Adds the collision offset (modulo
     * the table size) to the slot number.
     *
     * @param slot slot number to be stepped
     * @return stepped slot number
     */
    private final int stepSlot(int slot) {
        return (slot + m_hitOffset) % m_arraySize;
    }

    /**
     * Find free slot number for entry. Starts at the slot based directly
     * on the hashed key value. If this slot is already occupied, it adds
     * the collision offset (modulo the table size) to the slot number and
     * checks that slot, repeating until an unused slot is found.
     *
     * @param slot initial slot computed from key
     * @return slot at which entry was added
     */
    private final int freeSlot(int slot) {
        while (m_keyTable[slot] != null) {
            slot = stepSlot(slot);
        }
        return slot;
    }

    /**
     * Standard base slot computation for a key.
     *
     * @param key key value to be computed
     * @return base slot for key
     */
    private final int standardSlot(Object key) {
        return (key.hashCode() & Integer.MAX_VALUE) % m_arraySize;
    }

    /**
     * Standard find key in table. This method may be used directly for key
     * lookup using either the <code>hashCode()</code> method defined for the
     * key objects or the <code>System.identityHashCode()</code> method, and
     * either the <code>equals()</code> method defined for the key objects or
     * the <code>==</code> operator, as selected by the hash technique
     * constructor parameter. To implement a hash class based on some other
     * methods of hashing and/or equality testing, define a separate method in
     * the subclass with a different name and use that method instead. This
     * avoids the overhead caused by overrides of a very heavily used method.
     *
     * @param key to be found in table
     * @return index of matching key, or <code>-index-1</code> of slot to be
     * used for inserting key in table if not already present (always negative)
     */
    private int standardFind(Object key) {

        // find the starting point for searching table
        int slot = standardSlot(key);

        // scan through table to find target key
        while (m_keyTable[slot] != null) {

            // check if we have a match on target key
            if (m_keyTable[slot].equals(key)) {
                return slot;
            }

            slot = stepSlot(slot);
        }

        return -slot-1;
    }

    /**
     * Reinsert an entry into the hash map. This is used when the table is being
     * directly modified, and does not adjust the count present or check the
     * table capacity.
     * 
     * @param slot position of entry to be reinserted into hash map
     * @return <code>true</code> if the slot number used by the entry has has
     * changed, <code>false</code> if not
     */
    private boolean reinsert(int slot) {
        String key = m_keyTable[slot];
        m_keyTable[slot] = null;
        return assignSlot(key, m_valueTable[slot]) != slot;
    }

    /**
     * Internal remove pair from the table. Removes the pair from the table
     * by setting the key entry to <code>null</code> and adjusting the count
     * present, then chains through the table to reinsert any other pairs
     * which may have collided with the removed pair. If the associated value
     * is an object reference, it should be set to <code>null</code> before
     * this method is called.
     *
     * @param slot index number of pair to be removed
     */
    protected void internalRemove(int slot) {

        // delete pair from table
        m_keyTable[slot] = null;
        m_entryCount--;
        while (m_keyTable[(slot = stepSlot(slot))] != null) {

            // reinsert current entry in table to fill holes
            reinsert(slot);

        }
    }

    /**
     * Restructure the table. This is used when the table is increasing or
     * decreasing in size, and works directly with the old table representation
     * arrays. It inserts pairs from the old arrays directly into the table
     * without adjusting the count present or checking the table size.
     * 
     * @param keys array of keys
     * @param values array of values
     */
    private void restructure(String[] keys, int[] values) {
        for (int i = 0; i < keys.length; i++) {
            if (keys[i] != null) {
                assignSlot(keys[i], values[i]);
            }
        }
    }

    /**
     * Assign slot for entry. Starts at the slot found by the hashed key value.
     * If this slot is already occupied, it steps the slot number and checks the
     * resulting slot, repeating until an unused slot is found. This method does
     * not check for duplicate keys, so it should only be used for internal
     * reordering of the tables.
     * 
     * @param key to be added to table
     * @param value associated value for key
     * @return slot at which entry was added
     */
    private int assignSlot(String key, int value) {
        int offset = freeSlot(standardSlot(key));
        m_keyTable[offset] = key;
        m_valueTable[offset] = value;
        return offset;
    }

    /**
     * Add an entry to the table. If the key is already present in the table,
     * this replaces the existing value associated with the key.
     * 
     * @param key key to be added to table (non- <code>null</code>)
     * @param value associated value for key
     * @return value previously associated with key, or reserved not found value
     * if key not previously present in table
     */
    public int add(String key, int value) {

        // first validate the parameters
        if (key == null) {
            throw new IllegalArgumentException("null key not supported");
        } else if (value == m_notFoundValue) {
            throw new IllegalArgumentException(
                "value matching not found return not supported");
        } else {

            // check space available
            int min = m_entryCount + 1;
            if (min > m_entryLimit) {
                
                // find the array size required
                int size = m_arraySize;
                int limit = m_entryLimit;
                while (limit < min) {
                    size = size * 2 + 1;
                    limit = (int) (size * m_fillFraction);
                }
            
                // set parameters for new array size
                m_arraySize = size;
                m_entryLimit = limit;
                m_hitOffset = size / 2;
                
                // restructure for larger arrays
                String[] keys = m_keyTable;
                m_keyTable = new String[m_arraySize];
                int[] values = m_valueTable;
                m_valueTable = new int[m_arraySize];
                restructure(keys, values);
            }
            
            // find slot of table
            int offset = standardFind(key);
            if (offset >= 0) {

                // replace existing value for key
                int prior = m_valueTable[offset];
                m_valueTable[offset] = value;
                return prior;

            }

            // add new pair to table
            m_entryCount++;
            offset = -offset - 1;
            m_keyTable[offset] = key;
            m_valueTable[offset] = value;

            return m_notFoundValue;
        }
    }

    /**
     * Check if an entry is present in the table. This method is supplied to
     * support the use of values matching the reserved not found value.
     * 
     * @param key key for entry to be found
     * @return <code>true</code> if key found in table, <code>false</code>
     * if not
     */
    public final boolean containsKey(String key) {
        return standardFind(key) >= 0;
    }

    /**
     * Find an entry in the table.
     * 
     * @param key key for entry to be returned
     * @return value for key, or reserved not found value if key not found
     */
    public final int get(String key) {
        int slot = standardFind(key);
        if (slot >= 0) {
            return m_valueTable[slot];
        }

        return m_notFoundValue;
    }

    /**
     * Remove an entry from the table. If multiple entries are present with the
     * same key value, only the first one found will be removed.
     * 
     * @param key key to be removed from table
     * @return value associated with removed key, or reserved not found value if
     * key not found in table
     */
    public int remove(String key) {
        int slot = standardFind(key);
        if (slot >= 0) {
            int value = m_valueTable[slot];
            internalRemove(slot);
            return value;
        }

        return m_notFoundValue;
    }

    /**
     * Construct a copy of the table.
     * 
     * @return shallow copy of table
     */
    public Object clone() {
        return new StringIntHashMap(this);
    }
}