import { assert } from "../../../../../assert.js";
import { align_4 } from "../../../../../binary/align_4.js";
import { BitSet } from "../../../../../binary/BitSet.js";
import { makeArrayBuffer } from "../../../../../binary/makeArrayBuffer.js";
import { array_swap } from "../../../../../collection/array/array_swap.js";
import { array_buffer_copy } from "../../../../../collection/array/typed/array_buffer_copy.js";
import { typed_array_copy } from "../../../../../collection/array/typed/typed_array_copy.js";
import { max3 } from "../../../../../math/max3.js";

/**
 * How many items to reserve by default
 * @readonly
 * @type {number}
 */
const INITIAL_CAPACITY = 128;

/**
 * @readonly
 * @type {number}
 */
const CAPACITY_GROW_MULTIPLIER = 1.2;

/**
 * @readonly
 * @type {number}
 */
const CAPACITY_GROW_MIN_STEP = 32;

/**
 * @see https://github.com/blender/blender/blob/master/source/blender/blenlib/intern/BLI_mempool.c
 * @implements {Iterable<number>}
 */
export class BinaryElementPool {

    /**
     * Unused slots
     * @type {number[]}
     * @private
     */
    __free = [];

    /**
     * Tracks last unallocated item in the list,
     * this separate cursor is necessary to prevent re-allocation of the 'free' array
     * @type {number}
     * @private
     */
    __free_pointer = 0;

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

    /**
     *
     * @param {number} item_size in bytes
     * @param {number} [initial_capacity] how many items to reverse in the newly created pool
     * @param {boolean} [use_shared_buffer]
     */
    constructor(item_size, initial_capacity = INITIAL_CAPACITY, use_shared_buffer = false) {
        assert.isNonNegativeInteger(item_size, 'item_size');
        assert.greaterThan(item_size, 0, 'item_size must be greater than 0');

        assert.isNonNegativeInteger(initial_capacity, 'initial_capacity');
        assert.isBoolean(use_shared_buffer, 'use_shared_buffer');

        /**
         * Size of a single pool item in bytes
         * @type {number}
         * @private
         */
        this.__item_size = item_size;


        /**
         *
         * @type {ArrayBuffer}
         * @private
         */
        this.__data_buffer = makeArrayBuffer(align_4(initial_capacity * item_size), use_shared_buffer);
        /**
         *
         * @type {Uint8Array}
         * @private
         */
        this.__data_uint8 = new Uint8Array(this.__data_buffer);
        /**
         *
         * @type {Uint32Array}
         * @private
         */
        this.__data_uint32 = new Uint32Array(this.__data_buffer);
        /**
         *
         * @type {Float32Array}
         * @private
         */
        this.__data_float32 = new Float32Array(this.__data_buffer);
        this.data_view = new DataView(this.__data_buffer);

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

        /**
         * Occupancy index: bit `id` is 1 when slot `id` is allocated, 0 when
         * free. Lets {@link is_allocated} answer in O(1) instead of scanning
         * {@link __free}. The {@link __free} stack remains the allocator; this
         * is purely a membership accelerator kept in lockstep with it.
         * `preventShrink` stops the bitset reallocating smaller when top slots
         * are cleared — its capacity tracks the pool, not the live set.
         * @type {BitSet}
         * @private
         */
        this.__occupancy = new BitSet(this.__capacity);
        this.__occupancy.preventShrink();

    }

    /**
     *
     * @param {ArrayBuffer} buffer
     * @param {number} allocated_record_count
     */
    fromArrayBuffer(buffer, allocated_record_count = 0) {
        assert.defined(buffer, 'buffer');
        assert.notNull(buffer, 'buffer');

        const capacity = Math.floor(buffer.byteLength / this.__item_size);

        assert.isNonNegativeInteger(allocated_record_count, 'allocated_record_count');
        assert.lessThanOrEqual(allocated_record_count, capacity, 'allocated_record_count is higher than capacity');

        this.__data_buffer = buffer;
        this.__data_uint8 = new Uint8Array(buffer);
        this.__data_uint32 = new Uint32Array(buffer);
        this.__data_float32 = new Float32Array(buffer);
        this.data_view = new DataView(buffer);

        this.__capacity = capacity;

        // drop free slots
        this.__free_pointer = 0;
        this.__size = allocated_record_count;

        // Rebuild occupancy for the adopted buffer: the free set is dropped, so
        // exactly [0, allocated_record_count) is allocated.
        this.__occupancy = new BitSet(capacity);
        this.__occupancy.preventShrink();
        if (allocated_record_count > 0) {
            this.__occupancy.setRange(0, allocated_record_count - 1);
        }
    }

