/*  CSci 658, Software Language Engineering
    State Machine Semantic Model
    State Machine (Secret Panel) DSL Examples
    H. Conrad Cunningham

1234567890123456789012345678901234567890123456789012345678901234567890

2009-04-14: (V1) Original
2009-04-16: (V2) Added methods to get events, commands, and
            resetEvents from a StateMachine for use with other
	    of Fowler's examples
2009-05-13: (V3) Cleaned up Scala programming style some
            (changed var's to val's where possible, used class
	    parameters without separate attributes, added return
	    types on all public features to clearly define interface)
2018-02-01: (V3a) Updated comments, Created compile script

This is a Scala reimplementation of Martin Fowler's State Machine
Semantic Model (for the Secret Panel Controller) given in the chapter
"An Introductory Example" of his book Domain-Specific Languages. (It
was actually based on a preliminary version of the example from 2009.)

The author of this code had much of the Java code for Fowler's model
from the introductory chapter.  However, there are a few guesses at
what was intended.  Also the integration of the code generation
example from the "Transformer Generation" chapter required further
enhancements of the model's implementation to provide the needed
accessor methods. The overall implementation in Scala can probably
be redesigned to take better advantage of Scala features.

*/


import scala.collection.mutable.Map
import scala.collection.mutable.Set
import scala.collection.mutable.ListBuffer


/* State Machine Event and Command objects.  

   This was changed from Fowler's Java code to use Scala case classes
   for conciseness and convenience.  These do expose the parameters of
   the constructors, but it did not seem to be a huge compromise in
   this application.  It does, however, make the code fragile with
   respect to the way names or codes are represented.
*/

abstract class AbstractEvent
case class Command(name: String, code: Symbol) extends AbstractEvent
case class Event  (name: String, code: Symbol) extends AbstractEvent


/* State machine Transition objects. This was also changed to be a
   case class for the Scala version for conciseness and convenience,
   with the same caveat as above.

   This code overrides the toString method generated by the case class
   mechanism.
*/

case class Transition(source: State, trigger: Event, target: State) {
  override def toString = "Transition(" + source.name + ", " + 
                           trigger.name + ", " + target.name + ")"
}


/* State Machine State objects.  Similar in style to the case classes,
   this exposes the "name" constructor parameter.
*/

class State(val name: String) {

  private var stateMachine: StateMachine = null
  private val actions                    = Set[Command]()
  private val transitions                = Map[Symbol,Transition]()

  // Mutator methods

  // Allow parent machine to register interest in subsequent changes
  def setMachine(mach: StateMachine) { stateMachine =  mach }

  def addAction(command: Command) { 
    actions += command 
    if (stateMachine != null)
      stateMachine.stateChanged(this)
  }

  def addTransition(event: Event, targetState: State) {
    transitions(event.code) = Transition(this,event,targetState)
    if (stateMachine != null)
      stateMachine.stateChanged(this)
  }

  // Accessor methods
  def getMachine: StateMachine =
    stateMachine

  def getActions: List[Command] =
    actions.toList

  def getEvents: List[Event] = 
    transitions.values.toList.map(_.trigger)

  def getAllTargets: List[State] = 
    transitions.values.toList.map(_.target)

  def getTransitions: List[Transition] = 
    transitions.values.toList

  def hasTransition(eventCode: Symbol): Boolean = 
     transitions.isDefinedAt(eventCode)

  def targetState(eventCode: Symbol): State = 
    transitions(eventCode).target

  def executeActions(commandsChannel: CommandChannel) {
    for (c <- actions) { commandsChannel.send(c.code) }
  }

  override def toString = 
    "State \"" + name + "\" \nwith actions " +
    actions.mkString(", ") + "\nand transitions " +
    transitions.values.toList.mkString(", ")
}


/* Object representing the entire State Machine with a given
   start state.
*/

class StateMachine(startState: State) {

  private val resetEvents = new ListBuffer[Event]

