/*
    UnitParser.js  -  by Don Cross, 23 June 2007.
    This is a recursive descent parser geared toward expressions involving physical units.
    
    The grammar is as follows.  
    Each line has optional precedence, description, and ::= definition.
    
    3   exp      ::=  value [compop exp]
        compop   ::=  = | < | <= | > | >= | <> | !=
    4   value    ::=  term {addop term}
        addop    ::=  + | - | ~
    5   term     ::=  factor {multop factor}
        multop   ::=  * | / | %
    6   factor   ::=  atom {^ atom}
    7   atom     ::=  (exp[,exp]) | float | function | -atom | ident
    8   function ::=  name [(exp {,exp})]
    9   float    ::=  digit {digit} [. {digit}] [e|E [+|-] digit {digit}]
        digit    ::=  [0-9]
        ident    ::=  [a-zA-Z_][a-zA-Z0-9_]*
*/

function StringToUnitExpression (string)
{
    var expr = Parse_ExpressionString (string);
    var unit = expr.eval();
    return unit;
}

function Parse_ExpressionString (string)
{
    var lineArray = [string];
    var scanner = Tokenize (lineArray);
    var expr = Parse_Expression (scanner);
    if (scanner.moreTokensExist()) {
        throw TokenError (scanner.peekNextToken(), "Syntax error");
    }
    return expr;
}

function Parse_Expression (scanner)
{
    var expr = Parse_BinaryOp (scanner, 4);     // for now, skip comparison operators; start at +/- instead
    return expr;
}

function Parse_BinaryOp (scanner, precedence)
{
    var expr = null;
    if (precedence > 6) {
        expr = Parse_Atom (scanner);
    } else {
        expr = Parse_BinaryOp (scanner, 1+precedence);
        while (PrecedenceOf(scanner.peekNextToken()) == precedence) {
            var op = scanner.getNextToken();
            var rvalue = Parse_BinaryOp (scanner, 1+precedence);
            expr = new BinaryOperator (expr, rvalue, op);
        }
    }
    if (expr == null) {
        throw "Internal error in Parse_BinaryOp: was about to return null!";
    }
    return expr;
}

function Parse_Atom (scanner)
{
    var expr = null;
    var token = scanner.getNextToken();
    if (token.value == "(") {
        expr = Parse_Expression (scanner);
        scanner.expectToken (")");
    } else if (token.value == "-") {
        expr = new UnaryOperator (Parse_Atom(scanner), token, Operator_Negate);
    } else {
        switch (token.type) {
            case "string":
            case "identifier":
            case "number":
                expr = token;
                break;
                
            default:
                throw TokenError (token, "Expected string, identifier, or numeric constant");
        }
    }
    return expr;
}

function UnaryOperator (arg, op, evalfunc)
{
    this.type = "operator";
    op.precedence = 7;          // for now assume all unary operators have the same high precedence (disambiguates -a from a-b).
    op.eval = evalfunc;
    this.eval = evalfunc;
    this.op = op;
    this.arg = [arg];
}

function BinaryOperator (left, right, op)
{
    this.type = "operator";
    this.op = op;
    this.eval = op.eval;
    this.arg = [left, right];
}

function EvalArguments (arg)
{
    var evaluatedArgs = [];
    for (var i=0; i < arg.length; ++i) {
        evaluatedArgs[i] = arg[i].eval();
        if (!evaluatedArgs[i].is_reduced) {
            throw TokenError (this.op, "Internal error: found unreduced subexpression");
        }
        uq_test (evaluatedArgs[i].factor);
    }
    return evaluatedArgs;
}

function RequireCompatibleUnits (evaluatedArgs, op)
{
    for (var i=1; i < evaluatedArgs.length; ++i) {
        if (!AreReducedUnitsCompatible(evaluatedArgs[0], evaluatedArgs[i])) {
            throw TokenError (op, "Incompatible units");
        }
    }
}

function RequireDimensionless (unit, op)
{
    for (var i=0; i < unit.definition.length; ++i) {
        if (unit.definition[i].exponent != 0) {
            throw TokenError (op, "Exponent must be dimensionless");
        }
    }
}

