/* sbt -- Simple Build Tool
 * Copyright 2009  Mark Harrah
 */
package xsbt
package datatype

import java.io.File
import sbt.IO.readLines
import Function.tupled
import java.util.regex.Pattern

class DatatypeParser extends NotNull {
  val WhitespacePattern = Pattern compile """\s*""" //(?>\#(.*))?"""
  val EnumPattern = Pattern compile """enum\s+(\S+)\s*:\s*(.+)"""
  val ClassPattern = Pattern compile """(\t*)(\S+)\s*"""
  val MemberPattern = Pattern compile """(\t*)(\S+)\s*:\s*([^\s*]+)([*]?)"""

  def processWhitespaceLine(l: Array[String], line: Int) = new WhitespaceLine(l.mkString, line)
  def processEnumLine(l: Array[String], line: Int) = new EnumLine(l(0), l(1).split(",").map(_.trim), line)
  def processClassLine(l: Array[String], line: Int) = new ClassLine(l(1), l(0).length, line)
  def processMemberLine(l: Array[String], line: Int) = new MemberLine(l(1), l(2), l(3).isEmpty, l(0).length, line)

  def error(l: Line, msg: String): Nothing = error(l.line, msg)
  def error(line: Int, msg: String): Nothing = throw new RuntimeException("{line " + line + "} " + msg)

  def parseFile(file: File): Seq[Definition] =
    {
      val (open, closed) = ((Array[ClassDef](), List[Definition]()) /: parseLines(file)) {
        case ((open, defs), line) => processLine(open, defs, line)
      }
      open ++ closed
    }
  def parseLines(file: File): Seq[Line] = readLines(file).zipWithIndex.map(tupled(parseLine))
  def parseLine(line: String, lineNumber: Int): Line =
    matchPattern(WhitespacePattern -> processWhitespaceLine _, EnumPattern -> processEnumLine _,
      ClassPattern -> processClassLine _, MemberPattern -> processMemberLine _)(line, lineNumber)
  type Handler = (Array[String], Int) => Line
  def matchPattern(patterns: (Pattern, Handler)*)(line: String, lineNumber: Int): Line =
    patterns.flatMap { case (pattern, f) => matchPattern(pattern, f)(line, lineNumber) }.headOption.getOrElse {
      error(lineNumber, "Invalid line, expected enum, class, or member definition")
    }
  def matchPattern(pattern: Pattern, f: Handler)(line: String, lineNumber: Int): Option[Line] =
    {
      val matcher = pattern.matcher(line)
      if (matcher.matches) {
        val count = matcher.groupCount
        val groups = (for (i <- 1 to count) yield matcher.group(i)).toArray[String]
        Some(f(groups, lineNumber))
      } else
        None
    }

  def processLine(open: Array[ClassDef], definitions: List[Definition], line: Line): (Array[ClassDef], List[Definition]) =
    {
      def makeAbstract(x: ClassDef) = new ClassDef(x.name, x.parent, x.members, true)

      line match {
        case w: WhitespaceLine => (open, definitions)
        case e: EnumLine       => (Array(), new EnumDef(e.name, e.members) :: open.toList ::: definitions)
        case m: MemberLine =>
          if (m.level == 0 || m.level > open.length) error(m, "Member must be declared in a class definition")
          else withCurrent(open, definitions, m.level) { c => List(c + m) }
        case c: ClassLine =>
          if (c.level == 0) (Array(new ClassDef(c.name, None, Nil, false)), open.toList ::: definitions)
          else if (c.level > open.length) error(c, "Class must be declared as top level or as a subclass")
          else withCurrent(open, definitions, c.level) { p => val p1 = makeAbstract(p); p1 :: new ClassDef(c.name, Some(p1), Nil, false) :: Nil }
      }
    }
  private def withCurrent(open: Array[ClassDef], definitions: List[Definition], level: Int)(onCurrent: ClassDef => Seq[ClassDef]): (Array[ClassDef], List[Definition]) =
    {
      require(0 < level && level <= open.length)
      val closed = open.drop(level).toList
      val newOpen = open.take(level - 1) ++ onCurrent(open(level - 1))
      (newOpen.toArray, closed ::: definitions)
    }
}