  /* The following were added by Cunningham to support getEvents and
     getCommands methods needed by code generators.  This only
     considers events and actions that are registered on states
     reachable from the start state.  This might not be sufficient
     for some purposes.  The alternative would be to have methods
     to set or add these into the model during configuration
     (e.g., buidling from DSL input).

     Deficiency: The current optimization to avoid doing a graph
     traversal for each call of getStates, getEvents, and getCommands
     will not give the correct results if any new transitions or
     states are added after the first call.  As a work-around, the
     capability to have State objects to notify its enclosing machine
     of changes was added.  The way this capability is designed should
     be readdressed.

*/ 

    private var allStates: ListBuffer[State] = null
    private val allEvents                    = Set[Event]() 
    private val allCommands                  = Set[Command]()

  // Mutator methods
  def addResetEvents(events: Event*) { 
    for (e <- events)
      resetEvents += e
    allStates = null // force a new traversal of state machine graph
  }

  def stateChanged(s: State) { 
    allStates = null;  allEvents.clear; allCommands.clear 
  }

  // NOT USED CURRENTLY
  private def addResetEvent_byAddingTransitions(e: Event) {
    for (s <- getStates)
      if (!s.hasTransition(e.code))
        s.addTransition(e, startState)
  }

  // Accessor methods
  def getStart: State = startState

  def getEvents: List[Event] = {
    if (allStates == null) {
      allEvents.clear; allCommands.clear
      gatherForwards(startState)
    }
    allEvents ++= resetEvents
    allEvents.toList
  }

  def getCommands: List[Command] = {
    if (allStates == null) {
      allEvents.clear; allCommands.clear
      gatherForwards(startState)
    }
    allCommands.toList
  }

  /* This was changed from Fowler's design to store the result of the
     graph traversal in an attribute of the object.  It also creates
     the allEvents and commands attributes.
  */

  def getStates: List[State] = {
    if (allStates == null) {
      allStates = new ListBuffer[State]
      allEvents.clear; allCommands.clear
      gatherForwards(startState)
    }
    allStates.toList
  }

  /*  Traverse the graph of states reachable from the start state via
      transitions to gather the states, events, and commands.
  */
  private def gatherForwards(start: State) {
    if (start == null)
      return
    if (allStates.contains(start))
      return
    else {
      allStates   +=  start
      allEvents   ++= start.getEvents
      allCommands ++= start.getActions
      for (next <- start.getAllTargets)
        gatherForwards(next)
      return
    }
  }

  def isResetEvent(eventCode: Symbol): Boolean = 
    resetEvents.map(_.code).contains(eventCode)

  def getResetEvents: List[Event] = resetEvents.toList

  override def toString = 
    "STATE MACHINE with START STATE \"" + startState.name + 
    "\" \nREACHABLE STATES \n" + getStates.mkString("\n") + 
    " \nRESET EVENTS \n" + resetEvents.mkString(", ")
}


/* A Controller for the State Machine. */

class Controller
    (stateMachine: StateMachine, commandsChannel: CommandChannel) {

  private var currentState: State = stateMachine.getStart

  // Mutator methods
  def handle(eventCode: Symbol) {
    if (currentState.hasTransition(eventCode))
      transitionTo(currentState.targetState(eventCode))
    else if (stateMachine.isResetEvent(eventCode))
       transitionTo(stateMachine.getStart)
    else
      println("Error:  Unknown event code " + eventCode)
      // ignore unknown events
  }

  private def transitionTo(target: State) {
    currentState = target
    currentState.executeActions(commandsChannel)
  }

  // Overloaded handle methods added for convenience (not in Fowler)
  def handle(event: Event) {
    handle(event.code)
  }

  def handle(events: Seq[Event]) {
    for (e <- events)
      handle(e.code)
  }

  // Accessor Methods
  def getCommandChannel: CommandChannel = commandsChannel

  override def toString = "CONTROLLER for \n" + stateMachine
}
