import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.lang.Math;

public class HashTable <K,V> implements Map<K,V> {
    protected HashTableNode<K,V>[] table;
    protected Hasher<K> hasher;
    public int load;

    public String toString() {
	String rval = "";
	for (int i = 0; i < table.length; i++){
	    rval = rval + i + ": ";
	    HashTableNode<K,V> N = table[i];
		while (N != null){ 
		    rval = rval + N.toString() + ", ";
		    N = N.next;
		}
		rval = rval + "\n";
}	
	return rval;
    }

    public HashTableStats calculateStats() {
	//make a list of all the lengths
	ArrayList<Integer> l = new ArrayList<Integer>(table.length);
	for (int i = 0; i < table.length; i++){
	    HashTableNode<K,V> N = table[i];
	    int length = 0;
	    while (N != null){
		length++;
		N = N.next;
	    }
	    if(length != 0){
		l.add(length);
	    }
	}

	//sort it.
	Collections.sort(l);
	int sum = 0;
	
	for(int i : l){
	    sum += i;
	}
	
	HashTableStats stats = new HashTableStats();
	//do the easy ones
	stats.loadFactor = ((double) sum) / ((double) table.length);
	stats.meanChainLength = stats.loadFactor;
	stats.longestChainLength = l.get(l.size() -1);
	stats.medianChainLength = l.get(l.size() / 2);

	int mode = -1;
	int modeOccurances = 0;
	int current = -1;
	int currentOccurances = 0;
	for(int i : l){
	    if(i == current){
		currentOccurances++;
	    } else {
		if (currentOccurances > modeOccurances){
		    mode = current;
		    modeOccurances = currentOccurances;
		}
		current = i;
		currentOccurances =0;
	    }
	}
	stats.modeChainLength = mode;

	double stdev = 0;
	for(int i : l){
	    stdev += (stats.meanChainLength - i)* (stats.meanChainLength - i);
	}
	stats.stdevChainLength = Math.sqrt(stdev / table.length);
	return stats;

    }

    public HashTable(int size, Hasher<K> h) {   // Constructor
	//This operation is not type safe, but is OK for our purposes
        table = new HashTableNode[size];
	hasher = h;
	load = 0;
    }

    // Hashes int x to a value between 0 and L-1;
    protected int getIndex(K key) {
	//if we have a negative hashcode,
	//we get problems using it as an index!
	//fix by taking the absolute value
	int hash = Math.abs(hasher.getHash(key));
	return hash % table.length;
    }

    //  Find node for  Key if it exists.
    // Returns null otherwise
    private HashTableNode<K,V> FindNode(K key, int index) throws HashTableException {
	if(key == null)
	    throw new HashTableException("Null keys not allowed in the hash table");

	HashTableNode<K,V> N = table[index];
        while (N != null && !(N.getKey().equals(key))) 
	    //search the linked list until we either reach the end
	    //or find the node we are looking for
	    N = N.getNext(); 
        return N;
    }

    // Add Value V under key Key (updates if exists)
    public void put(K key, V value) throws HashTableException {
	if(key == null)
	    throw new HashTableException("Null keys not allowed in the hash table");
	if(value == null) 
	    throw new HashTableException("Null values not allowed in the hash table");

        int index = getIndex(key);
	HashTableNode<K,V> node = FindNode(key,index);
	if (node != null){
	    //the node already exists, just change its value
	    node.setValue(value);
	} else {
	    //add it to the front of the list
	    table[index] = new HashTableNode<K,V>(key,value,table[index]);
	    load++;
	}
    }

    // Locate the Value associated with key
    // If the key is not in the set, return null
    public V get(K key) throws HashTableException {
	if(key == null)
	    throw new HashTableException("Null keys not allowed in the hash table");

        int index = getIndex(key);
        HashTableNode<K,V> node = FindNode(key,index);
	if(node == null) return null;
	else return node.getValue();
    }

    // If the key is in the set, remove it and return true
    //otherwise return false
    public boolean remove(K key) throws HashTableException {
	if(key == null)
	    throw new HashTableException("Null keys not allowed in the hash table");

        int index = getIndex(key);
	HashTableNode<K,V> N = table[index];
	if (N == null){
	    //node wasn't here
	    return false;
	} else if (N.getKey().equals(key)){
	    table[index] = N.getNext();
	    load--;
	    return true;
	} else {
	    while (N.getNext() != null){
		if(N.getNext().getKey().equals(key)){
		    N.setNext(N.getNext().getNext());
		    load--;
		    return true;
		}
		N = N.getNext();
	    }
	    return false;
	}
    }


    public static void main(String [] args)
    {
	try{
	    HashTable<String,Integer> t = 
		new HashTable<String,Integer>(10,
					      new StringHasher2());
	    	    System.out.print(t);
	    t.put("hi ", 30);
	    t.put("hi t", 35);
	    t.put("hi q", 34);
	    t.put("hi ", 39);
	    System.out.println(t);
	    System.out.println(t.remove("hi q"));
	} catch (Exception e) 
	    {e.printStackTrace();}
    }
}
