import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.Test;

public class HashtableMap<KeyType, ValueType> implements MapADT<KeyType, ValueType> {

  protected class Pair {

    public KeyType key;
    public ValueType value;

    public Pair(KeyType key, ValueType value) {
      this.key = key;
      this.value = value;
    }

  }

  protected LinkedList<Pair>[] table;
  protected int capacity;
  protected int size;

  @SuppressWarnings("unchecked")
  public HashtableMap(int capacity) {
    this.capacity = capacity;
    this.table = (LinkedList<Pair>[]) new LinkedList[capacity];
  }

  public HashtableMap() {
    this(64);
    // with default capacity = 64
  }
  @Override
  public List<KeyType> getKeys() {
    List<KeyType> keys = new LinkedList<>();
    for (int i = 0; i < capacity; i++) {
      if (table[i] != null) {
        for (Pair pair : table[i]) {
          keys.add(pair.key);
        }
      }
    }
    return keys;
  }


  @Override
  public void put(KeyType key, ValueType value) throws IllegalArgumentException {
    // when the key is null
    if (key == null) {
      throw new IllegalArgumentException();
    }
    // assign the index
    int index = (Math.abs(key.hashCode())) % capacity;
    // if it is empty for that index position
    if (table[index] == null) {
      table[index] = new LinkedList<>();
    }
    // loop through every index position of the table, if the key already existed
    for (Pair pair : table[index]) {
      if (pair.key.equals(key)) {
        throw new IllegalArgumentException();
      }
    }
    // add the pair to the table
    table[index].add(new Pair(key, value));
    // increase the size
    size++;
    // whenever its load factor becomes greater than or equal to 80%
    if ((double) size / capacity >= 0.8)
      resize();

  }

  private void resize() {
    LinkedList<Pair>[] oldTable = table;
    // doubling the capacity
    capacity *= 2;
    // creat new table with new capacity
    table = (LinkedList<Pair>[]) new LinkedList[capacity];
    size = 0;
    for (LinkedList<Pair> bucket : oldTable) {
      if (bucket != null) {
        for (Pair pair : bucket) {
          // reinsert pairs to new table
          put(pair.key, pair.value);
        }
      }
    }
  }

 @Override
  public boolean containsKey(KeyType key) {
  int index = (Math.abs(key.hashCode())) % capacity;

  if (table[index] == null) {
    return false;
  }

  for (Pair pair : table[index]) {
    if (pair.key.equals(key)) {
      return true;
    }
  }
  return false;
}

  @Override
  public ValueType get(KeyType key) throws NoSuchElementException {
    int index = (Math.abs(key.hashCode())) % capacity;
	if (table[index] != null) {
		for (Pair pair : table[index]) {
      // when found the key
			if (pair.key.equals(key)) {
        // get the value
				return pair.value;
		}
	}
}
    // no key in the table
    throw new NoSuchElementException();
  }

  @Override
  public ValueType remove(KeyType key) throws NoSuchElementException {
    int index = (Math.abs(key.hashCode())) % capacity;
    if (table[index] != null) {
      for (Pair pair : table[index]) {
        // found the key
        if (pair.key.equals(key)) {
          // get its value for later use
          ValueType value = pair.value;
          // clear the pair
          table[index].remove(pair);
          size--;
          return value;
        }
      }
    }
    throw new NoSuchElementException();
  }

  @Override
  public void clear() {
    // remove all the pairs
    for (int i = 0; i < capacity; i++) {
      table[i] = null;
    }
    size = 0;
  }

  @Override
  public int getSize() {
    return size;
  }

  @Override
  public int getCapacity() {
    return capacity;
  }

  // testing if hashtable is initialized correctly
  @Test
  public void testInitialSize() {
    // initilize the hashtable
    HashtableMap<String, Integer> map = new HashtableMap<>(5);
    // because we did not add any key-val pair to the hashtable, so the size should be zero
    assertEquals(0, map.getSize());

  }

  // testing if get method is performing correctly
  @Test
  public void testGet() {
    // initialize the hashtable
	HashtableMap<String, Integer> map = new HashtableMap<>(5);
	map.put("key",0);
    assertEquals(0, map.get("key"));
  }

  // testing if put method is performing correctly
  @Test
  public void testPut() {
    // initialize the hashtable
    HashtableMap<String, Integer> map = new HashtableMap<>(5);
    // add "key"-5 pair to the hashtable
    map.put("key", 5);
    // hashtable should contain "key" key
    assertTrue(map.containsKey("key"), "condition should be true");
  }

  // testing if remove method is performing correctly
  @Test
  public void testRemove() {
    // initialize the hashtable
    HashtableMap<String, Integer> map = new HashtableMap<>(5);
    // add "key"-5 pair to the hashtable
    map.put("key", 5);
    // remove "key" key from the hashtable
    map.remove("key");
    // hashtable should not contain "key" key after remove
    assertFalse(map.containsKey("key"), "condition should not be true");

  }

  // testing if getcapacity method is working correctly
  @Test
  public void testGetCapacity() {
    // initialize a hashtable with capacity 5
    HashtableMap<String, Integer> map = new HashtableMap<>(5);
    // use getcapacity method, should return 5
    assertEquals(5, map.getCapacity());
  }


}
