// Copyright 2018, Oath Inc.
// Licensed under the terms of the Apache License 2.0 license. See LICENSE file in Ultrabrew Metrics
// for terms.

package io.ultrabrew.metrics;

import io.ultrabrew.metrics.data.UnsafeHelper;
import java.util.Arrays;
import sun.misc.Unsafe;

public class RawHashTable {

  private static final Unsafe unsafe = UnsafeHelper.unsafe;
  private static final int RECORD_SIZE = 8; // align to 64-byte cache line
  private static final long usedOffset;
  private static final long scanLengthOffset;

  static {
    try {
      usedOffset = unsafe.objectFieldOffset(RawHashTable.class.getDeclaredField("used"));
      scanLengthOffset = unsafe
          .objectFieldOffset(RawHashTable.class.getDeclaredField("scanLength"));
    } catch (NoSuchFieldException e) {
      throw new Error(e);
    }
  }

  private final long[] table;
  private final int capacity;
  private int used = 0;
  private int scanLength = 0;

  public RawHashTable(final int capacity) {
    this.capacity = capacity;
    table = new long[this.capacity * RECORD_SIZE];
  }

  public void put(final String[] tags, final long value) {
    final long hashCode = Arrays.hashCode(tags);
    final int i = index(hashCode);
    final long base = Unsafe.ARRAY_LONG_BASE_OFFSET + i * Unsafe.ARRAY_LONG_INDEX_SCALE;

    unsafe.getAndAddLong(table, base + Unsafe.ARRAY_LONG_INDEX_SCALE, 1L);
    unsafe.getAndAddLong(table, base + 2 * Unsafe.ARRAY_LONG_INDEX_SCALE, value);
    min(base + 3 * Unsafe.ARRAY_LONG_INDEX_SCALE, value);
    max(base + 4 * Unsafe.ARRAY_LONG_INDEX_SCALE, value);
  }

  public long getCount(final String[] tags) {
    return getLong(tags, 1);
  }

  public long getSum(final String[] tags) {
    return getLong(tags, 2);
  }

  public long getMin(final String[] tags) {
    return getLong(tags, 3);
  }

  public long getMax(final String[] tags) {
    return getLong(tags, 4);
  }

  public int size() {
    return unsafe.getIntVolatile(this, usedOffset);
  }

  public int capacity() {
    return capacity;
  }

  public int lastScanLength() {
    return unsafe.getIntVolatile(this, scanLengthOffset);
  }

  private long getLong(final String[] tags, int offset) {
    final long hashCode = Arrays.hashCode(tags);
    final int i = index(hashCode);
    final long base = Unsafe.ARRAY_LONG_BASE_OFFSET + i * Unsafe.ARRAY_LONG_INDEX_SCALE;

    return unsafe.getLongVolatile(table, base + offset * Unsafe.ARRAY_LONG_INDEX_SCALE);
  }

  private void min(final long offset, final long value) {
    long old;
    do {
      old = unsafe.getLong(table, offset);
      if (value >= old) {
        return;
      }
      ///CLOVER:OFF
      // No reliable way to test without being able to mock unsafe
    } while (!unsafe.compareAndSwapLong(table, offset, old, value));
    ///CLOVER:ON
  }

  private void max(final long offset, final long value) {
    long old;
    do {
      old = unsafe.getLong(table, offset);
      if (value <= old) {
        return;
      }
      ///CLOVER:OFF
      // No reliable way to test without being able to mock unsafe
    } while (!unsafe.compareAndSwapLong(table, offset, old, value));
    ///CLOVER:ON
  }

  int index(final long key) {
    int start = (Math.abs((int) key) % capacity);
    int i = start < 0 ? 0 : start * RECORD_SIZE;
    boolean failSafe = false;

    for (int counter = 1; ; counter++) {

      final long offset = Unsafe.ARRAY_LONG_BASE_OFFSET + i * Unsafe.ARRAY_LONG_INDEX_SCALE;

      // check if we found our key
      final long candidate = unsafe.getLongVolatile(table, offset);
      if (key == candidate) {
        unsafe.putIntVolatile(this, scanLengthOffset, counter);
        return i;
      }

      // check if we found empty slot
      if (0L == candidate) {
        // try to reserve it

        ///CLOVER:OFF
        // No reliable way to test without being able to mock unsafe
        if (unsafe.compareAndSwapLong(table, offset, 0L, key)) {
          ///CLOVER:ON

          final int localUsed = unsafe.getAndAddInt(this, usedOffset, 1) + 1;
          unsafe.putLongVolatile(table, offset + 3 * Unsafe.ARRAY_LONG_INDEX_SCALE, Long.MAX_VALUE);
          unsafe.putLongVolatile(table, offset + 4 * Unsafe.ARRAY_LONG_INDEX_SCALE, Long.MIN_VALUE);

          unsafe.putIntVolatile(this, scanLengthOffset, counter);
          return i;
        }
      } else {
        // go to next record
        i += RECORD_SIZE;
        if (i >= table.length) {
          if (failSafe) {
            throw new IllegalStateException("No more space in linear probing table");
          } else {
            i = 0;
            failSafe = true;
          }
        }
      }
    }
  }
}