import { assert } from "../../assert.js";
import { BinaryElementPool } from "../../geom/3d/topology/struct/binary/BinaryElementPool.js";

const UINT32_MAX = 4294967295;

/**
 * Sentinel for "no such node" — matches Boost's `(size_type)(-1)` convention.
 * Valid node handles are 0 .. UINT32_MAX - 1.
 * @type {number}
 */
const NULL_HANDLE = UINT32_MAX;

/*
 * Node record layout — 32 bytes / 8 words per node, kept word-aligned so that one record fits inside half
 * a typical 64 B cache line. All fields are uint32 except `key`, which is float32; both views are aliased
 * onto the same backing ArrayBuffer via {@link BinaryElementPool}.
 *
 *   word 0 : key      (float32)  — priority / score
 *   word 1 : id       (uint32)   — client-supplied identifier
 *   word 2 : parent   (uint32)   — handle, NULL_HANDLE if at root
 *   word 3 : child    (uint32)   — handle of an arbitrary child, NULL_HANDLE if leaf
 *   word 4 : left     (uint32)   — left  sibling in the circular doubly-linked list
 *   word 5 : right    (uint32)   — right sibling in the circular doubly-linked list
 *   word 6 : degree   (uint32)   — number of children
 *   word 7 : mark     (uint32)   — 0 or 1, set by cascading cuts
 */
const RECORD_WORD_SIZE = 8;
const RECORD_BYTE_SIZE = RECORD_WORD_SIZE * 4;

const FIELD_KEY = 0;
const FIELD_ID = 1;
const FIELD_PARENT = 2;
const FIELD_CHILD = 3;
const FIELD_LEFT = 4;
const FIELD_RIGHT = 5;
const FIELD_DEGREE = 6;
const FIELD_MARK = 7;

const DEFAULT_CAPACITY = 64;

/**
 * Fibonacci heap implementation that stores uint32 ID along with a floating point score value, intended to be
 * API-compatible with {@link Uint32Heap}.
 *
 * Storage is struct-of-arrays on top of a single {@link BinaryElementPool} — each "node" is a 32-byte
 * record in a contiguous ArrayBuffer, and "node references" are uint32 handles rather than object pointers.
 * This keeps all per-node state in continuous memory, avoiding the per-element allocation and pointer chasing
 * of an object-graph implementation.
 *
 * Fibonacci heaps have theoretically excellent asymptotic bounds:
 *   - insert, find-min, decrease-key: amortized O(1)
 *   - extract-min, delete: amortized O(log n)
 *
 * In practice they often lose to a simple binary or 4-ary heap on graph-search workloads because of poor
 * cache behavior; this layout closes most of that gap by removing the indirection. See Larkin, Sen &amp; Tarjan
 * (2014), "A Back-to-Basics Empirical Study of Priority Queues" (arXiv:1403.0252) for the empirical context.
 *
 * Reference: Cormen, Leiserson, Rivest, Stein, "Introduction to Algorithms", 3rd ed., chapter 19.
 *
 * @see Uint32Heap
 * @see Uint32Heap4
 */
export class FibonacciHeap {

    /**
     *
     * @param {number} [initial_capacity] reserves space for this many nodes up front. The pool grows on demand.
     */
    constructor(initial_capacity = DEFAULT_CAPACITY) {
        assert.isNonNegativeInteger(initial_capacity, 'initial_capacity');

        /**
         * @type {BinaryElementPool}
         * @private
         */
        this.__pool = new BinaryElementPool(RECORD_BYTE_SIZE, initial_capacity);

        /**
         * Handle of the node with the smallest key, or NULL_HANDLE if empty.
         * @type {number}
         * @private
         */
        this.__min = NULL_HANDLE;

        /**
         * Number of nodes currently in the heap.
         * @type {number}
         * @private
         */
        this.__size = 0;

        /**
         * Id -> handle lookup, allows update_score / remove / contains / get_score in O(1) expected.
         *
         * Empirically (see the bench tests in FibonacciHeap.spec.js), V8's built-in Map beats a hand-rolled
         * Uint32Array-backed Robin Hood hash table for this workload by 20-90 % on hot paths. Reason: every
         * key and value here is SMI (ids and handles are both small), so V8 Map's native C++ SMI fast path
         * never boxes; meanwhile a JS-side flat-array implementation pays per-probe Math.imul + bounds-checks
         * that V8's compiled Map sidesteps. The Uint32Map primitive lives at core/collection/Uint32Map.js
         * and is the right choice when keys/values exceed 2^30 (V8 Map boxes) or when delete churn is
         * extreme (V8 Map tombstone-rots until rehash) — but not here.
         *
         * @type {Map<number, number>}
         * @private
         */
        this.__handles = new Map();
    }

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

