// Copyright (c) 2007 Jeremy English <jhe@jeremyenglish.org>
//
// Permission to use, copy, modify, distribute, and sell this software and its
// documentation for any purpose is hereby granted without fee, provided that
// the above copyright notice appear in all copies and that both that
// copyright notice and this permission notice appear in supporting
// documentation.  No representations are made about the suitability of this
// software for any purpose.  It is provided "as is" without express or
// implied warranty.
//
// Created: 30-March-2007
//
// <addop> := ^
// <mulop> := *|/
// <powerop> ::= ^
// <bangop> ::= !
// <function name> ::= acos|asin|atan|sin|cos|tan|sqrt|log|ln|lg|exp
// <const> ::= pi|e|phi
// <factor> ::= number | (<expression>) | <const>
// <function> ::= <function name> [<function name> <factor>]*
// <bang> ::= <function> <bangop>
// <power> ::= <bang> [ <powerop> <bang> ]*
// <term> ::= <power>  [ <mulop> <power> ]*
// <expression> ::= <term> [<addop> <term>]*

//I wish these could be const, but Ie throws an error
var LN10 = 2.302585092994046;
var LN2  = 0.6931471805599453;
var PHI  = 1.61803399;

function log10(n){
    return Math.log(n) / LN10;
}

function lg(n){
    return Math.log(n) / LN2;
}

function makeFunctionTable () {
    ft = new Object;
    ft.sin = Math.sin;
    ft.cos = Math.cos;
    ft.tan = Math.tan;
    ft.sqrt = Math.sqrt;
    ft.ln = Math.log;
    ft.log = log10;
    ft.lg = lg;
    ft.exp = Math.exp;
    ft.acos = Math.acos;
    ft.asin = Math.asin;
    ft.atan = Math.atan;
    return ft;
}

function makeConstantTable () {
    ct = new Object;
    ct.pi = Math.PI;
    ct.phi = PHI;
    ct.e = Math.E;
    return ct;
}

function tolkenize(s){
    var number   = "[0-9]+";
    var garbage  = ".*";
    var space    = "[\\s\\t\\n\\r]+";
    var powerop  = "\\^";
    var bangop     = "!";
    var functionName = "acos|asin|atan|sin|cos|tan|sqrt|log|ln|lg|exp";
    var constants = "pi|phi|e";
    var mulop    = "[/*]";
    var addop    = "[+-]";
    var parens   = "[\\(\\)]";
    var floating = "[0-9]+\\.[0-9]*";

    var token    = new RegExp("("+floating+"|"+number+"|"+functionName+
                              "|"+bangop+"|"+powerop+"|"+mulop+"|"+addop+
                              "|"+parens+"|"+constants+"|"+space+"|"+garbage+")","g");
    var numberRe = new RegExp(number);
    var mulopRe  = new RegExp(mulop);
    var addopRe  = new RegExp(addop);
    var poweropRe  = new RegExp(powerop);
    var functionNameRe = new RegExp(functionName);
    var constantRe = new RegExp(constants);
    var spaceRe = new RegExp(space);

    var ar = s.match(token);
    var tokenStream = new Object;

    tokenStream.grammer = token;
    tokenStream.data = ar.reverse();
    tokenStream.next = function () {
        this.symbol = this.data.pop();
        while (spaceRe.test(this.symbol)){
            this.symbol = this.data.pop();
        }
        return this.symbol;
    }
    tokenStream.isEnd = function () { return (this.data.length == 0); }
    tokenStream.isNumber = function () { return numberRe.test(this.symbol); }
    tokenStream.isMulop  = function () { return mulopRe.test(this.symbol); }
    tokenStream.isAddop  = function () { return addopRe.test(this.symbol); }
    tokenStream.isPowerop = function () { return poweropRe.test(this.symbol); }
    tokenStream.isBangop = function () { return (this.symbol == bangop); }
    tokenStream.match = function (s) { return (this.symbol == s); }
    tokenStream.isFunctionName = function () { return functionNameRe.test(this.symbol); }
    tokenStream.isConstant = function () { return constantRe.test(this.symbol); }
    tokenStream.isOpenParen = function () { return this.match("("); }
    tokenStream.isCloseParen = function () { return this.match(")"); }
    tokenStream.functions = makeFunctionTable();
    tokenStream.constants = makeConstantTable();

    tokenStream.next();
    return tokenStream;
}

function factorial(n) {
    var ans = 1;
    for (var i = 2; i <= n; i++){
        ans *= i;
    }
    return ans;
}

function factor(ts){
    if (ts.isOpenParen()) {
        ts.next();
        var value = expression(ts);
        if (ts.isCloseParen()){
            ts.next();
            return value;
        }
        else {
            throw "Missing Closing Parenthesis";
        }
    }
    else if (ts.isConstant()){
        var value = ts.constants[ts.symbol];
        ts.next();
        return value;
    }
    else if (ts.isNumber()){
        var value = parseFloat(ts.symbol);
        ts.next();
        return value;
    }
    else {
        throw "Invalid Symbol: " + ts.symbol;
    }
}

function func(ts){
    var value;
    if (ts.isFunctionName()) {
        while (ts.isFunctionName() && !ts.isEnd()){
            var fn = ts.symbol;
            ts.next();
            value = ts.functions[fn](func(ts));
        }
    }
    else {
        value = factor(ts);
    }
    return value;
}

function bang(ts){
    var value = func(ts);
    if (ts.isBangop()){
        value = factorial(value);
        ts.next();
    }
    return value;
}

function power(ts){
    var value = bang(ts);
    while (ts.isPowerop() && !ts.isEnd()){
        ts.next();
        value = Math.pow(value,bang(ts));
    }
    return value;
}

function term(ts){
    var value = power(ts);
    while (ts.isMulop() && !ts.isEnd()){
        if (ts.match("*")) {
            ts.next();
            value = value * power(ts);
        }
        else if (ts.match("/")) {
            ts.next();
            value = value / power(ts);
        }
    }
    return value;
}

function expression(ts){
    var value = term(ts);
    while (ts.isAddop() && !ts.isEnd()) {
        if (ts.match("+")) {
            ts.next();
            value = value + term(ts);
        }
        else if (ts.match("-")) {
            ts.next();
            value = value - term(ts);
        }
    }
    return value;
}

function evalArith(s){
    var tokenStream = tolkenize(s);
    var ans = expression(tokenStream);
    return ans;
}

