/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Content MathML Evaluator for JavaScript (Simple).
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

/* Here's how you use this.

First, create a variable map.  This is an object whose property names are the
variables you wish to use.  If you have a variable which is an unknown, create
a new TargetVariable object and assign it the name of that variable.

For instance, if I have y = f(x), and x is 3, my variable map would look like:

var variableMap = {
  x: 6,
  y: new TargetVariable()
}

The equation "y = f(x)" is marked up as a content MathML node.  Let's assume
y = ((x * x) - 3).  The content MathML for this would look like:

  <math:apply id="start">
    <math:eq/>
    <math:ci>y</math:ci>
    <math:apply>
      <math:minus/>
      <math:apply>
        <math:power/>
        <math:ci>x</math:ci>
        <math:cn>2</math:cn>
      </math:apply>
      <math:cn>3</math:cn>
    </math:apply>
  </math:apply>

You would then call:

getComputedValue(aMathNode, variableMap);

If you look at variableMap.y.value, it comes out to 33.

Suppose, however, you wanted to graph y = f(x).  One way you could do it is:

var variableMap = {
  x: undefined,
  y: new TargetVariable()
}
for (var x = -10; x <= 10; x += 0.1) {
  variableMap.x = x;
  variableMap.y = getComputedValue(aMathNode, variableMap);
  // Plot y as variableMap.y.value on the graph.
}

However, this executes a lot of DOM calls (aMathNode.childNodes[i]).  To save time,
there is also a getComputingFunction() call which returns a composite function based on
the MathML node.  Here's how you would use it to do the same.

var variableMap = {
  x: undefined,
  y: new TargetVariable()
}
var computeFunc = getComputingFunction(aMathNode);
for (var x = -10; x <= 10; x += 0.1) {
  variableMap.x = x;
  variableMap.y = computeFunc(variableMap);
  // Plot y as variableMap.y.value on the graph.
}

Supported content MathML elements:
- apply
- eq
- cn (decimal numbers only)
- ci
- plus
- minus
- times
- divide
- rem
- power
- root
- max
- min
- ln
- exp
- sin
- cos
- tan
- floor
- abs
- ceiling

 */

/**
 * Constructor for target variables (in y = f(x) cases, y would be the target)
 */
function TargetVariable() {
  this.value = undefined;
}
TargetVariable.prototype = {
  /**
   * Reset this element's value to be undefined.
   */
  reset: function reset() {
    this.value = undefined;
  }
}

/**
 * Set a computed value on a target variable.
 *
 * @param aVariableMap Object mapping properties to values or TargetVariables.
 * @param aValueObject TargetVariable object within aVariableMap.
 * @param aNewValue    Value to set on the target variable.
 */
function setComputedValue(aVariableMap, aValueObject, aNewValue) {
  for (var prop in aVariableMap) {
    if (aVariableMap[prop] == aValueObject) {
      aVariableMap[prop].value = aNewValue;
      return;
    }
  }
}

/**
 * Evaluate a content MathML node given an object mapping variables.
 *
 * @param aMathNode    Content MathML node to evaluate.
 * @param aVariableMap Object with (variableName: variableValue) properties.
 *
 * @return Number representing evaluation of content MathML node.
 */