    /**
     * Node with the lowest score
     * @returns {number}
     */
    get top_id() {
        if (this.__min === NULL_HANDLE) {
            return 0;
        }
        return this.__pool.data_uint32[(this.__min * RECORD_WORD_SIZE) + FIELD_ID];
    }

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

    /**
     * Returns the score of the node with the lowest score
     * @returns {number}
     */
    peek_score() {
        if (this.__min === NULL_HANDLE) {
            return Number.NaN;
        }
        return this.__pool.data_float32[(this.__min * RECORD_WORD_SIZE) + FIELD_KEY];
    }

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

    /**
     * Splice `handle` (a single-element circular list) into the root list to the right of `__min`. The caller
     * is responsible for setting parent and degree on `handle`.
     *
     * @private
     * @param {number} handle
     */
    __root_insert(handle) {
        const u32 = this.__pool.data_uint32;
        const f32 = this.__pool.data_float32;
        const min = this.__min;
        const handle_base = handle * RECORD_WORD_SIZE;

        if (min === NULL_HANDLE) {
            // first node — it is its own root list
            u32[handle_base + FIELD_LEFT] = handle;
            u32[handle_base + FIELD_RIGHT] = handle;
            this.__min = handle;
        } else {
            // splice in to the right of min
            const min_base = min * RECORD_WORD_SIZE;
            const min_right = u32[min_base + FIELD_RIGHT];

            u32[handle_base + FIELD_LEFT] = min;
            u32[handle_base + FIELD_RIGHT] = min_right;
            u32[(min_right * RECORD_WORD_SIZE) + FIELD_LEFT] = handle;
            u32[min_base + FIELD_RIGHT] = handle;

            if (f32[handle_base + FIELD_KEY] < f32[min_base + FIELD_KEY]) {
                this.__min = handle;
            }
        }
    }

    /**
     * Remove `handle` from its sibling list (root list or child list of some parent). Sibling pointers of
     * `handle` are left dangling; the caller is expected to re-link it immediately.
     *
     * @private
     * @param {number} handle
     */
    __unlink_from_siblings(handle) {
        const u32 = this.__pool.data_uint32;
        const base = handle * RECORD_WORD_SIZE;

        const left = u32[base + FIELD_LEFT];
        const right = u32[base + FIELD_RIGHT];

        u32[(left * RECORD_WORD_SIZE) + FIELD_RIGHT] = right;
        u32[(right * RECORD_WORD_SIZE) + FIELD_LEFT] = left;
    }

    /**
     *
     * @param {number} id
     * @param {number} score
     */
    insert(id, score) {
        assert.isNonNegativeInteger(id, 'id');
        assert.lessThanOrEqual(id, UINT32_MAX - 1, 'id must be <= (2^32 - 2)');
        assert.isNumber(score, 'score');
        assert.notNaN(score, 'score');

        // pool.allocate() may grow the underlying buffer, so re-fetch views *after* the call
        const handle = this.__pool.allocate();
        const u32 = this.__pool.data_uint32;
        const f32 = this.__pool.data_float32;
        const base = handle * RECORD_WORD_SIZE;

        // a recycled slot retains stale bytes from its previous occupant — initialize every field explicitly
        f32[base + FIELD_KEY] = score;
        u32[base + FIELD_ID] = id;
        u32[base + FIELD_PARENT] = NULL_HANDLE;
        u32[base + FIELD_CHILD] = NULL_HANDLE;
        u32[base + FIELD_LEFT] = handle;
        u32[base + FIELD_RIGHT] = handle;
        u32[base + FIELD_DEGREE] = 0;
        u32[base + FIELD_MARK] = 0;

        this.__root_insert(handle);

        this.__handles.set(id, handle);
        this.__size++;
    }

