'use strict';

var digits = require('./../../../utils/number').digits;
// TODO this could be improved by simplifying seperated constants under associative and commutative operators
function factory(type, config, load, typed, math) {
  var util = load(require('./util'));
  var isNumeric = load(require('../../utils/isNumeric'));
  var isCommutative = util.isCommutative;
  var isAssociative = util.isAssociative;
  var allChildren = util.allChildren;
  var createMakeNodeFunction = util.createMakeNodeFunction;
  var ConstantNode = math.expression.node.ConstantNode;
  var OperatorNode = math.expression.node.OperatorNode;
  var FunctionNode = math.expression.node.FunctionNode;

  function simplifyConstant(expr) {
    var res = foldFraction(expr);
    return type.isNode(res) ? res : _toNode(res);
  }

  function _eval(fnname, args) {
    try {
      return _toNumber(math[fnname].apply(null, args));
    }
    catch (ignore) {
      // sometimes the implicit type conversion causes the evaluation to fail, so we'll try again after removing Fractions
      args = args.map(function(x){
        if (type.isFraction(x)) {
          return x.valueOf();
        }
        return x;
      });
      return _toNumber(math[fnname].apply(null, args));
    }
  }

  var _toNode = typed({
    'Fraction': _fractionToNode,
    'number': function(n) {
      if (n < 0) {
        return unaryMinusNode(new ConstantNode(-n));
      }
      return new ConstantNode(n);
    },
    'BigNumber': function(n) {
      if (n < 0) {
        return unaryMinusNode(new ConstantNode(n.negated().toString(), 'number'));
      }
      return new ConstantNode(n.toString(), 'number');
    },
    'Complex': function(s) {
      throw 'Cannot convert Complex number to Node';
    }
  });

  // convert a number to a fraction only if it can be expressed exactly
  function _exactFraction(n) {
    if (isFinite(n)) {
      var f = math.fraction(n);
      if (f.valueOf() === n) {
        return f;
      }
    }
    return n;
  }

  // Convert numbers to a preferred number type in preference order: Fraction, number, Complex
  // BigNumbers are left alone
  var _toNumber = typed({
    'string': function(s) {
      if (config.number === 'BigNumber') {
        return math.bignumber(s);
      }
      else if (config.number === 'Fraction') {
        return math.fraction(s);
      }
      else {
        return _exactFraction(parseFloat(s));
      }
    },

    'Fraction': function(s) { return s; },

    'BigNumber': function(s) { return s; },

    'number': function(s) {
      return _exactFraction(s);
    },

    'Complex': function(s) {
      if (s.im !== 0) {
        return s;
      }
      return _exactFraction(s.re);
    },
  });

  function unaryMinusNode(n) {
    return new OperatorNode('-', 'unaryMinus', [n]);
  }

  function _fractionToNode(f) {
    var n;
    var vn = f.s*f.n;
    if (vn < 0) {
      n = new OperatorNode('-', 'unaryMinus', [new ConstantNode(-vn)])
    }
    else {
      n = new ConstantNode(vn);
    }

    if (f.d === 1) {
      return n;
    }
    return new OperatorNode('/', 'divide', [n, new ConstantNode(f.d)]);
  }

  /*
   * Create a binary tree from a list of Fractions and Nodes.
   * Tries to fold Fractions by evaluating them until the first Node in the list is hit, so
   * `args` should be sorted to have the Fractions at the start (if the operator is commutative).
   * @param args - list of Fractions and Nodes
   * @param fn - evaluator for the binary operation evaluator that accepts two Fractions
   * @param makeNode - creates a binary OperatorNode/FunctionNode from a list of child Nodes
   * if args.length is 1, returns args[0]
   * @return - Either a Node representing a binary expression or Fraction
   */
  function foldOp(fn, args, makeNode) {
    return args.reduce(function(a, b) {
      if (!type.isNode(a) && !type.isNode(b)) {
        try {
          return _eval(fn, [a,b]);
        }
        catch (ignoreandcontinue) {}
        a = _toNode(a);
        b = _toNode(b);
      }
      else if (!type.isNode(a)) {
        a = _toNode(a);
      }
      else if (!type.isNode(b)) {
        b = _toNode(b);
      }

      return makeNode([a, b]);
    });
  }

  // destroys the original node and returns a folded one
  function foldFraction(node) {
    switch(node.type) {
      case 'SymbolNode':
        return node;
      case 'ConstantNode':
        if (typeof node.value === 'number') {
          return _toNumber(node.value);
        }
        return node;
      case 'FunctionNode':
        if (math[node.name] && math[node.name].rawArgs) {
          return node;
        }

        // Process operators as OperatorNode
        var operatorFunctions = [ 'add', 'multiply' ];
        if (operatorFunctions.indexOf(node.name) === -1) {
          var args = node.args.map(foldFraction);

          // If all args are numbers
          if (!args.some(type.isNode)) {
            try {
              return _eval(node.name, args);
            }
            catch (ignoreandcontine) {}
          }

          // Convert all args to nodes and construct a symbolic function call
          args = args.map(function(arg) {
            return type.isNode(arg) ? arg : _toNode(arg);
          });
          return new FunctionNode(node.name, args);
        }
        else {
          // treat as operator
        }
        /* falls through */
      case 'OperatorNode':
        var fn = node.fn.toString();
        var args;
        var res;
        var makeNode = createMakeNodeFunction(node);
        if (node.isUnary()) {
          args = [foldFraction(node.args[0])];
          if (!type.isNode(args[0])) {
            res = _eval(fn, args);
          }
          else {
            res = makeNode(args);
          }
        }
        else if (isAssociative(node)) {
          args = allChildren(node);
          args = args.map(foldFraction);

          if (isCommutative(fn)) {
            // commutative binary operator
            var consts = [], vars = [];

            for (var i=0; i < args.length; i++) {
              if (!type.isNode(args[i])) {
                consts.push(args[i]);
              }
              else {
                vars.push(args[i]);
              }
            }

            if (consts.length > 1) {
              res = foldOp(fn, consts, makeNode);
              vars.unshift(res);
              res = foldOp(fn, vars, makeNode);
            }
            else {
              // we won't change the children order since it's not neccessary
              res = foldOp(fn, args, makeNode);
            }
          }
          else {
            // non-commutative binary operator
            res = foldOp(fn, args, makeNode);
          }
        }
        else {
          // non-associative binary operator
          args = node.args.map(foldFraction);
          res = foldOp(fn, args, makeNode);
        }
        return res;
      case 'ParenthesisNode':
        // remove the uneccessary parenthesis
        return foldFraction(node.content);
      case 'AccessorNode':
        /* falls through */
      case 'ArrayNode':
        /* falls through */
      case 'AssignmentNode':
        /* falls through */
      case 'BlockNode':
        /* falls through */
      case 'FunctionAssignmentNode':
        /* falls through */
      case 'IndexNode':
        /* falls through */
      case 'ObjectNode':
        /* falls through */
      case 'RangeNode':
        /* falls through */
      case 'UpdateNode':
        /* falls through */
      case 'ConditionalNode':
        /* falls through */
      default:
        throw 'Unimplemented node type in simplifyConstant: '+node.type;
    }
  }

  return simplifyConstant;
}

exports.math = true;
exports.name = 'simplifyConstant';
exports.path = 'algebra.simplify';
exports.factory = factory;