function Operator_PlusOrMinus()     // experiment with uncertainty
{
    var evaluatedArgs = EvalArguments (this.arg);
    RequireCompatibleUnits (evaluatedArgs, this.op);
    var a = evaluatedArgs[0].factor;
    var b = evaluatedArgs[1].factor;
    uq_test (a);
    uq_test (b);
    var c = uq_from_list (
        (a.value + a.uncertainty) + (b.value + b.uncertainty),
        (a.value + a.uncertainty) + (b.value - b.uncertainty),
        (a.value - a.uncertainty) + (b.value + b.uncertainty),
        (a.value - a.uncertainty) + (b.value - b.uncertainty),
        (a.value + a.uncertainty) - (b.value + b.uncertainty),
        (a.value + a.uncertainty) - (b.value - b.uncertainty),
        (a.value - a.uncertainty) - (b.value + b.uncertainty),
        (a.value - a.uncertainty) - (b.value - b.uncertainty)
    );
    evaluatedArgs[0].factor = c;
    return evaluatedArgs[0];
}

function Operator_Add()
{
    var evaluatedArgs = EvalArguments (this.arg);
    RequireCompatibleUnits (evaluatedArgs, this.op);
    evaluatedArgs[0].factor = uq_add (evaluatedArgs[0].factor, evaluatedArgs[1].factor);
    return evaluatedArgs[0];
}

function Operator_Subtract()
{
    var evaluatedArgs = EvalArguments (this.arg);
    RequireCompatibleUnits (evaluatedArgs, this.op);
    evaluatedArgs[0].factor = uq_subtract (evaluatedArgs[0].factor, evaluatedArgs[1].factor);
    return evaluatedArgs[0];
}

function Operator_Multiply()
{
    var evaluatedArgs = EvalArguments (this.arg);
    evaluatedArgs[0].factor = uq_multiply (evaluatedArgs[0].factor, evaluatedArgs[1].factor);
    for (var i=0; i < evaluatedArgs[0].definition.length; ++i) {
        evaluatedArgs[0].definition[i].exponent += evaluatedArgs[1].definition[i].exponent;
    }
    return evaluatedArgs[0];
}

function Operator_Divide()
{
    var evaluatedArgs = EvalArguments (this.arg);
    evaluatedArgs[0].factor = uq_divide (evaluatedArgs[0].factor, evaluatedArgs[1].factor);
    for (var i=0; i < evaluatedArgs[0].definition.length; ++i) {
        evaluatedArgs[0].definition[i].exponent -= evaluatedArgs[1].definition[i].exponent;
    }
    return evaluatedArgs[0];
}

function Operator_Exponentiate()
{
    var evaluatedArgs = EvalArguments (this.arg);
    RequireDimensionless (evaluatedArgs[1], this.op);
    if (evaluatedArgs[1].factor.uncertainty != 0) {
        throw TokenError (this.op, "Uncertainty not allowed in exponent");
    }
    evaluatedArgs[0].factor = uq_power (evaluatedArgs[0].factor, evaluatedArgs[1].factor.value);
    for (var i=0; i < evaluatedArgs[0].definition.length; ++i) {
        evaluatedArgs[0].definition[i].exponent *= evaluatedArgs[1].factor.value;
    }
    return evaluatedArgs[0];
}

function Operator_Negate()
{
    var evaluatedArgs = EvalArguments (this.arg);
    evaluatedArgs[0].factor.value *= -1;        // uncertainty is unaffacted; keep nonnegative.
    return evaluatedArgs[0];
}

function Operator_Number()
{
    // Create a dimensionless numeric unit.
    // For example:  17  ==>  17*unity
    var unit = new Unit ("unity");
    unit.factor = uq_exact (this.number);
    return UnitReduce(unit);
}

function Operator_Symbol()
{
    return UnitReduce (new Unit (this.value));
}

