typedef import(T);

from collections.set(T=T) access Iter_T, Iterable_T, Set_T;

private struct HashEntry {
  T item;
  int hash = -1;
  HashEntry newer = null;
  HashEntry older = null;
}

struct HashSet_T {
  restricted Set_T super;
  from super unravel nullT, equiv, isNullT;

  // These fields are mutable.
  private HashEntry[] buckets = array(16, (HashEntry)null);
  buckets.cyclic = true;
  private int size = 0;
  private int zombies = 0;
  private int numChanges = 0;  // Detect concurrent modification.
  HashEntry newest = null;
  HashEntry oldest = null;

  void operator init() {
    using F = void();
    ((F)super.operator init)();
  }
  void operator init(T nullT,
      bool equiv(T a, T b) = operator ==,
      bool isNullT(T) = new bool(T t) { return equiv(t, nullT); }) {
    using F = void(T, bool equiv(T, T), bool isNullT(T));
    ((F)super.operator init)(nullT, equiv, isNullT);
  }

  super.newEmpty = new Set_T() {
    return HashSet_T(nullT, equiv, isNullT).super;
  };

  super.size = new int() {
    return size;
  };

  super.contains = new bool(T item) {
    if (isNullT != null && isNullT(item)) {
      return false;
    }
    int bucket = item.hash();
    for (int index = bucket - buckets.length; index < bucket; ++index) {
      HashEntry entry = buckets[index];
      if (entry == null) {
        return false;
      }
      if (entry.hash == bucket && equiv(entry.item, item)) {
        return true;
      }
    }
    return false;
  };

  super.get = new T(T item) {
    if (isNullT != null && isNullT(item)) {
      return super.nullT;
    }
    int bucket = item.hash();
    int end = bucket;
    int start = end - buckets.length;
    for (int i = start; i < end; ++i) {
      HashEntry entry = buckets[i];
      if (entry == null) {
        assert(isNullT != null, 'Item is not present.');
        return super.nullT;
      }
      if (entry.hash == bucket) {
        var entryItem = entry.item;
        if (equiv(entryItem, item)) {
          return entryItem;
        }
      }
    }
    assert(isNullT != null, 'Item is not present.');
    return super.nullT;
  };

  super.operator iter = new Iter_T() {
    Iter_T result = new Iter_T;
    HashEntry current = oldest;
    int expectedChanges = numChanges;
    result.valid = new bool() {
      assert(numChanges == expectedChanges, 'Concurrent modification');
      return current != null;
    };
    result.get = new T() {
      assert(numChanges == expectedChanges, 'Concurrent modification');
      assert(result.valid());
      return current.item;
    };
    result.advance = new void() {
      assert(numChanges == expectedChanges, 'Concurrent modification');
      assert(result.valid());
      current = current.newer;
    };
    return result;
  };

  private void changeCapacity() {
    ++numChanges;
    int newCapacity = (zombies > size ? buckets.length : 2 * buckets.length);
    zombies = 0;
    buckets = array(newCapacity, (HashEntry)null);
    buckets.cyclic = true;
    for (HashEntry current = oldest; current != null; current = current.newer) {
      int bucket = current.hash;
      for (int i = 0; i < buckets.length; ++i) {
        if (buckets[bucket + i] == null) {
          buckets[bucket + i] = current;
          break;
        }
        assert(i < buckets.length - 1, 'No space in hash table; '
                                       'is the linked list circular?');
      }
    }
  }

  // Returns an int as follows (note: "index" is modulo buckets.length):
  //   * If an equivalent item is in the set, returns its index.
  //   * Otherwise, if least one bucket is empty, returns the index of the empty
  //     bucket in which the item should be placed if added.
  //   * Otherwise, returns -1.
  private int find(T item, int hash) {
    for (int index = hash - buckets.length; index < hash; ++index) {
      HashEntry entry = buckets[index];
      if (entry == null) {
        return index;
      }
      if (entry.hash == hash && equiv(entry.item, item)) {
        return index;
      }
    }
    return -1;
  }

