/* Ice Cream Store Application of Event-Driven Simulation Framework
   H. Conrad Cunningham
   Version #1: 31 March 2010

   This is a Scala translation of the C++ discrete event simulation of
   an ice cream store given in section 21.2 of Timothy Budd's _An
   Introduction to Object-Oriented Programming_, Third Edition.  It
   uses the Simulation Framework also defined in that section.

   Significant differences from the Budd C++ version:
   - making instance variables private
   - overriding toString method in most classes
   - using symbolic constants defined in object IceCreamStore instead of 
     literal constants
   - separating Budd's side-effecting function "canSeat" into pure
     accessor "canSeat" and mutator "seat"
   - making Budd's main method IceCreamStore.simulate, adding singleton 
     object IceCreamStore to hold entry point
   - adding several convenience methods in singleton object
     IceCreamStore to enable simpler access the state of the simulation

123456789012345678901234567890123456789012345678901234567890123456789012345678

*/


/* Class ArriveEvent models the arrival of "groupSize" customers at
   the ice cream store at the simulation time "time".
*/

class ArriveEvent(time: Int, groupSize: Int) extends Event(time) {

  def processEvent {
    if (IceCreamStore.canSeat(groupSize)) {
      IceCreamStore.seat(groupSize)
      val delay = Simulation.randBetween(
                  IceCreamStore.MIN_ORDER, IceCreamStore.MAX_ORDER )
      IceCreamStore.scheduleEvent(
        new OrderEvent(IceCreamStore.currentTime + delay, groupSize))
    }
    else
      println("No room to seat " + groupSize + " people with only " + 
              IceCreamStore.availableChairs + " available." )
  }

  override def toString = "ArriveEvent(" + time + "," + groupSize + ")"
} 


/* Class OrderEvent models "groupSize" customers, each ordering some
   number of scoops of ice cream at simulation time "time".  
*/

class OrderEvent(time: Int, groupSize: Int) extends Event(time) {

  def processEvent {
    for (i <- 1 to groupSize)
      IceCreamStore.order(1 + Simulation.rand(IceCreamStore.MAX_SCOOPS))
    val delay = Simulation.randBetween(
                IceCreamStore.MIN_LEAVE, IceCreamStore.MAX_LEAVE)
    IceCreamStore.scheduleEvent(
      new LeaveEvent(IceCreamStore.currentTime + delay,  groupSize))
  }

  override def toString = "OrderEvent(" + time + "," + groupSize + ")"
}


/* Class LeaveEvent models "groupSize" customers leaving the ice cream
   store at simulation time "time".  
*/

class LeaveEvent(time: Int, groupSize: Int) extends Event(time) {

  def processEvent { IceCreamStore.leave(groupSize) }

  override def toString = "LeaveEvent(" + time + "," + groupSize + ")"
}


/* Class IceCreamStore models the overall simulation of the ice cream store.

   This design differs from Budd's design in that it splits Budd's
   side-effecting functions "canSeat" into an accessor "canSeat" and a
   mutatort "seat". (Use of side-effecting functions like Budd's
   "canSeat" should, in general, be avoided.)

 */

class IceCreamStore(chairs: Int) {

  private var freeChairs: Int   = chairs
  private var curProfit: Double = 0.0

  // is there a sufficient number of chairs to seat group
  def canSeat(numberOfPeople: Int): Boolean = numberOfPeople <= freeChairs

  // seat new group in available chairs, making them unavailable
  def seat(numberOfPeople: Int) {
    println("Current time is " + IceCreamStore.currentTime + ".")
    if (canSeat(numberOfPeople)) {
      freeChairs -= numberOfPeople
      println("Group of " + numberOfPeople + " people are seated.")
    }
    else {
      print  ("Error in seat method.  Should not occur. ")
      println("Attempt to seat group larger than number of chairs available.")
    }
    println("Number of free seats is " + freeChairs + ".")
  }

  // serve ice cream, compute profits
  def order(numberOfScoops: Int) {
    println("Current time is " + IceCreamStore.currentTime + ".")
    println("Serviced order for " + numberOfScoops + " scoops.")
    curProfit += IceCreamStore.SCOOP_PROFIT * numberOfScoops
  }

  // people leave, free up chairs
  def leave(numberOfPeople: Int) {
    println("Current time is " + IceCreamStore.currentTime + ".")
    println("Group of size " + numberOfPeople + " leaves.")
    freeChairs += numberOfPeople
    println("Number of free seats is " + freeChairs + ".")
  }

  // compute profit
  def profit: Double = curProfit

  // return number of free chairs
  def availableChairs: Int = freeChairs
}


/* Singleton object IceCreamStore holds the parameters, the
   simulation driver, and various convenience methods for the ice
   cream store simulation.
*/

object IceCreamStore {

  val BEGIN_TIME   = 0    // beginning of simulation time
  val END_TIME     = 120  // end of simulation time
  val MIN_GROUP    = 1    // minimum group size
  val MAX_GROUP    = 5    // maximum group size
  val MIN_ARRIVE   = 1    // minumum arrival interval
  val MAX_ARRIVE   = 10   // maximum arrival interval
  val MIN_ORDER    = 2    // minimum order delay
  val MAX_ORDER    = 10   // maximum order delay
  val MAX_SCOOPS   = 3    // maximum number of scoops ordered
  val MIN_LEAVE    = 15   // minimum eating time for group
  val MAX_LEAVE    = 35   // maximum eating time for group
  val NUM_CHAIRS   = 35   // number of chairs in store
  val SCOOP_PROFIT = 0.35 // profit per scoop

  val theSimulation = new Simulation(BEGIN_TIME)
  val theStore      = new IceCreamStore(NUM_CHAIRS)

  // Driver method for the ice cream store simulation, was main in Budd code
  def simulate {
    // load queue with some number of events
    var t = BEGIN_TIME
    while (t < END_TIME) {
      t += Simulation.randBetween(MIN_ARRIVE,MAX_ARRIVE)
      theSimulation.scheduleEvent(
        new ArriveEvent(t, Simulation.randBetween(MIN_GROUP,MAX_GROUP)) )
    }
    // then run the simulation and print profits
    println("Begin simulation run at time " + BEGIN_TIME + ".")
    theSimulation.run
    println("Total profit is " + theStore.profit + ".")
    println("End simulation run at time " + theSimulation.currentTime + ".")
  }

  // convenience methods to delegate calls to the IceCreamStore instance
  def canSeat(numberOfPeople: Int): Boolean = theStore.canSeat(numberOfPeople)
  def seat(numberOfPeople: Int)  { theStore.seat(numberOfPeople)  }
  def order(numberOfScoops: Int) { theStore.order(numberOfScoops) }
  def leave(numberOfPeople: Int) { theStore.leave(numberOfPeople) }
  def availableChairs: Int = theStore.availableChairs

  // convenience methods to delegate calls to the Simulation instance
  def scheduleEvent(newEvent: Event) { theSimulation.scheduleEvent(newEvent) }
  def currentTime: Int = theSimulation.currentTime

}


/* Singleton object IceCreamStoreSim is the entry point for the program.
*/
 
object IceCreamStoreSim {

  def main(args: Array[String]) { IceCreamStore.simulate }

}