    /**
     * Removed and returns the ID of the node with the lowest score.
     *
     * @returns {number}
     */
    pop_min() {
        const z = this.__min;

        assert.notEqual(z, NULL_HANDLE, 'heap is empty');

        const u32 = this.__pool.data_uint32;
        const z_base = z * RECORD_WORD_SIZE;
        const z_id = u32[z_base + FIELD_ID];

        // promote all of z's children to the root list
        const child = u32[z_base + FIELD_CHILD];
        if (child !== NULL_HANDLE) {
            // walk the child list, clearing parent pointers
            let c = child;
            do {
                u32[(c * RECORD_WORD_SIZE) + FIELD_PARENT] = NULL_HANDLE;
                c = u32[(c * RECORD_WORD_SIZE) + FIELD_RIGHT];
            } while (c !== child);

            // splice the child list into the root list, immediately to the right of z.
            // both are circular doubly-linked lists; standard list-concat trick:
            const z_right = u32[z_base + FIELD_RIGHT];
            const child_base = child * RECORD_WORD_SIZE;
            const child_left = u32[child_base + FIELD_LEFT];

            u32[z_base + FIELD_RIGHT] = child;
            u32[child_base + FIELD_LEFT] = z;

            u32[(child_left * RECORD_WORD_SIZE) + FIELD_RIGHT] = z_right;
            u32[(z_right * RECORD_WORD_SIZE) + FIELD_LEFT] = child_left;

            u32[z_base + FIELD_CHILD] = NULL_HANDLE;
        }

        // snapshot before the unlink rewrites z's sibling pointers
        const z_right_after_merge = u32[z_base + FIELD_RIGHT];

        // remove z from the root list
        this.__unlink_from_siblings(z);

        if (z === z_right_after_merge) {
            // z was the only node remaining
            this.__min = NULL_HANDLE;
        } else {
            // tentatively set min to z's old right neighbor; consolidation will pick the real min
            this.__min = z_right_after_merge;
            this.__consolidate();
        }

        this.__size--;
        this.__handles.delete(z_id);
        this.__pool.release(z);

        return z_id;
    }

    /**
     * Merge trees of equal degree until every degree is unique among the roots. Sets `__min` to the true
     * minimum.
     *
     * @private
     */
    __consolidate() {
        const u32 = this.__pool.data_uint32;
        const f32 = this.__pool.data_float32;

        // upper bound on max degree D(n) <= floor(log_phi(n)).
        // log_phi(n) = log2(n) / log2(phi) ~= log2(n) * 1.4404; pad generously.
        const D = Math.floor(Math.log2(this.__size)) * 2 + 8;
        const A = new Array(D);
        for (let i = 0; i < D; i++) {
            A[i] = NULL_HANDLE;
        }

        // snapshot the current root list — the list is mutated during link, so iterate over a stable copy
        const roots = [];
        const start = this.__min;
        let w = start;
        do {
            roots.push(w);
            w = u32[(w * RECORD_WORD_SIZE) + FIELD_RIGHT];
        } while (w !== start);

        for (let i = 0; i < roots.length; i++) {
            let x = roots[i];
            let d = u32[(x * RECORD_WORD_SIZE) + FIELD_DEGREE];

            while (A[d] !== NULL_HANDLE) {
                let y = A[d];

                if (f32[(x * RECORD_WORD_SIZE) + FIELD_KEY] > f32[(y * RECORD_WORD_SIZE) + FIELD_KEY]) {
                    // swap so x is the smaller-key root
                    const tmp = x;
                    x = y;
                    y = tmp;
                }

                this.__link(y, x);

                A[d] = NULL_HANDLE;
                d++;
            }

            A[d] = x;
        }

        // rebuild the root list from A[] and find the new min
        this.__min = NULL_HANDLE;
        for (let i = 0; i < D; i++) {
            const a = A[i];
            if (a !== NULL_HANDLE) {
                // detach (it may still point to old neighbors that no longer reference it)
                const a_base = a * RECORD_WORD_SIZE;
                u32[a_base + FIELD_LEFT] = a;
                u32[a_base + FIELD_RIGHT] = a;
                this.__root_insert(a);
            }
        }
    }

    /**
     * Make `y` (currently in the root list) a child of `x`.
     *
     * @private
     * @param {number} y
     * @param {number} x
     */
    __link(y, x) {
        const u32 = this.__pool.data_uint32;

        // remove y from the root list
        this.__unlink_from_siblings(y);

        const x_base = x * RECORD_WORD_SIZE;
        const y_base = y * RECORD_WORD_SIZE;

        const x_child = u32[x_base + FIELD_CHILD];
        if (x_child === NULL_HANDLE) {
            u32[x_base + FIELD_CHILD] = y;
            u32[y_base + FIELD_LEFT] = y;
            u32[y_base + FIELD_RIGHT] = y;
        } else {
            // splice y into x's child list
            const x_child_base = x_child * RECORD_WORD_SIZE;
            const x_child_right = u32[x_child_base + FIELD_RIGHT];

            u32[y_base + FIELD_LEFT] = x_child;
            u32[y_base + FIELD_RIGHT] = x_child_right;
            u32[(x_child_right * RECORD_WORD_SIZE) + FIELD_LEFT] = y;
            u32[x_child_base + FIELD_RIGHT] = y;
        }

        u32[y_base + FIELD_PARENT] = x;
        u32[x_base + FIELD_DEGREE]++;
        u32[y_base + FIELD_MARK] = 0;
    }

