/*
 JessieCode Computer algebra algorithms

    Copyright 2011-2016
        Michael Gerhaeuser,
        Alfred Wassermann

    JessieCode is free software dual licensed under the GNU LGPL or MIT License.

    You can redistribute it and/or modify it under the terms of the

      * GNU Lesser General Public License as published by
        the Free Software Foundation, either version 3 of the License, or
        (at your option) any later version
      OR
      * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT

    JessieCode is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License and
    the MIT License along with JessieCode. If not, see <http://www.gnu.org/licenses/>
    and <http://opensource.org/licenses/MIT/>.
 */

 /*global JXG: true, define: true, window: true, console: true, self: true, document: true, parser: true*/
 /*jslint nomen: true, plusplus: true*/

 /* depends:
  jxg
  parser/geonext
  base/constants
  base/text
  math/math
  math/geometry
  math/statistics
  utils/type
  utils/uuid
  */

 /**
  * @fileoverview Here, the computer algebra algorithms are implemented.
  */

 define([
     'jxg', 'base/constants', 'base/text', 'math/math', 'math/geometry', 'math/statistics', 'utils/type', 'utils/env'
 ], function (JXG, Const, Text, Mat, Geometry, Statistics, Type, Env) {

     "use strict";

     /**
      * A JessieCode object provides an interface to the parser and stores all variables and objects used within a JessieCode script.
      * The optional argument <tt>code</tt> is interpreted after initializing. To evaluate more code after initializing a JessieCode instance
      * please use {@link JXG.JessieCode#parse}. For code snippets like single expressions use {@link JXG.JessieCode#snippet}.
      * @constructor
      * @param {String} [code] Code to parse.
      * @param {Boolean} [geonext=false] Geonext compatibility mode.
      */
     JXG.CA = function (node, createNode, parser) {
         this.node = node;
         this.createNode = createNode;
         this.parser = parser;
     };

     JXG.extend(JXG.CA.prototype, /** @lends JXG.CA.prototype */ {
         findMapNode: function(mapname, node) {
             var i, len, ret;

             //console.log("FINDMAP", node);
             if (node.value === 'op_assign' && node.children[0].value === mapname) {
                 return node.children[1];
             } else if (node.children) {
                 len = node.children.length;
                 for (i = 0; i < len; ++i) {
                     ret = this.findMapNode(mapname, node.children[i]);
                     if (ret !== null) {
                         return ret;
                     }
                 }
             }
             return null;
         },

         /**
          * Declare all subnodes as math nodes,
          * i.e recursively set node.isMath = true;
          */
         setMath: function(node) {
             var i, len;

             if ((node.type == 'node_op' && (
                 node.value == 'op_add' || node.value == 'op_sub' ||
                 node.value == 'op_mul' || node.value == 'op_div' ||
                 node.value == 'op_neg' || node.value == 'op_execfun' ||
                 node.value == 'op_exp')) ||
                 node.type == 'node_var' || node.type == 'node_const') {

                 node.isMath = true;
             }
             if (node.children) {
                 len = node.children.length;
                 for (i = 0; i < len; ++i) {
                     this.setMath(node.children[i]);
                 }
             }
         },

         deriveElementary: function(node, varname) {
             var fun = node.children[0].value,
                 arg = node.children[1],
                 newNode;


             switch (fun) {
             case 'abs':
                 // x / sqrt(x * x)
                 newNode = this.createNode('node_op', 'op_div',
                         arg[0],
                         this.createNode('node_op', 'op_execfun',
                             this.createNode('node_var', 'sqrt'),
                             [this.createNode('node_op', 'op_mul',
                                 Type.deepCopy(arg[0]),
                                 Type.deepCopy(arg[0])
                             )]
                         )
                     );
                 break;

             case 'sqrt':
                 newNode = this.createNode('node_op', 'op_div',
                         this.createNode('node_const', 1.0),
                         this.createNode('node_op', 'op_mul',
                             this.createNode('node_const', 2.0),
                             this.createNode(node.type, node.value,
                                 Type.deepCopy(node.children[0]),
                                 Type.deepCopy(node.children[1])
                             )
                         )
                     );
                 break;

             case 'sin':
                 newNode = this.createNode('node_op', 'op_execfun',
                         this.createNode('node_var', 'cos'),
                         Type.deepCopy(arg)
                     );
                 break;

             case 'cos':
                 newNode = this.createNode('node_op', 'op_neg',
                             this.createNode('node_op', 'op_execfun',
                                 this.createNode('node_var', 'sin'),
                                 Type.deepCopy(arg)
                             )
                         );
                 break;

             case 'tan':
                 newNode = this.createNode('node_op', 'op_div',
                             this.createNode('node_const', 1.0),
                             this.createNode('node_op', 'op_exp',
                                 this.createNode('node_op', 'op_execfun',
                                     this.createNode('node_var', 'cos'),
                                     Type.deepCopy(arg)
                                 ),
                                 this.createNode('node_const', 2)
                             )
                         );
                 break;

             case 'exp':
                 newNode = this.createNode(node.type, node.value,
                             Type.deepCopy(node.children[0]),
                             Type.deepCopy(node.children[1])
                         );
                 break;

             case 'pow':
                 // (f^g)' = f^g*(f'g/f + g' log(f))
                 newNode = this.createNode('node_op', 'op_mul',
                         this.createNode('node_op', 'op_execfun',
                             Type.deepCopy(node.children[0]),
                             Type.deepCopy(node.children[1])
                         ),
                         this.createNode('node_op', 'op_add',
                             this.createNode('node_op', 'op_mul',
                                 this.derivative(node.children[1][0], varname),
                                 this.createNode('node_op', 'op_div',
                                     Type.deepCopy(node.children[1][1]),
                                     Type.deepCopy(node.children[1][0])
                                 )
                             ),
                             this.createNode('node_op', 'op_mul',
                                 this.derivative(node.children[1][1], varname),
                                 this.createNode('node_op', 'op_execfun',
                                     this.createNode('node_var', 'log'),
                                     [Type.deepCopy(node.children[1][0])]
                                 )
                             )
                         )
                     );
                 break;

             case 'log':
             case 'ln':
                 newNode = this.createNode('node_op', 'op_div',
                             this.createNode('node_const', 1.0),
                             // Attention: single variable mode
                             Type.deepCopy(arg[0])
                         );
                 break;

             case 'log2':
             case 'lb':
             case 'ld':
                 newNode = this.createNode('node_op', 'op_mul',
                             this.createNode('node_op', 'op_div',
                                 this.createNode('node_const', 1.0),
                                 // Attention: single variable mode
                                 Type.deepCopy(arg[0])
                             ),
                             this.createNode('node_const', 1.4426950408889634)  // 1/log(2)
                         );
                 break;

             case 'log10':
             case 'lg':
                 newNode = this.createNode('node_op', 'op_mul',
                             this.createNode('node_op', 'op_div',
                                 this.createNode('node_const', 1.0),
                                 // Attention: single variable mode
                                 Type.deepCopy(arg[0])
                             ),
                             this.createNode('node_const', 0.43429448190325176)  // 1/log(10)
                         );
                 break;

             case 'asin':
                 newNode = this.createNode('node_op', 'op_div',
                             this.createNode('node_const', 1.0),
                             this.createNode('node_op', 'op_execfun',
                                 this.createNode('node_var', 'sqrt'),
                                 [
                                     this.createNode('node_op', 'op_sub',
                                         this.createNode('node_const', 1.0),
                                         this.createNode('node_op', 'op_mul',
                                             Type.deepCopy(arg[0]),
                                             Type.deepCopy(arg[0])
                                         )
                                     )
                                 ]
                             )
                         );
                 break;

             case 'acos':
                 newNode = this.createNode('node_op', 'op_neg',
                         this.createNode('node_op', 'op_div',
                             this.createNode('node_const', 1.0),
                             this.createNode('node_op', 'op_execfun',
                                 this.createNode('node_var', 'sqrt'),
                                 [
                                     this.createNode('node_op', 'op_sub',
                                         this.createNode('node_const', 1.0),
                                         this.createNode('node_op', 'op_mul',
                                             Type.deepCopy(arg[0]),
                                             Type.deepCopy(arg[0])
                                         )
                                     )
                                 ]
                             )
                         )
                     );
                 break;

             case 'atan':
                 newNode = this.createNode('node_op', 'op_div',
                             this.createNode('node_const', 1.0),
                             this.createNode('node_op', 'op_add',
                                 this.createNode('node_const', 1.0),
                                 this.createNode('node_op', 'op_mul',
                                     Type.deepCopy(arg[0]),
                                     Type.deepCopy(arg[0])
                                 )
                             )
                         );
                 break;

             //case 'atan2':
             case 'sinh':
                 newNode = this.createNode('node_op', 'op_execfun',
                             this.createNode('node_var', 'cosh'),
                             [Type.deepCopy(arg[0])]
                         );
                 break;

             case 'cosh':
                 newNode = this.createNode('node_op', 'op_execfun',
                             this.createNode('node_var', 'sinh'),
                             [Type.deepCopy(arg[0])]
                         );
                 break;

             case 'tanh':
                 newNode = this.createNode('node_op', 'op_sub',
                             this.createNode('node_const', 1.0),
                             this.createNode('node_op', 'op_exp',
                                 this.createNode('node_op', 'op_execfun',
                                     this.createNode('node_var', 'tanh'),
                                     [Type.deepCopy(arg[0])]
                                 ),
                                 this.createNode('node_const', 2.0)
                             )
                         );
                 break;

             case 'asinh':
                 newNode = this.createNode('node_op', 'op_div',
                             this.createNode('node_const', 1.0),
                             this.createNode('node_op', 'op_execfun',
                                 this.createNode('node_var', 'sqrt'),
                                 [
                                     this.createNode('node_op', 'op_add',
                                         this.createNode('node_op', 'op_mul',
                                             Type.deepCopy(arg[0]),
                                             Type.deepCopy(arg[0])
                                         ),
                                         this.createNode('node_const', 1.0)
                                     )
                                 ]
                             )
                         );
                 break;

             case 'acosh':
                 newNode = this.createNode('node_op', 'op_div',
                             this.createNode('node_const', 1.0),
                             this.createNode('node_op', 'op_execfun',
                                 this.createNode('node_var', 'sqrt'),
                                 [
                                     this.createNode('node_op', 'op_sub',
                                         this.createNode('node_op', 'op_mul',
                                             Type.deepCopy(arg[0]),
                                             Type.deepCopy(arg[0])
                                         ),
                                         this.createNode('node_const', 1.0)
                                     )
                                 ]
                             )
                         );
                 break;

             case 'atanh':
                 newNode = this.createNode('node_op', 'op_div',
                             this.createNode('node_const', 1.0),
                             this.createNode('node_op', 'op_sub',
                                 this.createNode('node_const', 1.0),
                                 this.createNode('node_op', 'op_mul',
                                     Type.deepCopy(arg[0]),
                                     Type.deepCopy(arg[0])
                                 )
                             )
                         );
                 break;

             default:
                 newNode = this.createNode('node_const', 0.0);
                 this._error('Derivative of "' + fun + '" not yet implemented');
             }

             return newNode;
         },

         derivative: function(node, varname) {
             var i, len, newNode;

             switch (node.type) {
             case 'node_op':
                 switch (node.value) {
                 /*
                 case 'op_map':
                     if (true) {
                         newNode = this.createNode('node_op', 'op_map',
                                 Type.deepCopy(node.children[0]),
                                 this.derivative(node.children[1], varname)
                             );
                     } else {
                         newNode = this.derivative(node.children[1], varname);
                     }
                     break;
                 */
                 case 'op_execfun':
                     // f'(g(x))g'(x)
                     if (node.children[0].value == 'pow') {
                         newNode = this.deriveElementary(node, varname);
                     } else {
                         newNode = this.createNode('node_op', 'op_mul',
                                     this.deriveElementary(node, varname),
                                     // Warning: single variable mode
                                     this.derivative(node.children[1][0], varname)
                                 );

                     }
                     break;

                 case 'op_div':
                     // (f'g − g'f )/(g*g)
                     newNode = this.createNode('node_op', 'op_div',
                                 this.createNode('node_op', 'op_sub',
                                     this.createNode('node_op', 'op_mul',
                                         this.derivative(node.children[0], varname),
                                         Type.deepCopy(node.children[1])
                                     ),
                                     this.createNode('node_op', 'op_mul',
                                         Type.deepCopy(node.children[0]),
                                         this.derivative(node.children[1], varname)
                                     )
                                 ),
                                 this.createNode('node_op', 'op_mul',
                                     Type.deepCopy(node.children[1]),
                                     Type.deepCopy(node.children[1])
                                 )
                             );
                     break;

                 case 'op_mul':
                     // fg' + f'g
                     newNode = this.createNode('node_op', 'op_add',
                                 this.createNode('node_op', 'op_mul',
                                     Type.deepCopy(node.children[0]),
                                     this.derivative(node.children[1], varname)),
                                 this.createNode('node_op', 'op_mul',
                                     this.derivative(node.children[0], varname),
                                     Type.deepCopy(node.children[1]))
                             );
                     break;

                 case 'op_neg':
                     newNode = this.createNode('node_op', 'op_neg',
                                 this.derivative(node.children[0], varname)
                             );
                     break;

                 case 'op_add':
                 case 'op_sub':
                     newNode = this.createNode('node_op', node.value,
                                 this.derivative(node.children[0], varname),
                                 this.derivative(node.children[1], varname)
                             );
                     break;

                 case 'op_exp':
                     // (f^g)' = f^g*(f'g/f + g' log(f))
                     newNode = this.createNode('node_op', 'op_mul',
                                 Type.deepCopy(node),
                                 this.createNode('node_op', 'op_add',
                                     this.createNode('node_op', 'op_mul',
                                         this.derivative(node.children[0], varname),
                                         this.createNode('node_op', 'op_div',
                                             Type.deepCopy(node.children[1]),
                                             Type.deepCopy(node.children[0])
                                         )
                                     ),
                                     this.createNode('node_op', 'op_mul',
                                         this.derivative(node.children[1], varname),
                                         this.createNode('node_op', 'op_execfun',
                                             this.createNode('node_var', 'log'),
                                             [Type.deepCopy(node.children[0])]
                                         )
                                     )
                                 )
                             );
                     break;
                 }
                 break;

             case 'node_var':
                 //console.log('node_var', node);
                 if (node.value === varname) {
                     newNode = this.createNode('node_const', 1.0);
                 } else {
                     newNode = this.createNode('node_const', 0.0);
                 }
                 break;

             case 'node_const':
                 newNode = this.createNode('node_const', 0.0);
                 break;

             case 'node_const_bool':
                 break;

             case 'node_str':
                 break;

             }

             return newNode;
         },

         /**
          * f = map (x) -> x*sin(x);
          * Usages:
          * h = D(f, x);
          * h = map (x) -> D(f, x);
          *
          */
         expandDerivatives: function(node, parent, ast) {
             var len, i, j, mapNode, codeNode, ret, node2, newNode,
                 mapName, varname, vArray, order;

             ret = 0;
             if (!node) {
                 return ret;
             }

             this.line = node.line;
             this.col = node.col;

             // First we have to go down in the tree.
             // This ensures that in cases like D(D(f,x),x) the inner D is expanded first.
             len = node.children.length;
             for (i = 0; i < len; ++i) {
                 if (node.children[i] && node.children[i].type) {
                     node.children[i] = this.expandDerivatives(node.children[i], node, ast);
                 } else if (Type.isArray(node.children[i])) {
                     for (j = 0; j < node.children[i].length; ++j) {
                         if (node.children[i][j] && node.children[i][j].type) {
                             node.children[i][j] = this.expandDerivatives(node.children[i][j], node, ast);
                         }
                     }
                 }
             }

             switch (node.type) {
             case 'node_op':
                 switch (node.value) {
                 case 'op_execfun':
                     if (node.children[0] && node.children[0].value === 'D') {
                         if (node.children[1][0].type == 'node_var') {
                             /*
                              * Derive map, that is compute D(f,x)
                              * where e.g. f = map (x) -> x^2
                              *
                              * First step: find node where the map is defined
                              */
                             mapName = node.children[1][0].value;
                             mapNode = this.findMapNode(mapName, ast);
                             vArray = mapNode.children[0];

                             // Variable name for differentiation
                             if (node.children[1].length >= 2) {
                                 varname = node.children[1][1].value;
                             } else {
                                 varname = mapNode.children[0][0]; // Usually it's 'x'
                             }
                             codeNode = mapNode.children[1];
                         } else {
                             /*
                              * Derive expression, e.g.
                              *     D(2*x, x)
                              */
                             codeNode = node.children[1][0];
                             vArray = ['x'];

                             // Variable name for differentiation and order
                             if (node.children[1].length >= 2) {
                                 varname = node.children[1][1].value;
                             } else {
                                 varname = 'x';
                             }
                         }

                         // Differentiation order
                         if (node.children[1].length >= 3) {
                             order = node.children[1][2].value;
                         } else {
                             order = 1;
                         }

                         // Create node which contains the derivative
                         newNode = codeNode;
                         //newNode = this.removeTrivialNodes(newNode);
                         if (order >= 1) {
                             while (order >= 1) {
                                 newNode = this.derivative(newNode, varname);
                                 newNode = this.removeTrivialNodes(newNode);
                                 order--;
                             }
                         }

                         // Replace the node containing e.g. D(f,x) by the derivative.
                         if (parent.type == 'node_op' && parent.value == 'op_assign') {
                             // If D is an assignment it has to be replaced by a map
                             // h = D(f, x)
                             node2 = this.createNode('node_op', 'op_map',
                                     vArray,
                                     newNode
                                 );
                         } else {
                             node2 = newNode;
                         }

                         this.setMath(node2);
                         node.type = node2.type;
                         node.value = node2.value;
                         node.children[0] = node2.children[0];
                         node.children[1] = node2.children[1];
                     }
                 }
                 break;

             case 'node_var':
             case 'node_const':
             case 'node_const_bool':
             case 'node_str':
                 break;
             }

             return node;
         },

         removeTrivialNodes: function(node) {
             var i, len, n0, n1, swap;

             // In case of 'op_execfun' the children[1] node is an array.
             if (Type.isArray(node)) {
                 len = node.length;
                 for (i = 0; i < len; ++i) {
                     node[i] = this.removeTrivialNodes(node[i]);
                 }
             }
             if (node.type != 'node_op' || !node.children) {
                 return node;
             }

             len = node.children.length;
             for (i = 0; i < len; ++i) {
                 this.mayNotBeSimplified = false;
                 do {
                     node.children[i] = this.removeTrivialNodes(node.children[i]);
                 } while (this.mayNotBeSimplified);

             }

             switch (node.value) {
             // Allow maps of the form
             //  map (x) -> x;
             case 'op_map':
                 n0 = node.children[0];
                 n1 = node.children[1];
                 if (n1.type == 'node_var') {
                     for (i = 0; i < n0.length; ++i) {
                         // Allow maps of the form map(x) -> x
                         if (n0[i] == n1.value) {
                             n1.isMath = true;
                             break;
                         }
                     }
                 }
                 break;

             // a + 0 -> a
             // 0 + a -> a
             case 'op_add':
                 n0 = node.children[0];
                 n1 = node.children[1];
                 if (n0.type == 'node_const' && n0.value === 0.0) {
                     return n1;
                 }
                 if (n1.type == 'node_const' && n1.value === 0.0) {
                     return n0;
                 }

                 // const + const -> const
                 if (n0.type == 'node_const' && n1.type == 'node_const') {
                     n0.value += n1.value;
                     return n0;
                 }
                 break;

             // 1 * a = a
             // a * 1 = a
             // a * 0 = 0
             // 0 * a = 0
             // - * - = +
             // Order children
             case 'op_mul':
                 n0 = node.children[0];
                 n1 = node.children[1];
                 if (n0.type == 'node_const' && n0.value == 1.0) {
                     return n1;
                 }
                 if (n1.type == 'node_const' && n1.value == 1.0) {
                     return n0;
                 }
                 if (n0.type == 'node_const' && n0.value === 0.0) {
                     return n0;
                 }
                 if (n1.type == 'node_const' && n1.value === 0.0) {
                     return n1;
                 }
                 if (n1.type == 'node_const' && n1.value === 0.0) {
                     return n1;
                 }

                 // (-a) * (-b) -> a*b
                 if (n0.type == 'node_op' && n0.value == 'op_neg' &&
                     n1.type == 'node_op' && n1.value == 'op_neg' ) {
                     node.children = [n0.children[0], n1.children[0]];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 // (-a) * b -> -(a*b)
                 if (n0.value == 'op_neg' && n1.value != 'op_neg' ) {
                     node.type = 'node_op';
                     node.value = 'op_neg';
                     node.children = [this.createNode('node_op', 'op_mul', n0.children[0], n1)];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 // a * (-b) -> -(a*b)
                 if (n0.value != 'op_neg' && n1.value == 'op_neg' ) {
                     node.type = 'node_op';
                     node.value = 'op_neg';
                     node.children = [this.createNode('node_op', 'op_mul', n0, n1.children[0])];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 // (1 / a) * b -> a / b
                 if (n0.value == 'op_div' &&
                     n0.children[0].type == 'node_const' && n0.children[0].value == 1.0) {
                     node.type = 'node_op';
                     node.value = 'op_div';
                     node.children = [n1, n0.children[1]];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 // a * (1 / b) -> a / b
                 if (n1.value == 'op_div' &&
                     n1.children[0].type == 'node_const' && n1.children[0].value == 1.0) {
                     node.type = 'node_op';
                     node.value = 'op_div';
                     node.children = [n0, n1.children[1]];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 // Order children
                 // a * const -> const * a
                 if (n0.type != 'node_const' && n1.type == 'node_const') {
                     node.children = [n1, n0];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 // a + (-const) -> -const * a
                 if (n0.type != 'node_const' && n1.type == 'node_op' &&
                     n1.value == 'op_neg' && n1.children[0].type == 'node_const') {
                     node.children = [n1, n0];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 // a * var -> var * a
                 // a * fun -> fun * a
                 if (n0.type == 'node_op' && n0.value != 'op_execfun' &&
                     (n1.type == 'node_var' || (n1.type == 'node_op' && n1.value == 'op_execfun'))) {
                     node.children = [n1, n0];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 // a + (-var) -> -var * a
                 if (n0.type != 'node_op' && n1.type == 'node_op' &&
                     n1.value == 'op_neg' && n1.children[0].type == 'node_var') {
                     node.children = [n1, n0];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 // a * (const * b) -> const * (a*b)
                 // a * (const / b) -> const * (a/b)
                 if (n0.type != 'node_const' && n1.type == 'node_op' &&
                     (n1.value == 'op_mul' || n1.value == 'op_div') &&
                     n1.children[0].type == 'node_const') {
                     swap = n1.children[0];
                     n1.children[0] = n0;
                     node.children = [swap, n1];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 // (const * a) * b -> const * (a * b)
                 if (n1.type != 'node_const' && n0.type == 'node_op' &&
                     n0.value == 'op_mul' &&
                     n0.children[0].type == 'node_const') {
                     node.children = [
                         n0.children[0],
                         this.createNode('node_op', 'op_mul', n0.children[1], n1)
                     ];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 // const * const -> const
                 if (n0.type == 'node_const' && n1.type == 'node_const') {
                     n0.value *= n1.value;
                     return n0;
                 }

                 // const * (const * a) -> const * a
                 // const * (const / a) -> const / a
                 if (n0.type == 'node_const' && n1.type == 'node_op' &&
                     (n1.value == 'op_mul' || n1.value == 'op_div') &&
                     n1.children[0].type == 'node_const') {
                     n1.children[0].value *= n0.value;
                     return n1;
                 }

                 // a * a-> a^2
                 n0.hash = this.parser.compile(n0);
                 n1.hash = this.parser.compile(n1);
                 if (n0.hash === n1.hash) {
                     node.value = 'op_exp';
                     node.children[1] = this.createNode('node_const', 2.0);
                     return node;
                 }

                 if (n0.type == 'node_const' && n1.type == 'node_op' &&
                     (n1.value == 'op_mul' || n1.value == 'op_div') &&
                     n1.children[0].type == 'node_const') {
                     n1.children[0].value *= n0.value;
                     return n1;
                 }

                 // a * a^b -> a^(b+1)
                 if (n1.type == 'node_op' && n1.value == 'op_exp') {
                     if (!n0.hash) {
                         n0.hash = this.parser.compile(n0);
                     }
                     if (!n1.children[0].hash) {
                         n1.children[0].hash = this.parser.compile(n1.children[0]);
                     }
                     if (n0.hash === n1.children[0].hash) {
                         n1.children[1] = this.createNode('node_op', 'op_add',
                             n1.children[1],
                             this.createNode('node_const', 1.0)
                         );
                         this.mayNotBeSimplified = true;
                         return n1;
                     }
                 }

                 // a^b * a^c -> a^(b+c)
                 if (n0.type == 'node_op' && n0.value == 'op_exp' &&
                     n1.type == 'node_op' && n1.value == 'op_exp') {
                     n0.children[0].hash = this.parser.compile(n0.children[0]);
                     n1.children[0].hash = this.parser.compile(n1.children[0]);
                     if (n0.children[0].hash === n1.children[0].hash) {
                         n0.children[1] = this.createNode('node_op', 'op_add',
                             n0.children[1],
                             n1.children[1]
                         );
                         this.mayNotBeSimplified = true;
                         return n0;
                     }
                 }

                 break;

             // 0 - a -> -a
             // a - 0 -> a
             // a - a -> 0
             case 'op_sub':
                n0 = node.children[0];
                n1 = node.children[1];
                if (n0.type == 'node_const' && n0.value === 0.0) {
                     node.value = 'op_neg';
                     node.children[0] = n1;
                     return node;
                 }
                 if (n1.type == 'node_const' && n1.value === 0.0) {
                     return n0;
                 }
                 if (n0.type == 'node_const' && n1.type == 'node_const' &&
                     n0.value == n1.value) {
                     return this.createNode('node_const', 0.0);
                 }
                 if (n0.type == 'node_var' && n1.type == 'node_var' &&
                     n0.value == n1.value) {
                     return this.createNode('node_const', 0.0);
                 }

                 // const - const -> const
                 if (n0.type == 'node_const' && n1.type == 'node_const') {
                     n0.value -= n1.value;
                     return n0;
                 }

                 // const * a - const * a -> const * a
                 if (n0.type == 'node_op' && n0.value == 'op_mul' &&
                     n1.type == 'node_op' && n1.value == 'op_mul') {

                    n0.children[1].hash = this.parser.compile(n0.children[1]);
                    n1.children[1].hash = this.parser.compile(n1.children[1]);
                    if (n0.children[1].hash === n1.children[1].hash) {

                        node.value = 'op_mul';
                        node.children = [
                            this.createNode('node_op', 'op_sub',
                                n0.children[0],
                                n1.children[0]),
                            n0.children[1]
                            ];
                        this.mayNotBeSimplified = true;
                        return node;
                    }
                 }
                 // const * a - a -> (const - 1) * a
                 if (n0.type == 'node_op' && n0.value == 'op_mul') {

                    n0.children[1].hash = this.parser.compile(n0.children[1]);
                    n1.hash = this.parser.compile(n1);
                    if (n0.children[1].hash === n1.hash) {

                        node.value = 'op_mul';
                        node.children = [
                            this.createNode('node_op', 'op_sub',
                                n0.children[0],
                                this.createNode('node_const', 1.0)),
                            n1
                            ];
                        this.mayNotBeSimplified = true;
                        return node;
                    }
                 }
                 // a - const*a -> (const - 1) * a
                 if (n1.type == 'node_op' && n1.value == 'op_mul') {

                    n1.children[1].hash = this.parser.compile(n1.children[1]);
                    n0.hash = this.parser.compile(n0);
                    if (n1.children[1].hash === n0.hash) {

                        node.value = 'op_mul';
                        node.children = [
                            this.createNode('node_op', 'op_sub',
                                this.createNode('node_const', 1.0),
                                n1.children[0]),
                            n0
                            ];
                        this.mayNotBeSimplified = true;
                        return node;
                    }
                 }

                 break;

             // -0 -> 0
             // -(-b) = b
             case 'op_neg':
                 n0 = node.children[0];
                 if (n0.type == 'node_const' && n0.value === 0.0) {
                     return n0;
                 }
                 if (n0.type == 'node_op' && n0.value == 'op_neg') {
                     return n0.children[0];
                 }
                 break;

             // a / a -> 1, a != 0
             // 0 / a -> 0, a != 0
             // a / 0 -> Infinity, a != 0
             // 0 / 0 -> NaN, a == 0
             case 'op_div':
                 n0 = node.children[0];
                 n1 = node.children[1];
                 if (n0.type == 'node_const' && n1.type == 'node_const' &&
                     n0.value == n1.value && n0.value !== 0) {
                     n0.value = 1.0;
                     return n0;
                 }
                 if (n0.type == 'node_const' && n0.value === 0 &&
                     n1.type == 'node_const' && n1.value !== 0) {
                     n0.value = 0.0;
                     return n0;
                 }

                 // Risky: 0 / (something != 0) -> 0.0
                 if (n0.type == 'node_const' && n0.value === 0 &&
                     (n1.type == 'node_op' || n1.type == 'node_var')) {
                     node.type = 'node_const';
                     node.value = 0.0;
                     return node;
                 }

                 if (n0.type == 'node_var' && n1.type == 'node_var' &&
                     n0.value == n1.value) {
                     return this.createNode('node_const', 1.0);
                 }
                 if (n0.type == 'node_const' && n0.value !== 0 &&
                     n1.type == 'node_const' && n1.value === 0) {
                     if (n0.value > 0.0) {
                         n0.value = Infinity;
                     } else {
                         n0.value = -Infinity; // Do we ever need this?
                     }
                     return n0;
                 }

                 // (-a) / (-b) -> a/b
                 if (n0.type == 'node_op' && n0.value == 'op_neg' &&
                     n1.type == 'node_op' && n1.value == 'op_neg' ) {
                     node.children = [n0.children[0], n1.children[0]];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 // (-a) / b -> -(a/b)
                 if (n0.value == 'op_neg' && n1.value != 'op_neg' ) {
                     node.type = 'node_op';
                     node.value = 'op_neg';
                     node.children = [this.createNode('node_op', 'op_div', n0.children[0], n1)];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 // a / (-b) -> -(a/b)
                 if (n0.value != 'op_neg' && n1.value == 'op_neg' ) {
                     node.type = 'node_op';
                     node.value = 'op_neg';
                     node.children = [this.createNode('node_op', 'op_div', n0, n1.children[0])];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 // a^b / a -> a^(b-1)
                 if (n0.type == 'node_op' && n0.value == 'op_exp') {
                     if (!n1.hash) {
                         n1.hash = this.parser.compile(n1);
                     }
                     if (!n0.children[0].hash) {
                         n0.children[0].hash = this.parser.compile(n0.children[0]);
                     }
                     if (n1.hash === n0.children[0].hash) {
                         n0.children[1] = this.createNode('node_op', 'op_sub',
                             n0.children[1],
                             this.createNode('node_const', 1.0)
                         );
                         this.mayNotBeSimplified = true;
                         return n0;
                     }
                 }

                 // (const * a) / b -> const * (a / b)
                 if (n1.type != 'node_const' && n0.type == 'node_op' &&
                     n0.value == 'op_mul' &&
                     n0.children[0].type == 'node_const') {
                     node.value = 'op_mul';
                     node.children = [
                         n0.children[0],
                         this.createNode('node_op', 'op_div', n0.children[1], n1)
                     ];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 // a^b / a^c -> a^(b-c)
                 if (n0.type == 'node_op' && n0.value == 'op_exp' &&
                     n1.type == 'node_op' && n1.value == 'op_exp') {
                     n0.children[0].hash = this.parser.compile(n0.children[0]);
                     n1.children[0].hash = this.parser.compile(n1.children[0]);
                     if (n0.children[0].hash === n1.children[0].hash) {
                         n0.children[1] = this.createNode('node_op', 'op_sub',
                             n0.children[1],
                             n1.children[1]
                         );
                         this.mayNotBeSimplified = true;
                         return n0;
                     }
                 }


                 break;

             // a^0 = 1
             // a^1 -> a
             // 1^a -> 1
             // 0^a -> 0: a const != 0
             case 'op_exp':
                 n0 = node.children[0];
                 n1 = node.children[1];
                 if (n1.type == 'node_const' && n1.value === 0.0) {
                     n1.value = 1.0;
                     return n1;
                 }
                 if (n1.type == 'node_const' && n1.value == 1.0) {
                     return n0;
                 }
                 if (n0.type == 'node_const' && n0.value == 1.0) {
                     return n0;
                 }
                 if (n0.type == 'node_const' && n0.value === 0.0 &&
                     n1.type == 'node_const' && n1.value !== 0.0) {
                     return n0;
                 }

                 // (a^b)^c -> a^(b*c)
                 if (n0.type == 'node_op' && n0.value == 'op_exp') {
                     node.children = [
                         n0.children[0],
                         this.createNode('node_op', 'op_mul',
                            n0.children[1],
                            n1)
                     ];
                     return node;
                 }
                 break;
             }

             switch (node.value) {
             // const_1 + const_2 -> (const_1 + const_2)
             // a + a -> 2*a
             // a + (-b) = a - b
             case 'op_add':
                 n0 = node.children[0];
                 n1 = node.children[1];
                 if (n0.type == 'node_const' && n1.type == 'node_const' &&
                     n0.value == n1.value) {
                     n0.value += n1.value;
                     return n0;
                 }

                 if (n0.type == 'node_var' && n1.type == 'node_var' &&
                     n0.value == n1.value) {
                     node.children[0] = this.createNode('node_const', 2.0);
                     node.value = 'op_mul';
                     return node;
                 }

                 if (n0.type == 'node_op' && n0.value == 'op_neg') {
                     node.value = 'op_sub';
                     node.children[0] = n1;
                     node.children[1] = n0.children[0];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 if (n1.type == 'node_op' && n1.value == 'op_neg') {
                     node.value = 'op_sub';
                     node.children[1] = n1.children[0];
                     this.mayNotBeSimplified = true;
                     return node;
                 }

                 // const * a + const * a -> const * a
                 if (n0.type == 'node_op' && n0.value == 'op_mul' &&
                     n1.type == 'node_op' && n1.value == 'op_mul') {

                    n0.children[1].hash = this.parser.compile(n0.children[1]);
                    n1.children[1].hash = this.parser.compile(n1.children[1]);
                    if (n0.children[1].hash === n1.children[1].hash) {

                        node.value = 'op_mul';
                        node.children = [
                            this.createNode('node_op', 'op_add',
                                n0.children[0],
                                n1.children[0]),
                            n0.children[1]
                            ];
                        this.mayNotBeSimplified = true;
                        return node;
                    }
                 }
                 // const * a + a -> (const + 1) * a
                 if (n0.type == 'node_op' && n0.value == 'op_mul') {

                    n0.children[1].hash = this.parser.compile(n0.children[1]);
                    n1.hash = this.parser.compile(n1);
                    if (n0.children[1].hash === n1.hash) {

                        node.value = 'op_mul';
                        node.children = [
                            this.createNode('node_op', 'op_add',
                                n0.children[0],
                                this.createNode('node_const', 1.0)),
                            n1
                            ];
                        this.mayNotBeSimplified = true;
                        return node;
                    }
                 }
                 // a + const*a -> (const + 1) * a
                 if (n1.type == 'node_op' && n1.value == 'op_mul') {

                    n1.children[1].hash = this.parser.compile(n1.children[1]);
                    n0.hash = this.parser.compile(n0);
                    if (n1.children[1].hash === n0.hash) {

                        node.value = 'op_mul';
                        node.children = [
                            this.createNode('node_op', 'op_add',
                                this.createNode('node_const', 1.0),
                                n1.children[0]),
                            n0
                            ];
                        this.mayNotBeSimplified = true;
                        return node;
                    }
                 }

                 break;

             // a - (-b) = a + b
             case 'op_sub':
                 n0 = node.children[0];
                 n1 = node.children[1];
                 if (n1.type == 'node_op' && n1.value == 'op_neg') {
                     node.value = 'op_add';
                     node.children[1] = n1.children[0];
                     this.mayNotBeSimplified = true;
                     return node;
                 }
                 break;

             case 'op_execfun':
                 return this.simplifyElementary(node);
             }

             return node;
         },

         simplifyElementary: function(node) {
             var fun = node.children[0].value,
                 arg = node.children[1],
                 newNode;

             // Catch errors of the form sin()
             if (arg.length == 0) {
                 return node;
             }

             switch (fun) {
             // sin(0) -> 0
             // sin(PI) -> 0
             // sin (int * PI) -> 0
             // sin (PI * int) -> 0
             // Same for tan()
             case 'sin':
             case 'tan':
                 if (arg[0].type == 'node_const' && arg[0].value === 0) {
                     node.type = 'node_const';
                     node.value = 0.0;
                     return node;
                 }
                 if (arg[0].type == 'node_var' && arg[0].value == 'PI') {
                     node.type = 'node_const';
                     node.value = 0.0;
                     return node;
                 }
                 if (arg[0].type == 'node_op' && arg[0].value == 'op_mul' &&
                     arg[0].children[0].type == 'node_const' && arg[0].children[0].value % 1 === 0 &&
                      arg[0].children[1].type == 'node_var' && arg[0].children[1].value == 'PI') {
                     node.type = 'node_const';
                     node.value = 0.0;
                     return node;
                 }
                 break;

             // cos(0) -> 1.0
             // cos(PI) -> -1.0
             // cos(int * PI) -> +/- 1.0
             // cos(PI * int) -> +/- 1.0
             case 'cos':
                 if (arg[0].type == 'node_const' && arg[0].value === 0) {
                     node.type = 'node_const';
                     node.value = 1.0;
                     return node;
                 }
                 if (arg[0].type == 'node_var' && arg[0].value == 'PI') {
                     node.type = 'node_op';
                     node.value = 'op_neg';
                     node.children = [this.createNode('node_const', 1.0)];
                     return node;
                 }
                 /*
                 if (arg[0].type == 'node_op' && arg[0].value == 'op_mul' &&
                     ((arg[0].children[0].type == 'node_const' && arg[0].children[0].value % 1 === 0 &&
                      arg[0].children[1].type == 'node_var' && arg[0].children[1].value == 'PI') ||
                      (arg[0].children[1].type == 'node_const' && arg[0].children[1].value % 1 === 0 &&
                       arg[0].children[0].type == 'node_var' && arg[0].children[0].value == 'PI'))) {
                     node.type = 'node_const';
                     node.value = 1.0;
                     return node;
                 }
                 */
                 break;

             // exp(0) -> 1
             case 'exp':
                 if (arg[0].type == 'node_const' && arg[0].value === 0) {
                     node.type = 'node_const';
                     node.value = 1.0;
                     return node;
                 }
                 break;

             // pow(a, 0) -> 1
             case 'pow':
                 if (arg[1].type == 'node_const' && arg[1].value === 0) {
                     node.type = 'node_const';
                     node.value = 1.0;
                     return node;
                 }
                 break;

             }

             return node;
         }

     });

     return JXG.CA;
 });
