/**
 * 
 * @author Neil Erdwien
 * 
 */

YAHOO.namespace("ksu.expression");

(function() { // Start of closure idiom to provide private variable scoping

   // //////////////////////////////////////////////
   // Private static properties
   // //////////////////////////////////////////////

   var LOG;

   KSU.expression.Evaluator = function() {

      if (!LOG) {
         LOG = new YAHOO.widget.LogWriter("Expression");
      }
      this.symbolTable = {};
      this.tokens = null;
      this.sym = null;
      this.symindex = null;
      this.watchedSymbols = {};

   };

   // //////////////////////////////////////////////
   // Private methods
   // //////////////////////////////////////////////

   var exprValue = function() {

      // console.log(" exprValue", this);

      var val, delim;

      if (this.sym === '(') {
         this.nextSym();
         val = exprOr.call(this);
         if (this.sym === ')') {
            this.nextSym(); // Eat the closing parenthesis
            // console.log("subexpression result = ", val);
            return val;
         }
         // Syntax error
         return undefined;
      }

      if (this.sym === "'" || this.sym === '"') {
         // start a quoted string
         delim = this.sym;
         val = "";
         this.nextSym();
         while (this.sym !== delim) {
            val += this.sym;
            this.nextSym();
         }
         this.nextSym();
         return val;
      }

      if (this.sym === 'true') {
         this.nextSym();
         return true;
      }

      if (this.sym === 'false') {
         this.nextSym();
         return false;
      }

      if (/^\d/.test(this.sym)) {
         val = parseFloat(this.sym);
         this.nextSym();
         return val;
      }

      val = this.symbolTable[this.sym];
      //console.log("Mapping '" + this.sym + "' to '" + val + '"');

      this.symbolsUsed[this.sym] = true;

      this.nextSym();
      return val;

   };

   var exprUnary = function() {
      //console.log(" exprUnary", this);
      var op, val, v2;
      if (this.sym === '!' || this.sym === '+' || this.sym === '-') {
         op = this.sym;
         this.nextSym();
         v2 = exprUnary.call(this);
         switch (op) {
         case '!':
            val = !v2;
            break;
         case '+':
            val = +v2;
            break;
         case '-':
            val = -v2;
            break;
         }
         //console.log("Performing Unary, result = ", val);
      } else {
         val = exprValue.call(this);
      }
      return val;
   };

   var exprMult = function() {
      //console.log(" exprMult", this);
      var val = exprUnary.call(this);
      var op, v2;
      while (this.sym === '*' || this.sym === '/' || this.sym === '%') {
         op = this.sym;
         this.nextSym();
         v2 = exprUnary.call(this);
         switch (op) {
         case '*':
            val = val * v2;
            break;
         case '/':
            val = val / v2;
            break;
         case '%':
            val = val % v2;
            break;
         }
         //console.log(" performing Mult, result = ", val);
      }
      //console.log(" returning '" + val + "'");
      return val;
   };

   var exprAdd = function() {
      // console.log(" exprAdd", this);
      var val = exprMult.call(this);
      var op, v2;
      while (this.sym === '+' || this.sym === '-') {
         op = this.sym;
         this.nextSym();
         v2 = exprMult.call(this);
         switch (op) {
         case '+':
            val = val + v2;
            break;
         case '-':
            val = val - v2;
            break;
         }
         // console.log("Performing Add, result = ", val);
      }
      //console.log("  returning '" + val + "'");
      return val;
   };

   var exprRel = function() {
      //      console.log(" exprRel", this);
      var val = exprAdd.call(this);
      var op, v2;
      while (this.sym === '>' || this.sym === '>=' || this.sym === '<' || this.sym === '<=') {
         op = this.sym;
         this.nextSym();
         v2 = exprAdd.call(this);
         switch (op) {
         case '>':
            val = val > v2;
            break;
         case '>=':
            val = val >= v2;
            break;
         case '<':
            val = val < v2;
            break;
         case '<=':
            val = val <= v2;
            break;
         }
         //         console.log("Performing Rel, result = ", val);
      }
      //      console.log("   returning '" + val + "'");
      return val;
   };

   var exprEq = function() {
      //      console.log(" exprEq", this);
      var val = exprRel.call(this);
      var op, v2;
      while (this.sym === '===' || this.sym === '!==' || this.sym === '==' || this.sym === '!=') {
         op = this.sym;
         this.nextSym();
         v2 = exprRel.call(this);
         switch (op) {
         case '===':
            val = val === v2;
            break;
         case '!==':
            val = val !== v2;
            break;
         case '==':
            val = val == v2;
            break;
         case '!=':
            val = val != v2;
            break;
         }
         //         console.log("Performing Eq, result = ", val);
      }
      //      console.log("    returning '" + val + "'");
      return val;
   };

   var exprAnd = function() {
      // console.log(" exprAnd", this);
      var val = exprEq.call(this);
      var v2;
      while (this.sym === '&&') {
         this.nextSym();
         v2 = exprEq.call(this);
         val = val && v2;
         // console.log("Performing And, result = ", val);
      }
      //console.log("     returning '" + val + "'");
      return val;
   };

   var exprOr = function() {
      // console.log(" exprOr", this);
      var val = exprAnd.call(this);
      var v2;
      while (this.sym === '||') {
         this.nextSym();
         v2 = exprAnd.call(this);
         val = val || v2;
//         console.log("Performing Or, result:", val + "||" + v2 + "=" + val);
      }
//      console.log("      exprOr returning '" + val + "'");
      return val;
   };

   var callHandler = function(result, calltrue, callfalse) {

      var handler;

      if (result) {
         handler = calltrue;
         //         console.log("Calling TRUE handler");
      } else {
         handler = callfalse;
         //         console.log("Calling FALSE handler");
      }

      if (handler) {
         handler.fcn.apply(handler.scope, handler.args);
      }

   };

   var watch = function(expr, tokens, result, symbolsUsed, calltrue, callfalse) {

      var i, symbol, newdependency;

      /*
       * Loop over all the used symbols (potentially none), and add this expression evaluation to the list of
       * expressions that depend on that symbol.
       */
      newdependency = {
         expr: expr,
         tokens: tokens,
         value: result,
         calltrue: calltrue,
         callfalse: callfalse
      };
      for (symbol in symbolsUsed) {
         if (!this.watchedSymbols[symbol]) {
            // This symbol hasn't been used yet--create an empty one.
            this.watchedSymbols[symbol] = {
               value: this.symbolTable[symbol],
               deps: []
            };
         }

         // Add this dependent to the list
         this.watchedSymbols[symbol].deps.push(newdependency);
      }

   };

   var runWatchList = function(name, val) {

      var deps, dep, i, newval;

      //      LOG.log("reevaluating expressions", "debug");

      if (!this.watchedSymbols || !this.watchedSymbols[name] || this.watchedSymbols[name].value === val) {
         // Nothing to do
         return;
      }

      this.watchedSymbols[name].value = val;

      deps = this.watchedSymbols[name].deps;
      for (i = 0; i < deps.length; i++) {
         dep = deps[i];

         // Reevaluate the expression and see if the value has changed.
         newval = this.evaluate(dep.expr, dep.tokens);
         //         console.log("reeval", newval, dep.value, !!newval, !!dep.value);
         if (!!newval !== !!dep.value) {
            // The "!!" is to turn everything into true or false.  The actual value
            // doesn't matter, only the truthiness.
            // The value has changed -- call the handler
            callHandler(newval, dep.calltrue, dep.callfalse);
            dep.value = newval;
         }
         //console.log("dep.value is", dep.value);
      }

   };

   // //////////////////////////////////////////////
   // Public methods
   // //////////////////////////////////////////////

   KSU.expression.Evaluator.prototype = {

      nextSym: function() {
         if (this.symindex < this.tokens.length) {
            this.sym = this.tokens[this.symindex++];
         } else {
            this.sym = null;
         }

         return this.sym;
      },

      evaluateTokenized: function(tokens) {

         var result;

         this.tokens = tokens;
         this.symindex = 0;
         this.nextSym();

         result = exprOr.call(this);

         return result;
      },

      evaluate: function(expr, tokens) {

         var i, result;

         //      this.expr = expr;
      //      console.log("evaluating '" + expr + "'");

      if (!tokens) {
         //TODO tokenizing ahead of time this way will eat blanks inside strings.
         //tokens = expr.match(/\s*(\|\||&&|===?|!==?|\+|-|\*|\/|!|\(|\)|[a-zA-Z._][a-zA-Z._0-9]*|\d+|'|")\s*/g);
         tokens = expr.match(/\s*(\|\||&&|===?|!==?|>=?|<=?|\+|-|\*|\/|!|\(|\)|[a-zA-Z._][a-zA-Z._0-9]*|\d+|'|")\s*/g);
         for (i = 0; i < tokens.length; i++) {
            tokens[i] = tokens[i].replace(/^\s*/, "").replace(/\s*$/, "");
         }
   }

   this.tokens = tokens;
   this.symindex = 0;
   // console.log(this.tokens);
   this.nextSym();

   result = exprOr.call(this);

   LOG.log('Evaluating "' + expr + '" ==> ' + result, "info");

   return result;

},

evaluateAndWatch: function(expr, truefunction, falsefunction) {

   var result;

   this.symbolsUsed = {};

   result = this.evaluate(expr);
   //      console.log("result: ", result, "symbolsUsed", this.symbolsUsed);

      callHandler(result, truefunction, falsefunction);

      watch.call(this, expr, this.tokens, result, this.symbolsUsed, truefunction, falsefunction);

      return result;
   },

   setValue: function(name, val) {
      LOG.log("Setting '" + name + "' to '" + val + "', previous value '" + this.symbolTable[name] + "'", "debug");

      this.symbolTable[name] = val;

      runWatchList.call(this, name, val);

      //console.log(this.watchedSymbols);
   },

   getValue: function(name) {

      var result;

      result = this.symbolTable[name];
      if (typeof result === 'undefined') {
         LOG.log(name + " is undefined", "warning");
      }

      return result;

   }
   };

})(); // End of idiom to provide private variable scoping

