// https://github.com/wangchen/Programming-Game-AI-by-Example-src/blob/master/Common/fuzzy/FuzzyRule.h


// FUZZY SETS


// Definition of the base fuzzy set class.
class FuzzySet {
    constructor(peakPoint, leftOffset, rightOffset, representativeValue) {
        // Members that define the shape.
        this.peakPoint = peakPoint;
        this.leftOffset = leftOffset;
        this.rightOffset = rightOffset;

        // representativeValue - the maximum of the set's membership function. 
        // For instance, if the set is triangular then this will be the peak 
        // point of the triangular. If the set has a plateau then this value 
        // will be the mid point of the plateau. This value is set in creation 
        // to avoid run-time calculation of mid-point values.
        this.representativeValue = representativeValue;

        // DOM - holds the degree of membership of a given value in this set.
        this.DOM = 0.0;
    }

    // calculateDOM - calculates and returns the degree of membership for a 
    // particular value. NOTE, this does not set DOM to the value passed as 
    // the parameter. This is because the centroid defuzzification method also 
    // uses this method to determine the degree of memberships of the values 
    // it uses as its sample points.
    calculateDOM(val) {
    }

    clearDOM() { this.DOM = 0.0; }

    getDOM() { return this.DOM; }

    // ORwithDOM - If this fuzzy set is part of a consequent variable, and 
    // it is fired by a rule then this method sets the DOM (in this context, 
    // the DOM represents a confidence level) to the maximum of the parameter 
    // value or the set's existing member DOM value.
    ORwithDOM(val) {
        if (val > this.DOM)
            this.DOM = val;
    }

    // Fuzzify a value by calculating its degree of membership.
    fuzzify(val) {
        this.DOM = this.calculateDOM(val);
        return this;
    }
}

//  Definition of a fuzzy set that has a left shoulder shape.
class FuzzySet_LeftShoulder extends FuzzySet {
    constructor(peakPoint, leftOffset, rightOffset) {
        super(peakPoint, leftOffset, rightOffset, peakPoint - leftOffset / 2);
    }

    calculateDOM(val) {
        // Test for the case where the left or right offsets are zero.
        // (to prevent divide by zero errors below).
        if (isEqual(this.peakPoint, val) &&
            (isEqual(this.leftOffset, 0.0) || isEqual(this.rightOffset, 0.0)))
            return 1.0;

        // Find DOM if right of center.
        if ((val >= this.peakPoint) && (val < (this.peakPoint + this.rightOffset)))
            return (1.0 / -this.rightOffset) * (val - this.peakPoint) + 1.0;

        // Find DOM if left of center.
        if ((val < this.peakPoint) && (val >= this.peakPoint - this.leftOffset))
            return 1.0;

        // Out of range.
        return 0.0;
    }
}

// Definition of a fuzzy set that has a right shoulder shape.
class FuzzySet_RightShoulder extends FuzzySet {
    constructor(peakPoint, leftOffset, rightOffset) {
        super(peakPoint, leftOffset, rightOffset, peakPoint + rightOffset / 2);
    }

    calculateDOM(val) {
        // Test for the case where the left or right offsets are zero
        // (to prevent divide by zero errors below).
        if (isEqual(this.peakPoint, val) &&
            (isEqual(this.leftOffset, 0.0) || isEqual(this.rightOffset, 0.0)))
            return 1.0;

        // Find DOM if left of center.
        if ((val <= this.peakPoint) && (val > (this.peakPoint - this.leftOffset)))
            return (1.0 / this.leftOffset) * (val - (this.peakPoint - this.leftOffset));

        // Find DOM if right of center and less than center + right offset.
        if ((val > this.peakPoint) && (val <= this.peakPoint + this.rightOffset))
            return 1.0;

        // Out of range.
        return 0.0;
    }
}

// This defines a fuzzy set that is a singleton (a range
// over which the DOM is always 1.0).
class FuzzySet_Singleton extends FuzzySet {
    constructor(peakPoint, leftOffset, rightOffset) {
        super(peakPoint, leftOffset, rightOffset, peakPoint);
    }

    calculateDOM(val) {
        if ((val >= this.peakPoint - this.leftOffset)
            && (val <= this.peakPoint + this.rightOffset))
            return 1.0;

        // Out of range.
        return 0.0;
    }
}

// This is a simple class to define fuzzy sets that have a triangular 
// shape and can be defined by a mid point, a left displacement and a
// right displacement. 
class FuzzySet_Triangle extends FuzzySet {
    constructor(peakPoint, leftOffset, rightOffset) {
        super(peakPoint, leftOffset, rightOffset, peakPoint);
    }