function getComputedValue(aMathNode, aVariableMap) {
  var stackCount = (arguments.length > 2) ? arguments[2] : 0;

  if (aMathNode.localName == "apply") {
    var values = [];
    for (var i = 1; i < aMathNode.childNodes.length; i++) {
      values[i - 1] = getComputedValue(aMathNode.childNodes[i], aVariableMap, stackCount + 1);
    }
    if (values.length == 0) {
      throw new Error("apply element must have at least two children!");
    }
    var retval;
    
    switch (aMathNode.firstChild.localName) {
      case "eq":
        if (stackCount != 0) {
          throw new Error("Equal sign must be at the top level!");
        }
        if (values.length != 2) {
          throw new Error("Assignment requires two arguments!");
        }

        if ((values[0] instanceof TargetVariable) && (values[1] instanceof TargetVariable)) {
          throw new Error("Only one target variable object permitted at a time!");
        }

        if (values[0] instanceof TargetVariable) {
          setComputedValue(aVariableMap, values[0], values[1]);
          return values[1];
        }

        if (values[1] instanceof TargetVariable) {
          setComputedValue(aVariableMap, values[1], values[0]);
          return values[0];
        }

        throw new Error("One target variable object is required!");

      case "plus":
        retval = 0;
        for (i = 0; i < values.length; i++) {
          retval += values[i];
        }
        return retval;

      case "minus":
        if (values.length == 1) {
          retval = -values[0];
          return retval;
        }

        if (values.length == 2) {
          retval = values[0] - values[1];
          return retval;
        }

        throw new Error("minus operator takes one or two arguments");

      case "times":
        retval = 1;
        for (i = 0; i < values.length; i++) {
          retval *= values[i];
        }
        return retval;

      case "divide":
        if (values.length == 2) {
          retval = values[0] / values[1];
          return retval;
        }

        throw new Error("divide operator takes precisely two arguments");

      case "rem":
        if (values.length == 2) {
          retval = values[0] % values[1];
          return retval;
        }

        throw new Error("rem operator takes precisely two arguments");

      case "power":
        if (values.length == 2) {
          retval = Math.pow(values[0], values[1]);
          return retval;
        }

        throw new Error("power operator takes precisely two arguments");

      case "root":
        if (values.length == 2) {
          retval = Math.pow(values[0], 1 / values[1]);
          return retval;
        }

        throw new Error("root operator takes precisely two arguments");

      case "max":
        retval = Math.max.apply(Math, values);
        return retval;

      case "min":
        retval = Math.min.apply(Math, values);
        return retval;

      case "ln":
        if (values.length == 1) {
          retval = Math.log(values[0]);
          return retval;
        }
        throw new Error("ln operator takes precisely one argument");

      case "exp":
      case "sin":
      case "cos":
      case "tan":
      case "floor":
      case "abs":
        var opName = aMathNode.firstChild.localName;
        if (values.length == 1) {
          retval = Math[opName](values[0]);
          return retval;
        }
        throw new Error(opName + " operator takes precisely one argument");

      case "ceiling":
        if (values.length == 1) {
          retval = Math.ceil(values[0]);
          return retval;
        }
        throw new Error("ceiling operator takes precisely one argument");

      default:
        throw new Error("not implemented: " + aMathNode.firstChild.nodeName);
    }
  }

  switch (aMathNode.localName) {
    case "cn":
      return Number(aMathNode.firstChild.nodeValue);

    case "ci":
      return aVariableMap[aMathNode.firstChild.nodeValue];

    default:
      throw new Error("not implemented: " + aMathNode.localName);    
  }
}

/**
 * Return a composite function to evaluate a content MathML node.
 *
 * @param aMathNode    Content MathML node to evaluate.
 *
 * @return Function representing evaluation of content MathML node.
 *
 * @note Returned function takes an object with (variableName: variableValue)
 * properties.  This function will return the value of the content MathML node,
 * given the variables in the variable map, as if you had called
 * getComputedValue directly.
 *
 * @note Call getComputingFunction(aMathNode) instead of this function directly.
 */
