import { assert } from "../assert.js";
import Signal from "../events/signal/Signal.js";

/**
 * Enumerable value abstraction. Think `enum` keyword in TypeScript or any other programming language.
 * Given a set of valid values, holds one of such values.
 * This is an observable value, meaning you can subscribe to be notified of changes via {@link onChanged} signal.
 *
 * @example
 *  const v = new ObservedEnum(7, {
 *      ONE: 1,
 *      SEVEN: 7
 *  });
 *
 *  v.getValue(); // 7
 *
 *  v.set(1);
 *
 *  v.getValue(); // 1
 *
 *  v.set(2); // will throw error, as 2 is not a valid enum value
 */
class ObservedEnum {
    /**
     * @template T
     * @param {T} value
     * @param {Object.<string,T>} validSet
     */
    constructor(value, validSet) {
        assert.isObject(validSet, "validSet");
        assert.enum(value, validSet, "value");

        /**
         *
         * @type {T}
         * @private
         */
        this.__value = value;
        /**
         *
         * @type {Object<string, T>}
         * @private
         */
        this.__validSet = validSet;

        /**
         * @readonly
         * @type {Signal<T,T>}
         */
        this.onChanged = new Signal();
    }

    /**
     * Do not modify result
     * @returns {Object<string, T>}
     */
    getValidValueSet() {
        return this.__validSet;
    }

    /**
     *
     * @param {T} value
     * @returns {ObservedEnum}
     */
    set(value) {
        assert.enum(value, this.__validSet, "value")

        const oldValue = this.__value;
        if (oldValue !== value) {
            this.__value = value;
            this.onChanged.dispatch(value, oldValue);
        }

        return this;
    }

    /**
     * @template T
     * @param {ObservedEnum<T>} other
     * @returns {boolean}
     */
    equals(other) {
        return this.__value === other.__value;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        let hash = 0;

        for (const n of this.__validSet) {
            if(this.__validSet[n] === this.__value)    {
                break;
            }

            hash++;
        }

        return hash;
    }

    /**
     *
     * @param {ObservedEnum} other
     */
    copy(other) {
        this.set(other.getValue());
    }

    /**
     *
     * @returns {T}
     */
    getValue() {
        return this.__value;
    }

    /**
     *
     * @param {function(T,T)} processor
     * @param {*} [thisArg]
     * @returns {this}
     */
    process(processor, thisArg) {
        this.onChanged.add(processor, thisArg);

        const v = this.__value;

        processor.call(thisArg, v, v);

        return this;
    }

    toJSON() {
        return this.__value;
    }

    fromJSON(obj) {
        this.set(obj);
    }
}

export default ObservedEnum;