    calculateDOM(val) {
        // Test for the case where the left or right offsets are zero
        // (to prevent divide by zero errors below).
        if (isEqual(this.peakPoint, val) &&
            (isEqual(this.leftOffset, 0.0) || isEqual(this.rightOffset, 0.0)))
            return 1.0;

        // Find DOM if left of center.
        if ((val <= this.peakPoint) && (val >= (this.peakPoint - this.leftOffset)))
            return (1.0 / this.leftOffset) * (val - (this.peakPoint - this.leftOffset));

        // Find DOM if right of center.
        if ((val > this.peakPoint) && (val < (this.peakPoint + this.rightOffset)))
            return (1.0 / -this.rightOffset) * (val - this.peakPoint) + 1.0;

        // Out of range.
        return 0.0;
    }
}


// FUZZY VARIABLES


class FuzzyVariable {
    constructor() {
        // A map of the fuzzy sets that comprise this variable.
        this.sets = {};

        // The minimum and maximum value of the range of this variable.
        this.minRange = 0.0;
        this.maxRange = 0.0;
    }

    // This method is called with the upper and lower bound of a set each time a
    // new set is added to adjust the upper and lower range values accordingly.
    adjustRangeToFit(minBound, maxBound) {
        if (minBound < this.minRange)
            this.minRange = minBound;
        if (maxBound > this.maxRange)
            this.maxRange = maxBound;
    }

    // The following methods create instances of the sets named in the method
    // name and add them to the member set map. Each time a set of any type is
    // added the minRange and maxRange are adjusted accordingly.

    // Adds a left shoulder type set.
    addLeftShoulderSet(name, minBound, peak, maxBound) {
        this.sets[name] = new FuzzySet_LeftShoulder(peak, peak - minBound, maxBound - peak);
        this.adjustRangeToFit(minBound, maxBound);
        return this;
    }

    // Adds a right shoulder type set.
    addRightShoulderSet(name, minBound, peak, maxBound) {
        this.sets[name] = new FuzzySet_RightShoulder(peak, peak - minBound, maxBound - peak);
        this.adjustRangeToFit(minBound, maxBound);
        return this;
    }

    // Adds a triangular shaped fuzzy set to the variable.
    addTriangularSet(name, minBound, peak, maxBound) {
        this.sets[name] = new FuzzySet_Triangle(peak, peak - minBound, maxBound - peak);
        this.adjustRangeToFit(minBound, maxBound);
        return this;
    }

    // Adds a singleton to the variable.
    addSingletonSet(name, minBound, peak, maxBound) {
        this.sets[name] = new FuzzySet_Singleton(peak, peak - minBound, maxBound - peak);
        this.adjustRangeToFit(minBound, maxBound);
        return this;
    }

    // Fuzzify a value by calculating its degree of membership in each of this variable's subsets
    // takes a crisp value and calculates its degree of membership for each set
    // in the variable.
    fuzzify(val) {
        // For each set in the flv calculate the degree of membership for the 
        // given value.
        for (let set in this.sets)
            this.sets[set].DOM = this.sets[set].calculateDOM(val);
        return this;
    }

    // Method for updating the DOM of a consequent when a rule fires.
    ORwithDOM(val) {
        for (let name in this.sets)
            this.sets[name].ORwithDOM(val);
    }

    // Defuzzifies the value by averaging the maxima of the sets that have fired.
    // Returns sum (maxima * degree of membership) / sum (degree of memberships).
    deFuzzifyMaxAv() {
        let bottom = 0.0;
        let top = 0.0;

        for (let set in this.sets) {
            bottom += this.sets[set].DOM;
            top += this.sets[set].representativeValue * this.sets[set].DOM;
        }

        // Make sure bottom is not equal to zero.
        if (isEqual(0, bottom))
            return 0.0;

        return top / bottom;
    }