function getComputingFunction_body(aMathNode) {
  var stackCount = (arguments.length > 1) ? arguments[1] : 0;

  if (aMathNode.localName == "apply") {
    var values = [];
    for (var i = 1; i < aMathNode.childNodes.length; i++) {
      values[i - 1] = getComputingFunction(aMathNode.childNodes[i], stackCount + 1);
    }
    if (values.length == 0) {
      throw new Error("apply element must have at least two children!");
    }
    
    switch (aMathNode.firstChild.localName) {
      case "eq":
        if (stackCount != 0) {
          throw new Error("Equal sign must be at the top level!");
        }
        if (values.length != 2) {
          throw new Error("Assignment requires two arguments!");
        }

        if ((values[0] instanceof TargetVariable) && (values[1] instanceof TargetVariable)) {
          throw new Error("Only one target variable object permitted at a time!");
        }

        /**
         * Assign a value to a TargetVariable object.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value assigned to TargetVariable object.
         */
        return function getAssignmentValue(aVariableMap) {
          var firstValue = values[0](aVariableMap);
          var secondValue = values[1](aVariableMap);
          if (firstValue instanceof TargetVariable) {
            setComputedValue(aVariableMap, firstValue, secondValue);
            return secondValue;
          }

          if (secondValue instanceof TargetVariable) {
            setComputedValue(aVariableMap, secondValue, firstValue);
            return firstValue;
          }

          throw new Error("One target variable object is required!");
        }

      case "plus":
        /**
         * Get the sum of a collection of values.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from summation.
         */
        return function plus_computing(aVariableMap) {
          var retval = 0;
          for (var i = 0; i < values.length; i++) {
            retval += values[i](aVariableMap);
          }
          return retval;
        }

      case "minus":
        /**
         * Get the difference from one value from another.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from subtraction.
         */
        if (values.length > 2) {
          throw new Error("minus operator takes at most two arguments");
        }

        if (values.length == 1) {
          values.unshift(function return_zero() { return 0; });
        }

        return function minus_computing(aVariableMap) {
          return values[0](aVariableMap) - values[1](aVariableMap);
        }

      case "times":
        /**
         * Get the product of a collection of values.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from multiplication.
         */
        return function times_computing(aVariableMap) {
          var retval = 1;
          for (var i = 0; i < values.length; i++) {
            retval *= values[i](aVariableMap);
          }
          return retval;
        }

      case "divide":
        if (values.length != 2) {
          throw new Error("divide operator takes at most two arguments");
        }

        /**
         * Get the quotient from one value divided by another.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from division.
         */
        return function divide_computing(aVariableMap) {
          return values[0](aVariableMap) / values[1](aVariableMap);
        }

      case "rem":
        if (values.length != 2) {
          throw new Error("rem operator takes at most two arguments");
        }

        /**
         * Get the remainder from one value divided by another.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from modulus.
         */
        return function rem_computing(aVariableMap) {
          return values[0](aVariableMap) % values[1](aVariableMap);
        }

      case "power":
        if (values.length != 2) {
          throw new Error("power operator takes precisely two arguments");
        }

        /**
         * Get the value from one value raised to the power of another value.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from exponential operation.
         */
        return function power_computing(aVariableMap) {
          return Math.pow(values[0](aVariableMap), values[1](aVariableMap));
        }

      case "root":
        if (values.length != 2) {
          throw new Error("divide operator takes at most two arguments");
        }

        /**
         * Get the nth root of one value, where n is another value.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from root operation.
         */
        return function root_computing(aVariableMap) {
          return Math.pow(values[0](aVariableMap), 1 / values[1](aVariableMap));
        }

      case "max":
        /**
         * Get the largest value from a set of values.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from max operation.
         */
        return function max_computing(aVariableMap) {
          var valueResults = [];
          for (var i = 0; i < values.length; i++) {
            valueResults[i] = values[i](aVariableMap);
          }

          return Math.max.apply(Math, valueResults);
        }

      case "min":
        /**
         * Get the smallest value from a set of values.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from min operation.
         */
        return function min_computing(aVariableMap) {
          var valueResults = [];
          for (var i = 0; i < values.length; i++) {
            valueResults[i] = values[i](aVariableMap);
          }

          return Math.min.apply(Math, valueResults);
        }

      case "ln":
        /**
         * Get the natural logarithm of a value.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from logarithm operation.
         */
        return function ln_computing(aVariableMap) {
          return Math.log(values[0](aVariableMap));
        }

      case "exp":
      case "sin":
      case "cos":
      case "tan":
      case "floor":
      case "abs":
        var opName = aMathNode.firstChild.localName;
        if (values.length != 1) {
          throw new Error(opName + " operator takes precisely one argument");
        }

        /**
         * Get the value from a Math object's method executed on a value.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return Value retrieved from Math object's method execution.
         */
        return function mathOps_computing(aVariableMap) {
          return Math[opName](values[0](aVariableMap));
        }

      case "ceiling":
        if (values.length != 1) {
          throw new Error("ceiling operator takes precisely one argument");
        }

        return function ceiling_computing(aVariableMap) {
          return Math.ceil(values[0](aVariableMap));
        }

      default:
        throw new Error("not implemented: " + aMathNode.firstChild.nodeName);
    }
  }

  switch (aMathNode.localName) {
    case "cn":
      /**
       * Convert a constant into a getter function.
       *
       * @param aConstant to convert.
       *
       * @return getValue with aConstant in local scope.
       */
      function getConstantFunction(aConstant) {
        /**
         * Get a constant value.
         *
         * @return The constant value.
         */
        function getValue() {
          return Number(aConstant);
        }

        return getValue;
      }
      var retval = Number(aMathNode.firstChild.nodeValue);
      return getConstantFunction(retval);

    case "ci":
      /**
       * Convert a variable name into a getter function.
       *
       * @param aVariable Name of the variable to retrieve.
       *
       * @return getValue with aVariable in local scope.
       */
      function getVariableFunction(aVariable) {
        /**
         * Get the value from a variable map corresponding to a variable.
         *
         * @param aVariableMap with (variableName: variableValue) properties.
         *
         * @return the variable's value.
         */
        function getValue(aVariableMap) {
          return aVariableMap[aVariable];
        }
        return getValue;
      }
      return getVariableFunction(aMathNode.firstChild.nodeValue);
  }
}

var getComputingFunction;
if (typeof getContractFunction == "function") {
  getComputingFunction = getContractFunction({
    /**
     * Precondition function for getComputingFunction() call.
     *
     * @param aMathNode Content MathML node passed into getComputingFunction.
     */
    precondition: function precondition(aMathNode) {
      const nsIDOMElement = Components.interfaces.nsIDOMElement;
      const MATH_NS = "http://www.w3.org/1998/Math/MathML";
      const msg = "aMathNode isn't a MathML element!";
      assert(aMathNode instanceof nsIDOMElement, msg, true);
      assert(aMathNode.namespaceURI == MATH_NS, msg, true);
    },

    body: getComputingFunction_body,

    /**
     * Postcondition function for getComputingFunction() call.
     *
     * @param aMathNode Content MathML node passed into getComputingFunction.
     * @param aReturnValue Value returned from getComputingFunction_body.
     */
    postcondition: function postcondition(aMathNode, rv) {
      const msg = "getComputingFunction didn't return a function!";
      assert(typeof rv == "function", msg, true);
    }
  });
} else {
  getComputingFunction = getComputingFunction_body;
  getComputingFunction_body = undefined;
  delete getComputingFunction_body;
}