    /**
     * Decrease the key of `handle` to `new_key`. `new_key` must be <= current key.
     *
     * @private
     * @param {number} handle
     * @param {number} new_key
     */
    __decrease_key(handle, new_key) {
        const u32 = this.__pool.data_uint32;
        const f32 = this.__pool.data_float32;
        const base = handle * RECORD_WORD_SIZE;

        assert.lessThanOrEqual(new_key, f32[base + FIELD_KEY], 'new key is greater than current key');

        f32[base + FIELD_KEY] = new_key;

        const parent = u32[base + FIELD_PARENT];
        if (parent !== NULL_HANDLE && new_key < f32[(parent * RECORD_WORD_SIZE) + FIELD_KEY]) {
            this.__cut(handle, parent);
            this.__cascading_cut(parent);
        }

        if (new_key < f32[(this.__min * RECORD_WORD_SIZE) + FIELD_KEY]) {
            this.__min = handle;
        }
    }

    /**
     * Cut `x` from `y`'s child list and promote it to the root list.
     *
     * @private
     * @param {number} x child being cut
     * @param {number} y parent of x
     */
    __cut(x, y) {
        const u32 = this.__pool.data_uint32;
        const x_base = x * RECORD_WORD_SIZE;
        const y_base = y * RECORD_WORD_SIZE;

        const x_right = u32[x_base + FIELD_RIGHT];

        if (x_right === x) {
            // x was the only child of y
            u32[y_base + FIELD_CHILD] = NULL_HANDLE;
        } else {
            if (u32[y_base + FIELD_CHILD] === x) {
                // y was pointing at x — point at a sibling instead
                u32[y_base + FIELD_CHILD] = x_right;
            }
            this.__unlink_from_siblings(x);
        }

        u32[y_base + FIELD_DEGREE]--;

        // single-element circular list, then insert at root
        u32[x_base + FIELD_LEFT] = x;
        u32[x_base + FIELD_RIGHT] = x;
        u32[x_base + FIELD_PARENT] = NULL_HANDLE;
        u32[x_base + FIELD_MARK] = 0;

        this.__root_insert(x);
    }

    /**
     * Walk up from `y`, cutting marked ancestors and marking unmarked ones.
     *
     * @private
     * @param {number} y
     */
    __cascading_cut(y) {
        const u32 = this.__pool.data_uint32;
        const y_base = y * RECORD_WORD_SIZE;

        const z = u32[y_base + FIELD_PARENT];
        if (z !== NULL_HANDLE) {
            if (u32[y_base + FIELD_MARK] === 0) {
                u32[y_base + FIELD_MARK] = 1;
            } else {
                this.__cut(y, z);
                this.__cascading_cut(z);
            }
        }
    }

    /**
     * Delete the node referenced by `handle` by decreasing its key to -Infinity and popping the min.
     *
     * @private
     * @param {number} handle
     */
    __delete_handle(handle) {
        this.__decrease_key(handle, Number.NEGATIVE_INFINITY);
        this.pop_min();
    }

    /**
     *
     * @param {number} id
     * @returns {boolean}
     */
    contains(id) {
        return this.__handles.has(id);
    }

    /**
     *
     * @param {number} id
     * @returns {number} score of the element with the given id, NaN if not found
     */
    get_score(id) {
        const handle = this.__handles.get(id);
        if (handle === undefined) {
            return Number.NaN;
        }
        return this.__pool.data_float32[(handle * RECORD_WORD_SIZE) + FIELD_KEY];
    }

    /**
     *
     * @param {number} id
     * @param {number} score
     */
    update_score(id, score) {
        const handle = this.__handles.get(id);

        if (handle === undefined) {
            throw new Error('Not found');
        }

        assert.isNumber(score, 'score');
        assert.notNaN(score, 'score');

        const current = this.__pool.data_float32[(handle * RECORD_WORD_SIZE) + FIELD_KEY];

        if (score < current) {
            this.__decrease_key(handle, score);
        } else if (score > current) {
            // increase-key: delete and re-insert
            this.__delete_handle(handle);
            this.insert(id, score);
        }
        // score === current: no-op
    }

    /**
     * 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) {
        if (this.__handles.has(id)) {
            this.update_score(id, score);
        } else {
            this.insert(id, score);
        }
    }

    /**
     *
     * @param {number} id
     * @returns {boolean}
     */
    remove(id) {
        const handle = this.__handles.get(id);

        if (handle === undefined) {
            return false;
        }

        this.__delete_handle(handle);

        return true;
    }

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