/* Engr 691-6, Special Topics, Software Language Engineering
   Fowler's Delimiter-Directed Translation of a Custom External DSL 
     for the State Machine Controller (Secret Panel)
   H. Conrad Cunningham
   Version 1:  23 April 2009
   Version 2:  27 April 2009 Improved comments, added exception-handling 
                             for RecognitionException.
   Version 3:  13 May 2009
     Refactored to use Source i/o and trait IncrementalStateMachineBuilder.
     Added tracing and improved error messages.

123456789012345678901234567890123456789012345678901234567890123456789012345678

This is a Scala implementation of Martin Fowler's Custom External DSL
for the Secret Panel Controller (State Machine).  The language is
defined in the "An Introductory Example" chapter of his in-progress
DSL book http://www.martinfowler.com/dslwip/intro.html.

This program uses the Delimiter-Directed Translation technique.  A
delimiter-directed parser is one that reads chunks of text terminated
(or separated) by specified delimiters. It is a simple technique for
parsing relatively simple languages consisting of autonomous
statements without much nesting.

This program is based on the Java program given in Fowler's Delimiter
Directed Translation chapter.  In this case, the parser uses the end
of line as the delimiter.  The author had most of Fowler's code from
his chapter.  The original version stayed relatively close to the Java
code.  However, subsequent versions evolved somewhat to use the
IncrementalStateMachineBuilder trait (that the author factored out
from several of his examples) and more Scala features.

Here is Fowler's example Custom External DSL input file:

events
  doorClosed  D1CL
  drawOpened  D2OP
  lightOn     L1ON
  doorOpened  D1OP
  panelClosed PNCL
end

resetEvents
  doorOpened
end

commands
  unlockPanel PNUL
  lockPanel   PNLK
  lockDoor    D1LK
  unlockDoor  D1UL
end


state idle
  actions unlockDoor lockPanel
  doorClosed => active
end

state active
  drawOpened => waitingForLight
  lightOn    => waitingForDraw
end

state waitingForLight
  lightOn => unlockedPanel
end

state waitingForDraw
  drawOpened => unlockedPanel
end

state unlockedPanel
  actions unlockPanel lockDoor
  panelClosed => idle
end

*/

import scala.collection.mutable.ListBuffer
import scala.collection.mutable.Map
import scala.io.Source
import java.io._

/* The singleton object StateMachineParser provides a method to load
   the DSL input file and parse it to configure a state machine.
*/
 
object StateMachineParser {

  def loadFile(fileName: String): StateMachine  = {
    var dsl: Source = null
    try   { dsl = Source.fromFile(fileName)  }
    catch { case e: FileNotFoundException => 
              throw new RuntimeException(e.getMessage) 
    }
    val loader = new StateMachineParser(dsl)
    loader.trace("Loaded file '" + fileName + "'.")
    loader.run
    val stateMachine = loader.getMachine
    loader.trace("Completed definition of state machine with start state '" 
                 + stateMachine.getStart.name + "'.")
    stateMachine
  }
}


/* Class StateMachineParser implements a driver for the Delimiter-
   Directed Translation parser where the delimiter is the end of the
   line. Each line is read and then parsed by an appropriate line
   parser.  The line parsers are implemented using the State design
   pattern.
*/

class StateMachineParser(input: Source) 
  extends IncrementalStateMachineBuilder {

  // Line parser "state" variable
  private var lineParser: LineParser = null

  /* Method "run" repeatedly reads a line from the DSL file and parses it 
     with the line parser for the current "state" of parser.
  */
  def run {
    setLineParser(new TopLevelLineParser(this))
    try {
      for (line <- input.getLines)
        lineParser.parse(line)
    } catch {
      case e: IOException => throw new RuntimeException(e)
    }
    finishMachine
  }

  // Set the "state" object used by the parser for continuing processing.
  def setLineParser(linePar: LineParser) { lineParser = linePar }
}


/* The LineParser hierarchy implements the line parsers plugged into
   the StateMachineParser object as State design pattern objects.  This
   hierarchy is implemented according to the Template Method design pattern.
   The abstract method doParse is the hook method where the different 
   parsing actions are implemented for each block of DSL code.  Each 
   subclass will implement a parser for a specific block of DSL code.

   This Scala version caches the parsed words, which differs from the
   approach that Fowler took in the Java version.  
*/
  
abstract class LineParser(context: StateMachineParser) {

  // Current line 
  protected var line: String = null

  // Current line after spliting into words
  protected var words: Array[String] = null

  // Template method. 
  final def parse(s: String) {
    line = removeComment(s).trim()
    if (!isBlankLine) {
      words = lexWords
      try {
        doParse
      } catch {
        case e: RecognitionException => 
          println(e.getMessage + "\n..Unrecognized line skipped.")
      }
    }
  }

  // Hook method for parsing action
  def doParse: Unit

  // Break the line up into items surrounded by white spaces
  protected def lexWords: Array[String] = line.split("\\s+")

  // Does the line contain only one word?
  protected def hasOnlyWord(word: String): Boolean = {
    if (words(0) == word) {
      if (words.length != 1) failToRecognizeLine
      true
    }
    else 
      false
  }

  // Does the line begin with the given argument keyword?
  protected def hasKeyword(keyword: String): Boolean = (keyword == words(0))

  // Error condition when line unrecognizable
  protected def failToRecognizeLine { throw new RecognitionException(line) }

