// Probing Hash Set class
// Depending on the mode, it implements LINEAR, DOUBLE, and QUADRATIC Probing resolution
// CONSTRUCTION: an approximate initial size or default of 101
//
// ******************PUBLIC OPERATIONS*********************
// bool insert( x )       --> Insert x
// bool remove( x )       --> Remove x
// bool contains( x )     --> Return true if x is present
// void makeEmpty( )      --> Remove all items


/**
 * Probing table implementation of hash tables.
 * Note that all "matching" is based on the equals method.
 * @author Mark Allen Weiss
 */
public class ProbingHashSet<E>
{
    private enum ProbeMode{
        LINEAR, DOUBLE, QUADRATIC;
    }

    private static final int DEFAULT_TABLE_SIZE = 101;
    private static final double LOAD_FACTOR = .5;
    private HashEntry<E> [ ] array; // The array of elements
    private int size;                  // Current size
    private ProbeMode mode;            // Probing mode
    private int prevPrime;              //Used for double probing

    /**
     * Construct the hash table.
     */
    public ProbingHashSet(){
        this(ProbeMode.QUADRATIC);
    }

    public ProbingHashSet(ProbeMode mode){
        this(DEFAULT_TABLE_SIZE, mode);
    }

    public ProbingHashSet(int size)
    {
        this( DEFAULT_TABLE_SIZE, ProbeMode.QUADRATIC);
    }

    /**
     * Construct the hash table.
     * @param size the approximate initial size.
     */
    public ProbingHashSet(int size , ProbeMode mode) {
        allocateArray( size );
        makeEmpty( );
        this.mode = mode;
    }

    /**
     * Internal method to allocate array.
     * @param arraySize the size of the array.
     */
    private void allocateArray( int arraySize ) {
        array = new HashEntry[ PrimeFinder.nextPrime( arraySize ) ];
        prevPrime = PrimeFinder.prevPrime(arraySize);
    }


    /**
     * Make the hash table logically empty.
     */
    public void makeEmpty( ) {
        size = 0;
        for( int i = 0; i < array.length; i++ )
            array[ i ] = null;
    }

    /**
     * Find an item in the hash table.
     * @param x the item to search for.
     * @return the matching item.
     */
    public boolean contains( E x ) {
        int currentPos = findPos( x );
        return isActive( currentPos );
    }

    /**
     * Method that performs probing resolution based on current probe mode.
     * @param x the item to search for.
     * @return the position where the search terminates.
     */
    private int findPos( E x ) {
        int offset = (this.mode == ProbeMode.DOUBLE) ? myhash2(x) : 1;
        int currentPos = myhash( x );

        while( array[ currentPos ] != null &&
                !array[ currentPos ].element.equals( x ) ) {
            currentPos += offset;  // Compute ith probe
            if(this.mode == ProbeMode.QUADRATIC) {
                offset += 2; //each time offset increases by 2 (1, 3, 5, 7, 9, 11 etc.)
                // So that currentPos becomes +1, +4, +9, +16, +25, +36, etc
            }
            if( currentPos >= array.length ) //wrap around behavior
                currentPos -= array.length;
        }

        return currentPos;
    }

    private int myhash( E x ){
        int hashVal = x.hashCode( );

        hashVal %= array.length;
        if( hashVal < 0 )
            hashVal += array.length;

        return hashVal;
    }

    private int myhash2(E x)
    {
        int hashVal = myhash(x);
        return prevPrime - hashVal % prevPrime;
    }

    /**
     * Return true if currentPos exists and is active.
     * @param currentPos the result of a call to findPos.
     * @return true if currentPos is active.
     */
    private boolean isActive( int currentPos ) {
        return array[ currentPos ] != null && array[ currentPos ].isActive;
    }

    /**
     * Insert into the hash table. If the item is
     * already present, do nothing.
     * @param x the item to insert.
     */
    public boolean insert( E x )
    {
        // Insert x as active
        int currentPos = findPos( x );
        if( isActive( currentPos ) )
            return false;

        array[ currentPos ] = new HashEntry<>( x, true );

        // Rehash; see Section 5.5
        if( ++size > array.length * LOAD_FACTOR )
            rehash( );

        return true;
    }

    /**
     * Remove from the hash table.
     * @param x the item to remove.
     * @return true if item removed
     */
    public boolean remove( E x ) {
        int currentPos = findPos( x );
        if( isActive( currentPos ) ) {
            array[ currentPos ].isActive = false;
            size--;
            return true;
        }
        else
            return false;
    }

    /**
     * Expand the hash table.
     */
    private void rehash() {
        HashEntry<E> [ ] oldArray = array;

        // Create a new double-sized, empty table
        allocateArray( 2 * oldArray.length );
        size = 0;

        // Copy table over
        for( HashEntry<E> entry : oldArray )
            if( entry != null && entry.isActive )
                insert( entry.element );
    }
    /**
     * Get current size.
     * @return the size.
     */
    public int size( )
    {
        return size;
    }

    /**
     * Get length of internal table.
     * @return the size.
     */
    public int capacity( )
    {
        return array.length;
    }

    private static class HashEntry<E> {
        public E element;   // the element
        public boolean isActive;  // false if marked deleted

        public HashEntry( E e )
        {
            this( e, true );
        }

        public HashEntry(E e, boolean i )
        {
            element  = e;
            isActive = i;
        }
    }
}
