/*
 *  This file is part of the Jikes RVM project (http://jikesrvm.org).
 *
 *  This file is licensed to You under the Common Public License (CPL);
 *  You may not use this file except in compliance with the License. You
 *  may obtain a copy of the License at
 *
 *      http://www.opensource.org/licenses/cpl1.0.php
 *
 *  See the COPYRIGHT.txt file distributed with this work for information
 *  regarding copyright ownership.
 */
package org.mmtk.utility;

import org.mmtk.policy.RawPageSpace;
import org.mmtk.vm.VM;

import org.vmmagic.pragma.*;
import org.vmmagic.unboxed.*;

/**
 * This class implements a simple hashtable. It is intended for use
 * in sanity checking or debugging, not high-performance algorithms.<p>
 *
 * This class is not thread safe.
 */
@Uninterruptible public abstract class SimpleHashtable implements Constants {
  /** The number of low order bits to ignore */
  private static final int HASH_SHIFT = 3;

  /** Offset to the key */
  private static final Offset KEY_OFFSET = Offset.zero();

  /** Offset to the data */
  private static final Offset DATA_OFFSET = Offset.fromIntSignExtend(BYTES_IN_WORD);

  /** The size of each entry in the table */
  private Extent entrySize;

  /** The mask to use to get the hash code */
  private Word mask;

  /** The start address of the data table */
  private Address base;

  /** The full size of the table */
  private Extent size;

  /** The space to use for allocating the data structure */
  private RawPageSpace space;

  /** Is this table valid (created) */
  private boolean valid;

  /**
   * Create a new data table of a specified size.
   *
   * @param rps The space to acquire the data structure from.
   * @param logSize The log of the number of table entries.
   * @param es The size of each entry.
   */
  protected SimpleHashtable(RawPageSpace rps, int logSize, Extent es) {
    mask = Word.fromIntZeroExtend((1 << logSize) - 1);
    entrySize = es.plus(BYTES_IN_WORD);
    size = Extent.fromIntZeroExtend((1 << logSize) * entrySize.toInt());
    base = Address.zero();
    space = rps;
    valid = false;
  }

  /**
   * Create a (zeroed) table.
   */
  public final void acquireTable() {
    base = space.acquire(Conversions.bytesToPages(size));
    VM.memory.zero(base, size);
    valid = true;
  }

  /**
   * Drop the table (after collection).
   */
  public final void releaseTable() {
    space.release(base);
    valid = false;
  }

  /**
   * @return True if this table has backing data and is ready for use.
   */
  public final boolean isValid() {
    return valid;
  }

  /**
   * Retrieve a pointer to the entry for the given object, or zero if one
   * does not exist, unless create is passed.<p>
   *
   * If create is true, the return is guaranteed to be non-null.
   *
   * @param key The key used to lookup.
   * @param create Create a new entry if not found.
   * @return A pointer to the reference or null.
   */
  @Inline
  public final Address getEntry(Word key, boolean create) {
    int startIndex = computeHash(key);
    int index = startIndex;
    Word curAddress;
    Address entry;
    do {
      entry = getEntry(index);
      curAddress = entry.loadWord(KEY_OFFSET);
      index = (index + 1) & mask.toInt();
    } while(curAddress.NE(key) &&
            !curAddress.isZero() &&
            index != startIndex);

    if (index == startIndex) {
      VM.assertions.fail("No room left in table!");
    }

    if (curAddress.isZero()) {
      if (!create) return Address.zero();
      entry.store(key, KEY_OFFSET);
    }

    return entry;
  }

  /**
   * Compute the hashtable index for a given object.
   *
   * @param key The key.
   * @return The index.
   */
  @Inline
  private int computeHash(Word key) {
    return key.rshl(HASH_SHIFT).and(mask).toInt();
  }

  /**
   * Return the address of a specified entry in the table.
   *
   * @param index The index of the entry.
   * @return An address to the entry.
   */
  @Inline
  private Address getEntry(int index) {
    return base.plus(Extent.fromIntZeroExtend(index * entrySize.toInt()));
  }

  /**
   * Does the passed object have an entry in the table?
   *
   * @param key The key to find an entry for
   * @return True if there is an entry for that object.
   */
  public final boolean contains(Word key) {
    return !getEntry(key, false).isZero();
  }

  /**
   * @return The first non-zero element in the table, or null if
   * the table is empty.
   */
  public final Address getFirst() {
    return getNext(base.minus(entrySize));
  }

  /**
   * The next element in the table after the passed entry, or
   * null if it is the last entry.
   *
   * @param curr The object to look for the next entry from.
   * @return The next entry or null.
   */
  public final Address getNext(Address curr) {
    Address entry = curr.plus(entrySize);
    while (entry.LT(base.plus(size))) {
      if (!entry.loadWord().isZero()) return entry;
      entry = entry.plus(entrySize);
    }
    return Address.zero();
  }

  /**
   * Given an address of an entry, return a pointer to the payload.
   *
   * @param entry The entry
   * @return The object reference.
   */
  public static Address getPayloadAddress(Address entry) {
    return entry.plus(DATA_OFFSET);
  }

  /**
   * Given a key, return a pointer to the payload.
   *
   * @param key The key
   * @return The object reference.
   */
  public final Address getPayloadAddress(Word key) {
    Address entry = getEntry(key, false);
    if (entry.isZero()) return Address.zero();

    return entry.plus(DATA_OFFSET);
  }


  /**
   * Return the key for a given entry.
   *
   * @param entry The entry.
   * @return The key.
   */
  public static Word getKey(Address entry) {
    return entry.loadWord(KEY_OFFSET);
  }

  /**
   * Update the key for a given entry. This operation is not
   * safe without rehashing
   *
   * @param entry The entry to update.
   * @param key The new key.
   */
  public static void replaceKey(Address entry, Word key) {
    entry.store(key, KEY_OFFSET);
  }

}