  // Change parser state to look for autonomous statements
  protected def returnToTopLevel {
    context.setLineParser(new TopLevelLineParser(context))
  }

  // Is the line only white space?
  private def isBlankLine: Boolean = line.matches("^\\s*$")

  // Remove comments from line
  private def removeComment(line: String): String = 
    line.replaceFirst("#.*", "")
}


/* Class TopLevelLineParser implements a top-level line parser that
   switches the parser state to other line parsers based on the keyword
   at the beginning of the line.  
*/

class TopLevelLineParser(context: StateMachineParser) 
  extends LineParser(context) {
 
  // Implementation of the hook method
  def doParse {
    if (hasOnlyWord("events")) {
      context.trace("Parsing 'events' block.")
      context.setLineParser(new EventLineParser(context))
    }
    else if (hasOnlyWord("resetEvents")) {
      context.trace("Parsing 'resetEvents' block.")
      context.setLineParser(new ResetEventLineParser(context))
    }
    else if (hasOnlyWord("commands")) {
      context.trace("Parsing 'commands' block.")
      context.setLineParser(new CommandLineParser(context))
    }
    else if (hasKeyword("state"))
      processState
    else 
      failToRecognizeLine
  }

  // Process the state block, which has the state name on the line.
  private def processState {
    context.trace("Parsing 'state' named '" + words(1) + "'.")
    val curState = context.obtainState(words(1)) // bypass addState 
    context.primeMachine(curState)               //   in 'context'
    context.setLineParser(new StateLineParser(context,curState))
  }
}


/* Class EventLineParser implements a line parser for "event" blocks in
   the DSL code. 
*/

class EventLineParser(context: StateMachineParser) 
  extends LineParser(context) {

  // Implementation of the hook method
  def doParse {
    if (hasOnlyWord("end"))
      returnToTopLevel
    else if (words.length == 2) {
      context.trace("..Define event '" + words(0) + "'.")
      context.addEvent(words(0), words(1))
    }
    else 
      failToRecognizeLine
  }
}


/* Class ResetEventLineParser implements the line parser for
   "resetEvent" blocks in the DSL code. 
*/

class ResetEventLineParser(context: StateMachineParser) 
  extends LineParser(context) {

  // Implementation of the hook method
  def doParse {
    if (hasOnlyWord("end"))
      returnToTopLevel
    else if (words.length == 1) {
      context.trace("..Define resetEvent '" + words(0) + "'.")
      context.addResetEvent(words(0))
    }
    else 
      failToRecognizeLine
  }
}



/* Class CommandLineParser implements a line parser for "command" blocks
   in the DSL code. 
*/

class CommandLineParser(context: StateMachineParser) 
  extends LineParser(context) {

  // Implementation of the hook method
  def doParse {
    if (hasOnlyWord("end"))
      returnToTopLevel
    else if (words.length == 2) {
      context.trace("..Define event '" + words(0) + "'.")
      context.addCommand(words(0), words(1))
    }
    else 
      failToRecognizeLine
  }
}


/* Class StateLineParser implements a line parser for the "state"
   definition blocks in the DSL code. This block is more complex than the
   others because it includes transition and action statements.  
*/

class StateLineParser(context: StateMachineParser, curState: State) 
  extends LineParser(context) {

  // Implementation of the hook method
  def doParse {
    if (hasOnlyWord("end"))
      context.setLineParser(new TopLevelLineParser(context))
    else if (isTransition) 
      processTransition
    else if (hasKeyword("actions")) 
      processActions
    else 
      throw new RecognitionException(line)
  }

  // Does the line contain a "=>" symbol?
  private def isTransition = line.matches(".*=>.*")

  // Reparse the line and process the "transition" statement.
  private def processTransition {
    val tokens  = for (s <- line.split("=>")) yield s.trim
    context.trace("..Add transition on '" + tokens(0) + "' to '" + 
                  words(1) + "'")
    val trigger = context.getEvent(tokens(0))    // trigger event
    val target  = context.obtainState(tokens(1)) // target state
    trigger match {          // bypass addTransition in 'context'
      case Some(e) => curState.addTransition(e,target)
      case None    => 
        context.syntaxError("Undefined event '" + tokens(0) + 
                            "' in transition.")
    }
  }

  // Handle the "actions" statement
  private def processActions {
    for (name <- words.drop(1)) {
      context.trace("..Add action '" + name + "'.")
      context.getCommand(name) match {  // bypass addAction in 'context'
        case Some(ac) => 
            curState.addAction(ac)
        case None     => 
          println("[Error] Attempt to add undefined action '" + name
                  + "' to state '" + curState.name + "'."
                  + "\n..Unrecognized name skipped." )
      }
    }
  }

}


/*  Some limited testing.  */

object DelimiterDSLTest {

  def main(args: Array[String]) {
    println("\nDelimiter-Directed Translation External DSL test beginning.\n")

    // Load and parse the DSL to configure the State Machine
    val machine = 
      StateMachineParser.loadFile("CustomExternalStateMachineDSL.dsl")

    println
    println(machine)

    // Build the controller
    val commandsChannel = new CommandChannel
    val control = new Controller(machine,commandsChannel)
    println(control.toString)

    // Execute the model
    control.handle('D1CL)
    control.handle('L1ON)
    control.handle('D2OP)
    control.handle('PNCL)

    println("\nCommands output:  " + commandsChannel.getOutput)

    println("\nDelimiter-Directed Translation External DSL test ending.\n")
  }
}
