/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.stream.impl

import scala.util.control.NonFatal

import org.apache.pekko
import pekko.actor.Actor
import pekko.annotation.InternalApi

/**
 * INTERNAL API
 */
@InternalApi private[pekko] class SubReceive(initial: Actor.Receive) extends Actor.Receive {
  private var currentReceive = initial

  override def isDefinedAt(msg: Any): Boolean = currentReceive.isDefinedAt(msg)
  override def apply(msg: Any): Unit = currentReceive.apply(msg)

  def become(newBehavior: Actor.Receive): Unit = {
    currentReceive = newBehavior
  }
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] trait Inputs {
  def NeedsInput: TransferState
  def NeedsInputOrComplete: TransferState

  def dequeueInputElement(): Any

  def subreceive: SubReceive

  def cancel(): Unit
  def isClosed: Boolean
  def isOpen: Boolean = !isClosed

  def inputsDepleted: Boolean
  def inputsAvailable: Boolean
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] trait DefaultInputTransferStates extends Inputs {
  override val NeedsInput: TransferState = new TransferState {
    def isReady = inputsAvailable
    def isCompleted = inputsDepleted
  }
  override val NeedsInputOrComplete: TransferState = new TransferState {
    def isReady = inputsAvailable || inputsDepleted
    def isCompleted = false
  }
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] trait Outputs {
  def NeedsDemand: TransferState
  def NeedsDemandOrCancel: TransferState

  def demandAvailable: Boolean
  def enqueueOutputElement(elem: Any): Unit

  def subreceive: SubReceive

  // FIXME: This is completely unnecessary, refactor MapFutureProcessorImpl
  def demandCount: Long = -1L

  def complete(): Unit
  def cancel(): Unit
  def error(e: Throwable): Unit
  def isClosed: Boolean
  def isOpen: Boolean = !isClosed
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] trait DefaultOutputTransferStates extends Outputs {
  override val NeedsDemand: TransferState = new TransferState {
    def isReady = demandAvailable
    def isCompleted = isClosed
  }
  override def NeedsDemandOrCancel: TransferState = new TransferState {
    def isReady = demandAvailable || isClosed
    def isCompleted = false
  }
}

// States of the operation that is executed by this processor
/**
 * INTERNAL API
 */
@InternalApi private[pekko] trait TransferState {
  def isReady: Boolean
  def isCompleted: Boolean
  def isExecutable = isReady && !isCompleted

  def ||(other: TransferState): TransferState = new TransferState {
    def isReady: Boolean = TransferState.this.isReady || other.isReady
    def isCompleted: Boolean = TransferState.this.isCompleted && other.isCompleted
  }

  def &&(other: TransferState): TransferState = new TransferState {
    def isReady: Boolean = TransferState.this.isReady && other.isReady
    def isCompleted: Boolean = TransferState.this.isCompleted || other.isCompleted
  }
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] object Completed extends TransferState {
  def isReady = false
  def isCompleted = true
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] object NotInitialized extends TransferState {
  def isReady = false
  def isCompleted = false
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] case class WaitingForUpstreamSubscription(remaining: Int, andThen: TransferPhase)
    extends TransferState {
  def isReady = false
  def isCompleted = false
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] object Always extends TransferState {
  def isReady = true
  def isCompleted = false
}

/**
 * INTERNAL API
 */
@InternalApi private[pekko] final case class TransferPhase(precondition: TransferState)(val action: () => Unit)

/**
 * INTERNAL API
 */
@InternalApi private[pekko] trait Pump {
  private var transferState: TransferState = NotInitialized
  private var currentAction: () => Unit =
    () => throw new IllegalStateException("Pump has been not initialized with a phase")

  final def initialPhase(waitForUpstream: Int, andThen: TransferPhase): Unit = {
    require(waitForUpstream >= 1, s"waitForUpstream must be >= 1 (was $waitForUpstream)")
    if (transferState != NotInitialized)
      throw new IllegalStateException(s"initialPhase expected NotInitialized, but was [$transferState]")
    transferState = WaitingForUpstreamSubscription(waitForUpstream, andThen)
  }

  final def waitForUpstreams(waitForUpstream: Int): Unit = {
    require(waitForUpstream >= 1, s"waitForUpstream must be >= 1 (was $waitForUpstream)")
    transferState = WaitingForUpstreamSubscription(waitForUpstream, TransferPhase(transferState)(currentAction))
  }

  def gotUpstreamSubscription(): Unit = {
    transferState match {
      case WaitingForUpstreamSubscription(1, andThen) =>
        transferState = andThen.precondition
        currentAction = andThen.action
      case WaitingForUpstreamSubscription(remaining, andThen) =>
        transferState = WaitingForUpstreamSubscription(remaining - 1, andThen)
      case _ => // ok, initial phase not used, or passed already
    }
    pump()
  }

  final def nextPhase(phase: TransferPhase): Unit = transferState match {
    case WaitingForUpstreamSubscription(remaining, _) =>
      transferState = WaitingForUpstreamSubscription(remaining, phase)
    case _ =>
      transferState = phase.precondition
      currentAction = phase.action
  }

  final def isPumpFinished: Boolean = transferState.isCompleted

  protected final val completedPhase = TransferPhase(Completed) { () =>
    throw new IllegalStateException("The action of completed phase must be never executed")
  }

  // Exchange input buffer elements and output buffer "requests" until one of them becomes empty.
  // Generate upstream requestMore for every Nth consumed input element
  final def pump(): Unit = {
    try while (transferState.isExecutable) {
        currentAction()
      }
    catch { case NonFatal(e) => pumpFailed(e) }

    if (isPumpFinished) pumpFinished()
  }

  protected def pumpFailed(e: Throwable): Unit
  protected def pumpFinished(): Unit

}
