/**
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// TODO(William Farner): A unit test is sorely needed here.  Add a unit test for jsirois to use
// as a starter for pants support for jsunit.

// Declare namespace.
parser = {};

parser.Abs = function(args) {
  if (args.length != 1) {
    throw "abs() accepts exactly one argument.";
  }
  this.arg = args[0];
};
parser.Abs.help = "abs(num)";
parser.Abs.prototype.evaluate = function(vars) {
  return Math.abs(this.arg.evaluate(vars));
};


parser.Ceil = function(args) {
  if (args.length != 1) {
    throw "ceil() accepts exactly one argument.";
  }
  this.arg = args[0];
};
parser.Ceil.help = "ceil(num)";
parser.Ceil.prototype.evaluate = function(vars) {
  return Math.ceil(this.arg.evaluate(vars));
};


parser.Floor = function(args) {
  if (args.length != 1) {
    throw "floor() accepts exactly one argument.";
  }
  this.arg = args[0];
};
parser.Floor.help = "floor(num)";
parser.Floor.prototype.evaluate = function(vars) {
  return Math.floor(this.arg.evaluate(vars));
};


parser.Log = function(args) {
  if (args.length != 1) {
    throw "log() accepts exactly one argument.";
  }
  this.arg = args[0];
};
parser.Log.help = "log(num)";
parser.Log.prototype.evaluate = function(vars) {
  return Math.log(this.args.evaluate(vars));
};


parser.Rate = function(args) {
  if (args.length == 1) {
    this.winLen = 1;
    this.arg = args[0];
  } else if (args.length == 2) {
    if (!(args[0] instanceof parser.Constant)) {
      throw "the first argument to rate() must be a constant.";
    }
    this.winLen = args[0].c;
    this.arg = args[1];
  } else {
    throw "rate() accepts one or two arguments.";
  }

  this.samples = [];
  this.timeInput = new parser.Var(-1, "time");
};
parser.Rate.help = "rate([window size,] var)";
parser.Rate.prototype.evaluate = function(vars) {
  var newY = this.arg.evaluate(vars);
  var newT = this.timeInput.evaluate(vars);
  this.samples.push([newY, newT]);
  var oldest = this.samples[0];
  if (this.samples.length > this.winLen) {
    this.samples.splice(0, this.samples.length - this.winLen);
  }
  var denom = newT - oldest[1];
  // Assumes time unit is milliseconds.
  return (denom == 0) ? 0 : ((1000 * (newY - oldest[0])) / denom);
};


parser.Round = function(args) {
  if (args.length != 1) {
    throw "round() accepts exactly one argument.";
  }
  this.arg = args[0];
};
parser.Round.help = "round(num)";
parser.Round.prototype.evaluate = function(vars) {
  return Math.round(this.arg.evaluate(vars));
};


parser.Sqrt = function(args) {
  if (args.length != 1) {
    throw "sqrt() accepts exactly one argument.";
  }
  this.arg = args[0];
};
parser.Sqrt.help = "sqrt(num)";
parser.Sqrt.prototype.evaluate = function(vars) {
  return Math.sqrt(this.arg.evaluate(vars));
};


parser.functions = {
  'abs':    parser.Abs,
  'ceil':   parser.Ceil,
  'floor':  parser.Floor,
  'log':    parser.Log,
  'rate':   parser.Rate,
  'round':  parser.Round,
  'sqrt':   parser.Sqrt
};


parser.Operator = function(evaluator) {
  this.evaluator = evaluator;
};
parser._operators = {
    '+': new parser.Operator(function(a, b) { return a + b; }),
    '-': new parser.Operator(function(a, b) { return a - b; }),
    '*': new parser.Operator(function(a, b) { return a * b; }),
    '/': new parser.Operator(function(a, b) { return a / b; })
};


parser.Token = function(start, text) {
  this.start = start;
  this.text = text;
};


parser.Part = function(start) {
  this.start = start;
};
parser.Part.prototype.getVars = function() {
  return [];
};


parser.MetaPart = function(start, args) {
  this.Part = parser.Part;
  this.Part(start);
  this.args = args || [];
};
parser.MetaPart.prototype.getVars = function() {
  var all = [];
  $.each(this.args, function(i, arg) {
    all = all.concat(arg.getVars());
  });
  return all;
};


parser.Function = function(start, evaluator, args) {
  this.MetaPart = parser.MetaPart;
  this.MetaPart(start, args);
  this.evaluator = evaluator;
};
parser.Function.prototype = new parser.MetaPart();
parser.Function.prototype.evaluate = function(vars) {
  return this.evaluator.evaluate(vars);
};


parser.Operation = function(start, op) {
  this.MetaPart = parser.MetaPart;
  this.MetaPart(start, []);
  this.op = op;
};
parser.Operation.prototype = new parser.MetaPart();
parser.Operation.prototype.evaluate = function(vars) {
  var result = this.args[0].evaluate(vars);
  for (var i = 1; i < this.args.length; i++) {
    result = this.op.evaluator(result, this.args[i].evaluate(vars));
  }
  return result;
};


parser.Constant = function(start, c) {
  this.Part = parser.Part;
  this.Part(start);
  this.c = parseFloat(c);
};
parser.Constant.prototype = new parser.Part();
parser.Constant.prototype.evaluate = function() {
  return this.c;
};


parser.Var = function(start, name) {
  this.Part = parser.Part;
  this.Part(start);
  this.name = name;
};
parser.Var.prototype.evaluate = function(vars) {
  // TODO(William Farner): Clean this up - currently it's reaching out
  // to state within grapher.js.
  return vars[metrics[this.name]];
};
parser.Var.prototype.getVars = function() {
  return [this.name];
};


parser.tokenize = function(str, offset, isDelimiter) {
  if (offset === undefined) {
    offset = 0;
  }

  var level = 0;
  var start = 0;
  var tokens = [];
  for (var i = 0; i < str.length; i++) {
    var c = str.charAt(i);
    if (c == '(') {
      level += 1;
      continue;
    } else if (c == ')') {
      level -= 1;
      continue;
    }

    if (level == 0) {
      if (isDelimiter(c)) {
        var token = str.substring(start, i);
        if (token.length == 0) {
          addError(str, 'Missing operand', i + offset);
        }
        tokens.push(new parser.Token(start + offset, token));
        tokens.push(new parser.Token(start, c));
        start = i + 1;
      }
    }
  }

  var token = str.substring(start);
  if (token.length == 0) {
    addError(str, 'Expected expression but found operator', start + offset);
  }
  tokens.push(new parser.Token(start + offset, str.substring(start)));

  return tokens;
};

var _FUNCTION_CALL_RE = /^(\w+)\((.*)\)$/g;
var _SUB_EXPRESSION_RE = /^\((.*)\)$/g;
var _PAREN_RE = /([\(\)])/;

parser.parse = function(query, offset) {
  // Split the expression at operator boundaries in the top-level scope.
  var tokens = parser.tokenize(query, offset, function(c) {
    return parser._operators[c];
  });
  tokens = $.map(tokens, function(token) {
    var op = parser._operators[token.text];
    return op ? new parser.Operation(token.start, op) : token;
  });

  var result = [];
  $.each(tokens, function(i, token) {
    if (token instanceof parser.Operation) {
      token.args.push(result.splice(result.length - 1, 1)[0]);
      result.push(token);
      return;
    }

    // Match a function call.
    var parsed;
    var match = _FUNCTION_CALL_RE.exec(token.text);
    if (match) {
      var f = match[1];
      var arg = match[2];
      if (!parser.functions[f]) {
        addError(query, 'Unrecognized function ' + f, token.start);
      }
      var parsedArg = parser.parse(arg, token.start + 1);
      // Split and parse function args.
      var argTokens = parser.tokenize(arg, token.start + 1, function(c) { return c == ','; });
      argTokens = $.grep(argTokens, function(argToken) { return argToken.text != ','; });
      var parsedArgs = $.map(argTokens, function(argToken) {
        return parser.parse(argToken.text, argToken.start);
      });
      parsed = new parser.Function(
          token.start,
          new parser.functions[f](parsedArgs), parsedArgs);
    } else {
      // Match a sub expression.
      match = _SUB_EXPRESSION_RE.exec(token.text);
      if (match) {
        parsed = parser.parse(match[1], token.start + 1);
      } else {
        match = _PAREN_RE.exec(token.text);
        if (match) {
          addError(query, 'Unmatched paren', token.start + token.text.indexOf(match[1]));
        }
        if (isNaN(token.text)) {
          parsed = new parser.Var(token.start, token.text);
        } else {
          parsed = new parser.Constant(token.start, token.text);
        }
      }
    }

    var lastResult = result.length == 0 ? null : result[result.length - 1];
    if (lastResult instanceof parser.Operation) {
      lastResult.args.push(parsed);
    } else {
      result.push(parsed);
    }
  });

  if (result.length != 1) {
    throw 'Unexpected state.';
  }
  return result[0];
};