  super.add = new bool(T item) {
    if (isNullT != null && isNullT(item)) {
      return false;
    }
    int capacity = buckets.length;
    if (2 * (size + zombies) >= capacity) {
      changeCapacity();
      capacity = buckets.length;
    }
    int bucket = item.hash();
    int index = find(item, bucket);
    if (index == -1) {
      ++numChanges;
      changeCapacity();
      capacity = buckets.length;
      index = find(item, bucket);
      assert(index != -1, 'No space in hash table');
    }
    HashEntry entry = buckets[index];
    if (entry != null) {
      return false;
    }

    ++numChanges;
    if (2 * (size + zombies) >= capacity) {
      changeCapacity();
      capacity = buckets.length;
      index = find(item, bucket);
      assert(index != -1);
      assert(buckets[index] == null);
    }
    entry = buckets[index] = new HashEntry;
    entry.item = item;
    entry.hash = bucket;
    entry.older = newest;
    if (newest != null) {
      newest.newer = entry;
    }
    newest = entry;
    if (oldest == null) {
      oldest = entry;
    }
    ++size;
    return true;
  };

  super.push = new T(T item) {
    if (isNullT != null && isNullT(item)) {
      return nullT;
    }
    int bucket = item.hash();
    int index = find(item, bucket);
    if (index == -1) {
      changeCapacity();
      index = find(item, bucket);
      assert(index != -1, 'No space in hash table');
    }
    HashEntry entry = buckets[index];
    if (entry != null) {
      T result = entry.item;
      entry.item = item;
      return result;
    }
    assert(isNullT != null,
           'Adding item via push() without defining nullT.');
    ++numChanges;
    if (2 * (size + zombies) >= buckets.length) {
      changeCapacity();
      index = find(item, bucket);
      assert(index != -1);
      assert(buckets[index] == null);
    }
    entry = buckets[index] = new HashEntry;
    entry.item = item;
    entry.hash = bucket;
    entry.older = newest;
    if (newest != null) {
      newest.newer = entry;
    }
    newest = entry;
    if (oldest == null) {
      oldest = entry;
    }
    ++size;
    return nullT;
  };

  // Marks the entry as a zombie and removes it from the linked list.
  // Also adjusts the numChanges, size, and zombies counters.
  private void makeZombie(HashEntry entry) {
    ++numChanges;
    entry.hash = -1;
    ++zombies;
    if (entry.older != null) {
      entry.older.newer = entry.newer;
    } else {
      oldest = entry.newer;
    }
    if (entry.newer != null) {
      entry.newer.older = entry.older;
    } else {
      newest = entry.older;
    }
    --size;
    // Since zombies + size has not changed, we don't need to
    // change the capacity.
  }

  super.extract = new T(T item) {
    if (isNullT != null && isNullT(item)) {
      return nullT;
    }
    int index = find(item, item.hash());
    if (index == -1) {
      assert(false, 'Overcrowded hash table; zombies: ' + string(zombies) +
             '; size: ' + string(size) +
             '; buckets.length: ' + string(buckets.length));
      return nullT;
    }
    HashEntry entry = buckets[index];
    if (entry == null) {
      assert(isNullT != null, 'Item is not present.');
      return nullT;
    }
    T result = entry.item;
    makeZombie(entry);
    return result;
  };

  super.delete = new bool(T item) {
    if (isNullT != null && isNullT(item)) {
      return false;
    }
    int index = find(item, item.hash());
    if (index == -1) {
      assert(false, 'Overcrowded hash table; zombies: ' + string(zombies) +
             '; size: ' + string(size) +
             '; buckets.length: ' + string(buckets.length));
      return false;
    }
    HashEntry entry = buckets[index];
    if (alias(entry, null)) {
      return false;
    }
    makeZombie(entry);
    return true;
  };

  super.getRandom = new T() {
    if (size == 0) {
      assert(isNullT != null, 'Cannot get a random item from an empty set');
      return nullT;
    }
    if (size # 2 > buckets.length # size) {
      // Most buckets are empty, so it's faster to iterate over the linked list
      // of full buckets.
      int index = rand(0, size - 1);
      for (T item : super) {
        if (index == 0) {
          return item;
        }
        --index;
      }
      assert(false, 'Unreachable code');
    }
    HashEntry entry = null;
    do {
      int index = rand(0, buckets.length - 1);
      entry = buckets[index];
    } while (entry == null || entry.hash == -1);
    return entry.item;
  };


  autounravel Set_T operator cast(HashSet_T set) {
    return set.super;
  }

  autounravel Iterable_T operator cast(HashSet_T set) {
    return Iterable_T(set.super.operator iter);
  }
  unravel super;
}
    
