// A HashIntSet object represents a set of integers using a hash table
// as the internal data structure.
// The hash table uses separate chaining (a linked list in each hash bucket
// index) to resolve collisions.

public class HashSet2<E> {
	private static final double MAX_LOAD = 0.5;   // load factor on which to rehash
	
	private Node[] elements;
	private int size;
	
	// Constructs a new empty set of integers.
	@SuppressWarnings("unchecked")
	public HashSet2() {
		elements = (Node[]) new HashSet2.Node[10];
		size = 0;
	}
	
	// Constructs a new set of integers.
	@SuppressWarnings("unchecked")
	public HashSet2(E... values) {
		this();
		for (E value : values) {
			add(value);
		}
	}
	
	// Adds the given value to this set,
	// if it was not already contained in the set.
	public void add(E value) {
		// linear probing to find proper index
		if (!contains(value)) {
			int h = hash(value);
			Node newNode = new Node(value);
			newNode.next = elements[h];
			elements[h] = newNode;
			size++;
		}

		// resize if necessary
		if (loadFactor() > MAX_LOAD) {
			rehash();
		}
	}

	// Returns true if o refers to another HashSet2 with the same elements as this set.
	public boolean equals(Object o) {
		if (o instanceof HashSet2) {
			HashSet2<E> other = (HashSet2<E>) o;
			for (Node front : elements) {
				Node current = front;
				while (current != null) {
					if (!other.contains(current.data)) {
						return false;
					}
					current = current.next;
				}
			}
			return size == other.size();
		} else {
			return false;
		}
	}
	
	// Returns true if o refers to another HashSet2 with the same elements as this set,
	// ignoring the value of the size field.
	public boolean equalsIgnoringSize(Object o) {
		if (o instanceof HashSet2) {
			HashSet2<E> other = (HashSet2<E>) o;
			for (Node front : elements) {
				Node current = front;
				while (current != null) {
					if (!other.contains(current.data)) {
						return false;
					}
					current = current.next;
				}
			}
			for (Node front : other.elements) {
				Node current = front;
				while (current != null) {
					if (!this.contains(current.data)) {
						return false;
					}
					current = current.next;
				}
			}
			return true;
		} else {
			return false;
		}
	}
	
	

	// Returns whether the given value is found in this set.
	public boolean contains(E value) {
		// linear probing to find proper index
		int h = hash(value);
		Node current = elements[h];
		while (current != null) {
			if (current.data.equals(value)) {
				return true;
			}
			current = current.next;
		}
		return false;
	}

	// Returns true if there are no elements in this set.
	public boolean isEmpty() {
		return size == 0;
	}
	
	// Returns the hash table's "load factor", its ratio of size to capacity.
	public double loadFactor() {
		return (double) size / elements.length;
	}
	
	// Removes the given element value from this set,
	// if it was found in the set.
	public void remove(E value) {
		// linear probing to find proper index
		int h = hash(value);
		
		if (elements[h] != null) {
			// front case
			if (elements[h].data.equals(value)) {
				elements[h] = elements[h].next;
			} else {
				// non-front case
				Node current = elements[h];
				while (current.next != null && 
						!current.next.data.equals(value)) {
					current = current.next;
				}
				
				// current.next == null 
				// || current.next.data == value
				if (current.next != null) {
					current.next = current.next.next;
				}
			}
		}
	}

	// Returns the number of elements in this set.
	public int size() {
		return size;
	}
	
	// Returns a text representation of this set.
	public String toString() {
		String result = "[";
		boolean first = true;
		for (Node node : elements) {
			Node current = node;
			while (current != null) {
				if (!first) {
					result += ", ";
				}
				result += current.data;
				first = false;
				current = current.next;
			}
		}
		result += "]";
		return result;
	}
	
	// Debugging helper that prints the inner hash table.
	public void debug() {
		System.out.println("debug() output:");
		System.out.printf("index   data\n");
		for (int i = 0; i < elements.length; i++) {
			System.out.printf("%5d   ", i);
			Node node = elements[i];
			if (node == null) {
				System.out.printf("%6s\n", "null");
			} else {
				boolean first = true;
				while (node != null) {
					if (!first) {
						System.out.print("  --> ");
					}
					System.out.printf("%8s", node.data);
					node = node.next;
					first = false;
				}
				System.out.println();
			}
		}
		System.out.printf("size   %d\n\n", size);
	}
	
	// hash function for mapping values to indexes
	private int hash(E value) {
		return Math.abs(value.hashCode()) % elements.length;
	}

	// Resizes the hash table to twice its original capacity.
	@SuppressWarnings("unchecked")
	private void rehash() {
		Node[] newElements = (Node[]) new HashSet2.Node[2 * elements.length];
		Node[] old = elements;
		elements = newElements;
		size = 0;
		for (Node node : old) {
			while (node != null) {
				add(node.data);
				node = node.next;
			}
		}
	}
	
	
	private class Node {
		public E data;
		public Node next;
		
		public Node(E data) {
			this.data = data;
		}
	}

// YOUR CODE GOES HERE

}
