import { assert } from "../../assert.js";
import List from "../../collection/list/List.js";
import { chain } from "../../function/chain.js";
import Vector1 from "../../geom/Vector1.js";
import { EPSILON } from "../../math/EPSILON.js";
import { epsilonEquals } from "../../math/epsilonEquals.js";
import { max2 } from "../../math/max2.js";
import { min2 } from "../../math/min2.js";
import LinearModifier from "./LinearModifier.js";

/**
 * Modifiable statistic.
 * Allows non-destructive linear arithmetic.
 * Useful when we wish to be able to reverse part or the whole of the computation
 *
 * Main purpose of the class is to facilitate implementation of RPG-like stats, such as maximum health, or armor.
 * These stats can then be modified by various skills, equipment and effects. Each such modification will be added as a separate {@link LinearModifier} and can be reversed by removing the modifier.
 */
class Stat extends Number {
    /**
     * Unique identifier of a stat, such a health, armor etc.
     * @type {number}
     */
    id = 0;

    /**
     * List of modifiers.
     * Order has no influence on final computed value
     * @private
     * @readonly
     * @type {List<LinearModifier>}
     */
    __modifiers = new List();

    /**
     * Computed value will be pass through this function before being exposed.
     * Useful for rounding and clamping.
     * see {@link Stat.Process} for some options.
     * @type {function(number): number}
     */
    postprocess = Stat.Process.NONE;

    /**
     * @param {number} [value]
     * @constructor
     */
    constructor(value = 0) {
        super();

        assert.isNumber(value, "value");


        /**
         * Base value that modifiers will affect
         * @type {Vector1}
         */
        this.base = new Vector1(value);

        /**
         * Final computed value. Do not modify this directly
         * @type {Vector1}
         */
        this.value = new Vector1(value);

        this.base.onChanged.add(this.updateValue, this);

    }

    /**
     *
     * @returns {Signal<LinearModifier>}
     */
    get onModifierAdded() {
        return this.__modifiers.on.added;
    }

    /**
     *
     * @returns {Signal<LinearModifier>}
     */
    get onModifierRemoved() {
        return this.__modifiers.on.removed;
    }

    /**
     * Remove all modifiers from the stat
     */
    resetModifiers() {
        this.__modifiers.reset();
    }

    /**
     *
     * @returns {number}
     */
    valueOf() {
        return this.getValue();
    }

    /**
     *
     * @returns {string}
     */
    toString() {
        return String(this.getValue());
    }

    /**
     *
     * @returns {number}
     */
    getValue() {
        return this.value.x;
    }

    updateValue() {
        const x = this.base.x;

        const modifiers = this.__modifiers;

        const newValue = Stat.applyModifiers(x, modifiers);

        const finalValue = this.postprocess(newValue);

        this.value.set(finalValue);
    }

    /**
     *
     * @returns {number}
     */
    getBaseValue() {
        return this.base.getValue();
    }

    /**
     *
     * @param {number} v
     */
    setBaseValue(v) {
        this.base.set(v);
    }

    /**
     * NOTE: do not modify result
     * @return {LinearModifier[]}
     */
    getModifiers() {
        return this.__modifiers.data;
    }

    /**
     *
     * @param {LinearModifier} mod
     */
    addModifier(mod) {
        this.__modifiers.add(mod);

        this.updateValue();
    }

    /**
     *
     * @param {LinearModifier} mod
     * @return {boolean}
     */
    hasModifier(mod) {
        return this.__modifiers.contains(mod);
    }

    /**
     *
     * @param {LinearModifier} mod
     * @returns {boolean}
     */
    removeModifier(mod) {
        const removed = this.__modifiers.removeOneOf(mod);

        this.updateValue();

        return removed;
    }

    /**
     *
     * @param {Stat} other
     * @returns {boolean}
     */
    equals(other) {

        const v0 = this.getValue();
        const b1 = other.getValue();

        // use small tolerance to allow for numeric error resulting from the order in which stat modifiers are combined
        return epsilonEquals(v0, b1, EPSILON);
    }

    /**
     *
     * @param {Stat} other
     */
    copy(other) {
        this.base.copy(other.base);
        this.__modifiers.copy(other.__modifiers);

        this.updateValue();
    }

    /**
     * Copy base value from another stat
     * @param {Stat} other
     */
    copyBase(other) {
        this.base.copy(other.base);
    }

    /**
     *
     * @param {function(number):number} f
     * @param {number} v
     */
    setBaseFromParametricFunction(f, v) {
        const base_value = f(v);
        this.base.set(base_value);
    }

    /**
     * @param {Stat} other
     */
    addNonTransientModifiersFromStat(other) {
        const modifiers = other.__modifiers.data;

        const n = modifiers.length;

        for (let i = 0; i < n; i++) {
            const linearModifier = modifiers[i];

            if (linearModifier.transient) {
                continue;
            }

            //make a copy
            const clone = linearModifier.clone();

            this.addModifier(clone);
        }
    }

    toJSON() {
        return {
            base: this.base.toJSON(),
            modifiers: this.__modifiers.toJSON()
        };
    }

    fromJSON(json) {
        this.base.fromJSON(json.base);
        this.__modifiers.fromJSON(json.modifiers, LinearModifier);
        this.updateValue();
    }

    /**
     *
     * @returns {Signal}
     */
    get onChanged() {
        return this.value.onChanged;
    }

    /**
     *
     * @param {number} input
     * @param {List<LinearModifier>}modifiers
     * @returns {number}
     */
    static applyModifiers(input, modifiers) {
        let a = 1;
        let b = 0;

        let i;
        const l = modifiers.length;

        const modifiersData = modifiers.data;

        // Combine all modifiers
        for (i = 0; i < l; i++) {
            const m = modifiersData[i];

            a += (m.a - 1);
            b += m.b;
        }

        return input * a + b;
    }
}

Stat.Process = {
    /**
     * @param {number} v
     * @return {number}
     */
    ROUND_DOWN: function (v) {
        return v | 0;
    },
    NONE: function (v) {
        return v;
    },
    /**
     * Clamp the lowest value forcing the result to always be >= value
     * @param {number} value
     * @returns {function(number): number}
     */
    clampMin: function (value) {
        return function max(v) {
            return max2(value, v);
        }
    },
    /**
     * Clamp the highest value, forcing the result to always be <= value
     * @param {number} value
     * @return {function(number): number}
     */
    clampMax: function (value) {
        return function min(v) {
            return min2(v, value);
        }
    },
    /**
     * Allows us to chain multiple effects
     */
    chain: chain
};

/**
 * @readonly
 * @type {boolean}
 */
Stat.prototype.isStat = true;


export default Stat;
