package akka.contrib.pattern

import akka.actor.Actor

object ReceivePipeline {
  /**
   * Result returned by an interceptor PF to determine what/whether to delegate to the next inner interceptor
   */
  sealed trait Delegation

  case class Inner(transformedMsg: Any) extends Delegation {
    /**
     * Add a block of code to be executed after the message (which may be further transformed and processed by
     * inner interceptors) is handled by the actor's receive.
     *
     * The block of code will be executed before similar blocks in outer interceptors.
     */
    def andAfter(after: ⇒ Unit): Delegation = InnerAndAfter(transformedMsg, (_ ⇒ after))
  }

  private[ReceivePipeline] case class InnerAndAfter(transformedMsg: Any, after: Unit ⇒ Unit) extends Delegation

  /**
   * Interceptor return value that indicates that the message has been handled
   * completely. The message will not be passed to inner interceptors
   * (or to the decorated actor's receive).
   */
  case object HandledCompletely extends Delegation

  private def withDefault(interceptor: Interceptor): Interceptor = interceptor.orElse({ case msg ⇒ Inner(msg) })

  type Interceptor = PartialFunction[Any, Delegation]

  private sealed trait HandlerResult
  private case object Done extends HandlerResult
  private case object Undefined extends HandlerResult

  private type Handler = Any ⇒ HandlerResult
}

/**
 * Trait implementing Receive Pipeline Pattern. Mixin this trait
 * for configuring a chain of interceptors to be applied around
 * Actor's current behavior.
 */
trait ReceivePipeline extends Actor {
  import ReceivePipeline._

  private var pipeline: Vector[Interceptor] = Vector.empty
  private var decoratorCache: Option[(Receive, Receive)] = None

  /**
   * Adds an inner interceptor, it will be applied lastly, near to Actor's original behavior
   * @param interceptor an interceptor
   */
  def pipelineInner(interceptor: Interceptor): Unit = {
    pipeline :+= withDefault(interceptor)
    decoratorCache = None
  }
  /**
   * Adds an outer interceptor, it will be applied firstly, far from Actor's original behavior
   * @param interceptor an interceptor
   */
  def pipelineOuter(interceptor: Interceptor): Unit = {
    pipeline +:= withDefault(interceptor)
    decoratorCache = None
  }

  private def combinedDecorator: Receive ⇒ Receive = { receive ⇒
    // So that reconstructed Receive PF is undefined only when the actor's 
    // receive is undefined for a transformed message that reaches it...     
    val innerReceiveHandler: Handler = {
      case msg ⇒ receive.lift(msg).map(_ ⇒ Done).getOrElse(Undefined)
    }

    val zipped = pipeline.foldRight(innerReceiveHandler) { (outerInterceptor, innerHandler) ⇒
      outerInterceptor.andThen {
        case Inner(msg)                ⇒ innerHandler(msg)
        case InnerAndAfter(msg, after) ⇒ try innerHandler(msg) finally after()
        case HandledCompletely         ⇒ Done
      }
    }

    toReceive(zipped)
  }

  private def toReceive(handler: Handler) = new Receive {
    def isDefinedAt(m: Any): Boolean = evaluate(m) != Undefined
    def apply(m: Any): Unit = evaluate(m)

    override def applyOrElse[A1 <: Any, B1 >: Unit](m: A1, default: A1 ⇒ B1): B1 = {
      val result = handler(m)

      if (result == Undefined) default(m)
    }

    private def evaluate(m: Any) = handler(m)
  }

  /**
   * INTERNAL API.
   */
  override protected[akka] def aroundReceive(receive: Receive, msg: Any): Unit = {
    def withCachedDecoration(decorator: Receive ⇒ Receive): Receive = decoratorCache match {
      case Some((`receive`, cached)) ⇒ cached
      case _ ⇒
        val decorated = decorator(receive)
        decoratorCache = Some((receive, decorated))
        decorated
    }

    super.aroundReceive(withCachedDecoration(combinedDecorator), msg)
  }
}