    /**
     *
     * @returns {ArrayBuffer}
     */
    get arrayBuffer() {
        return this.__data_buffer;
    }

    /**
     * Size of a single record in bytes
     * @return {number}
     */
    get item_size() {
        return this.__item_size;
    }

    /**
     * Returns size of used region, this includes both elements that are allocated and those that aren't
     * Please note that this value does not represent number of currently active elements, if you need that - you'll need to use something else
     * @return {number}
     */
    get size() {
        return this.__size;
    }

    /**
     *
     * @return {number}
     */
    get byteSize() {
        return this.__capacity * this.__item_size;
    }

    /**
     * Number of records that the pool can currently contain
     * @return {number}
     */
    get capacity() {
        return this.__capacity;
    }


    /**
     *
     * @return {Uint32Array}
     */
    get data_uint32() {
        return this.__data_uint32;
    }

    /**
     *
     * @return {Float32Array}
     */
    get data_float32() {
        return this.__data_float32;
    }

    /**
     * Get rid of excess capacity
     */
    trim() {
        this.__set_capacity(this.__size);
    }

    /**
     *
     * @param {number} id
     * @return {number}
     */
    element_address(id) {
        assert.isNonNegativeInteger(id, 'id');

        return this.__item_size * id;
    }

    /**
     * Returns word-offset of element
     * Word size is 4, so this is the same as `element_address(id) / 4`
     * @param {number} id
     * @return {number}
     */
    element_word(id) {
        return this.element_address(id) >> 2;
    }

    /**
     * Used alongside iterators to check if element is actually allocated or not
     * @param {number} id
     * @return {boolean}
     */
    is_allocated(id) {
        assert.isNonNegativeInteger(id, 'id');

        if (id >= this.__size) {
            // ID is past allocated region
            return false;
        }

        // O(1) occupancy bit read.
        return this.__occupancy.get(id);
    }

    /**
     *
     * @param {number} new_capacity
     * @private
     */
    __set_capacity(new_capacity) {
        if (this.__capacity === new_capacity) {
            // no point
            return;
        }

        const old_data_uint8 = this.__data_uint8;

        const aligned_byte_size = align_4(new_capacity * this.__item_size);

        const ArrayBufferType = this.__data_buffer.constructor;

        const new_data_buffer = new ArrayBufferType(aligned_byte_size);

        this.__data_buffer = new_data_buffer;
        this.__data_uint8 = new Uint8Array(new_data_buffer);
        this.__data_uint32 = new Uint32Array(this.__data_buffer);
        this.__data_float32 = new Float32Array(this.__data_buffer);
        this.data_view = new DataView(new_data_buffer);

        // copy old data
        typed_array_copy(old_data_uint8, this.__data_uint8);

        this.__capacity = new_capacity;

        // The bitset self-manages capacity on set(); preventShrink keeps it from
        // dropping below the used region on clears. Nothing to do here.
    }

    /**
     *
     * @param {number} min_capacity
     * @private
     */
    __grow_capacity(min_capacity) {
        const new_capacity = Math.ceil(max3(
            min_capacity,
            this.__capacity * CAPACITY_GROW_MULTIPLIER,
            this.__capacity + CAPACITY_GROW_MIN_STEP
        ));

        this.__set_capacity(new_capacity);
    }

    /**
     *
     * @param {number} capacity
     */
    ensure_capacity(capacity) {
        if (this.__capacity < capacity) {
            this.__grow_capacity(capacity);
        }
    }


    /**
     *
     * @return {number} ID of the allocated element
     */
    allocate() {
        if (this.__free_pointer > 0) {
            // get unused slot
            this.__free_pointer--;
            const id = this.__free[this.__free_pointer];
            this.__occupancy.set(id, true);
            return id;
        }

        // allocate new
        let result = this.__size;
        this.__size++;

        if (this.__size >= this.__capacity) {
            // grow if necessary
            this.__grow_capacity(this.__size);
        }

        // assert.greaterThan(this.__data_buffer.byteLength, result * this.__item_size, 'memory underflow');

        this.__occupancy.set(result, true);

        return result;
    }

    /**
     * Allocate a continuous range of IDs in bulk
     * @param {number} count
     * @return {number} offset where the range starts, this is your first ID basically
     */
    allocate_continuous(count) {
        const offset = this.__size;

        this.__size += count;

        if (this.__size >= this.__capacity) {
            this.__grow_capacity(this.__size);
        }

        // assert.greaterThanOrEqual(this.__data_buffer.byteLength, (offset + count) * this.__item_size, 'memory underflow');

        // Mark the whole range allocated.
        if (count > 0) {
            this.__occupancy.setRange(offset, offset + count - 1);
        }

        return offset;
    }

