// See http://study-func-prog.blogspot.com/2008/08/scala-how-to-use-combinator-parser-1.html

package calc.dsl

import java.math.BigInteger
import scala.util.parsing.combinator._
// BNF with Left Recursion
// expr ::= expr + term | expr - term | term
// term ::= term * factor | term / factor | factor
// factor ::= ( expr ) | number
//
// BNF without Left Recursion
// expr ::= term { (+|-) term }
// term ::= factor { (*|/) factor }
// factor ::= ( expr ) | number

class Calc0 extends JavaTokenParsers {
    def opTerm:Parser[OpTerm] = ("+" | "-")~term ^^ {case op~rhs => OpTerm(op, rhs)}
    def expr:Parser[Expr] = term~rep(opTerm) ^^ {case lhs~terms => Expr(lhs, terms)}
    def opFactor:Parser[OpFactor] = ("*" | "/")~factor ^^ {case op~rhs => OpFactor(op, rhs)}
    def term:Parser[Term] = factor~rep(opFactor) ^^ {case lhs~factors => Term(lhs, factors)}
    def factor:Parser[Factor] =
    wholeNumber ^^ {case s => Num(new BigInteger(s.toString))} |
    "("~expr~")" ^^ {case lp~exp~rp => Parens(exp)}

    case class OpTerm(op:String, t:Term) { // op = "+" or "-"
        def eval(n:BigInteger):BigInteger = op match {
            case "+" => n.add(t.eval())
            case "-" => n.subtract(t.eval())
            case _   => error(this.toString)
        }
    }
    case class Expr(t:Term, ots:List[OpTerm]) {
        def eval():BigInteger = ots.foldLeft(t.eval()){(n:BigInteger, ot:OpTerm) => ot.eval(n)}
    }
    case class OpFactor(op:String, f:Factor) { // op = "*" or "/"
        def eval(n:BigInteger):BigInteger = op match {
            case "*" => n.multiply(f.eval())
            case "/" => n.divide(f.eval())
            case _   => error(this.toString)
        }
    }
    case class Term(f:Factor, ofs:List[OpFactor]) {
        def eval():BigInteger = ofs.foldLeft(f.eval()){(n:BigInteger, of:OpFactor) => of.eval(n)}
    }
    abstract class Factor {
        def eval():BigInteger
    }
    case class Num(n:BigInteger) extends Factor {
        def eval():BigInteger = n
    }
    case class Parens(e:Expr) extends Factor {
        def eval():BigInteger = e.eval()
    }
}

object Test1 extends Calc0 {
    def main(args:Array[String]) {
        // val s = "-1 * (2 * 3) - 4*5+6*7 + 8*9"
        // val s = "10000*10000"
        val s = "9 * 12345679"
        println(s)
        println(parse(expr, s))
        println(parse(expr, s).get.eval) // -> 100
    }
}