import akka.actor._
import akka.actor.SupervisorStrategy._
import scala.concurrent.duration._

// Coffee types
trait CoffeeType
case object BlackCoffee extends CoffeeType
case object Latte extends CoffeeType
case object Espresso extends CoffeeType

// Commands
case class Coins(number: Int)
case class Selection(coffee: CoffeeType)
case object TriggerOutOfCoffeeBeansFailure

// Replies
case class Beverage(coffee: CoffeeType)

// Errors
case class NotEnoughCoinsError(message: String)

// Failures
case class OutOfCoffeeBeansFailure(customer: ActorRef,
                                   pendingOrder: Selection,
                                   nrOfInsertedCoins: Int) extends Exception

class CoffeeMachineManager extends Actor {
  override val supervisorStrategy =
    OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1.minute) {
      case e: OutOfCoffeeBeansFailure =>
        println(s"ServiceGuy notified: $e")
        Restart
      case _: Exception =>
        Escalate
    }

  // to simplify things he is only managing 1 single machine
  val machine = context.actorOf(Props[CoffeeMachine], name = "coffeeMachine")

  def receive = {
    case request => machine.forward(request)
  }
}

class CoffeeMachine extends Actor {
  val price = 2
  var nrOfInsertedCoins = 0
  var outOfCoffeeBeans = false
  var totalNrOfCoins = 0

  def receive = {
    case Coins(nr) =>
      nrOfInsertedCoins += nr
      totalNrOfCoins += nr
      println(s"Inserted [$nr] coins")
      println(s"Total number of coins in machine is [$totalNrOfCoins]")

    case selection @ Selection(coffeeType) =>
       if (nrOfInsertedCoins < price)
         sender.tell(NotEnoughCoinsError(s"Please insert [${price - nrOfInsertedCoins}] coins"), self)
       else {
         if (outOfCoffeeBeans)
           throw new OutOfCoffeeBeansFailure(sender, selection, nrOfInsertedCoins)
         println(s"Brewing your $coffeeType")
         sender.tell(Beverage(coffeeType), self)
         nrOfInsertedCoins = 0
       }

    case TriggerOutOfCoffeeBeansFailure =>
      outOfCoffeeBeans = true
  }

  override def postRestart(failure: Throwable): Unit = {
    println(s"Restarting coffee machine...")
    failure match {
      case OutOfCoffeeBeansFailure(customer, pendingOrder, coins) =>
        nrOfInsertedCoins = coins
        outOfCoffeeBeans = false
        println(s"Resubmitting pending order $pendingOrder")
        context.self.tell(pendingOrder, customer) // fake the sender to be the customer®
      }
  }
}

object VendingMachineDemo extends App {

  val system = ActorSystem("vendingMachineDemo")
  val coffeeMachine = system.actorOf(Props[CoffeeMachineManager], "coffeeMachineManager")
  val customer = Inbox.create(system) // emulates the customer

  println("-----------------------------------------")
  // Insert 2 coins and get an Espresso
  customer.send(coffeeMachine, Coins(2))
  customer.send(coffeeMachine, Selection(Espresso))
  val Beverage(coffee1) = customer.receive(5.seconds)
  println(s"Got myself an $coffee1")
  assert(coffee1 == Espresso)

  println("-----------------------------------------")
  // Insert 1 coin and fail to get a Latte
  customer.send(coffeeMachine, Coins(1))
  customer.send(coffeeMachine, Selection(Latte))
  val NotEnoughCoinsError(message) = customer.receive(5.seconds)
  println(s"Got myself a validation error: $message")
  assert(message == "Please insert [1] coins")

  println("-----------------------------------------")
  // Insert 1 coin (had 1 before) and try to get my Latte
  // Machine should:
  //   1. Fail
  //   2. Restart
  //   3. Resubmit my order
  //   4. Give me my coffee
  customer.send(coffeeMachine, Coins(1))
  customer.send(coffeeMachine, TriggerOutOfCoffeeBeansFailure)
  customer.send(coffeeMachine, Selection(Latte))
  val Beverage(coffee2) = customer.receive(5.seconds)
  println(s"Got myself a $coffee2")
  assert(coffee2 == Latte)

  system.shutdown()
}