import { assert } from "../../assert.js";
import { max2 } from "../../math/max2.js";

const UINT32_MAX = 4294967295;

const DEFAULT_CAPACITY = 64;
const ELEMENT_BYTE_SIZE = 8;

/**
 * % to increase capacity by when growing
 * NOTE: Must be greater than 1
 * @type {number}
 */
const RESIZE_GROW_FACTOR = 1.2;
/**
 * Minimum number of elements to expand the size by when growing
 * NOTE: Must be an integer
 * NOTE: Must be greater than 0
 * @type {number}
 */
const RESIZE_GROW_MIN_COUNT = 16;

/**
 *
 * @param {number} i
 * @returns {number}
 */
function HEAP_PARENT(i) {
    return ((i) - 1) >> 1;
}

/**
 *
 * @param {number} i
 * @returns {number}
 */
function HEAP_LEFT(i) {
    return ((i) << 1) + 1;
}

/**
 *
 * @param {number} i
 * @returns {number}
 */
function HEAP_RIGHT(i) {
    return ((i) << 1) + 2;
}

/**
 * Binary Heap implementation that stores uin32 ID along with a floating point score value.
 * Very fast and compact.
 *
 * @see BinaryHeap
 */
export class Uint32Heap {
    // Inspired by Blender's heap implementation found here: https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenlib/intern/BLI_heap.c

    /**
     *
     * @param {number} [initial_capacity] Can supply initial capacity, heap will still grow when necessary. This allows to prevent needless re-allocations when max heap size is known in advance
     */
    constructor(initial_capacity = DEFAULT_CAPACITY) {
        assert.isNonNegativeInteger(initial_capacity, 'capacity');

        this.__data_buffer = new ArrayBuffer(initial_capacity * ELEMENT_BYTE_SIZE);

        /**
         * Used to access stored IDs
         * @type {Uint32Array}
         * @private
         */
        this.__data_uint32 = new Uint32Array(this.__data_buffer);

        /**
         * Used to access stored Score values
         * @type {Float32Array}
         * @private
         */
        this.__data_float32 = new Float32Array(this.__data_buffer);

        /**
         *
         * @type {number}
         * @private
         */
        this.__capacity = initial_capacity;

        /**
         *
         * @type {number}
         * @private
         */
        this.__size = 0;
    }

    /**
     *
     * @private
     */
    __capacity_grow() {
        const old_capacity = this.__capacity;

        const new_capacity = Math.ceil(max2(
            old_capacity * RESIZE_GROW_FACTOR,
            old_capacity + RESIZE_GROW_MIN_COUNT
        ));

        assert.greaterThan(new_capacity, old_capacity, 'invalid growth');

        const new_buffer = new ArrayBuffer(new_capacity * ELEMENT_BYTE_SIZE);
        const new_uint32 = new Uint32Array(new_buffer);

        // copy old data into new container
        new_uint32.set(this.__data_uint32);

        this.__data_buffer = new_buffer;
        this.__data_uint32 = new_uint32;
        this.__data_float32 = new Float32Array(new_buffer);

        // update capacity
        this.__capacity = new_capacity;
    }

    /**
     * @private
     * @param {number} a index of an element
     * @param {number} b index of an element
     * @returns {boolean}
     */
    compare(a, b) {
        const float32 = this.__data_float32;

        const a2 = a << 1; // same as a*2
        const b2 = b << 1; // same as b*2

        return float32[a2] < float32[b2];
    }

    /**
     * Swap two elements
     * @private
     * @param {number} i element index
     * @param {number} j element index
     */
    swap(i, j) {
        // fast multiplication by 2
        const i2 = i << 1; // same as i*2
        const j2 = j << 1; // same as j*2

        const uint32 = this.__data_uint32;

        const mem_0 = uint32[i2];
        uint32[i2] = uint32[j2];
        uint32[j2] = mem_0;

        const i21 = i2 + 1;
        const j21 = j2 + 1;

        const mem_1 = uint32[i21];
        uint32[i21] = uint32[j21];
        uint32[j21] = mem_1;
    }

    /**
     * @private
     * @param {number} index
     */
    heap_down(index) {
        let i = index;

        // size does not change, cache it for performance
        const size = this.__size;

        while (true) {
            const left = (i << 1) + 1;
            const right = left + 1;

            let smallest = i;

            if (left < size && this.compare(left, smallest)) {
                smallest = left;
            }

            if (right < size && this.compare(right, smallest)) {
                smallest = right;
            }

            if (smallest === i) {
                break;
            }

            this.swap(i, smallest);

            i = smallest;
        }

    }

    /**
     * Bubble up given element into its correct position
     * @private
     * @param {number} index
     */
    heap_up(index) {
        let i = index;

        while (i > 0) {
            // get parent
            const p = ((i) - 1) >> 1;

            if (this.compare(p, i)) {
                break;
            }

            this.swap(p, i);

            i = p;
        }
    }

