import { invokeObjectEquals } from "../../model/object/invokeObjectEquals.js";
import { invokeObjectHash } from "../../model/object/invokeObjectHash.js";
import { HashMap } from "../map/HashMap.js";


/**
 * Spec-compliant with ES Set interface
 * @template T
 * @extends {Set}
 */
export class HashSet {
    /**
     * @template T
     */
    constructor({
                    keyHashFunction = invokeObjectHash,
                    keyEqualityFunction = invokeObjectEquals,
                    capacity
                } = {}) {

        /**
         * Using HashMap as a backing, this is not the most efficient approach, but it works
         * @type {HashMap<T,undefined>}
         * @private
         */
        this.__map = new HashMap({
            keyHashFunction,
            keyEqualityFunction,
            capacity
        });
    }

    get size() {
        return this.__map.size;
    }

    /**
     *
     * @param {T} value
     * @returns {this}
     */
    add(value) {
        this.__map.set(value, value);

        return this;
    }

    clear() {
        this.__map.clear();
    }

    /**
     *
     * @param {T} value
     * @returns {boolean}
     */
    delete(value) {
        return this.__map.delete(value);
    }

    /**
     *
     * @param {T} value
     * @returns {boolean}
     */
    has(value) {
        return this.__map.has(value);
    }

    * [Symbol.iterator]() {
        for (const key of this.__map.keys()) {
            yield key;
        }
    }

    keys() {
        return this.__map.keys();
    }

    values() {
        return this.__map.keys();
    }

    entries() {
        throw new Error('Not implemented');
    }

    /**
     *
     * @param {T} value
     * @returns {T|undefined}
     */
    get(value) {
        return this.__map.get(value);
    }

    /**
     *
     * @param {function(T, T, this)} callback
     * @param {*} [thisArg]
     */
    forEach(callback, thisArg) {
        for (const value of this.values()) {
            callback.call(thisArg, value, value, this);
        }
    }

    /**
     * Get the equivalent value that's already in the set or add the supplied value if it's not present
     * @param {T} value
     * @return {T}
     */
    ensure(value) {
        const existing = this.get(value);
        if (existing !== undefined) {
            return existing;
        }

        this.add(value);

        return value;
    }
}