function Tokenize (lineArray)
{
    var OperatorTable = {
        '~': {'precedence':4, 'eval': Operator_PlusOrMinus},
        '+': {'precedence':4, 'eval': Operator_Add},
        '-': {'precedence':4, 'eval': Operator_Subtract},
        '*': {'precedence':5, 'eval': Operator_Multiply},
        '/': {'precedence':5, 'eval': Operator_Divide},
        '^': {'precedence':6, 'eval': Operator_Exponentiate},
        '(': {'precedence':7, 'eval': null},
        ')': {'precedence':7, 'eval': null}
    };

    var inMultiLineComment = false;     // are we inside /* ... */ ?
    var tokenlist = [];
    var regexpToken = new RegExp ("([A-Za-z_\\$][A-Za-z_0-9]*)|(\\d+(\\.\\d*)?([eE][\\+\\-]?\\d)?)|(\\\"[^\\\"]*\\\")|(//)|(/\\*)|(\\*/)|(\\S)", "g");
    for (var i in lineArray) {
        i = parseInt(i);    // for some reason, 'i' was a string!!!
        var m = lineArray[i].match (regexpToken);
        if (m != null) {
            for (var k=0; k < m.length; ++k) {
                var token = new Token (m[k], i+1);
                if (token.value == "/*") {
                    if (inMultiLineComment) {
                        throw TokenError (token, "Not allowed to nest multi-line comments");
                    } else {
                        inMultiLineComment = true;
                    }
                } else if (token.value == "*/") {
                    if (inMultiLineComment) {
                        inMultiLineComment = false;
                    } else {
                        throw TokenError (token, "Was not inside multi-line comment but found '*/' anyway");
                    }
                } else if (token.value == "//") {
                    break;      // ignore entire rest of line
                } else {
                    if (!inMultiLineComment) {
                        // Validate that the token makes sense.
                        if (token.value.length == 0) {
                            throw TokenError (token, "Internal tokenizer error: empty token!");
                        }
                    
                        if (token.value == '"') {
                            throw TokenError (token, "Unterminated string constant");
                        }
                        
                        token.precedence = 1000;    // assume highest precedence (operand) unless proven to be an operator
                        
                        // Assign a type to the token, and do any cleanup...
                        var firstChar = token.value.charAt(0).toLowerCase();
                        if (firstChar == '"') {
                            token.type = "string";
                            token.value = token.value.substring (1, token.value.length-1);
                        } else if (/[a-z_]/.test(firstChar)) {
                            token.type = "identifier";
                            token.eval = Operator_Symbol;
                        } else if (/[0-9]/.test(firstChar)) {
                            token.type = "number";
                            token.eval = Operator_Number;
                            if (/[\.eE]/.test(token.value)) {
                                token.numberType = "float";
                                token.number = parseFloat (token.value);
                            } else {
                                token.numberType = "int";
                                token.number = parseInt (token.value);
                            }
                        } else {
                            token.type = "punctuation";
                            var opInfo = OperatorTable[token.value];
                            if (opInfo == null) {
                                throw TokenError (token, "Unknown operator");
                            } else {
                                token.precedence = opInfo.precedence;
                                token.eval = opInfo.eval;
                            }
                        }
                        tokenlist[tokenlist.length] = token;
                    }
                }
            }
        }
    }
    return new TokenScanner (tokenlist);
}

function TokenScanner (tokenlist)
{
    this.tokenlist  = tokenlist;
    this.tokenindex = 0;
    this.peekNextToken = Scanner_PeekNextToken;
    this.getNextToken = Scanner_GetNextToken;
    this.moreTokensExist = Scanner_MoreTokensExist;
    this.expectToken = Scanner_ExpectToken;
}

function Scanner_GetNextToken()
{
    var token = this.tokenlist [this.tokenindex];
    if (token == null) {
        throw "Unexpected end of expression";
    } else {
        ++this.tokenindex;
        return token;
    }
}

function Scanner_MoreTokensExist()
{
    return this.tokenlist[this.tokenindex] != null;
}

function Scanner_PeekNextToken()
{
    return this.tokenlist[this.tokenindex];
}

function Scanner_ExpectToken (symbol)
{
    var token = this.tokenlist [this.tokenindex];
    if (token == null || token.value != symbol) {
        throw TokenError (token, "Expected '" + symbol + "'");
    }
    ++this.tokenindex;
}

//var TokenError_ReportLineNumber = true;

function TokenError (token, message)
{
    if (token == null) {
        return "ERROR: " + message;
    } else {
        var report = "ERROR near '" + token.value + "'";
        //if (TokenError_ReportLineNumber) {
        //    report += " [line " + token.lineNumber + "]";
        //}
        report += ": " + message;
        return report;
    }
}

function Token (value, lnum)
{
    this.value = value;
    this.lineNumber = lnum;
}

function PrecedenceOf (token)
{
    if (token == null) {
        return -1;
    } else {
        return token.precedence;
    }
}