    /**
     *
     * @returns {number}
     */
    get size() {
        return this.__size;
    }

    /**
     *
     * @returns {number}
     */
    get capacity() {
        return this.__capacity;
    }

    /**
     * Node with the lowest score
     * @returns {number}
     */
    get top_id() {
        return this.__data_uint32[1];
    }

    /**
     *
     * @returns {boolean}
     */
    is_empty() {
        return this.__size === 0;
    }

    /**
     * Returns the score of the node with the lowest score
     * @returns {number}
     */
    peek_score() {
        return this.__data_float32[0];
    }

    /**
     * Returns the ID of the node with the lowest score
     * @returns {number}
     */
    peek_min() {
        return this.top_id;
    }

    /**
     * Removed and returns the ID of the node with the lowest score
     * @returns {number}
     */
    pop_min() {
        assert.greaterThan(this.__size, 0, 'heap is empty');

        const new_size = this.__size - 1;
        this.__size = new_size;

        const uint32 = this.__data_uint32;
        const top_id = uint32[1];

        // move bottom element to the top.
        // the top doesn't need to be moved down as we have discarded it already
        const i2 = new_size << 1; // same as *2

        uint32[0] = uint32[i2];
        uint32[1] = uint32[i2 + 1];

        // re-balance
        this.heap_down(0);

        return top_id;
    }

    /**
     *
     * @param {number} id
     * @returns {number}
     */
    find_index_by_id(id) {
        const n = this.__size
        const n2 = n << 1; // fast *2 multiplication

        const uint32 = this.__data_uint32;

        for (let address = 1; address < n2; address += 2) {
            const _id = uint32[address];

            if (_id === id) {
                // reverse address to index
                return (address >>> 1);
            }
        }

        // not found
        return -1;
    }

    /**
     *
     * @param {number} id
     * @returns {boolean}
     */
    contains(id) {
        return this.find_index_by_id(id) !== -1;
    }

    /**
     * Clear out all the data, heap will be made empty
     */
    clear() {
        this.__size = 0;
    }

    /**
     *
     * @param {number} id
     * @returns {boolean}
     */
    remove(id) {
        const i = this.find_index_by_id(id);

        if (i === -1) {
            return false;
        }

        this.__remove_by_index(i);
        return true;
    }

    /**
     *
     * @param {number} index
     */
    __remove_by_index(index) {
        assert.greaterThan(this.__size, 0, 'heap is empty');

        let i = index;

        while (i > 0) {
            const p = HEAP_PARENT(i);
            this.swap(p, i);

            i = p;
        }

        this.pop_min();
    }

    /**
     *
     * @param {number} id
     * @param {number} score
     */
    update_score(id, score) {
        const index = this.find_index_by_id(id);

        if (index === -1) {
            throw new Error('Not found');
        }

        this.__update_score_by_index(index, score);
    }

    /**
     * Update score of an element referenced directly by index, this is a fast method, but you're generally not going to know the index so in most cases it's best to use "update_score" instead
     * @param {number} index
     * @param {number} score
     */
    __update_score_by_index(index, score) {

        const float32 = this.__data_float32;

        const index2 = index << 1; // fast *2 multiplication

        const existing_score = float32[index2];

        if (score < existing_score) {
            float32[index2] = score;
            this.heap_up(index);
        } else if (score > existing_score) {
            float32[index2] = score;
            this.heap_down(index);
        }
    }

    /**
     *
     * @param {number} id
     * @returns {number}
     */
    get_score(id) {
        const index = this.find_index_by_id(id);

        if (index === -1) {
            return Number.NaN;
        }

        const index2 = index << 1; // fast *2 multiplication

        return this.__data_float32[index2];
    }


    /**
     * Insert or update an element, if the element already exists, its score will be updated.
     *
     * @param {number} id
     * @param {number} score
     */
    insert_or_update(id, score) {
        const i = this.find_index_by_id(id);

        if (i === -1) {
            this.insert(id, score);
        } else {
            this.__update_score_by_index(i, score);
        }
    }

    /**
     *
     * @param {number} id
     * @param {number} score
     */
    insert(id, score) {
        assert.isNonNegativeInteger(id, 'value');
        assert.lessThanOrEqual(id, UINT32_MAX - 1, 'must be less than or equal to (2^32 - 2)');
        assert.isNumber(score, 'score');
        assert.notNaN(score, 'score');

        const current_size = this.__size;

        if (current_size >= this.__capacity) {
            // need to re-allocate
            this.__capacity_grow();
        }

        // insert at the end
        const index = current_size;
        const address = index << 1; // same as *2

        // write data
        this.__data_float32[address] = score;
        this.__data_uint32[address + 1] = id;

        // record increased size
        this.__size = current_size + 1;

        this.heap_up(index);
    }


}