    // Defuzzify the variable using the centroid method.
    deFuzzifyCentroid(numSamples) {

        if (numSamples <= 0)
            throw new Error("Number of samples must be greater than zero");

        // Calculate the step size.
        let stepSize = (this.maxRange - this.minRange) / numSamples;

        let totalArea = 0.0;
        let sumOfMoments = 0.0;

        // Step through the range of this variable in increments equal to stepSize
        // adding up the contribution (lower of calculateDOM 
        // or the actual degree of membership of this
        // variable's fuzzified value) for each subset. This gives an approximation of
        // the total area of the fuzzy manifold.(This is similar to how the area under
        // a curve is calculated using calculus... the heights of lots of 'slices' are
        // summed to give the total area.)
        // In addition the moment of each slice is calculated and summed. Dividing
        // the total area by the sum of the moments gives the centroid. (Just like
        // calculating the center of mass of an object.)
        for (let samp = 1; samp <= numSamples; ++samp) {
            // For each set get the contribution to the area. This is the lower of the
            // value returned from calculateDOM or 
            // the actual degree of membership of the fuzzified value itself.
            for (let set in this.sets) {
                let contribution = Math.min(
                    this.sets[set].calculateDOM(this.minRange + samp * stepSize),
                    this.sets[set].DOM);

                totalArea += contribution;

                sumOfMoments += (this.minRange + samp * stepSize) * contribution;
            }
        }

        // Make sure total area is not equal to zero.
        if (isEqual(0, totalArea))
            return 0.0;

        return sumOfMoments / totalArea;
    }
}


// FUZZY OPERATORS


// Fuzzy logic works with membership values in a way that mimics Boolean logic. 
// Replacements for basic logic operators AND and OR are defined here.

class FuzzyOperator {
    constructor(sets = []) {
        this.sets = sets;
    }

    clearDOM() {
        for (let set of this.sets)
            set.clearDOM();
    }

    ORwithDOM(val) {
        for (let set of this.sets)
            set.ORwithDOM(val);
    }
}

class FuzzyAND extends FuzzyOperator {
    constructor(sets = []) {
        super(sets);
    }

    // Returns the minimum DOM of the sets it is operating on.
    getDOM() {
        let smallest = Number.MAX_VALUE;
        for (let set of this.sets) {
            let cur = set.getDOM();
            if (cur < smallest)
                smallest = cur;
        }
        return smallest == Number.MAX_VALUE ? 0.0 : smallest;
    }
}

class FuzzyOR extends FuzzyOperator {
    constructor(sets = []) {
        super(sets);
    }

    // Returns the maximum DOM of the sets it is operating on.
    getDOM() {
        let largest = -Number.MAX_VALUE;
        for (let set of this.sets) {
            let cur = set.getDOM();
            if (cur > largest)
                largest = cur;
        }
        return largest == -Number.MAX_VALUE ? 0.0 : largest;
    }
}


// FUZZY RULES


class FuzzyRule {
    constructor(antecedent, consequence) {
        // Antecedent (usually a composite of several fuzzy sets and operators).
        this.antecedent = antecedent;

        // Consequence (usually a single fuzzy set, but can be several 
        // ANDed together).
        this.consequence = consequence;
    }

    // Updates the DOM (the confidence) of the consequent set with
    // the DOM of the antecedent set. 
    calculate() {
        this.consequence.ORwithDOM(this.antecedent.getDOM());
        return this.consequence;
    }

    setConfidenceOfConsequentToZero() { 
        this.consequence.clearDOM();
    }
}


// FUZZY MODULE


class FuzzyModule {
    // Defuzzify methods supported by this module.
    static DefuzzifyMethod = { max_av: 0, centroid: 1 };

    constructor() {
        // When calculating the centroid of the fuzzy manifold this value is used
        // to determine how many cross-sections should be sampled.
        this.numSamples = 15;

        // A map of all the fuzzy variables this module uses.
        this.variables = {};

        // An array containing all the fuzzy rules.
        this.rules = [];
    }

    // Zeros the DOMs of the consequents of each rule. Used by Defuzzify().
    setConfidencesOfConsequentsToZero() {
        for (let rule of this.rules)
            rule.setConfidenceOfConsequentToZero();
    }

    // Creates and return a new 'empty' fuzzy variable.
    createVariable(varName) {
        this.variables[varName] = new FuzzyVariable();
        return this.variables[varName];
    }

    // Adds a rule to the module.
    addRule(antecedent, consequence) {
        this.rules.push(new FuzzyRule(antecedent, consequence));
    }

    // Calls the Fuzzify method of the variable with the same name.
    fuzzify(varName, val) {
        this.variables[varName].fuzzify(val);
    }

    // Given a fuzzy variable and a defuzzification method
    // this returns a crisp value.
    deFuzzify(varName, method) {
        // Clear the DOMs of all the consequents of all the rules.
        this.setConfidencesOfConsequentsToZero();

        // Process the rules.
        for (let rule of this.rules)
            rule.calculate();

        // Defuzzify the resultant conclusion using the specified method.
        switch (method) {
            case FuzzyModule.DefuzzifyMethod.centroid:
                return this.variables[varName].deFuzzifyCentroid(this.numSamples);
            case FuzzyModule.DefuzzifyMethod.max_av:
                return this.variables[varName].deFuzzifyMaxAv();
        }
        return 0.0;
    }
}