import { CacheElement } from "../../cache/CacheElement.js";
import { HashMap } from "../../collection/map/HashMap.js";
import Signal from "../../events/signal/Signal.js";

/**
 * @template Key, Value
 */
export class ImmutableObjectPool {
    /**
     * @readonly
     * @type {Signal<Key,Value>}
     */
    onRemoved = new Signal()
    /**
     *
     * @type {HashMap<Key, CacheElement<Key,Value>[]>}
     */
    data = new HashMap();

    /**
     *
     * @type {CacheElement<Key,Value>|null}
     * @private
     */
    __first = null;
    /**
     *
     * @type {CacheElement<Key,Value>|null}
     * @private
     */
    __last = null;

    constructor({ capacity = 100, perKeyCapacity = 10 } = {}) {


        /**
         * How many items in total the pool can hold
         * @type {number}
         */
        this.capacity = capacity;

        /**
         * Maximum number of items that can be stored for the same key
         * @type {number}
         */
        this.perKeyCapacity = perKeyCapacity;

        /**
         * Current number of elements in the pool
         * @type {number}
         */
        this.size = 0;
    }

    get(key) {
        // find the right bucket
        const elements = this.data.get(key);

        if (elements === undefined) {
            return undefined;
        }

        let element;

        if (elements.length > 1) {
            element = elements.pop();
        } else {
            // last element
            element = elements[0];
            this.data.delete(key);
        }

        element.unlink();

        this.size--;

        return element.value;
    }

    /**
     * Removed all elements from cache
     * NOTE: {@link onRemoved} signal *does* get triggered for each element
     */
    clear() {
        while (this.__first !== null) {
            this.__removeElement(this.__first);
        }
    }

    /**
     *
     * @param {CacheElement<Key,Value>} el
     * @private
     */
    __removeElement(el) {
        if (el === this.__first) {
            this.__first = el.next;
        }

        if (el === this.__last) {
            this.__last = el.previous;
        }

        el.unlink();

        const key = el.key;

        const elements = this.data.get(key);

        const i = elements.indexOf(el);

        elements.splice(i, 1);

        if (elements.length === 0) {
            this.data.delete(key);
        }

        this.size--;

        this.onRemoved.send2(key, el.value);
    }

    add(key, value) {

        let elements = this.data.get(key);

        if (elements === undefined) {
            // no elements for this key, create container
            elements = [];
            this.data.set(key, elements);

        } else if (elements.length >= this.perKeyCapacity) {
            // reached capacity for this key, discard element
            return false;
        }

        if (this.size >= this.capacity) {
            // at the capacity, need to evict one element
            // eviction allows the pool to adapt to changing usage patterns, preventing it from getting saturated with values that are no longer relevant
            this.__removeElement(this.__first);
        }

        const cache_element = new CacheElement();

        cache_element.value = value;
        cache_element.key = key;

        // attach at the end of the list
        cache_element.previous = this.__last;

        if (this.__last !== null) {
            this.__last.next = cache_element;
        }

        this.__last = cache_element;

        if (this.__first === null) {
            this.__first = cache_element;
        }

        elements.push(cache_element);

        this.size++;

        return true;
    }
}
