import { assert } from "../assert.js";
import { bitCount } from "../binary/operations/bitCount.js";
import { ceilPowerOfTwo } from "../binary/operations/ceilPowerOfTwo.js";
import { min2 } from "../math/min2.js";

const SEED = [ // A mixture of seeds from FNV-1a, CityHash, and Murmur3
    0x97cb3127,
    0xbe98f273,
    0x2f90404f,
    0x84222325
];

const RESET_MASK = 0x77777777;
const ONE_MASK = 0x11111111;

/**
 * Maximum value of a signed 32bit integer
 * @type {number}
 */
const MAX_INT_32 = 2147483647;

/**
 * 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 an aging process periodically
 * halves the popularity of all elements.
 *
 *
 * This class maintains a 4-bit CountMinSketch [1] with periodic aging to provide the popularity
 * history for the TinyLfu admission policy [2]. The time and space efficiency of the sketch
 * allows it to cheaply estimate the frequency of an entry in a stream of cache access events.
 *
 * The counter matrix is represented as a single dimensional array holding 16 counters per slot. A
 * fixed depth of four balances the accuracy and cost, resulting in a width of four times the
 * length of the array. To retain an accurate estimation the array's length equals the maximum
 * number of entries in the cache, increased to the closest power-of-two to exploit more efficient
 * bit masking. This configuration results in a confidence of 93.75% and error bound of e / width.
 *
 * The frequency of all entries is aged periodically using a sampling window based on the maximum
 * number of entries in the cache. This is referred to as the reset operation by TinyLfu and keeps
 * the sketch fresh by dividing all counters by two and subtracting based on the number of odd
 * counters found. The O(n) cost of aging is amortized, ideal for hardware prefetching, and uses
 * inexpensive bit manipulations per array location.
 *
 * [1] An Improved Data Stream Summary: The Count-Min Sketch and its Applications
 * http://dimacs.rutgers.edu/~graham/pubs/papers/cm-full.pdf
 * [2] TinyLFU: A Highly Efficient Cache Admission Policy
 * https://dl.acm.org/citation.cfm?id=3149371
 *
 * NOTE: ported from "caffeine"
 * @see https://github.com/ben-manes/caffeine/blob/master/caffeine/src/main/java/com/github/benmanes/caffeine/cache/FrequencySketch.java
 *
 * @author ben.manes@gmail.com (Ben Manes)
 * @author Alex Goldring 2021/04/08
 * @template T
 */
export class FrequencySketch {
    /**
     *
     * @type {number}
     */
    sampleSize = 0;
    /**
     *
     * @type {number}
     */
    tableMask = 0;
    /**
     *
     * @type {Uint32Array|null}
     */
    table = null;
    /**
     *
     * @type {number}
     */
    size = 0;

    /**
     * Initializes and 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 {number} maximumSize the maximum size of the cache
     */
    ensureCapacity(maximumSize) {
        assert.isNonNegativeInteger(maximumSize, 'maximumSize');

        if (this.table !== null && this.table.length >= maximumSize) {
            return;
        }

        const table_length = maximumSize <= 0 ? 1 : ceilPowerOfTwo(maximumSize);

        this.table = new Uint32Array(table_length);

        this.tableMask = Math.max(0, table_length - 1);

        this.sampleSize = (maximumSize <= 0) ? 10 : 10 * maximumSize;

        if (this.sampleSize <= 0) {
            this.sampleSize = Number.MAX_SAFE_INTEGER;
        }

        this.size = 0;
    }

    /**
     * Returns if the sketch has not yet been initialized, requiring that {@link #ensureCapacity} is
     * called before it begins to track frequencies.
     *
     * @returns {boolean}
     */
    isNotInitialized() {
        return (this.table == null);
    }

    /**
     * Returns the estimated number of occurrences of an element, up to the maximum (15).
     *
     * @param {number} input_hash the element to count occurrences of
     * @returns {number} the estimated number of occurrences of the element; possibly zero but never negative
     */
    frequency(input_hash) {
        const hash = this.spread(input_hash);
        const start = (hash & 3) << 2;
        let frequency = MAX_INT_32;

        for (let i = 0; i < 4; i++) {
            const index = this.indexOf(hash, i);
            const count = (this.table[index] >>> ((start + i) << 2)) & 0xf;
            frequency = min2(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 {number} input_hash the element to add
     */
    increment(input_hash) {
        const hash = this.spread(input_hash);
        const start = (hash & 3) << 2;

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

        const added = this.incrementAt(index0, start)
            || this.incrementAt(index1, start + 1)
            || this.incrementAt(index2, start + 2)
            || this.incrementAt(index3, start + 3);

        if (added && (++this.size === this.sampleSize)) {
            this.reset();
        }
    }

    /**
     * Increments the specified counter by 1 if it is not already at the maximum value (15).
     *
     * @param {number} i the table index (8 counters)
     * @param {number} j the counter to increment
     * @returns {boolean} if incremented
     */
    incrementAt(i, j) {
        const offset = j << 2;
        const mask = (0xF << offset);

        const table = this.table;

        const existing_table_value = table[i];

        if ((existing_table_value & mask) !== mask) {
            table[i] = existing_table_value + (1 << offset);
            return true;
        }

        return false;
    }

    /**
     * Reduces every counter by half of its original value.
     */
    reset() {
        let count = 0;
        const table = this.table;
        const table_size = table.length;

        for (let i = 0; i < table_size; i++) {
            const existing_table_value = table[i];

            count += bitCount(existing_table_value & ONE_MASK);
            table[i] = (existing_table_value >>> 1) & RESET_MASK;
        }

        this.size = (this.size >>> 1) - (count >>> 2);
    }

    /**
     * Returns the table index for the counter at the specified depth.
     * @param {number} item the element's hash
     * @param {number} i the counter depth
     * @returns {number} the table index
     */
    indexOf(item, i) {
        let hash = (item + SEED[i]) * SEED[i];
        hash += (hash >>> 16);

        return hash & this.tableMask;
    }

    /**
     * Applies a supplemental hash function to a given hash code, which defends against poor quality
     * @param {number} v
     * @returns {number}
     */
    spread(v) {
        let x = v;

        x = ((x >>> 16) ^ x) * 0x45d9f3b;
        x = ((x >>> 16) ^ x) * 0x45d9f3b;

        return (x >>> 16) ^ x;
    }
}
