package skeb

import expression._

/** A function that can parse a string to an expression. The implementat is
  * not relevant for this tutorial and is not commented. It is based around
  * a simple LL(1) recursive descent parser. */
class Parser (sheet: Sheet) extends Function1[String, Expression] {
  def apply (string: String): Expression =
    try { new Builder(string).parse }
    catch { case BadSyntax(msg) => Text(msg) }
  object Tokens extends Enumeration {
    type Token = Value;
    val EOF = Value("<eof>")
    val BAD = Value("<bad>")
    val IDENT = Value("ident")
    val NUMBER = Value("number")
    val STRING = Value("string")
    val LPAREN = Value("(")
    val RPAREN = Value(")")
    val COMMA = Value(",")
    val ADD = Value("+")
    val SUB = Value("-")
    val MUL = Value("*")
    val DIV = Value("/")
    val MOD = Value("%")
    val HASH = Value("#") }
  private case class BadSyntax(msg: String) extends Exception
  private def error(message: String): All = throw BadSyntax(message)
  class Builder(in: String) extends Scanner(in) {
    import Tokens._
    private def accept(expected: Token): Unit =
      if (token == expected) nextToken
      else error("Expected '"+expected+"' but found '"+token+"'")
    private def check(expected: Token): Boolean =
      if (token == expected) { nextToken; true } else false
    private def ident: String = {
      val name = chars
      accept(IDENT)
      name }
    private def number: Int = {
      val num = chars
      accept(NUMBER)
      try { Integer.parseInt(num) }
      catch {
        case e: NumberFormatException => error("Malformed number '"+num+"'") }}
    def parse: Expression = {
      val result = expression
      accept(EOF)
      result }
    private def expression: Expression = {
      var expr = term
      while (token == ADD || token == SUB) {
        val op = if (token == ADD) "+" else "-"
        nextToken
        expr = Apply(op, List(expr, term)) }
      expr }
    private def term: Expression = {
      var expr = signedFactor
      while (token == MUL || token == DIV || token == MOD) {
        val op = if (token == MUL) "*" else if (token == DIV) "/" else "%"
        nextToken
        expr = Apply(op, List(expr, signedFactor)) }
      expr }
    private def signedFactor: Expression = {
      if (token == SUB) {
        nextToken
        Apply("-", List(factor)) }
      else factor }
    private def factor: Expression = {
      token match {
        case IDENT =>
          val elem = ident
          if (token == LPAREN) Apply(elem, params)
          else error("Operation '"+ elem + "' takes parameters")
        case NUMBER =>
          val num = number
          if (check(HASH)) { val num2 = number; Reference(sheet(num, num2)) }
          else Number(num)
        case STRING =>
          nextToken
          Text(chars)
        case LPAREN =>
          nextToken
          val expr = expression
          accept(RPAREN)
          expr
        case _ => error("Could not parse formula") }}
    private def params: List[Expression] = {
      accept(LPAREN)
      val args = if (token != RPAREN) expressions else List()
      accept(RPAREN)
      args }
    private def expressions: List[Expression] = {
      var exprs = List(expression)
      while (token == COMMA) {
        nextToken
        exprs = expression :: exprs }
      exprs reverse }}
  class Scanner(in: String) {
    import Tokens._
    var token: Token = BAD
    var chars: String = _
    private val buf = new StringBuffer()
    private def readToken: Token = ch match {
      case EOF_CH => EOF
      case _ if (Character.isLetter(ch)) =>
        buf.setLength(0);
        do {
          buf.append(ch);
          nextCh
        } while (Character.isLetterOrDigit(ch) || ch == '_');
        chars = buf.toString();
        IDENT
      case '0' =>
        chars = "0"
        nextCh
        NUMBER
      case _ if (Character.isDigit(ch)) =>
        buf.setLength(0)
        do {
          buf.append(ch)
          nextCh
        } while (Character.isDigit(ch))
        chars = buf.toString()
        NUMBER
      case '"' =>
        nextCh;
        buf.setLength(0);
        while (ch != '"') {
          if (ch == EOF_CH) error("Unterminated string")
          buf.append(ch)
          nextCh }
        nextCh
        chars = buf.toString()
        STRING
      case '(' => nextCh; LPAREN
      case ')' => nextCh; RPAREN
      case ',' => nextCh; COMMA
      case '+' => nextCh; ADD
      case '-' => nextCh; SUB
      case '*' => nextCh; MUL
      case '/' => nextCh; DIV
      case '%' => nextCh; MOD
      case '#' => nextCh; HASH
      case _ => error("Invalid character '" + ch + "'")
    }
    private var ch = ' ';
    private val EOF_CH = java.lang.Character.MAX_VALUE
    private def nextCh: Unit =
      if (pos < in.length) {
        ch = in.charAt(pos)
        pos = pos + 1
      } else { ch = EOF_CH }
    def nextToken: Unit = {
      while (Character.isWhitespace(ch)) nextCh
      token = readToken }
    private var pos: Int = 0
    nextCh
    nextToken
  }
}
