const {Component} = require("./Component");
const {SignificanceEvent} = require("./Significance");
const {checkType, createHiddenProp} = require("./Utility");

const intrinsicList = new Map();

/**
 * A class to represent internal states that are relative to {@link Significance}
 *
 * @extends Component
 */
class Intrinsic extends Component {
    /**
     * Creates a new intrinsic value
     *
     * @param {string} name Name of the intrisic (e.g. hunger, pain, etc.)
     * @param opts
     */
    constructor(name, opts = {}) {
        checkType("Intrinsic.constructor", "name", name, "string");
        opts.max = (opts.max !== undefined) ? opts.max : 100;
        opts.min = (opts.min !== undefined) ? opts.min : 0;
        opts.positive = !!opts.positive;
        opts.converter = opts.converter || Intrinsic.defaultConverter;
        checkType("Intrinsic.constructor", "opts.max", opts.max, "number");
        checkType("Intrinsic.constructor", "opts.min", opts.min, "number");
        checkType("Intrinsic.constructor", "opts.positive", opts.positive, "boolean");
        checkType("Intrinsic.constructor", "opts.converter", opts.converter, "function");

        if (opts.min >= opts.max) {
            throw new RangeError("Intrinsic.constructor: opts.min must be less than opts.max");
        }

        super(name, "intrinsic", SignificanceEvent);
        createHiddenProp(this, "_value", null);
        createHiddenProp(this, "_max", opts.max, true);
        createHiddenProp(this, "_min", opts.min, true);
        createHiddenProp(this, "_range", opts.max - opts.min, true);
        createHiddenProp(this, "_positive", opts.positive, true);
        createHiddenProp(this, "_converter", opts.converter, true);
        intrinsicList.set(name, this);
    }

    /**
     * Set the value of the intrinsic and emit a `change` event if the value changed.
     *
     * @param {*} val The value to set for the intrinsic, which is specific to the implementation of the intrinsic.
     * @returns {Promise} Returns a promise that resolves to the value of `sendEvent` if an event was emitted, or null if no change occurred.
     */
    async setValue(val) {
        val = this._converter(val);

        checkType("Intrinsic.setValue", "val", val, "number");

        if (val > this._max) {
            throw new RangeError(`Intrinsic.setValue: attempted to set value (${val}) greater than max (${this._max})`);
        }

        if (val < this._min) {
            throw new RangeError(`Intrinsic.setValue: attempted to set value (${val}) less than min (${this._min})`);
        }

        if (this._value !== val) {
            let oldVal = this._value;
            let oldNormVal = this.normalizedValue;
            this._value = val;
            return this.sendEvent("change", {
                oldVal,
                oldNormVal,
                newVal: this._value,
                newNormVal: this.normalizedValue,
                intrinsic: this,
            });
        }

        return null;
    }

    /**
     * Returns the value of the intrinsic
     *
     * @returns {*} The value of the intrinsic
     */
    getValue() {
        return this._value;
    }

    // eslint-disable-next-line jsdoc/require-jsdoc
    set value(v) {
        throw new Error("The value setter has been deprecated");
    }

    // eslint-disable-next-line jsdoc/require-jsdoc
    get value() {
        throw new Error("The value getter has been deprecated");
    }

    /** minimim valume allowed for the intrinsic */
    get min() {
        return this._min;
    }

    /** maximum valume allowed for the intrinsic */
    get max() {
        return this._max;
    }

    /** the number of values between `min` and `max` */
    get range() {
        return this._range;
    }

    /** the value normalized to be a number between zero and one */
    get normalizedValue() {
        return ((this._value - this._min) / this._range);
    }

    /** whether the intrisic is positive ('true') or negative ('false') */
    get positive() {
        return this._positive;
    }

    /**
     * The default method for converting values to numbers. Mostly a wrapper for `parseFloat`.
     *
     * @param {*} val - The value to be converted to a number
     *
     * @returns {number} The numeric form of `val`
     */
    static defaultConverter(val) {
        if (typeof val === "string") {
            let tmp = parseFloat(val);
            if (Number.isNaN(tmp)) {
                throw new TypeError(`Intrinsic#defaultConverter couldn't parse string as float: '${val}'`);
            }

            val = tmp;
        }

        return val;
    }
}

module.exports = {
    Intrinsic,
};