// if you've ever seen a lisp evaluator, you'll know how this works.

var initial_environment = {
  type: 'environment-frame',
  parent: undefined,
  data: {
    abs: Math.abs,
    acos: Math.acos,
    asin: Math.asin,
    atan: Math.atan,
    cos: Math.cos,
    exp: Math.exp,
    ln: Math.log,
    max: Math.max,
    min: Math.min,
    power: Math.pow,
    sin: Math.sin,
    sqrt: Math.sqrt,
    tan: Math.tan,
    plus: function() { return fold(arguments, function(x, y) { return x + y; }); },
    minus: function(x, y) { return x - y; },
    multiply: function() { return fold(arguments, function(x, y) { return x * y; }); },
    divide: function(x, y) { return x / y; },
    pi: Math.PI
  }
};

// evaluates the expression in the global environment
function evalMath(expr, environ)
{
  switch (expr.localName)
  {
    // first, handle the special forms
    case 'cn':
      return Number(expr.firstChild.data);

    case 'ci':
      var name = expr.firstChild.data;
      var value = lookup(environ, name);
      if (value != undefined)
        return value;

      return environ.data[name] = undefined;

    case 'declare':
      var name = expr.firstChild.firstChild.data; // assumes it's a ci
      environ.data[name] = evalMath(expr.childNodes[1], environ);

    case 'lambda':
      var formals = [];
      var nodes = expr.getElementsByTagName('bvar');
      var len = nodes.length;

      for (var i = 0; i < len; i++)
        formals.push(nodes[i].firstChild.firstChild.data); // assumes the bvar has a ci in it

      return { type: 'closure', formals: formals, body: expr.lastChild, environment: make_frame(environ) };

    case 'apply':
      var func = evalMath(expr.firstChild, environ);

      var args = [];
      var temp = expr.childNodes;
      var len = temp.length;

      for (var i = 1; i < len; i++)
        args.push(temp[i]);
      args = args.map(function (item) { return evalMath(item, environ); });
      return applyMath(func, args);

    // TODO: implement the other special forms (sum, int, diff, etc)

    // everything else is a function call
    default:
      var value = lookup(environ, expr.localName);
      return value;
  }
}

// applies a function to it's arguments. primitive operations are defined as js functions
function applyMath(func, args)
{
  switch (typeof func)
  {
    case 'function':
      return func.apply(undefined, args);
    case 'object':
      if (func.type == 'closure')
      {
        var newframe = make_frame(func.environment);
        var len = func.formals.length;

        for (var i = 0; i < len; i++)
          newframe.data[func.formals[i]] = (i in args && args[i]);
        return evalMath(func.body, newframe);
      }
  }

  return undefined; // should have an error message or something
}

// the built in Array is sorely lacking, since it doesn't have a fold method
function fold(arry, op)
{
  var accum = arry[0];
  var len = arry.length;

  for (var i = 1; i < len; i++)
    accum = op(accum, arry[i]);

  return accum;
}

// now we have to define some stuff for using environments
function make_frame(parent)
{
  return { type: 'environment-frame', parent: parent, data: { } };
}

function lookup(environ, variable)
{
  var value;
  var e = environ;

  while (e && e.type == 'environment-frame')
  {
    value = e.data[variable];
    if (value != undefined)
      return value;

    e = e.parent;
  }

  return undefined;
}