    /**
     * Swap the bytes of two items in-place.
     * No-op when `a === b`.
     * @param {number} a item ID
     * @param {number} b item ID
     */
    swap(a, b) {
        assert.isNonNegativeInteger(a, 'a');
        assert.isNonNegativeInteger(b, 'b');

        if (a === b) {
            return;
        }

        const item_size = this.__item_size;

        array_swap(
            this.__data_uint8, a * item_size,
            this.__data_uint8, b * item_size,
            item_size
        );
    }

    /**
     * Please note that this method does not perform any checks at all.
     * You have to make sure that the item is actually unneeded and no duplicate calls are made
     * @param {number} id
     */
    release(id) {
        if (id === this.__size - 1) {
            // releasing item at the end of the reserved region, don't have to push it into free set
            this.__size--;
            this.__occupancy.set(id, false);
        } else if (id >= this.__size) {
            // do nothing, just throw it away
            // this should never happen
        } else {
            this.__free[this.__free_pointer++] = id;
            this.__occupancy.set(id, false);
        }
    }

    /**
     * Unsafe operation, clears the free set without any checks.
     */
    clear_free_set() {
        this.__free_pointer = 0;

        // With the free set empty, every slot in [0, size) is allocated again.
        if (this.__size > 0) {
            this.__occupancy.setRange(0, this.__size - 1);
        }
    }

    /**
     * Reduce the used region down to `new_size` and discard the free set.
     * Underlying memory is not reallocated; use {@link trim} for that.
     * The caller is responsible for ensuring that no live data lives at IDs >= new_size.
     * @param {number} new_size must not exceed the current {@link size}
     */
    truncate(new_size) {
        assert.isNonNegativeInteger(new_size, 'new_size');
        assert.lessThanOrEqual(new_size, this.__size, 'new_size must not exceed current size');

        this.__size = new_size;
        this.__free_pointer = 0;

        // Free set discarded: [0, new_size) is the live region with no holes.
        this.__occupancy.reset();
        if (new_size > 0) {
            this.__occupancy.setRange(0, new_size - 1);
        }
    }

    /**
     * Removed all data from the pool
     * Note that initial allocation pointer is set to 0
     */
    clear() {
        this.__size = 0;
        this.__free_pointer = 0;
        this.__occupancy.reset();
    }

    /**
     * Copy data from another pool
     * @param {BinaryElementPool} other
     */
    copy(other) {
        assert.defined(other, 'other');
        assert.notNull(other, 'other');
        assert.isObject(other, 'other');
        assert.equal(other.isBinaryElementPool, true, 'other.isBinaryElementPool !== true');

        this.__size = other.size;
        this.__item_size = other.item_size;

        const other_data_size = other.size * this.item_size;

        const this_data = this.__data_buffer;
        const other_data = other.__data_buffer;

        if (this_data.byteLength < other_data_size) {
            // pool is too small, re-allocate with a fast copy
            this.fromArrayBuffer(other_data.slice(0), this.__size);
        } else {
            // pool is big enough, copy the data
            array_buffer_copy(other_data, 0, this_data, 0, other_data_size);
        }

        this.__free_pointer = other.__free_pointer;
        this.__free = other.__free.slice();

        this.__rebuild_occupancy_from_free();
    }

    /**
     * Rebuild the occupancy index from `__size` and the current free set:
     * every id in `[0, size)` is allocated except those held in `__free`.
     * Used after {@link copy}, where the data buffer and free set are adopted
     * wholesale.
     * @private
     */
    __rebuild_occupancy_from_free() {
        this.__occupancy = new BitSet(Math.max(this.__capacity, this.__size));
        this.__occupancy.preventShrink();

        if (this.__size > 0) {
            this.__occupancy.setRange(0, this.__size - 1);
        }

        const free = this.__free;
        const pointer = this.__free_pointer;
        for (let i = 0; i < pointer; i++) {
            this.__occupancy.set(free[i], false);
        }
    }

    /**
     *
     * @return {Generator<number>}
     */
    * [Symbol.iterator]() {

        for (let i = 0; i < this.__size; i++) {

            if (this.is_allocated(i) === false) {
                continue;
            }

            yield i;
        }

    }
}

/**
 * Used for type checking
 * @readonly
 * @type {boolean}
 */
BinaryElementPool.prototype.isBinaryElementPool = true;
