/*
 * Copyright 2015 Ben Manes. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.benmanes.caffeine.cache.simulator.admission.countmin4;

import static com.google.common.base.Preconditions.checkArgument;

import javax.annotation.Nonnegative;

import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
import com.github.benmanes.caffeine.cache.simulator.admission.Frequency;
import com.typesafe.config.Config;

/**
 * A probabilistic multiset for estimating the popularity of an element within a time window. The
 * maximum frequency of an element is limited to 15 (4-bits) and extensions provide the aging
 * process.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public abstract class CountMin4 implements Frequency {
  static final long[] SEED = new long[] { // A mixture of seeds from FNV-1a, CityHash, and Murmur3
      0xc3a5c85c97cb3127L, 0xb492b66fbe98f273L, 0x9ae16a3b2f90404fL, 0xcbf29ce484222325L};
  static final long RESET_MASK = 0x7777777777777777L;

  protected final boolean conservative;
  protected final int randomSeed;

  protected int tableMask;
  protected long[] table;
  protected int step = 1;

  /**
   * Creates a frequency sketch that can accurately estimate the popularity of elements given
   * the maximum size of the cache.
   */
  protected CountMin4(Config config) {
    BasicSettings settings = new BasicSettings(config);
    conservative = settings.tinyLfu().conservative();
    checkArgument(settings.randomSeed() != 0);
    randomSeed = settings.randomSeed();

    double countersMultiplier = settings.tinyLfu().countMin4().countersMultiplier();
    long counters = (long) (countersMultiplier * settings.maximumSize());
    ensureCapacity(counters);

  }

  /**
   * Increases the capacity of this <tt>FrequencySketch</tt> instance, if necessary, to ensure that
   * it can accurately estimate the popularity of elements given the maximum size of the cache. This
   * operation forgets all previous counts when resizing.
   *
   * @param maximumSize the maximum size of the cache
   */
  protected void ensureCapacity(@Nonnegative long maximumSize) {
    checkArgument(maximumSize >= 0);
    int maximum = (int) Math.min(maximumSize, Integer.MAX_VALUE >>> 1);
    if ((table != null) && (table.length >= maximum)) {
      return;
    }

    table = new long[(maximum == 0) ? 1 : ceilingNextPowerOfTwo(maximum)];
    tableMask = Math.max(0, table.length - 1);
  }

  /**
   * Returns the estimated number of occurrences of an element, up to the maximum (15).
   *
   * @param e the element to count occurrences of
   * @return the estimated number of occurrences of the element; possibly zero but never negative
   */
  @Override
  @Nonnegative
  public int frequency(long e) {
    int hash = spread(Long.hashCode(e));
    int start = (hash & 3) << 2;
    int frequency = Integer.MAX_VALUE;
    for (int i = 0; i < 4; i++) {
      int index = indexOf(hash, i);
      int count = (int) ((table[index] >>> ((start + i) << 2)) & 0xfL);
      frequency = Math.min(frequency, count);
    }
    return frequency;
  }

  /**
   * Increments the popularity of the element if it does not exceed the maximum (15). The popularity
   * of all elements will be periodically down sampled when the observed events exceeds a threshold.
   * This process provides a frequency aging to allow expired long term entries to fade away.
   *
   * @param e the element to add
   */
  @Override
  public void increment(long e) {
    if (conservative) {
      conservativeIncrement(e);
    } else {
      regularIncrement(e);
    }
  }

  /** Increments all of the associated counters. */
  void regularIncrement(long e) {
    int hash = spread(Long.hashCode(e));
    int start = (hash & 3) << 2;

    // Loop unrolling improves throughput by 5m ops/s
    int index0 = indexOf(hash, 0);
    int index1 = indexOf(hash, 1);
    int index2 = indexOf(hash, 2);
    int index3 = indexOf(hash, 3);

    boolean added = incrementAt(index0, start, step);
    added |= incrementAt(index1, start + 1, step);
    added |= incrementAt(index2, start + 2, step);
    added |= incrementAt(index3, start + 3, step);

    tryReset(added);
  }

  /** Increments the associated counters that are at the observed minimum. */
  void conservativeIncrement(long e) {
    int hash = spread(Long.hashCode(e));
    int start = (hash & 3) << 2;

    int[] index = new int[4];
    int[] count = new int[4];
    int min = Integer.MAX_VALUE;
    for (int i = 0; i < 4; i++) {
      index[i] = indexOf(hash, i);
      count[i] = (int) ((table[index[i]] >>> ((start + i) << 2)) & 0xfL);
      min = Math.min(min, count[i]);
    }

    if (min == 15) {
      tryReset(false);
      return;
    }

    for (int i = 0; i < 4; i++) {
      if (count[i] == min) {
        incrementAt(index[i], start + i, step);
      }
    }
    tryReset(true);
  }

  /** Performs the aging process after an addition to allow old entries to fade away. */
  protected void tryReset(boolean added) {}

  /**
   * Increments the specified counter by 1 if it is not already at the maximum value (15).
   *
   * @param i the table index (16 counters)
   * @param j the counter to increment
   * @param step the increase amount
   * @return if incremented
   */
  boolean incrementAt(int i, int j, long step) {
    int offset = j << 2;
    long mask = (0xfL << offset);
    if ((table[i] & mask) != mask) {
      long current = (table[i] & mask) >>> offset;
      long update = Math.min(current + step, 15);
      table[i] = (table[i] & ~mask) | (update << offset);
      return true;
    }
    return false;
  }

  /**
   * Returns the table index for the counter at the specified depth.
   *
   * @param item the element's hash
   * @param i the counter depth
   * @return the table index
   */
  int indexOf(int item, int i) {
    long hash = SEED[i] * item;
    hash += hash >> 32;
    return ((int) hash) & tableMask;
  }

  /**
   * Applies a supplemental hash function to a given hashCode, which defends against poor quality
   * hash functions.
   */
  int spread(int x) {
    x = ((x >>> 16) ^ x) * 0x45d9f3b;
    x = ((x >>> 16) ^ x) * randomSeed;
    return (x >>> 16) ^ x;
  }

  static int ceilingNextPowerOfTwo(int x) {
    // From Hacker's Delight, Chapter 3, Harry S. Warren Jr.
    return 1 << -Integer.numberOfLeadingZeros(x - 1);
  }
}
