package edu.jhu.thrax.util;

import java.io.UnsupportedEncodingException;

/**
 * MurmurHash 2.0.
 * 
 * The murmur hash is a relative fast hash function from http://murmurhash.googlepages.com/ for
 * platforms with efficient multiplication.
 * 
 * This is a re-implementation of the original C code plus some additional features.
 * 
 * Public domain.
 * 
 * @author Viliam Holub
 * @version 1.0.2
 * 
 */
@SuppressWarnings("fallthrough")
public final class MurmurHash {

  private final static String ENCODING = "UTF-16";

  /**
   * Generates 32 bit hash from byte array of the given length and seed.
   * 
   * @param data byte array to hash
   * @param length length of the array to hash
   * @param seed initial seed value
   * @return 32 bit hash of the given array
   */
  public static int hash32(final byte[] data, int length, int seed) {
    // 'm' and 'r' are mixing constants generated offline.
    // They're not really 'magic', they just happen to work well.
    final int m = 0x5bd1e995;
    final int r = 24;
    // Initialize the hash to a random value
    int h = seed ^ length;
    int length4 = length / 4;

    for (int i = 0; i < length4; i++) {
      final int i4 = i * 4;
      int k =
          (data[i4 + 0] & 0xff) + ((data[i4 + 1] & 0xff) << 8) + ((data[i4 + 2] & 0xff) << 16)
              + ((data[i4 + 3] & 0xff) << 24);
      k *= m;
      k ^= k >>> r;
      k *= m;
      h *= m;
      h ^= k;
    }

    // Handle the last few bytes of the input array
    switch (length % 4) {
      case 3:
        h ^= (data[(length & ~3) + 2] & 0xff) << 16;
      case 2:
        h ^= (data[(length & ~3) + 1] & 0xff) << 8;
      case 1:
        h ^= (data[length & ~3] & 0xff);
        h *= m;
    }

    h ^= h >>> 13;
    h *= m;
    h ^= h >>> 15;

    return h;
  }


  /**
   * Generates 32 bit hash from byte array with default seed value.
   * 
   * @param data byte array to hash
   * @param length length of the array to hash
   * @return 32 bit hash of the given array
   */
  public static int hash32(final byte[] data, int length) {
    return hash32(data, length, 0x9747b28c);
  }


  /**
   * Generates 32 bit hash from a string.
   * 
   * @param text string to hash
   * @return 32 bit hash of the given string
   * @throws UnsupportedEncodingException
   */
  public static int hash32(final String text) throws UnsupportedEncodingException {
    final byte[] bytes = text.getBytes(ENCODING);
    return hash32(bytes, bytes.length);
  }


  /**
   * Generates 32 bit hash from a substring.
   * 
   * @param text string to hash
   * @param from starting index
   * @param length length of the substring to hash
   * @return 32 bit hash of the given string
   * @throws UnsupportedEncodingException
   */
  public static int hash32(final String text, int from, int length)
      throws UnsupportedEncodingException {
    return hash32(text.substring(from, from + length));
  }


  /**
   * Generates 64 bit hash from byte array of the given length and seed.
   * 
   * @param data byte array to hash
   * @param length length of the array to hash
   * @param seed initial seed value
   * @return 64 bit hash of the given array
   */
  public static long hash64(final byte[] data, int length, int seed) {
    final long m = 0xc6a4a7935bd1e995L;
    final int r = 47;

    long h = (seed & 0xffffffffl) ^ (length * m);

    int length8 = length / 8;

    for (int i = 0; i < length8; i++) {
      final int i8 = i * 8;
      long k =
          ((long) data[i8 + 0] & 0xff) + (((long) data[i8 + 1] & 0xff) << 8)
              + (((long) data[i8 + 2] & 0xff) << 16) + (((long) data[i8 + 3] & 0xff) << 24)
              + (((long) data[i8 + 4] & 0xff) << 32) + (((long) data[i8 + 5] & 0xff) << 40)
              + (((long) data[i8 + 6] & 0xff) << 48) + (((long) data[i8 + 7] & 0xff) << 56);

      k *= m;
      k ^= k >>> r;
      k *= m;

      h ^= k;
      h *= m;
    }

    switch (length % 8) {
      case 7:
        h ^= (long) (data[(length & ~7) + 6] & 0xff) << 48;
      case 6:
        h ^= (long) (data[(length & ~7) + 5] & 0xff) << 40;
      case 5:
        h ^= (long) (data[(length & ~7) + 4] & 0xff) << 32;
      case 4:
        h ^= (long) (data[(length & ~7) + 3] & 0xff) << 24;
      case 3:
        h ^= (long) (data[(length & ~7) + 2] & 0xff) << 16;
      case 2:
        h ^= (long) (data[(length & ~7) + 1] & 0xff) << 8;
      case 1:
        h ^= (long) (data[length & ~7] & 0xff);
        h *= m;
    };

    h ^= h >>> r;
    h *= m;
    h ^= h >>> r;

    return h;
  }


  /**
   * Generates 64 bit hash from byte array with default seed value.
   * 
   * @param data byte array to hash
   * @param length length of the array to hash
   * @return 64 bit hash of the given string
   */
  public static long hash64(final byte[] data, int length) {
    return hash64(data, length, 0xe17a1465);
  }


  /**
   * Generates 64 bit hash from a string.
   * 
   * @param text string to hash
   * @return 64 bit hash of the given string
   * @throws UnsupportedEncodingException
   */
  public static long hash64(final String text) throws UnsupportedEncodingException {
    byte[] bytes;
    bytes = text.getBytes(ENCODING);
    return hash64(bytes, bytes.length);
  }


  /**
   * Generates 64 bit hash from a substring.
   * 
   * @param text string to hash
   * @param from starting index
   * @param length length of the substring to hash
   * @return 64 bit hash of the given array
   * @throws UnsupportedEncodingException
   */
  public static long hash64(final String text, int from, int length)
      throws UnsupportedEncodingException {
    return hash64(text.substring(from, from + length));
  }
}
