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)
}
}