All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zio.stream.ZStream.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2018-2024 John A. De Goes and the ZIO Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package zio.stream

import zio._
import zio.internal.{PartitionedRingBuffer, SingleThreadedRingBuffer, UniqueKey}
import zio.metrics.MetricLabel
import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.stm._
import zio.stream.ZStream.{DebounceState, HandoffSignal, failCause, zipChunks}
import zio.stream.internal.{ZInputStream, ZReader}

import java.io.{IOException, InputStream}
import scala.annotation.tailrec
import scala.collection.mutable
import scala.reflect.ClassTag

final class ZStream[-R, +E, +A] private (val channel: ZChannel[R, Any, Any, Any, E, Chunk[A], Any]) { self =>

  import ZStream.HaltStrategy

  /**
   * Returns a new ZStream that applies the specified aspect to this ZStream.
   * Aspects are "transformers" that modify the behavior of their input in some
   * well-defined way (for example, adding a timeout).
   */
  def @@[LowerR <: UpperR, UpperR <: R, LowerE >: E, UpperE >: LowerE, LowerA >: A, UpperA >: LowerA](
    aspect: => ZStreamAspect[LowerR, UpperR, LowerE, UpperE, LowerA, UpperA]
  )(implicit trace: Trace): ZStream[UpperR, LowerE, LowerA] =
    ZStream.suspend(aspect(self))

  /**
   * Symbolic alias for [[ZStream#cross]].
   */
  def <*>[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit
    zippable: Zippable[A, A2],
    trace: Trace
  ): ZStream[R1, E1, zippable.Out] =
    self cross that

  /**
   * Symbolic alias for [[ZStream#crossLeft]].
   */
  def <*[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit trace: Trace): ZStream[R1, E1, A] =
    self crossLeft that

  /**
   * Symbolic alias for [[ZStream#crossRight]].
   */
  def *>[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit trace: Trace): ZStream[R1, E1, A2] =
    self crossRight that

  /**
   * Symbolic alias for [[ZStream#zip]].
   */
  def <&>[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit
    zippable: Zippable[A, A2],
    trace: Trace
  ): ZStream[R1, E1, zippable.Out] =
    self zip that

  /**
   * Symbolic alias for [[ZStream#zipLeft]].
   */
  def <&[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit trace: Trace): ZStream[R1, E1, A] =
    self zipLeft that

  /**
   * Symbolic alias for [[ZStream#zipRight]].
   */
  def &>[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit trace: Trace): ZStream[R1, E1, A2] =
    self zipRight that

  /**
   * Symbolic alias for [[ZStream#via]].
   */
  def >>>[R1 <: R, E1 >: E, B](pipeline: => ZPipeline[R1, E1, A, B])(implicit
    trace: Trace
  ): ZStream[R1, E1, B] =
    via(pipeline)

  /**
   * Symbolic alias for [[[zio.stream.ZStream!.run[R1<:R,E1>:E,B]*]]].
   */
  def >>>[R1 <: R, E1 >: E, A2 >: A, Z](sink: => ZSink[R1, E1, A2, Any, Z])(implicit
    trace: Trace
  ): ZIO[R1, E1, Z] =
    self.run(sink)

  /**
   * Symbolic alias for [[ZStream#concat]].
   */
  def ++[R1 <: R, E1 >: E, A1 >: A](that: => ZStream[R1, E1, A1])(implicit trace: Trace): ZStream[R1, E1, A1] =
    self concat that

  /**
   * Symbolic alias for [[ZStream#orElse]].
   */
  def <>[R1 <: R, E2, A1 >: A](
    that: => ZStream[R1, E2, A1]
  )(implicit ev: CanFail[E], trace: Trace): ZStream[R1, E2, A1] =
    self orElse that

  /**
   * A symbolic alias for `orDie`.
   */
  def !(implicit ev1: E <:< Throwable, ev2: CanFail[E], trace: Trace): ZStream[R, Nothing, A] =
    self.orDie

  /**
   * Returns a stream that submerges the error case of an `Either` into the
   * `ZStream`.
   */
  def absolve[R1 <: R, E1, A1](implicit
    ev: ZStream[R, E, A] <:< ZStream[R1, E1, Either[E1, A1]],
    trace: Trace
  ): ZStream[R1, E1, A1] =
    ZStream.absolve(ev(self))

  /**
   * Aggregates elements of this stream using the provided sink for as long as
   * the downstream operators on the stream are busy.
   *
   * This operator divides the stream into two asynchronous "islands". Operators
   * upstream of this operator run on one fiber, while downstream operators run
   * on another. Whenever the downstream fiber is busy processing elements, the
   * upstream fiber will feed elements into the sink until it signals
   * completion.
   *
   * Any sink can be used here, but see [[ZSink.foldWeightedZIO]] and
   * [[ZSink.foldUntilZIO]] for sinks that cover the common usecases.
   */
  def aggregateAsync[R1 <: R, E1 >: E, A1 >: A, B](
    sink: => ZSink[R1, E1, A1, A1, B]
  )(implicit trace: Trace): ZStream[R1, E1, B] =
    aggregateAsyncWithin(sink, Schedule.forever)

  /**
   * Like `aggregateAsyncWithinEither`, but only returns the `Right` results.
   *
   * @param sink
   *   used for the aggregation
   * @param schedule
   *   signalling for when to stop the aggregation
   * @return
   *   `ZStream[R1, E2, B]`
   */
  def aggregateAsyncWithin[R1 <: R, E1 >: E, A1 >: A, B](
    sink: => ZSink[R1, E1, A1, A1, B],
    schedule: => Schedule[R1, Option[B], Any]
  )(implicit trace: Trace): ZStream[R1, E1, B] =
    aggregateAsyncWithinEither(sink, schedule).collectRight

  /**
   * Aggregates elements using the provided sink until it completes, or until
   * the delay signalled by the schedule has passed.
   *
   * This operator divides the stream into two asynchronous islands. Operators
   * upstream of this operator run on one fiber, while downstream operators run
   * on another. Elements will be aggregated by the sink until the downstream
   * fiber pulls the aggregated value, or until the schedule's delay has passed.
   *
   * Aggregated elements will be fed into the schedule to determine the delays
   * between pulls.
   *
   * @param sink
   *   used for the aggregation
   * @param schedule
   *   signalling for when to stop the aggregation
   * @return
   *   `ZStream[R1, E2, Either[C, B]]`
   */
  def aggregateAsyncWithinEither[R1 <: R, E1 >: E, A1 >: A, B, C](
    sink: => ZSink[R1, E1, A1, A1, B],
    schedule: => Schedule[R1, Option[B], C]
  )(implicit trace: Trace): ZStream[R1, E1, Either[C, B]] = {
    type HandoffSignal = ZStream.HandoffSignal[E1, A]
    import ZStream.HandoffSignal._
    type SinkEndReason = ZStream.SinkEndReason
    import ZStream.SinkEndReason._

    val layer =
      ZStream.Handoff.make[HandoffSignal] <*>
        Ref.make[SinkEndReason](ScheduleEnd) <*>
        Ref.make(Chunk[A1]()) <*>
        schedule.driver <*>
        Ref.make(false) <*>
        Ref.make(false)

    ZStream.fromZIO(layer).flatMap {
      case (handoff, sinkEndReason, sinkLeftovers, scheduleDriver, consumed, endAfterEmit) =>
        lazy val handoffProducer: ZChannel[Any, E1, Chunk[A], Any, Nothing, Nothing, Any] =
          ZChannel.readWithCause(
            (in: Chunk[A]) => ZChannel.fromZIO(handoff.offer(Emit(in)).when(in.nonEmpty)) *> handoffProducer,
            (cause: Cause[E1]) => ZChannel.fromZIO(handoff.offer(Halt(cause))),
            (_: Any) => ZChannel.fromZIO(handoff.offer(End(UpstreamEnd)))
          )

        lazy val handoffConsumer: ZChannel[Any, Any, Any, Any, E1, Chunk[A1], Unit] =
          ZChannel.unwrap(
            sinkLeftovers.getAndSet(Chunk.empty).flatMap { leftovers =>
              if (leftovers.nonEmpty) {
                consumed.set(true) *> ZIO.succeed(ZChannel.write(leftovers) *> handoffConsumer)
              } else
                handoff.take.map {
                  case Emit(chunk) =>
                    ZChannel.fromZIO(consumed.set(true)) *>
                      ZChannel.write(chunk) *>
                      ZChannel.fromZIO(endAfterEmit.get).flatMap {
                        case false => handoffConsumer
                        case true  => ZChannel.unit
                      }
                  case Halt(cause) => ZChannel.refailCause(cause)
                  case End(ScheduleEnd) =>
                    ZChannel.unwrap {
                      consumed.get.map { p =>
                        if (p) ZChannel.fromZIO(sinkEndReason.set(ScheduleEnd) *> endAfterEmit.set(true))
                        else
                          ZChannel
                            .fromZIO(sinkEndReason.set(ScheduleEnd) *> endAfterEmit.set(true)) *> handoffConsumer
                      }
                    }
                  case End(reason) => ZChannel.fromZIO(sinkEndReason.set(reason) *> endAfterEmit.set(true))
                }
            }
          )

        def timeout(lastB: Option[B]): ZIO[R1, None.type, C] =
          scheduleDriver.next(lastB)

        def scheduledAggregator(
          sinkFiber: Fiber.Runtime[E1, (Chunk[Chunk[A1]], B)],
          scheduleFiber: Fiber.Runtime[None.type, C],
          scope: Scope
        ): ZChannel[R1, Any, Any, Any, E1, Chunk[Either[C, B]], Any] = {

          val forkSink =
            consumed.set(false) *> endAfterEmit
              .set(false) *> (handoffConsumer pipeToOrFail sink.channel).collectElements.run.forkIn(scope)

          def handleSide(leftovers: Chunk[Chunk[A1]], b: B, c: Option[C]) =
            ZChannel.unwrap(
              sinkLeftovers.set(leftovers.flatten) *>
                sinkEndReason.get.map {
                  case ScheduleEnd =>
                    ZChannel.unwrap {
                      for {
                        consumed      <- consumed.get
                        sinkFiber     <- forkSink
                        scheduleFiber <- timeout(Some(b)).forkIn(scope)
                        toWrite        = c.fold[Chunk[Either[C, B]]](Chunk.single(Right(b)))(c => Chunk(Right(b), Left(c)))
                      } yield
                        if (consumed)
                          ZChannel
                            .write(toWrite) *> scheduledAggregator(sinkFiber, scheduleFiber, scope)
                        else scheduledAggregator(sinkFiber, scheduleFiber, scope)
                    }
                  case UpstreamEnd =>
                    ZChannel.unwrap {
                      consumed.get.map { p =>
                        if (p) ZChannel.write(Chunk.single(Right(b)))
                        else ZChannel.unit
                      }
                    }
                }
            )

          ZChannel.unwrap {
            sinkFiber.join.raceWith(scheduleFiber.join)(
              (sinkExit, _) =>
                scheduleFiber.interrupt *>
                  ZIO.done(sinkExit).map { case (leftovers, b) => handleSide(leftovers, b, None) },
              (scheduleExit, _) =>
                ZIO
                  .done(scheduleExit)
                  .foldCauseZIO(
                    _.failureOrCause match {
                      case Left(_) =>
                        handoff.offer(End(ScheduleEnd)).forkDaemon *> sinkFiber.join.map { case (leftovers, b) =>
                          handleSide(leftovers, b, None)
                        }
                      case Right(cause) =>
                        handoff.offer(Halt(cause)).forkDaemon *> sinkFiber.join.map { case (leftovers, b) =>
                          handleSide(leftovers, b, None)
                        }
                    },
                    c =>
                      handoff.offer(End(ScheduleEnd)).forkDaemon *>
                        sinkFiber.join.map { case (leftovers, b) =>
                          handleSide(leftovers, b, Some(c))
                        }
                  )
            )
          }
        }

        ZStream.unwrapScopedWith { scope =>
          for {
            _             <- (self.channel >>> handoffProducer).run.forkIn(scope)
            sinkFiber     <- (handoffConsumer pipeToOrFail sink.channel).collectElements.run.forkIn(scope)
            scheduleFiber <- timeout(None).forkIn(scope)
          } yield new ZStream(scheduledAggregator(sinkFiber, scheduleFiber, scope))
        }
    }
  }

  /**
   * Maps the success values of this stream to the specified constant value.
   */
  def as[A2](A2: => A2)(implicit trace: Trace): ZStream[R, E, A2] =
    map(_ => A2)

  /**
   * Fan out the stream, producing a list of streams that have the same elements
   * as this stream. The driver stream will only ever advance the `maximumLag`
   * chunks before the slowest downstream stream.
   */
  def broadcast(n: => Int, maximumLag: => Int)(implicit
    trace: Trace
  ): ZIO[R with Scope, Nothing, Chunk[ZStream[Any, E, A]]] =
    self
      .broadcastedQueues(n, maximumLag)
      .map(_.map(ZStream.fromQueueWithShutdown(_).flattenTake))

  /**
   * Fan out the stream, producing a dynamic number of streams that have the
   * same elements as this stream. The driver stream will only ever advance the
   * `maximumLag` chunks before the slowest downstream stream.
   */
  def broadcastDynamic(
    maximumLag: => Int
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, ZStream[Any, E, A]] =
    self
      .broadcastedQueuesDynamic(maximumLag)
      .map(ZStream.scoped(_).flatMap(ZStream.fromQueue(_)).flattenTake)

  /**
   * Converts the stream to a scoped list of queues. Every value will be
   * replicated to every queue with the slowest queue being allowed to buffer
   * `maximumLag` chunks before the driver is back pressured.
   *
   * Queues can unsubscribe from upstream by shutting down.
   */
  def broadcastedQueues(
    n: => Int,
    maximumLag: => Int
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Chunk[Dequeue[Take[E, A]]]] =
    for {
      hub    <- Hub.bounded[Take[E, A]](maximumLag)
      queues <- ZIO.collectAll(Chunk.fill(n)(hub.subscribe))
      _      <- self.runIntoHubScoped(hub).forkScoped
    } yield queues

  /**
   * Converts the stream to a scoped dynamic amount of queues. Every chunk will
   * be replicated to every queue with the slowest queue being allowed to buffer
   * `maximumLag` chunks before the driver is back pressured.
   *
   * Queues can unsubscribe from upstream by shutting down.
   */
  def broadcastedQueuesDynamic(
    maximumLag: => Int
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, ZIO[Scope, Nothing, Dequeue[Take[E, A]]]] =
    toHub(maximumLag).map(_.subscribe)

  /**
   * Allows a faster producer to progress independently of a slower consumer by
   * buffering up to `capacity` elements in a queue.
   *
   * @note
   *   This combinator destroys the chunking structure. It's recommended to use
   *   rechunk afterwards.
   * @note
   *   Prefer capacities that are powers of 2 for better performance.
   */
  def buffer(capacity: => Int)(implicit trace: Trace): ZStream[R, E, A] = {
    val queue = self.toQueueOfElements(capacity)
    new ZStream(
      ZChannel.unwrapScoped[R] {
        queue.map { queue =>
          lazy val process: ZChannel[Any, Any, Any, Any, E, Chunk[A], Unit] =
            ZChannel.fromZIO {
              queue.take
            }.flatMap { (exit: Exit[Option[E], A]) =>
              exit.foldExit(
                Cause
                  .flipCauseOption(_)
                  .fold[ZChannel[Any, Any, Any, Any, E, Chunk[A], Unit]](ZChannel.unit)(ZChannel.refailCause),
                value => ZChannel.write(Chunk.single(value)) *> process
              )
            }

          process
        }
      }
    )
  }

  /**
   * Allows a faster producer to progress independently of a slower consumer by
   * buffering up to `capacity` chunks in a queue.
   *
   * @note
   *   Prefer capacities that are powers of 2 for better performance.
   */
  def bufferChunks(capacity: => Int)(implicit trace: Trace): ZStream[R, E, A] = {
    val queue = self.toQueue(capacity)
    new ZStream(
      ZChannel.unwrapScoped[R] {
        queue.map { queue =>
          lazy val process: ZChannel[Any, Any, Any, Any, E, Chunk[A], Unit] =
            ZChannel.fromZIO {
              queue.take
            }.flatMap { (take: Take[E, A]) =>
              take.fold(
                ZChannel.unit,
                error => ZChannel.refailCause(error),
                value => ZChannel.write(value) *> process
              )
            }

          process
        }
      }
    )
  }

  /**
   * Allows a faster producer to progress independently of a slower consumer by
   * buffering up to `capacity` chunks in a dropping queue.
   *
   * @note
   *   Prefer capacities that are powers of 2 for better performance.
   */
  def bufferChunksDropping(capacity: => Int)(implicit trace: Trace): ZStream[R, E, A] = {
    val queue =
      ZIO.acquireRelease(Queue.dropping[(Take[E, A], Promise[Nothing, Unit])](capacity))(_.shutdown)
    new ZStream(bufferSignal[R, E, A](queue, self.channel))
  }

  /**
   * Allows a faster producer to progress independently of a slower consumer by
   * buffering up to `capacity` chunks in a sliding queue.
   *
   * @note
   *   Prefer capacities that are powers of 2 for better performance.
   */
  def bufferChunksSliding(capacity: => Int)(implicit trace: Trace): ZStream[R, E, A] = {
    val queue =
      ZIO.acquireRelease(Queue.sliding[(Take[E, A], Promise[Nothing, Unit])](capacity))(_.shutdown)
    new ZStream(bufferSignal[R, E, A](queue, self.channel))
  }

  /**
   * Allows a faster producer to progress independently of a slower consumer by
   * buffering up to `capacity` elements in a dropping queue.
   *
   * @note
   *   This combinator destroys the chunking structure. It's recommended to use
   *   rechunk afterwards.
   * @note
   *   Prefer capacities that are powers of 2 for better performance.
   */
  def bufferDropping(capacity: => Int)(implicit trace: Trace): ZStream[R, E, A] = {
    val queue =
      ZIO.acquireRelease(Queue.dropping[(Take[E, A], Promise[Nothing, Unit])](capacity))(_.shutdown)
    new ZStream(bufferSignal[R, E, A](queue, self.rechunk(1).channel))
  }

  /**
   * Allows a faster producer to progress independently of a slower consumer by
   * buffering up to `capacity` elements in a sliding queue.
   *
   * @note
   *   This combinator destroys the chunking structure. It's recommended to use
   *   rechunk afterwards.
   * @note
   *   Prefer capacities that are powers of 2 for better performance.
   */
  def bufferSliding(capacity: => Int)(implicit trace: Trace): ZStream[R, E, A] = {
    val queue =
      ZIO.acquireRelease(Queue.sliding[(Take[E, A], Promise[Nothing, Unit])](capacity))(_.shutdown)
    new ZStream(bufferSignal[R, E, A](queue, self.rechunk(1).channel))
  }

  private def bufferSignal[R1 <: R, E1 >: E, A1 >: A](
    scoped: => ZIO[Scope, Nothing, Queue[(Take[E1, A1], Promise[Nothing, Unit])]],
    channel: => ZChannel[R1, Any, Any, Any, E1, Chunk[A1], Any]
  )(implicit trace: Trace): ZChannel[R1, Any, Any, Any, E1, Chunk[A1], Unit] = {
    def producer(
      queue: Queue[(Take[E1, A1], Promise[Nothing, Unit])],
      ref: Ref[Promise[Nothing, Unit]]
    ): ZChannel[R1, E1, Chunk[A1], Any, Nothing, Nothing, Any] = {
      def terminate(take: Take[E1, A1]): ZChannel[R1, E1, Chunk[A1], Any, Nothing, Nothing, Any] =
        ZChannel.fromZIO {
          for {
            latch <- ref.get
            _     <- latch.await
            p     <- Promise.make[Nothing, Unit]
            _     <- queue.offer((take, p))
            _     <- ref.set(p)
            _     <- p.await
          } yield ()
        }

      ZChannel.readWithCause[R1, E1, Chunk[A1], Any, Nothing, Nothing, Any](
        in =>
          ZChannel.fromZIO {
            for {
              p     <- Promise.make[Nothing, Unit]
              added <- queue.offer((Take.chunk(in), p))
              _     <- ref.set(p).when(added)
            } yield ()
          } *> producer(queue, ref),
        err => terminate(Take.failCause(err)),
        _ => terminate(Take.end)
      )
    }

    def consumer(
      queue: Queue[(Take[E1, A1], Promise[Nothing, Unit])]
    ): ZChannel[R1, Any, Any, Any, E1, Chunk[A1], Unit] = {
      lazy val process: ZChannel[Any, Any, Any, Any, E1, Chunk[A1], Unit] =
        ZChannel.fromZIO(queue.take).flatMap { case (take, promise) =>
          ZChannel.fromZIO(promise.succeed(())) *>
            take.fold(
              ZChannel.unit,
              error => ZChannel.refailCause(error),
              value => ZChannel.write(value) *> process
            )
        }

      process
    }

    ZChannel.unwrapScoped[R1] {
      for {
        queue <- scoped
        start <- Promise.make[Nothing, Unit]
        _     <- start.succeed(())
        ref   <- Ref.make(start)
        _     <- (channel >>> producer(queue, ref)).runScoped.forkScoped
      } yield consumer(queue)
    }
  }

  /**
   * Allows a faster producer to progress independently of a slower consumer by
   * buffering chunks into an unbounded queue.
   */
  def bufferUnbounded(implicit trace: Trace): ZStream[R, E, A] = {
    val queue = self.toQueueUnbounded
    new ZStream(
      ZChannel.unwrapScoped[R] {
        queue.map { queue =>
          lazy val process: ZChannel[Any, Any, Any, Any, E, Chunk[A], Unit] =
            ZChannel.fromZIO {
              queue.take
            }.flatMap { (take: Take[E, A]) =>
              take.fold(
                ZChannel.unit,
                error => ZChannel.refailCause(error),
                value => ZChannel.write(value) *> process
              )
            }

          process
        }
      }
    )
  }

  /**
   * Switches over to the stream produced by the provided function in case this
   * one fails with a typed error.
   */
  def catchAll[R1 <: R, E2, A1 >: A](
    f: E => ZStream[R1, E2, A1]
  )(implicit ev: CanFail[E], trace: Trace): ZStream[R1, E2, A1] =
    catchAllCause(_.failureOrCause.fold(f, ZStream.failCause(_)))

  /**
   * Switches over to the stream produced by the provided function in case this
   * one fails. Allows recovery from all causes of failure, including
   * interruption if the stream is uninterruptible.
   */
  def catchAllCause[R1 <: R, E2, A1 >: A](f: Cause[E] => ZStream[R1, E2, A1])(implicit
    trace: Trace
  ): ZStream[R1, E2, A1] =
    new ZStream(channel.catchAllCause(f(_).channel))

  /**
   * Switches over to the stream produced by the provided function in case this
   * one fails with some typed error.
   */
  def catchSome[R1 <: R, E1 >: E, A1 >: A](pf: PartialFunction[E, ZStream[R1, E1, A1]])(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] =
    catchAll(pf.applyOrElse[E, ZStream[R1, E1, A1]](_, ZStream.fail(_)))

  /**
   * Switches over to the stream produced by the provided function in case this
   * one fails with some errors. Allows recovery from all causes of failure,
   * including interruption if the stream is uninterruptible.
   */
  def catchSomeCause[R1 <: R, E1 >: E, A1 >: A](
    pf: PartialFunction[Cause[E], ZStream[R1, E1, A1]]
  )(implicit trace: Trace): ZStream[R1, E1, A1] =
    catchAllCause(pf.applyOrElse[Cause[E], ZStream[R1, E1, A1]](_, ZStream.failCause(_)))

  /**
   * Returns a new stream that only emits elements that are not equal to the
   * previous element emitted, using natural equality to determine whether two
   * elements are equal.
   */
  def changes(implicit trace: Trace): ZStream[R, E, A] =
    changesWith(_ == _)

  /**
   * Returns a new stream that only emits elements that are not equal to the
   * previous element emitted, using the specified function to determine whether
   * two elements are equal.
   */
  def changesWith(f: (A, A) => Boolean)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.changesWith(f)

  /**
   * Returns a new stream that only emits elements that are not equal to the
   * previous element emitted, using the specified effectual function to
   * determine whether two elements are equal.
   */
  def changesWithZIO[R1 <: R, E1 >: E](
    f: (A, A) => ZIO[R1, E1, Boolean]
  )(implicit trace: Trace): ZStream[R1, E1, A] =
    self >>> ZPipeline.changesWithZIO(f)

  /**
   * Exposes the underlying chunks of the stream as a stream of chunks of
   * elements.
   */
  def chunks(implicit trace: Trace): ZStream[R, E, Chunk[A]] =
    mapChunks(Chunk.single)

  /**
   * Performs the specified stream transformation with the chunk structure of
   * the stream exposed.
   */
  def chunksWith[R1, E1, A1](f: ZStream[R, E, Chunk[A]] => ZStream[R1, E1, Chunk[A1]])(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] =
    f(self.chunks).flattenChunks

  /**
   * Performs a filter and map in a single step.
   */
  def collect[B](f: PartialFunction[A, B])(implicit trace: Trace): ZStream[R, E, B] =
    mapChunks(_.collect(f))

  /**
   * Filters any `Right` values.
   */
  def collectLeft[L1, A1](implicit ev: A <:< Either[L1, A1], trace: Trace): ZStream[R, E, L1] =
    self.asInstanceOf[ZStream[R, E, Either[L1, A1]]] >>> ZPipeline.collectLeft

  /**
   * Filters any 'None' values.
   */
  def collectSome[A1](implicit ev: A <:< Option[A1], trace: Trace): ZStream[R, E, A1] =
    self.asInstanceOf[ZStream[R, E, Option[A1]]] >>> ZPipeline.collectSome[E, A1]

  /**
   * Filters any `Exit.Failure` values.
   */
  def collectSuccess[L1, A1](implicit ev: A <:< Exit[L1, A1], trace: Trace): ZStream[R, E, A1] =
    self.asInstanceOf[ZStream[R, E, Exit[L1, A1]]] >>> ZPipeline.collectSuccess

  /**
   * Filters any `Left` values.
   */
  def collectRight[L1, A1](implicit ev: A <:< Either[L1, A1], trace: Trace): ZStream[R, E, A1] =
    self.asInstanceOf[ZStream[R, E, Either[L1, A1]]] >>> ZPipeline.collectRight

  /**
   * Delays the emission of values by holding new values for a set duration. If
   * no new values arrive during that time the value is emitted, however if a
   * new value is received during the holding period the previous value is
   * discarded and the process is repeated with the new value.
   *
   * This operator is useful if you have a stream of "bursty" events which
   * eventually settle down and you only need the final event of the burst.
   *
   * @example
   *   A search engine may only want to initiate a search after a user has
   *   paused typing so as to not prematurely recommend results.
   */
  def debounce(d: => Duration)(implicit trace: Trace): ZStream[R, E, A] = {
    import DebounceState._
    import HandoffSignal._

    ZStream.unwrapScopedWith { scope =>
      for {
        d       <- ZIO.succeed(d)
        handoff <- ZStream.Handoff.make[HandoffSignal[E, A]]
      } yield {
        def enqueue(last: Chunk[A]) =
          for {
            f <- Clock.sleep(d).as(last).forkIn(scope)
          } yield consumer(Previous(f))

        lazy val producer: ZChannel[R, E, Chunk[A], Any, E, Nothing, Any] =
          ZChannel.readWithCause(
            (in: Chunk[A]) =>
              in.lastOption.fold(producer) { last =>
                ZChannel.fromZIO(handoff.offer(Emit(Chunk.single(last)))) *> producer
              },
            (cause: Cause[E]) => ZChannel.fromZIO(handoff.offer(Halt(cause))),
            (_: Any) => ZChannel.fromZIO(handoff.offer(End(ZStream.SinkEndReason.UpstreamEnd)))
          )

        def consumer(state: DebounceState[E, A]): ZChannel[R, Any, Any, Any, E, Chunk[A], Any] =
          ZChannel.unwrap(
            state match {
              case NotStarted =>
                handoff.take.map {
                  case HandoffSignal.Emit(last)  => ZChannel.unwrap(enqueue(last))
                  case HandoffSignal.Halt(error) => ZChannel.refailCause(error)
                  case HandoffSignal.End(_)      => ZChannel.unit
                }
              case Current(fiber) =>
                fiber.join.map {
                  case HandoffSignal.Emit(last)  => ZChannel.unwrap(enqueue(last))
                  case HandoffSignal.Halt(error) => ZChannel.refailCause(error)
                  case HandoffSignal.End(_)      => ZChannel.unit
                }
              case Previous(fiber) =>
                handoff.take.forkIn(scope).flatMap { handoffFib =>
                  fiber.join
                    .raceWith[R, E, E, HandoffSignal[E, A], ZChannel[
                      R,
                      Any,
                      Any,
                      Any,
                      E,
                      Chunk[A],
                      Any
                    ]](
                      handoffFib.join
                    )(
                      {
                        case (Exit.Success(a), current) =>
                          current.interrupt *> ZIO.succeed(ZChannel.write(a) *> consumer(Current(handoffFib)))
                        case (Exit.Failure(cause), current) =>
                          current.interrupt as ZChannel.refailCause(cause)
                      },
                      {
                        case (Exit.Success(Emit(last)), previous) =>
                          previous.interrupt *> enqueue(last)
                        case (Exit.Success(Halt(cause)), previous) =>
                          previous.interrupt as ZChannel.refailCause(cause)
                        case (Exit.Success(End(_)), previous) =>
                          previous.join.map(ZChannel.write(_) *> ZChannel.unit)
                        case (Exit.Failure(cause), previous) =>
                          previous.interrupt as ZChannel.refailCause(cause)
                      }
                    )
                }
            }
          )

        ZStream.scopedWith(scope => (self.channel >>> producer).runIn(scope).forkIn(scope)) *>
          new ZStream(consumer(NotStarted))
      }
    }
  }

  /**
   * Taps the stream, printing the result of calling `.toString` on the emitted
   * values.
   */
  def debug(implicit trace: Trace): ZStream[R, E, A] =
    self
      .tap(a => ZIO.debug(a))
      .tapErrorCause(e => ZIO.debug(s": $e"))

  /**
   * Taps the stream, printing the result of calling `.toString` on the emitted
   * values. Prefixes the output with the given label.
   */
  def debug(label: String)(implicit trace: Trace): ZStream[R, E, A] =
    self
      .tap(a => ZIO.debug(s"$label: $a"))
      .tapErrorCause(e => ZIO.debug(s" $label: $e"))

  /**
   * Creates a pipeline that groups on adjacent keys, calculated by function f.
   */
  def groupAdjacentBy[K](
    f: A => K
  )(implicit trace: Trace): ZStream[R, E, (K, NonEmptyChunk[A])] =
    self >>> ZPipeline.groupAdjacentBy(f)

  /**
   * Performs an effectful filter and map in a single step.
   */
  def collectZIO[R1 <: R, E1 >: E, A1](pf: PartialFunction[A, ZIO[R1, E1, A1]])(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] = {

    def loop(chunkIterator: Chunk.ChunkIterator[A], index: Int): ZChannel[R1, E, Chunk[A], Any, E1, Chunk[A1], Any] =
      if (chunkIterator.hasNextAt(index))
        ZChannel.unwrap {
          val a = chunkIterator.nextAt(index)
          pf.andThen(_.map(a1 => ZChannel.write(Chunk.single(a1)) *> loop(chunkIterator, index + 1)))
            .applyOrElse(a, (_: A) => ZIO.succeed(loop(chunkIterator, index + 1)))
        }
      else
        ZChannel.readWithCause(
          elem => loop(elem.chunkIterator, 0),
          err => ZChannel.refailCause(err),
          done => ZChannel.succeed(done)
        )

    new ZStream(self.channel >>> loop(Chunk.ChunkIterator.empty, 0))
  }

  /**
   * Transforms all elements of the stream for as long as the specified partial
   * function is defined.
   */
  def collectWhile[A1](pf: PartialFunction[A, A1])(implicit trace: Trace): ZStream[R, E, A1] =
    self >>> ZPipeline.collectWhile(pf)

  /**
   * Terminates the stream when encountering the first `Right`.
   */
  def collectWhileLeft[L1, A1](implicit ev: A <:< Either[L1, A1], trace: Trace): ZStream[R, E, L1] = {
    val _ = ev
    self.asInstanceOf[ZStream[R, E, Either[L1, A1]]].collectWhile { case Left(a) => a }
  }

  /**
   * Terminates the stream when encountering the first `None`.
   */
  def collectWhileSome[A1](implicit ev: A <:< Option[A1], trace: Trace): ZStream[R, E, A1] = {
    val _ = ev
    self.asInstanceOf[ZStream[R, E, Option[A1]]].collectWhile { case Some(a) => a }
  }

  /**
   * Terminates the stream when encountering the first `Left`.
   */
  def collectWhileRight[L1, A1](implicit ev: A <:< Either[L1, A1], trace: Trace): ZStream[R, E, A1] = {
    val _ = ev
    self.asInstanceOf[ZStream[R, E, Either[L1, A1]]].collectWhile { case Right(a) => a }
  }

  /**
   * Terminates the stream when encountering the first `Exit.Failure`.
   */
  def collectWhileSuccess[L1, A1](implicit ev: A <:< Exit[L1, A1], trace: Trace): ZStream[R, E, A1] = {
    val _ = ev
    self.asInstanceOf[ZStream[R, E, Exit[L1, A1]]].collectWhile { case Exit.Success(a) => a }
  }

  /**
   * Effectfully transforms all elements of the stream for as long as the
   * specified partial function is defined.
   */
  def collectWhileZIO[R1 <: R, E1 >: E, A1](
    pf: PartialFunction[A, ZIO[R1, E1, A1]]
  )(implicit trace: Trace): ZStream[R1, E1, A1] =
    self >>> ZPipeline.collectWhileZIO(pf)

  /**
   * Combines the elements from this stream and the specified stream by
   * repeatedly applying the function `f` to extract an element using both sides
   * and conceptually "offer" it to the destination stream. `f` can maintain
   * some internal state to control the combining process, with the initial
   * state being specified by `s`.
   *
   * Where possible, prefer [[ZStream#combineChunks]] for a more efficient
   * implementation.
   */
  def combine[R1 <: R, E1 >: E, S, A2, A3](that: => ZStream[R1, E1, A2])(s: => S)(
    f: (S, ZIO[R, Option[E], A], ZIO[R1, Option[E1], A2]) => ZIO[R1, Nothing, Exit[Option[E1], (A3, S)]]
  )(implicit trace: Trace): ZStream[R1, E1, A3] = {
    def producer[Err, Elem](
      handoff: ZStream.Handoff[Exit[Option[Err], Elem]],
      latch: ZStream.Handoff[Unit]
    ): ZChannel[R1, Err, Elem, Any, Nothing, Nothing, Any] =
      ZChannel.fromZIO(latch.take) *>
        ZChannel.readWithCause[R1, Err, Elem, Any, Nothing, Nothing, Any](
          value => ZChannel.fromZIO(handoff.offer(Exit.succeed(value))) *> producer(handoff, latch),
          cause => ZChannel.fromZIO(handoff.offer(Exit.failCause(cause.map(Some(_))))),
          _ => ZChannel.fromZIO(handoff.offer(Exit.failNone)) *> producer(handoff, latch)
        )

    new ZStream(
      ZChannel.unwrapScopedWith { scope =>
        for {
          left     <- ZStream.Handoff.make[Exit[Option[E], A]]
          right    <- ZStream.Handoff.make[Exit[Option[E1], A2]]
          latchL   <- ZStream.Handoff.make[Unit]
          latchR   <- ZStream.Handoff.make[Unit]
          _        <- (self.channel.concatMap(ZChannel.writeChunk(_)) >>> producer(left, latchL)).runIn(scope).forkIn(scope)
          _        <- (that.channel.concatMap(ZChannel.writeChunk(_)) >>> producer(right, latchR)).runIn(scope).forkIn(scope)
          pullLeft  = latchL.offer(()) *> left.take.flatMap(ZIO.done(_))
          pullRight = latchR.offer(()) *> right.take.flatMap(ZIO.done(_))
        } yield ZStream.unfoldZIO(s)(s => f(s, pullLeft, pullRight).flatMap(ZIO.done(_).unsome)).channel
      }
    )
  }

  /**
   * Combines the chunks from this stream and the specified stream by repeatedly
   * applying the function `f` to extract a chunk using both sides and
   * conceptually "offer" it to the destination stream. `f` can maintain some
   * internal state to control the combining process, with the initial state
   * being specified by `s`.
   */
  def combineChunks[R1 <: R, E1 >: E, S, A2, A3](that: => ZStream[R1, E1, A2])(s: => S)(
    f: (
      S,
      ZIO[R, Option[E], Chunk[A]],
      ZIO[R1, Option[E1], Chunk[A2]]
    ) => ZIO[R1, Nothing, Exit[Option[E1], (Chunk[A3], S)]]
  )(implicit trace: Trace): ZStream[R1, E1, A3] = {
    def producer[Err, Elem](
      handoff: ZStream.Handoff[Take[Err, Elem]],
      latch: ZStream.Handoff[Unit]
    ): ZChannel[R1, Err, Chunk[Elem], Any, Nothing, Nothing, Any] =
      ZChannel.fromZIO(latch.take) *>
        ZChannel.readWithCause[R1, Err, Chunk[Elem], Any, Nothing, Nothing, Any](
          chunk => ZChannel.fromZIO(handoff.offer(Take.chunk(chunk))) *> producer(handoff, latch),
          cause => ZChannel.fromZIO(handoff.offer(Take.failCause(cause))),
          _ => ZChannel.fromZIO(handoff.offer(Take.end))
        )

    new ZStream(
      ZChannel.unwrapScopedWith { scope =>
        for {
          left     <- ZStream.Handoff.make[Take[E, A]]
          right    <- ZStream.Handoff.make[Take[E1, A2]]
          latchL   <- ZStream.Handoff.make[Unit]
          latchR   <- ZStream.Handoff.make[Unit]
          _        <- (self.channel >>> producer(left, latchL)).runIn(scope).forkIn(scope)
          _        <- (that.channel >>> producer(right, latchR)).runIn(scope).forkIn(scope)
          pullLeft  = latchL.offer(()) *> left.take.flatMap(_.done)
          pullRight = latchR.offer(()) *> right.take.flatMap(_.done)
        } yield ZStream.unfoldChunkZIO(s)(s => f(s, pullLeft, pullRight).flatMap(ZIO.done(_).unsome)).channel
      }
    )
  }

  /**
   * Concatenates the specified stream with this stream, resulting in a stream
   * that emits the elements from this stream and then the elements from the
   * specified stream.
   */
  def concat[R1 <: R, E1 >: E, A1 >: A](that: => ZStream[R1, E1, A1])(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] =
    new ZStream(channel *> that.channel)

  /**
   * Composes this stream with the specified stream to create a cartesian
   * product of elements. The `that` stream would be run multiple times, for
   * every element in the `this` stream.
   *
   * See also [[ZStream#zip]] and [[ZStream#<&>]] for the more common point-wise
   * variant.
   */
  def cross[R1 <: R, E1 >: E, B](that: => ZStream[R1, E1, B])(implicit
    zippable: Zippable[A, B],
    trace: Trace
  ): ZStream[R1, E1, zippable.Out] =
    crossWith(that)((a, b) => zippable.zip(a, b))

  /**
   * Composes this stream with the specified stream to create a cartesian
   * product of elements, but keeps only elements from this stream. The `that`
   * stream would be run multiple times, for every element in the `this` stream.
   *
   * See also [[ZStream#zip]] and [[ZStream#<&>]] for the more common point-wise
   * variant.
   */
  def crossLeft[R1 <: R, E1 >: E, B](that: => ZStream[R1, E1, B])(implicit
    trace: Trace
  ): ZStream[R1, E1, A] =
    crossWith(that)((a, _) => a)

  /**
   * Composes this stream with the specified stream to create a cartesian
   * product of elements, but keeps only elements from the other stream. The
   * `that` stream would be run multiple times, for every element in the `this`
   * stream.
   *
   * See also [[ZStream#zip]] and [[ZStream#<&>]] for the more common point-wise
   * variant.
   */
  def crossRight[R1 <: R, E1 >: E, B](that: => ZStream[R1, E1, B])(implicit trace: Trace): ZStream[R1, E1, B] =
    self.flatMap(_ => that)

  /**
   * Composes this stream with the specified stream to create a cartesian
   * product of elements with a specified function. The `that` stream would be
   * run multiple times, for every element in the `this` stream.
   *
   * See also [[ZStream#zip]] and [[ZStream#<&>]] for the more common point-wise
   * variant.
   */
  def crossWith[R1 <: R, E1 >: E, A2, C](that: => ZStream[R1, E1, A2])(f: (A, A2) => C)(implicit
    trace: Trace
  ): ZStream[R1, E1, C] =
    self.flatMap(l => that.map(r => f(l, r)))

  /**
   * More powerful version of `ZStream#broadcast`. Allows to provide a function
   * that determines what queues should receive which elements. The decide
   * function will receive the indices of the queues in the resulting list.
   */
  def distributedWith[E1 >: E](
    n: => Int,
    maximumLag: => Int,
    decide: A => UIO[Int => Boolean]
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, List[Dequeue[Exit[Option[E1], A]]]] =
    Promise.make[Nothing, A => UIO[UniqueKey => Boolean]].flatMap { prom =>
      distributedWithDynamic(maximumLag, (a: A) => prom.await.flatMap(_(a)), _ => ZIO.unit).flatMap { next =>
        ZIO.collectAll {
          Range(0, n).map(id => next.map { case (key, queue) => ((key -> id), queue) })
        }.flatMap { entries =>
          val (mappings, queues) =
            entries.foldRight((Map.empty[UniqueKey, Int], List.empty[Dequeue[Exit[Option[E1], A]]])) {
              case ((mapping, queue), (mappings, queues)) =>
                (mappings + mapping, queue :: queues)
            }
          prom.succeed((a: A) => decide(a).map(f => (key: UniqueKey) => f(mappings(key)))).as(queues)
        }
      }
    }

  /**
   * More powerful version of `ZStream#distributedWith`. This returns a function
   * that will produce new queues and corresponding indices. You can also
   * provide a function that will be executed after the final events are
   * enqueued in all queues. Shutdown of the queues is handled by the driver.
   * Downstream users can also shutdown queues manually. In this case the driver
   * will continue but no longer backpressure on them.
   */
  def distributedWithDynamic(
    maximumLag: => Int,
    decide: A => UIO[UniqueKey => Boolean],
    done: Exit[Option[E], Nothing] => UIO[Any] = (_: Any) => ZIO.unit
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, UIO[(UniqueKey, Dequeue[Exit[Option[E], A]])]] =
    for {
      queuesRef <- ZIO.acquireReleaseExit(Ref.make[Map[UniqueKey, Queue[Exit[Option[E], A]]]](Map()))((map, exit) =>
                     map.get.flatMap(qs => ZIO.foreach(qs.values)(_.shutdown))
                   )
      add <- {
        val offer = (a: A) =>
          for {
            shouldProcess <- decide(a)
            queues        <- queuesRef.get
            _ <- ZIO
                   .foldLeft(queues)(List[UniqueKey]()) { case (acc, (id, queue)) =>
                     if (shouldProcess(id)) {
                       queue
                         .offer(Exit.succeed(a))
                         .foldCauseZIO(
                           {
                             // we ignore all downstream queues that were shut down and remove them later
                             case c if c.isInterrupted => ZIO.succeed(id :: acc)
                             case c                    => Exit.failCause(c)
                           },
                           _ => ZIO.succeed(acc)
                         )
                     } else ZIO.succeed(acc)
                   }
                   .flatMap(ids => if (ids.nonEmpty) queuesRef.update(_ -- ids) else ZIO.unit)
          } yield ()

        for {
          queuesLock <- Semaphore.make(1)
          newQueue <- Ref
                        .make[UIO[(UniqueKey, Queue[Exit[Option[E], A]])]] {
                          for {
                            queue <- Queue.bounded[Exit[Option[E], A]](maximumLag)
                            id     = UniqueKey()
                            _     <- queuesRef.update(_.updated(id, queue))
                          } yield (id, queue)
                        }
          finalize = (endTake: Exit[Option[E], Nothing]) =>
                       // we need to make sure that no queues are currently being added
                       queuesLock.withPermit {
                         for {
                           // all newly created queues should end immediately
                           _ <- newQueue.set {
                                  for {
                                    queue <- Queue.bounded[Exit[Option[E], A]](1)
                                    _     <- queue.offer(endTake)
                                    id     = UniqueKey()
                                    _     <- queuesRef.update(_.updated(id, queue))
                                  } yield (id, queue)
                                }
                           queues <- queuesRef.get.map(_.values)
                           _ <- ZIO.foreach(queues) { queue =>
                                  queue.offer(endTake).catchSomeCause {
                                    case c if c.isInterrupted => ZIO.unit
                                  }
                                }
                           _ <- done(endTake)
                         } yield ()
                       }
          _ <- self
                 .runForeachScoped(offer)
                 .foldCauseZIO(
                   cause => finalize(Exit.failCause(cause.map(Some(_)))),
                   _ => finalize(Exit.failNone)
                 )
                 .forkScoped
        } yield queuesLock.withPermit(newQueue.get.flatten)
      }
    } yield add

  /**
   * Converts this stream to a stream that executes its effects but emits no
   * elements. Useful for sequencing effects using streams:
   *
   * {{{
   * (Stream(1, 2, 3).tap(i => ZIO(println(i))) ++
   *   Stream.fromZIO(ZIO(println("Done!"))).drain ++
   *   Stream(4, 5, 6).tap(i => ZIO(println(i)))).run(Sink.drain)
   * }}}
   */
  def drain(implicit trace: Trace): ZStream[R, E, Nothing] =
    new ZStream(channel.drain)

  /**
   * Drains the provided stream in the background for as long as this stream is
   * running. If this stream ends before `other`, `other` will be interrupted.
   * If `other` fails, this stream will fail with that error.
   */
  def drainFork[R1 <: R, E1 >: E](
    other: => ZStream[R1, E1, Any]
  )(implicit trace: Trace): ZStream[R1, E1, A] =
    ZStream.fromZIO(Promise.make[E1, Nothing]).flatMap { bgDied =>
      ZStream
        .scopedWith(scope => other.channel.drain.runIn(scope).catchAllCause(bgDied.failCause(_)).forkIn(scope)) *>
        self.interruptWhen(bgDied)
    }

  /**
   * Drops the specified number of elements from this stream.
   */
  def drop(n: => Int)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.drop(n)

  /**
   * Drops the last specified number of elements from this stream.
   *
   * @note
   *   This combinator keeps `n` elements in memory. Be careful with big
   *   numbers.
   */
  def dropRight(n: => Int)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.dropRight(n)

  /**
   * Drops all elements of the stream for as long as the specified predicate
   * evaluates to `true`.
   */
  def dropWhile(f: A => Boolean)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.dropWhile(f)

  /**
   * Drops all elements of the stream until the specified predicate evaluates to
   * `true`.
   */
  def dropUntil(pred: A => Boolean)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.dropUntil(pred)

  /**
   * Drops all elements of the stream for as long as the specified predicate
   * produces an effect that evalutates to `true`
   *
   * @see
   *   [[dropWhile]]
   */
  def dropWhileZIO[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Boolean])(implicit
    trace: Trace
  ): ZStream[R1, E1, A] =
    pipeThrough(ZSink.dropWhileZIO[R1, E1, A](f))

  /**
   * Returns a stream whose failures and successes have been lifted into an
   * `Either`. The resulting stream cannot fail, because the failures have been
   * exposed as part of the `Either` success case.
   *
   * @note
   *   the stream will end as soon as the first error occurs.
   */
  def either(implicit ev: CanFail[E], trace: Trace): ZStream[R, Nothing, Either[E, A]] =
    self.map(Right(_)).catchAll(e => ZStream(Left(e)))

  /**
   * Executes the provided finalizer after this stream's finalizers run.
   */
  def ensuring[R1 <: R](fin: => ZIO[R1, Nothing, Any])(implicit trace: Trace): ZStream[R1, E, A] =
    new ZStream(channel.ensuring(fin))

  /**
   * Executes the provided finalizer after this stream's finalizers run.
   */
  def ensuringWith[R1 <: R](fin: Exit[E, Any] => ZIO[R1, Nothing, Any])(implicit trace: Trace): ZStream[R1, E, A] =
    new ZStream(channel.ensuringWith(fin))

  /**
   * Filters the elements emitted by this stream using the provided function.
   */
  def filter(f: A => Boolean)(implicit trace: Trace): ZStream[R, E, A] =
    mapChunks(_.filter(f))

  /**
   * Finds the first element emitted by this stream that satisfies the provided
   * predicate.
   */
  def find(f: A => Boolean)(implicit trace: Trace): ZStream[R, E, A] = {
    lazy val loop: ZChannel[R, E, Chunk[A], Any, E, Chunk[A], Any] =
      ZChannel.readWithCause(
        (in: Chunk[A]) => in.find(f).fold(loop)(i => ZChannel.write(Chunk.single(i))),
        (e: Cause[E]) => ZChannel.refailCause(e),
        (_: Any) => ZChannel.unit
      )

    new ZStream(self.channel >>> loop)
  }

  /**
   * Finds the first element emitted by this stream that satisfies the provided
   * effectful predicate.
   */
  def findZIO[R1 <: R, E1 >: E, S](
    f: A => ZIO[R1, E1, Boolean]
  )(implicit trace: Trace): ZStream[R1, E1, A] = {
    lazy val loop: ZChannel[R1, E, Chunk[A], Any, E1, Chunk[A], Any] =
      ZChannel.readWithCause(
        (in: Chunk[A]) => ZChannel.unwrap(in.findZIO(f).map(_.fold(loop)(i => ZChannel.write(Chunk.single(i))))),
        (e: Cause[E]) => ZChannel.refailCause(e),
        (_: Any) => ZChannel.unit
      )

    new ZStream(self.channel >>> loop)
  }

  /**
   * Consumes all elements of the stream, passing them to the specified
   * callback.
   */
  def foreach[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Any])(implicit trace: Trace): ZIO[R1, E1, Unit] =
    runForeach(f)

  /**
   * Repeats this stream forever.
   */
  def forever(implicit trace: Trace): ZStream[R, E, A] =
    new ZStream(channel.repeated)

  /**
   * Effectfully filters the elements emitted by this stream.
   */
  def filterZIO[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Boolean])(implicit trace: Trace): ZStream[R1, E1, A] = {

    def loop(chunkIterator: Chunk.ChunkIterator[A], index: Int): ZChannel[R1, E, Chunk[A], Any, E1, Chunk[A], Any] =
      if (chunkIterator.hasNextAt(index))
        ZChannel.unwrap {
          val a = chunkIterator.nextAt(index)
          f(a).map { b =>
            if (b) ZChannel.write(Chunk.single(a)) *> loop(chunkIterator, index + 1)
            else loop(chunkIterator, index + 1)
          }
        }
      else
        ZChannel.readWithCause(
          elem => loop(elem.chunkIterator, 0),
          err => ZChannel.refailCause(err),
          done => ZChannel.succeed(done)
        )

    new ZStream(self.channel >>> loop(Chunk.ChunkIterator.empty, 0))
  }

  /**
   * Filters this stream by the specified predicate, removing all elements for
   * which the predicate evaluates to true.
   */
  def filterNot(pred: A => Boolean)(implicit trace: Trace): ZStream[R, E, A] = filter(a => !pred(a))

  /**
   * Returns a stream made of the concatenation in strict order of all the
   * streams produced by passing each element of this stream to `f0`
   */
  def flatMap[R1 <: R, E1 >: E, B](f: A => ZStream[R1, E1, B])(implicit
    trace: Trace
  ): ZStream[R1, E1, B] =
    new ZStream(
      channel.concatMap(as =>
        as.foldLeft[ZChannel[R1, Any, Any, Any, E1, zio.Chunk[B], Any]](ZChannel.unit) { case (acc, a) =>
          acc *> f(a).channel
        }
      )
    )

  /**
   * Maps each element of this stream to another stream and returns the
   * non-deterministic merge of those streams, executing up to `n` inner streams
   * concurrently. Up to `bufferSize` elements of the produced streams may be
   * buffered in memory by this operator.
   */
  def flatMapPar[R1 <: R, E1 >: E, B](n: => Int, bufferSize: => Int = 16)(
    f: A => ZStream[R1, E1, B]
  )(implicit trace: Trace): ZStream[R1, E1, B] =
    new ZStream[R1, E1, B](
      channel.concatMap(ZChannel.writeChunk(_)).mergeMap[R1, Any, Any, Any, E1, Chunk[B]](n, bufferSize) {
        f(_).channel
      }
    )

  /**
   * Maps each element of this stream to another stream and returns the
   * non-deterministic merge of those streams, executing up to `n` inner streams
   * concurrently. When a new stream is created from an element of the source
   * stream, the oldest executing stream is cancelled. Up to `bufferSize`
   * elements of the produced streams may be buffered in memory by this
   * operator.
   */
  def flatMapParSwitch[R1 <: R, E1 >: E, B](n: => Int, bufferSize: => Int = 16)(
    f: A => ZStream[R1, E1, B]
  )(implicit trace: Trace): ZStream[R1, E1, B] =
    new ZStream[R1, E1, B](
      channel
        .concatMap(ZChannel.writeChunk(_))
        .mergeMap[R1, Any, Any, Any, E1, Chunk[B]](n, bufferSize, ZChannel.MergeStrategy.BufferSliding) {
          f(_).channel
        }
    )

  /**
   * Flattens this stream-of-streams into a stream made of the concatenation in
   * strict order of all the streams.
   */
  def flatten[R1 <: R, E1 >: E, A1](implicit ev: A <:< ZStream[R1, E1, A1], trace: Trace): ZStream[R1, E1, A1] =
    flatMap(ev(_))

  /**
   * Submerges the chunks carried by this stream into the stream's structure,
   * while still preserving them.
   */
  def flattenChunks[A1](implicit ev: A <:< Chunk[A1], trace: Trace): ZStream[R, E, A1] = {

    lazy val flatten: ZChannel[Any, E, Chunk[Chunk[A1]], Any, E, Chunk[A1], Any] =
      ZChannel.readWithCause(
        chunks => ZChannel.writeChunk(chunks) *> flatten,
        cause => ZChannel.refailCause(cause),
        _ => ZChannel.unit
      )

    new ZStream(self.asInstanceOf[ZStream[R, E, Chunk[A1]]].channel >>> flatten)
  }

  /**
   * Flattens [[Exit]] values. `Exit.Failure` values translate to stream
   * failures while `Exit.Success` values translate to stream elements.
   */
  def flattenExit[E1 >: E, A1](implicit ev: A <:< Exit[E1, A1], trace: Trace): ZStream[R, E1, A1] =
    mapZIO(a => ZIO.done(ev(a)))

  /**
   * Unwraps [[Exit]] values that also signify end-of-stream by failing with
   * `None`.
   *
   * For `Exit[E, A]` values that do not signal end-of-stream, prefer:
   * {{{
   * stream.mapZIO(ZIO.done(_))
   * }}}
   */
  def flattenExitOption[E1 >: E, A1](implicit
    ev: A <:< Exit[Option[E1], A1],
    trace: Trace
  ): ZStream[R, E1, A1] = {
    def processChunk(
      chunk: Chunk[Exit[Option[E1], A1]],
      cont: ZChannel[R, E, Chunk[Exit[Option[E1], A1]], Any, E1, Chunk[A1], Any]
    ): ZChannel[R, E, Chunk[Exit[Option[E1], A1]], Any, E1, Chunk[A1], Any] = {
      val (toEmit, rest) = chunk.splitWhere(!_.isSuccess)
      val next = rest.headOption match {
        case Some(Exit.Success(_)) => ZChannel.unit
        case Some(Exit.Failure(cause)) =>
          Cause.flipCauseOption(cause) match {
            case Some(cause) => ZChannel.refailCause(cause)
            case None        => ZChannel.unit
          }
        case None => cont
      }
      ZChannel.write(toEmit.collect { case Exit.Success(a) => a }) *> next
    }

    lazy val process: ZChannel[R, E, Chunk[Exit[Option[E1], A1]], Any, E1, Chunk[A1], Any] =
      ZChannel.readWithCause[R, E, Chunk[Exit[Option[E1], A1]], Any, E1, Chunk[A1], Any](
        chunk => processChunk(chunk, process),
        cause => ZChannel.refailCause(cause),
        _ => ZChannel.unit
      )

    new ZStream(channel.asInstanceOf[ZChannel[R, Any, Any, Any, E, Chunk[Exit[Option[E1], A1]], Any]] >>> process)
  }

  /**
   * Submerges the iterables carried by this stream into the stream's structure,
   * while still preserving them.
   */
  def flattenIterables[A1](implicit ev: A <:< Iterable[A1], trace: Trace): ZStream[R, E, A1] =
    map(a => Chunk.fromIterable(ev(a))).flattenChunks

  /**
   * Flattens a stream of streams into a stream by executing a non-deterministic
   * concurrent merge. Up to `n` streams may be consumed in parallel and up to
   * `outputBuffer` elements may be buffered by this operator.
   */
  def flattenPar[R1 <: R, E1 >: E, A1](n: => Int, outputBuffer: => Int = 16)(implicit
    ev: A <:< ZStream[R1, E1, A1],
    trace: Trace
  ): ZStream[R1, E1, A1] =
    flatMapPar[R1, E1, A1](n, outputBuffer)(ev(_))

  /**
   * Like [[flattenPar]], but executes all streams concurrently.
   */
  def flattenParUnbounded[R1 <: R, E1 >: E, A1](
    outputBuffer: => Int = 16
  )(implicit ev: A <:< ZStream[R1, E1, A1], trace: Trace): ZStream[R1, E1, A1] =
    flattenPar[R1, E1, A1](Int.MaxValue, outputBuffer)

  /**
   * Unwraps [[Exit]] values and flatten chunks that also signify end-of-stream
   * by failing with `None`.
   */
  def flattenTake[E1 >: E, A1](implicit ev: A <:< Take[E1, A1], trace: Trace): ZStream[R, E1, A1] =
    self.asInstanceOf[ZStream[R, E, Take[E1, A1]]] >>> ZPipeline.flattenTake

  def flattenZIO[R1 <: R, E1 >: E, A1](implicit ev: A <:< ZIO[R1, E1, A1], trace: Trace): ZStream[R1, E1, A1] =
    mapZIO(ev)

  /**
   * More powerful version of [[ZStream.groupByKey]]
   */
  def groupBy[R1 <: R, E1 >: E, K, V](
    f: A => ZIO[R1, E1, (K, V)],
    buffer: => Int = 16
  ): ZStream.GroupBy[R1, E1, K, V] =
    new ZStream.GroupBy[R1, E1, K, V] {
      def grouped(implicit trace: Trace): ZStream[R1, E1, (K, Dequeue[Take[E1, V]])] =
        ZStream.unwrapScoped[R1] {
          for {
            decider <- Promise.make[Nothing, (K, V) => UIO[UniqueKey => Boolean]]
            out <-
              ZIO.acquireRelease(Queue.bounded[Exit[Option[E1], (K, Dequeue[Take[E1, V]])]](buffer))(_.shutdown)
            ref <- Ref.make[Map[K, UniqueKey]](Map())
            add <- self
                     .mapZIO(f)
                     .distributedWithDynamic(
                       buffer,
                       (kv: (K, V)) => decider.await.flatMap(_.tupled(kv)),
                       out.offer
                     )
            _ <- decider.succeed { case (k, _) =>
                   ref.get.map(_.get(k)).flatMap {
                     case Some(idx) => ZIO.succeed(_ == idx)
                     case None =>
                       add.flatMap { case (idx, q) =>
                         (ref.update(_.updated(k, idx)) *>
                           out.offer(
                             Exit.succeed(
                               k -> ZStream.mapDequeue(q)(exit =>
                                 Take(exit.mapExit { case (_, value) => Chunk.single(value) })
                               )
                             )
                           )).as(_ == idx)
                       }
                   }
                 }
          } yield ZStream.fromQueueWithShutdown(out).flattenExitOption
        }
    }

  /**
   * Partition a stream using a function and process each stream individually.
   * This returns a data structure that can be used to further filter down which
   * groups shall be processed.
   *
   * After calling apply on the GroupBy object, the remaining groups will be
   * processed in parallel and the resulting streams merged in a
   * nondeterministic fashion.
   *
   * Up to `buffer` elements may be buffered in any group stream before the
   * producer is backpressured. Take care to consume from all streams in order
   * to prevent deadlocks.
   *
   * Example: Collect the first 2 words for every starting letter from a stream
   * of words.
   * {{{
   * ZStream.fromIterable(List("hello", "world", "hi", "holla"))
   *   .groupByKey(_.head) { case (k, s) => s.take(2).map((k, _)) }
   *   .runCollect
   *   .map(_ == List(('h', "hello"), ('h', "hi"), ('w', "world"))
   * }}}
   */
  def groupByKey[K](
    f: A => K,
    buffer: => Int = 16
  ): ZStream.GroupBy[R, E, K, A] =
    new ZStream.GroupBy[R, E, K, A] {
      def grouped(implicit trace: Trace): ZStream[R, E, (K, Dequeue[Take[E, A]])] = {

        def groupByKey(
          map: mutable.Map[K, Queue[Take[E, A]]],
          outerQueue: Queue[Take[E, (K, Queue[Take[E, A]])]]
        ): ZChannel[R, E, Chunk[A], Any, E, Any, Any] =
          ZChannel.readWithCause(
            in =>
              ZChannel.fromZIO {
                ZIO.foreachDiscard(ZStream.groupBy(in)(f)) { case (key, values) =>
                  map.get(key) match {
                    case Some(innerQueue) =>
                      innerQueue.offer(Take.chunk(values)).catchSomeCause {
                        case cause if cause.isInterruptedOnly => ZIO.unit
                      }
                    case None =>
                      Queue.bounded[Take[E, A]](buffer).flatMap { innerQueue =>
                        ZIO.succeed(map += key -> innerQueue) *>
                          outerQueue.offer(Take.single(key -> innerQueue)) *>
                          innerQueue.offer(Take.chunk(values)).catchSomeCause {
                            case cause if cause.isInterruptedOnly => ZIO.unit
                          }
                      }
                  }
                }
              } *> groupByKey(map, outerQueue),
            e => ZChannel.fromZIO(outerQueue.offer(Take.failCause(e))),
            _ =>
              ZChannel.fromZIO {
                ZIO.foreachDiscard(map) { case (_, innerQueue) =>
                  innerQueue.offer(Take.end).catchSomeCause { case cause if cause.isInterruptedOnly => ZIO.unit }
                } *> outerQueue.offer(Take.end)
              }
          )

        ZStream.unwrapScopedWith { scope =>
          for {
            map   <- ZIO.succeed(mutable.Map.empty[K, Queue[Take[E, A]]])
            queue <- Queue.unbounded[Take[E, (K, Queue[Take[E, A]])]]
            _     <- scope.addFinalizer(queue.shutdown)
            _     <- (self.channel >>> groupByKey(map, queue)).drain.runIn(scope).forkIn(scope)
          } yield ZStream.fromQueueWithShutdown(queue).flattenTake
        }
      }
    }

  /**
   * Partitions the stream with specified chunkSize
   * @param chunkSize
   *   size of the chunk
   */
  def grouped(chunkSize: => Int)(implicit trace: Trace): ZStream[R, E, Chunk[A]] =
    self >>> ZPipeline.grouped(chunkSize)

  /**
   * Partitions the stream with the specified chunkSize or until the specified
   * duration has passed, whichever is satisfied first.
   */
  def groupedWithin(chunkSize: => Int, within: => Duration)(implicit
    trace: Trace
  ): ZStream[R, E, Chunk[A]] =
    aggregateAsyncWithin(ZSink.collectAllN[A](chunkSize), Schedule.spaced(within))

  /**
   * Halts the evaluation of this stream when the provided IO completes. The
   * given IO will be forked as part of the returned stream, and its success
   * will be discarded.
   *
   * An element in the process of being pulled will not be interrupted when the
   * IO completes. See `interruptWhen` for this behavior.
   *
   * If the IO completes with a failure, the stream will emit that failure.
   */
  def haltWhen[R1 <: R, E1 >: E](io: ZIO[R1, E1, Any])(implicit trace: Trace): ZStream[R1, E1, A] = {
    def writer(fiber: Fiber[E1, Any]): ZChannel[R1, E1, Chunk[A], Any, E1, Chunk[A], Unit] =
      ZChannel.unwrap {
        fiber.poll.map {
          case None =>
            ZChannel.readWith[R1, E1, Chunk[A], Any, E1, Chunk[A], Unit](
              in => ZChannel.write(in) *> writer(fiber),
              err => ZChannel.fail(err),
              _ => ZChannel.unit
            )

          case Some(exit) =>
            exit.foldExit(ZChannel.refailCause, _ => ZChannel.unit)
        }
      }

    new ZStream(
      ZChannel.unwrapScopedWith { scope =>
        io.forkIn(scope).map { fiber =>
          self.channel >>> writer(fiber)
        }
      }
    )
  }

  /**
   * Specialized version of haltWhen which halts the evaluation of this stream
   * after the given duration.
   *
   * An element in the process of being pulled will not be interrupted when the
   * given duration completes. See `interruptAfter` for this behavior.
   */
  def haltAfter(duration: => Duration)(implicit trace: Trace): ZStream[R, E, A] =
    haltWhen(Clock.sleep(duration))

  /**
   * Halts the evaluation of this stream when the provided promise resolves.
   *
   * If the promise completes with a failure, the stream will emit that failure.
   */
  def haltWhen[E1 >: E](p: Promise[E1, _])(implicit trace: Trace): ZStream[R, E1, A] = {
    lazy val writer: ZChannel[R, E1, Chunk[A], Any, E1, Chunk[A], Unit] =
      ZChannel.unwrap {
        p.poll.map {
          case None =>
            ZChannel.readWith[R, E1, Chunk[A], Any, E1, Chunk[A], Unit](
              in => ZChannel.write(in) *> writer,
              err => ZChannel.fail(err),
              _ => ZChannel.unit
            )

          case Some(io) =>
            ZChannel.unwrap(io.fold(ZChannel.fail(_), _ => ZChannel.unit))
        }
      }

    new ZStream(self.channel >>> writer)
  }

  /**
   * Interleaves this stream and the specified stream deterministically by
   * alternating pulling values from this stream and the specified stream. When
   * one stream is exhausted all remaining values in the other stream will be
   * pulled.
   */
  def interleave[R1 <: R, E1 >: E, A1 >: A](that: => ZStream[R1, E1, A1])(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] =
    self.interleaveWith(that)(ZStream(true, false).forever)

  /**
   * Combines this stream and the specified stream deterministically using the
   * stream of boolean values `b` to control which stream to pull from next.
   * `true` indicates to pull from this stream and `false` indicates to pull
   * from the specified stream. Only consumes as many elements as requested by
   * `b`. If either this stream or the specified stream are exhausted further
   * requests for values from that stream will be ignored.
   */
  def interleaveWith[R1 <: R, E1 >: E, A1 >: A](
    that: => ZStream[R1, E1, A1]
  )(b: => ZStream[R1, E1, Boolean])(implicit trace: Trace): ZStream[R1, E1, A1] = {
    def producer(handoff: ZStream.Handoff[Take[E1, A1]]): ZChannel[R1, E1, A1, Any, Nothing, Nothing, Unit] =
      ZChannel.readWithCause[R1, E1, A1, Any, Nothing, Nothing, Unit](
        value => ZChannel.fromZIO(handoff.offer(Take.single(value))) *> producer(handoff),
        cause => ZChannel.fromZIO(handoff.offer(Take.failCause(cause))),
        _ => ZChannel.fromZIO(handoff.offer(Take.end))
      )

    new ZStream(
      ZChannel.unwrapScopedWith { scope =>
        for {
          left  <- ZStream.Handoff.make[Take[E1, A1]]
          right <- ZStream.Handoff.make[Take[E1, A1]]
          _     <- (self.channel.concatMap(ZChannel.writeChunk(_)) >>> producer(left)).runIn(scope).forkIn(scope)
          _     <- (that.channel.concatMap(ZChannel.writeChunk(_)) >>> producer(right)).runIn(scope).forkIn(scope)
        } yield {
          def process(leftDone: Boolean, rightDone: Boolean): ZChannel[R1, E1, Boolean, Any, E1, Chunk[A1], Unit] =
            ZChannel.readWithCause[R1, E1, Boolean, Any, E1, Chunk[A1], Unit](
              bool =>
                (bool, leftDone, rightDone) match {
                  case (true, false, _) =>
                    ZChannel.fromZIO(left.take).flatMap { take =>
                      take.fold(
                        if (rightDone) ZChannel.unit else process(true, rightDone),
                        cause => ZChannel.refailCause(cause),
                        chunk => ZChannel.write(chunk) *> process(leftDone, rightDone)
                      )
                    }
                  case (false, _, false) =>
                    ZChannel.fromZIO(right.take).flatMap { take =>
                      take.fold(
                        if (leftDone) ZChannel.unit else process(leftDone, true),
                        cause => ZChannel.refailCause(cause),
                        chunk => ZChannel.write(chunk) *> process(leftDone, rightDone)
                      )
                    }
                  case _ =>
                    process(leftDone, rightDone)
                },
              cause => ZChannel.refailCause(cause),
              _ => ZChannel.unit
            )

          b.channel.concatMap(ZChannel.writeChunk(_)) >>> process(false, false)
        }
      }
    )
  }

  /**
   * Intersperse stream with provided element similar to
   * List.mkString.
   */
  def intersperse[A1 >: A](middle: => A1)(implicit trace: Trace): ZStream[R, E, A1] =
    self >>> ZPipeline.intersperse(middle)

  /**
   * Intersperse and also add a prefix and a suffix
   */
  def intersperse[A1 >: A](start: => A1, middle: => A1, end: => A1)(implicit
    trace: Trace
  ): ZStream[R, E, A1] =
    self >>> ZPipeline.intersperse(start, middle, end)

  /**
   * Interrupts the evaluation of this stream when the provided IO completes.
   * The given IO will be forked as part of this stream, and its success will be
   * discarded. This combinator will also interrupt any in-progress element
   * being pulled from upstream.
   *
   * If the IO completes with a failure before the stream completes, the
   * returned stream will emit that failure.
   */
  def interruptWhen[R1 <: R, E1 >: E](io: ZIO[R1, E1, Any])(implicit trace: Trace): ZStream[R1, E1, A] =
    new ZStream(channel.interruptWhen(io))

  /**
   * Interrupts the evaluation of this stream when the provided promise
   * resolves. This combinator will also interrupt any in-progress element being
   * pulled from upstream.
   *
   * If the promise completes with a failure, the stream will emit that failure.
   */
  def interruptWhen[E1 >: E](p: Promise[E1, _])(implicit trace: Trace): ZStream[R, E1, A] =
    new ZStream(channel.interruptWhen(p.asInstanceOf[Promise[E1, Any]]))

  /**
   * Specialized version of interruptWhen which interrupts the evaluation of
   * this stream after the given duration.
   */
  def interruptAfter(duration: => Duration)(implicit trace: Trace): ZStream[R, E, A] =
    interruptWhen(Clock.sleep(duration))

  /**
   * Returns a combined string resulting from concatenating each of the values
   * from the stream
   */
  def mkString(implicit trace: Trace): ZIO[R, E, String] =
    run(ZSink.mkString)

  /**
   * Returns a combined string resulting from concatenating each of the values
   * from the stream beginning with `before` interspersed with `middle` and
   * ending with `after`.
   */
  def mkString(before: => String, middle: => String, after: => String)(implicit
    trace: Trace
  ): ZIO[R, E, String] =
    intersperse(before, middle, after).mkString

  /**
   * Transforms the elements of this stream using the supplied function.
   */
  def map[B](f: A => B)(implicit trace: Trace): ZStream[R, E, B] =
    new ZStream(channel.mapOut(_.map(f)))

  /**
   * Statefully maps over the elements of this stream to produce new elements.
   */
  def mapAccum[S, A1](s: => S)(f: (S, A) => (S, A1))(implicit trace: Trace): ZStream[R, E, A1] =
    ZStream.succeed(s).flatMap { s =>
      def accumulator(currS: S): ZChannel[Any, E, Chunk[A], Any, E, Chunk[A1], Unit] =
        ZChannel.readWithCause(
          (in: Chunk[A]) => {
            val (nextS, a1s) = in.mapAccum(currS)(f)
            ZChannel.write(a1s) *> accumulator(nextS)
          },
          (err: Cause[E]) => ZChannel.refailCause(err),
          (_: Any) => ZChannel.unit
        )

      new ZStream(self.channel >>> accumulator(s))
    }

  /**
   * Statefully and effectfully maps over the elements of this stream to produce
   * new elements.
   */
  def mapAccumZIO[R1 <: R, E1 >: E, S, A1](
    s: => S
  )(f: (S, A) => ZIO[R1, E1, (S, A1)])(implicit trace: Trace): ZStream[R1, E1, A1] =
    self >>> ZPipeline.mapAccumZIO(s)(f)

  /**
   * Returns a stream whose failure and success channels have been mapped by the
   * specified pair of functions, `f` and `g`.
   */
  def mapBoth[E1, A1](f: E => E1, g: A => A1)(implicit ev: CanFail[E], trace: Trace): ZStream[R, E1, A1] =
    mapError(f).map(g)

  /**
   * Transforms the chunks emitted by this stream.
   */
  def mapChunks[A2](f: Chunk[A] => Chunk[A2])(implicit trace: Trace): ZStream[R, E, A2] =
    new ZStream(channel.mapOut(f))

  /**
   * Effectfully transforms the chunks emitted by this stream.
   */
  def mapChunksZIO[R1 <: R, E1 >: E, A2](f: Chunk[A] => ZIO[R1, E1, Chunk[A2]])(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    new ZStream(channel.mapOutZIO(f))

  /**
   * Maps each element to an iterable, and flattens the iterables into the
   * output of this stream.
   */
  def mapConcat[A2](f: A => Iterable[A2])(implicit trace: Trace): ZStream[R, E, A2] =
    mapConcatChunk(a => Chunk.fromIterable(f(a)))

  /**
   * Maps each element to a chunk, and flattens the chunks into the output of
   * this stream.
   */
  def mapConcatChunk[A2](f: A => Chunk[A2])(implicit trace: Trace): ZStream[R, E, A2] =
    mapChunks(_.flatMap(f))

  /**
   * Effectfully maps each element to a chunk, and flattens the chunks into the
   * output of this stream.
   */
  def mapConcatChunkZIO[R1 <: R, E1 >: E, A2](f: A => ZIO[R1, E1, Chunk[A2]])(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    mapZIO(f).mapConcatChunk(identity)

  /**
   * Effectfully maps each element to an iterable, and flattens the iterables
   * into the output of this stream.
   */
  def mapConcatZIO[R1 <: R, E1 >: E, A2](f: A => ZIO[R1, E1, Iterable[A2]])(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    mapZIO(a => f(a).map(Chunk.fromIterable(_))).mapConcatChunk(identity)

  /**
   * Transforms the errors emitted by this stream using `f`.
   */
  def mapError[E2](f: E => E2)(implicit trace: Trace): ZStream[R, E2, A] =
    new ZStream(self.channel.mapError(f))

  /**
   * Transforms the full causes of failures emitted by this stream.
   */
  def mapErrorCause[E2](f: Cause[E] => Cause[E2])(implicit trace: Trace): ZStream[R, E2, A] =
    new ZStream(self.channel.mapErrorCause(f))

  /**
   * Transforms the errors emitted by this stream using `f`.
   */
  def mapErrorZIO[R1 <: R, E2](f: E => URIO[R1, E2])(implicit trace: Trace): ZStream[R1, E2, A] =
    new ZStream(self.channel.mapErrorZIO(f))

  /**
   * Maps over elements of the stream with the specified effectful function.
   */
  def mapZIO[R1 <: R, E1 >: E, A1](f: A => ZIO[R1, E1, A1])(implicit trace: Trace): ZStream[R1, E1, A1] = {

    def loop(chunkIterator: Chunk.ChunkIterator[A], index: Int): ZChannel[R1, E, Chunk[A], Any, E1, Chunk[A1], Any] =
      if (chunkIterator.hasNextAt(index))
        ZChannel.unwrap {
          val a = chunkIterator.nextAt(index)
          f(a).map { a1 =>
            ZChannel.write(Chunk.single(a1)) *> loop(chunkIterator, index + 1)
          }
        }
      else
        ZChannel.readWithCause(
          elem => loop(elem.chunkIterator, 0),
          err => ZChannel.refailCause(err),
          done => ZChannel.succeed(done)
        )

    new ZStream(self.channel >>> loop(Chunk.ChunkIterator.empty, 0))
  }

  /**
   * Maps over elements of the stream with the specified effectful function,
   * executing up to `n` invocations of `f` concurrently. Transformed elements
   * will be emitted in the original order.
   *
   * @note
   *   This combinator destroys the chunking structure. It's recommended to use
   *   rechunk afterwards.
   */
  def mapZIOPar[R1 <: R, E1 >: E, A2](n: => Int)(f: A => ZIO[R1, E1, A2])(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    mapZIOPar[R1, E1, A2](n, 16)(f)

  /**
   * Maps over elements of the stream with the specified effectful function,
   * executing up to `n` invocations of `f` concurrently. Transformed elements
   * will be emitted in the original order.
   *
   * @note
   *   This combinator destroys the chunking structure. It's recommended to use
   *   rechunk afterwards.
   */
  def mapZIOPar[R1 <: R, E1 >: E, A2](n: => Int, bufferSize: Int = 16)(f: A => ZIO[R1, E1, A2])(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    self >>> ZPipeline.mapZIOPar(n, bufferSize)(f)

  /**
   * Maps over elements of the stream with the specified effectful function,
   * partitioned by `p` executing invocations of `f` concurrently. The number of
   * concurrent invocations of `f` is determined by the number of different
   * outputs of type `K`. Up to `buffer` elements may be buffered per partition.
   * Transformed elements may be reordered but the order within a partition is
   * maintained.
   */
  def mapZIOParByKey[R1 <: R, E1 >: E, A2, K](
    keyBy: A => K,
    buffer: => Int = 16
  )(f: A => ZIO[R1, E1, A2])(implicit trace: Trace): ZStream[R1, E1, A2] =
    groupByKey(keyBy, buffer).apply { case (_, s) => s.mapZIO(f) }

  /**
   * Maps over elements of the stream with the specified effectful function,
   * executing up to `n` invocations of `f` concurrently. The element order is
   * not enforced by this combinator, and elements may be reordered.
   */
  def mapZIOParUnordered[R1 <: R, E1 >: E, A2](n: => Int)(f: A => ZIO[R1, E1, A2])(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    mapZIOParUnordered[R1, E1, A2](n, 16)(f)

  /**
   * Maps over elements of the stream with the specified effectful function,
   * executing up to `n` invocations of `f` concurrently. The element order is
   * not enforced by this combinator, and elements may be reordered.
   */
  def mapZIOParUnordered[R1 <: R, E1 >: E, A2](n: => Int, bufferSize: => Int = 16)(f: A => ZIO[R1, E1, A2])(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    self >>> ZPipeline.mapZIOParUnordered(n, bufferSize)(f)

  /**
   * Merges this stream and the specified stream together.
   *
   * New produced stream will terminate when both specified stream terminate if
   * no termination strategy is specified.
   */
  def merge[R1 <: R, E1 >: E, A1 >: A](
    that: => ZStream[R1, E1, A1],
    strategy: => HaltStrategy = HaltStrategy.Both
  )(implicit trace: Trace): ZStream[R1, E1, A1] = {
    import HaltStrategy.{Either, Left, Right}

    def handler(terminate: Boolean)(exit: Exit[E1, Any]): ZChannel.MergeDecision[R1, E1, Any, E1, Any] =
      if (terminate || !exit.isSuccess) ZChannel.MergeDecision.done(ZIO.done(exit))
      else ZChannel.MergeDecision.await(ZIO.done(_))

    new ZStream(
      ZChannel.succeed(strategy).flatMap { strategy =>
        self.channel
          .mergeWith(that.channel)(
            handler(strategy == Either || strategy == Left),
            handler(strategy == Either || strategy == Right)
          )
      }
    )
  }

  /**
   * Merges this stream and the specified stream together. New produced stream
   * will terminate when either stream terminates.
   */
  def mergeHaltEither[R1 <: R, E1 >: E, A1 >: A](that: => ZStream[R1, E1, A1])(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] =
    self.merge[R1, E1, A1](that, HaltStrategy.Either)

  /**
   * Merges this stream and the specified stream together. New produced stream
   * will terminate when this stream terminates.
   */
  def mergeHaltLeft[R1 <: R, E1 >: E, A1 >: A](that: => ZStream[R1, E1, A1])(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] =
    self.merge[R1, E1, A1](that, HaltStrategy.Left)

  /**
   * Merges this stream and the specified stream together. New produced stream
   * will terminate when the specified stream terminates.
   */
  def mergeHaltRight[R1 <: R, E1 >: E, A1 >: A](that: => ZStream[R1, E1, A1])(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] =
    self.merge[R1, E1, A1](that, HaltStrategy.Right)

  /**
   * Merges this stream and the specified stream together to produce a stream of
   * eithers.
   */
  def mergeEither[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit
    trace: Trace
  ): ZStream[R1, E1, Either[A, A2]] =
    self.mergeWith(that)(Left(_), Right(_))

  /**
   * Merges this stream and the specified stream together, discarding the values
   * from the right stream.
   */
  def mergeLeft[R1 <: R, E1 >: E, A2](
    that: => ZStream[R1, E1, A2],
    strategy: HaltStrategy = HaltStrategy.Both
  )(implicit
    trace: Trace
  ): ZStream[R1, E1, A] =
    self.merge(that.drain, strategy)

  /**
   * Merges this stream and the specified stream together, discarding the values
   * from the left stream.
   */
  def mergeRight[R1 <: R, E1 >: E, A2](
    that: => ZStream[R1, E1, A2],
    strategy: HaltStrategy = HaltStrategy.Both
  )(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    self.drain.merge(that, strategy)

  /**
   * Merges this stream that is sorted and the specified stream that is sorted
   * to produce a new stream that is sorted.
   */
  def mergeSorted[R1 <: R, E1 >: E, A1 >: A](
    that: => ZStream[R1, E1, A1]
  )(implicit ord: Ordering[A1], trace: Trace): ZStream[R1, E1, A1] = {
    sealed trait State
    case object DrainLeft                            extends State
    case object DrainRight                           extends State
    case object PullBoth                             extends State
    final case class PullLeft(rightChunk: Chunk[A1]) extends State
    final case class PullRight(leftChunk: Chunk[A1]) extends State

    def pull(
      state: State,
      pullLeft: ZIO[R, Option[E], Chunk[A1]],
      pullRight: ZIO[R1, Option[E1], Chunk[A1]]
    ): ZIO[R1, Nothing, Exit[Option[E1], (Chunk[A1], State)]] =
      state match {
        case DrainLeft =>
          pullLeft.fold(
            e => Exit.fail(e),
            leftChunk => Exit.succeed(leftChunk -> DrainLeft)
          )
        case DrainRight =>
          pullRight.fold(
            e => Exit.fail(e),
            rightChunk => Exit.succeed(rightChunk -> DrainRight)
          )
        case PullBoth =>
          pullLeft.unsome
            .zipPar(pullRight.unsome)
            .foldZIO(
              e => ZIO.succeed(Exit.fail(Some(e))),
              {
                case (Some(leftChunk), Some(rightChunk)) =>
                  if (leftChunk.isEmpty && rightChunk.isEmpty) pull(PullBoth, pullLeft, pullRight)
                  else if (leftChunk.isEmpty) pull(PullLeft(rightChunk), pullLeft, pullRight)
                  else if (rightChunk.isEmpty) pull(PullRight(leftChunk), pullLeft, pullRight)
                  else ZIO.succeed(Exit.succeed(mergeSortedChunk(leftChunk, rightChunk)))
                case (Some(leftChunk), None) =>
                  if (leftChunk.isEmpty) pull(DrainLeft, pullLeft, pullRight)
                  else ZIO.succeed(Exit.succeed(leftChunk -> DrainLeft))
                case (None, Some(rightChunk)) =>
                  if (rightChunk.isEmpty) pull(DrainRight, pullLeft, pullRight)
                  else
                    ZIO.succeed(Exit.succeed(rightChunk -> DrainRight))
                case (None, None) => ZIO.succeed(Exit.fail(None))
              }
            )
        case PullLeft(rightChunk) =>
          pullLeft.foldZIO(
            {
              case Some(e) => ZIO.succeed(Exit.fail(Some(e)))
              case None =>
                ZIO.succeed(Exit.succeed(rightChunk -> DrainRight))
            },
            leftChunk =>
              if (leftChunk.isEmpty) pull(PullLeft(rightChunk), pullLeft, pullRight)
              else ZIO.succeed(Exit.succeed(mergeSortedChunk(leftChunk, rightChunk)))
          )
        case PullRight(leftChunk) =>
          pullRight.foldZIO(
            {
              case Some(e) => ZIO.succeed(Exit.fail(Some(e)))
              case None    => ZIO.succeed(Exit.succeed(leftChunk -> DrainLeft))
            },
            rightChunk =>
              if (rightChunk.isEmpty) pull(PullRight(leftChunk), pullLeft, pullRight)
              else ZIO.succeed(Exit.succeed(mergeSortedChunk(leftChunk, rightChunk)))
          )
      }

    def mergeSortedChunk(
      leftChunk: Chunk[A1],
      rightChunk: Chunk[A1]
    ): (Chunk[A1], State) = {
      val builder       = ChunkBuilder.make[A1]()
      var state         = null.asInstanceOf[State]
      val leftIterator  = leftChunk.iterator
      val rightIterator = rightChunk.iterator
      var left          = leftIterator.next()
      var right         = rightIterator.next()
      var loop          = true
      while (loop) {
        val compare = ord.compare(left, right)
        if (compare == 0) {
          builder += left
          builder += right
          if (leftIterator.hasNext && rightIterator.hasNext) {
            left = leftIterator.next()
            right = rightIterator.next()
          } else if (leftIterator.hasNext) {
            state = PullRight(Chunk.fromIterator(leftIterator))
            loop = false
          } else if (rightIterator.hasNext) {
            state = PullLeft(Chunk.fromIterator(rightIterator))
            loop = false
          } else {
            state = PullBoth
            loop = false
          }
        } else if (compare < 0) {
          builder += left
          if (leftIterator.hasNext) {
            left = leftIterator.next()
          } else {
            val rightBuilder = ChunkBuilder.make[A1]()
            rightBuilder += right
            rightBuilder ++= rightIterator
            state = PullLeft(rightBuilder.result())
            loop = false
          }
        } else {
          builder += right
          if (rightIterator.hasNext) {
            right = rightIterator.next()
          } else {
            val leftBuilder = ChunkBuilder.make[A1]()
            leftBuilder += left
            leftBuilder ++= leftIterator
            state = PullRight(leftBuilder.result())
            loop = false
          }
        }
      }
      (builder.result(), state)
    }

    self.combineChunks[R1, E1, State, A1, A1](that)(PullBoth)(pull)
  }

  /**
   * Merges this stream and the specified stream together to a common element
   * type with the specified mapping functions.
   *
   * New produced stream will terminate when both specified stream terminate if
   * no termination strategy is specified.
   */
  def mergeWith[R1 <: R, E1 >: E, A2, A3](
    that: => ZStream[R1, E1, A2],
    strategy: => HaltStrategy = HaltStrategy.Both
  )(l: A => A3, r: A2 => A3)(implicit trace: Trace): ZStream[R1, E1, A3] =
    self
      .map(l)
      .merge(that.map(r), strategy)

  /**
   * Runs the specified effect if this stream fails, providing the error to the
   * effect if it exists.
   *
   * Note: Unlike [[ZIO.onError]], there is no guarantee that the provided
   * effect will not be interrupted.
   */
  def onError[R1 <: R](cleanup: Cause[E] => URIO[R1, Any])(implicit trace: Trace): ZStream[R1, E, A] =
    catchAllCause(cause => ZStream.fromZIO(cleanup(cause) *> Exit.failCause(cause)))

  /**
   * Locks the execution of this stream to the specified executor. Any streams
   * that are composed after this one will automatically be shifted back to the
   * previous executor.
   */
  def onExecutor(executor: => Executor)(implicit trace: Trace): ZStream[R, E, A] =
    ZStream.scoped(ZIO.onExecutorScoped(executor)) *> self

  /**
   * Translates any failure into a stream termination, making the stream
   * infallible and all failures unchecked.
   */
  def orDie(implicit ev1: E IsSubtypeOfError Throwable, ev2: CanFail[E], trace: Trace): ZStream[R, Nothing, A] =
    self.orDieWith(ev1)

  /**
   * Keeps none of the errors, and terminates the stream with them, using the
   * specified function to convert the `E` into a `Throwable`.
   */
  def orDieWith(f: E => Throwable)(implicit ev: CanFail[E], trace: Trace): ZStream[R, Nothing, A] =
    new ZStream(self.channel.orDieWith(f))

  /**
   * Switches to the provided stream in case this one fails with a typed error.
   *
   * See also [[ZStream#catchAll]].
   */
  def orElse[R1 <: R, E1, A1 >: A](
    that: => ZStream[R1, E1, A1]
  )(implicit ev: CanFail[E], trace: Trace): ZStream[R1, E1, A1] =
    new ZStream(self.channel.orElse(that.channel))

  /**
   * Switches to the provided stream in case this one fails with a typed error.
   *
   * See also [[ZStream#catchAll]].
   */
  def orElseEither[R1 <: R, E2, A2](
    that: => ZStream[R1, E2, A2]
  )(implicit ev: CanFail[E], trace: Trace): ZStream[R1, E2, Either[A, A2]] =
    self.map(Left(_)) orElse that.map(Right(_))

  /**
   * Fails with given error in case this one fails with a typed error.
   *
   * See also [[ZStream#catchAll]].
   */
  def orElseFail[E1](e1: => E1)(implicit ev: CanFail[E], trace: Trace): ZStream[R, E1, A] =
    orElse(ZStream.fail(e1))

  /**
   * Produces the specified element if this stream is empty.
   */
  def orElseIfEmpty[A1 >: A](a: A1)(implicit trace: Trace): ZStream[R, E, A1] =
    orElseIfEmpty(Chunk.single(a))

  /**
   * Produces the specified chunk if this stream is empty.
   */
  def orElseIfEmpty[A1 >: A](chunk: Chunk[A1])(implicit trace: Trace): ZStream[R, E, A1] =
    orElseIfEmpty(new ZStream(ZChannel.write(chunk)))

  /**
   * Switches to the provided stream in case this one is empty.
   */
  def orElseIfEmpty[R1 <: R, E1 >: E, A1 >: A](
    stream: ZStream[R1, E1, A1]
  )(implicit trace: Trace): ZStream[R1, E1, A1] = {
    lazy val writer: ZChannel[R1, E, Chunk[A], Any, E1, Chunk[A1], Any] =
      ZChannel.readWithCause(
        (in: Chunk[A]) => if (in.isEmpty) writer else ZChannel.write(in) *> ZChannel.identity[E, Chunk[A], Any],
        (e: Cause[E]) => ZChannel.refailCause(e),
        (_: Any) => stream.channel
      )

    new ZStream(self.channel >>> writer)
  }

  /**
   * Switches to the provided stream in case this one fails with the `None`
   * value.
   *
   * See also [[ZStream#catchAll]].
   */
  def orElseOptional[R1 <: R, E1, A1 >: A](
    that: => ZStream[R1, Option[E1], A1]
  )(implicit ev: E <:< Option[E1], trace: Trace): ZStream[R1, Option[E1], A1] =
    catchAll(ev(_).fold(that)(e => ZStream.fail(Some(e))))

  /**
   * Succeeds with the specified value if this one fails with a typed error.
   */
  def orElseSucceed[A1 >: A](A1: => A1)(implicit ev: CanFail[E], trace: Trace): ZStream[R, Nothing, A1] =
    orElse(ZStream.succeed(A1))

  /**
   * Partition a stream using a predicate. The first stream will contain all
   * element evaluated to true and the second one will contain all element
   * evaluated to false. The faster stream may advance by up to buffer elements
   * further than the slower one.
   */
  def partition(p: A => Boolean, buffer: => Int = 16)(implicit
    trace: Trace
  ): ZIO[R with Scope, Nothing, (ZStream[Any, E, A], ZStream[Any, E, A])] =
    self.partitionEither(a => if (p(a)) ZIO.succeed(Left(a)) else ZIO.succeed(Right(a)), buffer)

  /**
   * Split a stream by a predicate. The faster stream may advance by up to
   * buffer elements further than the slower one.
   */
  def partitionEither[R1 <: R, E1 >: E, A2, A3](
    p: A => ZIO[R1, E1, Either[A2, A3]],
    buffer: => Int = 16
  )(implicit trace: Trace): ZIO[R1 with Scope, Nothing, (ZStream[Any, E1, A2], ZStream[Any, E1, A3])] =
    self
      .mapZIO(p)
      .distributedWith(
        2,
        buffer,
        {
          case Left(_)  => ZIO.succeed(_ == 0)
          case Right(_) => ZIO.succeed(_ == 1)
        }
      )
      .flatMap {
        case q1 :: q2 :: Nil =>
          ZIO.succeed {
            (
              ZStream.fromQueueWithShutdown(q1).flattenExitOption.collectLeft,
              ZStream.fromQueueWithShutdown(q2).flattenExitOption.collectRight
            )
          }
        case otherwise => ZIO.dieMessage(s"partitionEither: expected two streams but got $otherwise")
      }

  /**
   * Peels off enough material from the stream to construct a `Z` using the
   * provided [[ZSink]] and then returns both the `Z` and the rest of the
   * [[ZStream]] in a scope. Like all scoped values, the provided stream is
   * valid only within the scope.
   */
  def peel[R1 <: R, E1 >: E, A1 >: A, Z](
    sink: => ZSink[R1, E1, A1, A1, Z]
  )(implicit trace: Trace): ZIO[R1 with Scope, E1, (Z, ZStream[Any, E, A1])] = {
    sealed trait Signal
    object Signal {
      case class Emit(els: Chunk[A1])  extends Signal
      case class Halt(cause: Cause[E]) extends Signal
      case object End                  extends Signal
    }

    (for {
      p       <- Promise.make[E1, Z]
      handoff <- ZStream.Handoff.make[Signal]
    } yield {
      val consumer: ZSink[R1, E1, A1, A1, Unit] = sink.collectLeftover
        .foldCauseSink(
          c => ZSink.fromZIO(p.failCause(c)) *> ZSink.failCause(c),
          { case (z1, leftovers) =>
            lazy val loop: ZChannel[Any, E, Chunk[A1], Any, E1, Chunk[A1], Unit] = ZChannel.readWithCause(
              (in: Chunk[A1]) => ZChannel.fromZIO(handoff.offer(Signal.Emit(in))) *> loop,
              (e: Cause[E]) => ZChannel.fromZIO(handoff.offer(Signal.Halt(e))) *> ZChannel.refailCause(e),
              (_: Any) => ZChannel.fromZIO(handoff.offer(Signal.End)) *> ZChannel.unit
            )

            ZSink.fromChannel(
              ZChannel.fromZIO(p.succeed(z1)) *>
                ZChannel.fromZIO(handoff.offer(Signal.Emit(leftovers))) *>
                loop
            )
          }
        )

      lazy val producer: ZChannel[Any, Any, Any, Any, E, Chunk[A1], Unit] = ZChannel.unwrap(
        handoff.take.map {
          case Signal.Emit(els)   => ZChannel.write(els) *> producer
          case Signal.Halt(cause) => ZChannel.refailCause(cause)
          case Signal.End         => ZChannel.unit
        }
      )

      for {
        _ <- self.tapErrorCause(cause => p.failCause(cause)).run(consumer).forkScoped
        z <- p.await
      } yield (z, new ZStream(producer))
    }).flatten
  }

  /**
   * Pipes all of the values from this stream through the provided sink.
   *
   * @see
   *   [[transduce]]
   */
  def pipeThrough[R1 <: R, E1 >: E, L, Z](sink: => ZSink[R1, E1, A, L, Z])(implicit
    trace: Trace
  ): ZStream[R1, E1, L] =
    new ZStream(self.channel pipeToOrFail sink.channel)

  /**
   * Pipes all the values from this stream through the provided channel
   */
  def pipeThroughChannel[R1 <: R, E2, A2](channel: => ZChannel[R1, E, Chunk[A], Any, E2, Chunk[A2], Any])(implicit
    trace: Trace
  ): ZStream[R1, E2, A2] =
    new ZStream(self.channel >>> channel)

  /**
   * Pipes all values from this stream through the provided channel, passing
   * through any error emitted by this stream unchanged.
   */
  def pipeThroughChannelOrFail[R1 <: R, E1 >: E, A2](channel: ZChannel[R1, Nothing, Chunk[A], Any, E1, Chunk[A2], Any])(
    implicit trace: Trace
  ): ZStream[R1, E1, A2] =
    new ZStream(self.channel pipeToOrFail channel)

  /**
   * Provides the stream with its required environment, which eliminates its
   * dependency on `R`.
   */
  def provideEnvironment(
    r: => ZEnvironment[R]
  )(implicit trace: Trace): ZStream[Any, E, A] =
    new ZStream(channel.provideEnvironment(r))

  /**
   * Provides a layer to the stream, which translates it to another level.
   */
  def provideLayer[E1 >: E, R0](
    layer: => ZLayer[R0, E1, R]
  )(implicit trace: Trace): ZStream[R0, E1, A] =
    new ZStream(
      ZChannel.unwrapScopedWith { scope =>
        layer.build(scope).map(r => self.channel.provideEnvironment(r))
      }
    )

  /**
   * Transforms the environment being provided to the stream with the specified
   * function.
   */
  def provideSomeEnvironment[R0](
    env: ZEnvironment[R0] => ZEnvironment[R]
  )(implicit trace: Trace): ZStream[R0, E, A] =
    ZStream.environmentWithStream[R0] { r0 =>
      self.provideEnvironment(env(r0))
    }

  /**
   * Splits the environment into two parts, providing one part using the
   * specified layer and leaving the remainder `R0`.
   *
   * {{{
   * val loggingLayer: ZLayer[Any, Nothing, Logging] = ???
   *
   * val stream: ZStream[Logging with Database, Nothing, Unit] = ???
   *
   * val stream2 = stream.provideSomeLayer[Database](loggingLayer)
   * }}}
   */
  def provideSomeLayer[R0]: ZStream.ProvideSomeLayer[R0, R, E, A] =
    new ZStream.ProvideSomeLayer[R0, R, E, A](self)

  /**
   * Re-chunks the elements of the stream into chunks of `n` elements each. The
   * last chunk might contain less than `n` elements
   */
  def rechunk(n: => Int)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.rechunk(n)

  /**
   * Keeps some of the errors, and terminates the fiber with the rest
   */
  def refineOrDie[E1](
    pf: PartialFunction[E, E1]
  )(implicit ev1: E <:< Throwable, ev2: CanFail[E], trace: Trace): ZStream[R, E1, A] =
    refineOrDieWith(pf)(identity(_))

  /**
   * Keeps some of the errors, and terminates the fiber with the rest, using the
   * specified function to convert the `E` into a `Throwable`.
   */
  def refineOrDieWith[E1](
    pf: PartialFunction[E, E1]
  )(f: E => Throwable)(implicit ev: CanFail[E], trace: Trace): ZStream[R, E1, A] =
    mapErrorCause(_.flatMap(pf.andThen(Cause.fail(_)).applyOrElse(_, (e: E) => Cause.die(f(e)))))

  /**
   * Repeats the entire stream using the specified schedule. The stream will
   * execute normally, and then repeat again according to the provided schedule.
   */
  def repeat[R1 <: R, B](schedule: => Schedule[R1, Any, B])(implicit
    trace: Trace
  ): ZStream[R1, E, A] =
    repeatEither(schedule) collect { case Right(a) => a }

  /**
   * Repeats the entire stream using the specified schedule. The stream will
   * execute normally, and then repeat again according to the provided schedule.
   * The schedule output will be emitted at the end of each repetition.
   */
  def repeatEither[R1 <: R, B](schedule: => Schedule[R1, Any, B])(implicit
    trace: Trace
  ): ZStream[R1, E, Either[B, A]] =
    repeatWith(schedule)(Right(_), Left(_))

  /**
   * Repeats each element of the stream using the provided schedule. Repetitions
   * are done in addition to the first execution, which means using
   * `Schedule.recurs(1)` actually results in the original effect, plus an
   * additional recurrence, for a total of two repetitions of each value in the
   * stream.
   */
  def repeatElements[R1 <: R](schedule: => Schedule[R1, A, Any])(implicit
    trace: Trace
  ): ZStream[R1, E, A] =
    repeatElementsEither(schedule).collect { case Right(a) => a }

  /**
   * Repeats each element of the stream using the provided schedule. When the
   * schedule is finished, then the output of the schedule will be emitted into
   * the stream. Repetitions are done in addition to the first execution, which
   * means using `Schedule.recurs(1)` actually results in the original effect,
   * plus an additional recurrence, for a total of two repetitions of each value
   * in the stream.
   */
  def repeatElementsEither[R1 <: R, E1 >: E, B](
    schedule: => Schedule[R1, A, B]
  )(implicit trace: Trace): ZStream[R1, E1, Either[B, A]] =
    repeatElementsWith(schedule)(Right.apply, Left.apply)

  /**
   * Repeats each element of the stream using the provided schedule. When the
   * schedule is finished, then the output of the schedule will be emitted into
   * the stream. Repetitions are done in addition to the first execution, which
   * means using `Schedule.recurs(1)` actually results in the original effect,
   * plus an additional recurrence, for a total of two repetitions of each value
   * in the stream.
   *
   * This function accepts two conversion functions, which allow the output of
   * this stream and the output of the provided schedule to be unified into a
   * single type. For example, `Either` or similar data type.
   */
  def repeatElementsWith[R1 <: R, E1 >: E, B, C](
    schedule: => Schedule[R1, A, B]
  )(f: A => C, g: B => C)(implicit trace: Trace): ZStream[R1, E1, C] = new ZStream(
    self.channel >>> ZChannel.unwrap {
      for {
        driver <- schedule.driver
      } yield {
        def feed(in: Chunk[A]): ZChannel[R1, E1, Chunk[A], Any, E1, Chunk[C], Unit] =
          in.headOption.fold(loop)(a => ZChannel.write(Chunk.single(f(a))) *> step(in.drop(1), a))

        def step(in: Chunk[A], a: A): ZChannel[R1, E1, Chunk[A], Any, E1, Chunk[C], Unit] = {
          val advance = driver.next(a).as(ZChannel.write(Chunk.single(f(a))) *> step(in, a))
          val reset: ZIO[R1, Nothing, ZChannel[R1, E1, Chunk[A], Any, E1, Chunk[C], Unit]] =
            for {
              b <- driver.last.orDie
              _ <- driver.reset
            } yield ZChannel.write(Chunk.single(g(b))) *> feed(in)

          ZChannel.unwrap(advance orElse reset)
        }

        lazy val loop: ZChannel[R1, E1, Chunk[A], Any, E1, Chunk[C], Unit] =
          ZChannel.readWithCause(
            feed,
            ZChannel.refailCause,
            (_: Any) => ZChannel.unit
          )

        loop
      }
    }
  )

  /**
   * Repeats the entire stream using the specified schedule. The stream will
   * execute normally, and then repeat again according to the provided schedule.
   * The schedule output will be emitted at the end of each repetition and can
   * be unified with the stream elements using the provided functions.
   */
  def repeatWith[R1 <: R, B, C](
    schedule: => Schedule[R1, Any, B]
  )(f: A => C, g: B => C)(implicit trace: Trace): ZStream[R1, E, C] =
    ZStream.unwrap(
      for {
        driver <- schedule.driver
      } yield {
        val scheduleOutput = driver.last.orDie.map(g)
        val process        = self.map(f).channel
        lazy val loop: ZChannel[R1, Any, Any, Any, E, Chunk[C], Unit] =
          ZChannel.unwrap(
            driver
              .next(())
              .fold(
                _ => ZChannel.unit,
                _ => process *> ZChannel.unwrap(scheduleOutput.map(o => ZChannel.write(Chunk.single(o)))) *> loop
              )
          )

        new ZStream(process *> loop)
      }
    )

  /**
   * When the stream fails, retry it according to the given schedule
   *
   * This retries the entire stream, so will re-execute all of the stream's
   * acquire operations.
   *
   * The schedule is reset as soon as the first element passes through the
   * stream again.
   *
   * @param schedule
   *   Schedule receiving as input the errors of the stream
   * @return
   *   Stream outputting elements of all attempts of the stream
   */
  def retry[R1 <: R](
    schedule: => Schedule[R1, E, _]
  )(implicit trace: Trace): ZStream[R1, E, A] =
    ZStream.unwrap {
      for {
        driver <- schedule.driver
      } yield {
        def loop: ZStream[R1, E, A] = self.catchAll { e =>
          ZStream.unwrap(
            driver
              .next(e)
              .foldZIO(
                _ => Exit.failCause(Cause.fail(e)),
                _ => ZIO.succeed(loop.tap(_ => driver.reset))
              )
          )
        }
        loop
      }
    }

  /**
   * Fails with the error `None` if value is `Left`.
   */
  def right[A1, A2](implicit ev: A <:< Either[A1, A2], trace: Trace): ZStream[R, Option[E], A2] =
    self.mapError(Some(_)).rightOrFail(None)

  /**
   * Fails with given error 'e' if value is `Left`.
   */
  def rightOrFail[A1, A2, E1 >: E](
    e: => E1
  )(implicit ev: A <:< Either[A1, A2], trace: Trace): ZStream[R, E1, A2] =
    self.mapZIO(ev(_).fold(_ => ZIO.fail(e), ZIO.succeed(_)))

  /**
   * Runs the sink on the stream to produce either the sink's result or an
   * error.
   */
  def run[R1 <: R, E1 >: E, Z](sink: => ZSink[R1, E1, A, Any, Z])(implicit trace: Trace): ZIO[R1, E1, Z] =
    (channel pipeToOrFail sink.channel).runDrain

  def runScoped[R1 <: R, E1 >: E, B](sink: => ZSink[R1, E1, A, Any, B])(implicit
    trace: Trace
  ): ZIO[R1 with Scope, E1, B] =
    (channel pipeToOrFail sink.channel).drain.runScoped

  /**
   * Runs the stream and collects all of its elements to a chunk.
   */
  def runCollect(implicit trace: Trace): ZIO[R, E, Chunk[A]] =
    run(ZSink.collectAll)

  /**
   * Runs the stream and emits the number of elements processed
   *
   * Equivalent to `run(ZSink.count)`
   */
  def runCount(implicit trace: Trace): ZIO[R, E, Long] =
    run(ZSink.count)

  /**
   * Runs the stream only for its effects. The emitted elements are discarded.
   */
  def runDrain(implicit trace: Trace): ZIO[R, E, Unit] =
    run(ZSink.drain)

  /**
   * Executes a pure fold over the stream of values - reduces all elements in
   * the stream to a value of type `S`.
   */
  def runFold[S](s: => S)(f: (S, A) => S)(implicit trace: Trace): ZIO[R, E, S] =
    runFoldWhile(s)(_ => true)((s, a) => f(s, a))

  /**
   * Executes a pure fold over the stream of values. Returns a scoped value that
   * represents the scope of the stream.
   */
  def runFoldScoped[S](s: => S)(f: (S, A) => S)(implicit trace: Trace): ZIO[R with Scope, E, S] =
    runFoldWhileScoped(s)(_ => true)((s, a) => f(s, a))

  /**
   * Executes an effectful fold over the stream of values. Returns a scoped
   * value that represents the scope of the stream.
   */
  def runFoldScopedZIO[R1 <: R, E1 >: E, S](s: => S)(f: (S, A) => ZIO[R1, E1, S])(implicit
    trace: Trace
  ): ZIO[R1 with Scope, E1, S] =
    runFoldWhileScopedZIO[R1, E1, S](s)(_ => true)(f)

  /**
   * Reduces the elements in the stream to a value of type `S`. Stops the fold
   * early when the condition is not fulfilled. Example:
   * {{{
   *   Stream(1).forever.foldWhile(0)(_ <= 4)(_ + _) // UIO[Int] == 5
   * }}}
   */
  def runFoldWhile[S](s: => S)(cont: S => Boolean)(f: (S, A) => S)(implicit trace: Trace): ZIO[R, E, S] =
    run(ZSink.fold(s)(cont)(f))

  /**
   * Executes an effectful fold over the stream of values. Returns a scoped
   * value that represents the scope of the stream. Stops the fold early when
   * the condition is not fulfilled. Example:
   * {{{
   *   Stream(1)
   *     .forever                                        // an infinite Stream of 1's
   *     .fold(0)(_ <= 4)((s, a) => ZIO.succeed(s + a))  // URIO[Scope, Int] == 5
   * }}}
   *
   * @param cont
   *   function which defines the early termination condition
   */
  def runFoldWhileScopedZIO[R1 <: R, E1 >: E, S](
    s: => S
  )(cont: S => Boolean)(f: (S, A) => ZIO[R1, E1, S])(implicit trace: Trace): ZIO[R1 with Scope, E1, S] =
    runScoped(ZSink.foldZIO(s)(cont)(f))

  /**
   * Executes an effectful fold over the stream of values. Stops the fold early
   * when the condition is not fulfilled. Example:
   * {{{
   *   Stream(1)
   *     .forever                                        // an infinite Stream of 1's
   *     .fold(0)(_ <= 4)((s, a) => ZIO.succeed(s + a))  // UIO[Int] == 5
   * }}}
   *
   * @param cont
   *   function which defines the early termination condition
   */
  def runFoldWhileZIO[R1 <: R, E1 >: E, S](s: => S)(cont: S => Boolean)(
    f: (S, A) => ZIO[R1, E1, S]
  )(implicit trace: Trace): ZIO[R1, E1, S] =
    run(ZSink.foldZIO(s)(cont)(f))

  /**
   * Executes a pure fold over the stream of values. Returns a scoped value that
   * represents the scope of the stream. Stops the fold early when the condition
   * is not fulfilled.
   */
  def runFoldWhileScoped[S](s: => S)(cont: S => Boolean)(f: (S, A) => S)(implicit
    trace: Trace
  ): ZIO[R with Scope, E, S] =
    runScoped(ZSink.fold(s)(cont)(f))

  /**
   * Executes an effectful fold over the stream of values.
   */
  def runFoldZIO[R1 <: R, E1 >: E, S](s: => S)(f: (S, A) => ZIO[R1, E1, S])(implicit
    trace: Trace
  ): ZIO[R1, E1, S] =
    runFoldWhileZIO[R1, E1, S](s)(_ => true)(f)

  /**
   * Consumes all elements of the stream, passing them to the specified
   * callback.
   */
  def runForeach[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Any])(implicit trace: Trace): ZIO[R1, E1, Unit] =
    run(ZSink.foreach(f))

  /**
   * Consumes all elements of the stream, passing them to the specified
   * callback.
   */
  def runForeachChunk[R1 <: R, E1 >: E](f: Chunk[A] => ZIO[R1, E1, Any])(implicit
    trace: Trace
  ): ZIO[R1, E1, Unit] =
    run(ZSink.foreachChunk(f))

  /**
   * Like [[ZStream#runForeachChunk]], but returns a scoped `ZIO` so the
   * finalization order can be controlled.
   */
  def runForeachChunkScoped[R1 <: R, E1 >: E](f: Chunk[A] => ZIO[R1, E1, Any])(implicit
    trace: Trace
  ): ZIO[R1 with Scope, E1, Unit] =
    runScoped(ZSink.foreachChunk(f))

  /**
   * Like [[ZStream#foreach]], but returns a scoped `ZIO` so the finalization
   * order can be controlled.
   */
  def runForeachScoped[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Any])(implicit
    trace: Trace
  ): ZIO[R1 with Scope, E1, Unit] =
    runScoped(ZSink.foreach(f))

  /**
   * Consumes elements of the stream, passing them to the specified callback,
   * and terminating consumption when the callback returns `false`.
   */
  def runForeachWhile[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Boolean])(implicit
    trace: Trace
  ): ZIO[R1, E1, Unit] =
    run(ZSink.foreachWhile(f))

  /**
   * Like [[ZStream#runForeachWhile]], but returns a scoped `ZIO` so the
   * finalization order can be controlled.
   */
  def runForeachWhileScoped[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Boolean])(implicit
    trace: Trace
  ): ZIO[R1 with Scope, E1, Unit] =
    runScoped(ZSink.foreachWhile(f))

  /**
   * Runs the stream to completion and yields the first value emitted by it,
   * discarding the rest of the elements.
   */
  def runHead(implicit trace: Trace): ZIO[R, E, Option[A]] =
    run(ZSink.head)

  /**
   * Publishes elements of this stream to a hub. Stream failure and ending will
   * also be signalled.
   */
  def runIntoHub[E1 >: E, A1 >: A](
    hub: => Hub[Take[E1, A1]]
  )(implicit trace: Trace): ZIO[R, Nothing, Unit] =
    runIntoQueue(hub)

  /**
   * Like [[ZStream#runIntoHub]], but provides the result as a scoped [[ZIO]] to
   * allow for scope composition.
   */
  def runIntoHubScoped[E1 >: E, A1 >: A](
    hub: => Hub[Take[E1, A1]]
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Unit] =
    runIntoQueueScoped(hub)

  /**
   * Enqueues elements of this stream into a queue. Stream failure and ending
   * will also be signalled.
   */
  def runIntoQueue(
    queue: => Enqueue[Take[E, A]]
  )(implicit trace: Trace): ZIO[R, Nothing, Unit] =
    ZIO.scoped[R](runIntoQueueScoped(queue))

  /**
   * Like [[ZStream#runIntoQueue]], but provides the result as a scoped [[ZIO]
   * to allow for scope composition.
   */
  def runIntoQueueScoped(
    queue: => Enqueue[Take[E, A]]
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Unit] = {
    lazy val writer: ZChannel[R, E, Chunk[A], Any, Nothing, Take[E, A], Any] = ZChannel
      .readWithCause[R, E, Chunk[A], Any, Nothing, Take[E, A], Any](
        in => ZChannel.write(Take.chunk(in)) *> writer,
        cause => ZChannel.write(Take.failCause(cause)),
        _ => ZChannel.write(Take.end)
      )

    (self.channel >>> writer)
      .mapOutZIO(queue.offer)
      .drain
      .runScoped
      .unit
  }

  /**
   * Like [[ZStream#runIntoQueue]], but provides the result as a scoped [[ZIO]]
   * to allow for scope composition.
   */
  def runIntoQueueElementsScoped(
    queue: => Enqueue[Exit[Option[E], A]]
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Unit] = {
    lazy val writer: ZChannel[R, E, Chunk[A], Any, Nothing, Exit[Option[E], A], Any] =
      ZChannel.readWithCause[R, E, Chunk[A], Any, Nothing, Exit[Option[E], A], Any](
        in => ZChannel.fromZIO(queue.offerAll(in.map(Exit.succeed(_)))) *> writer,
        err => ZChannel.fromZIO(queue.offer(Exit.failCause(err.map(Some(_))))),
        _ => ZChannel.fromZIO(queue.offer(Exit.fail(None)))
      )

    (self.channel >>> writer).drain.runScoped.unit
  }

  /**
   * Runs the stream to completion and yields the last value emitted by it,
   * discarding the rest of the elements.
   */
  def runLast(implicit trace: Trace): ZIO[R, E, Option[A]] =
    run(ZSink.last)

  /**
   * Runs the stream to a sink which sums elements, provided they are Numeric.
   *
   * Equivalent to `run(Sink.sum[A])`
   */
  def runSum[A1 >: A](implicit ev: Numeric[A1], trace: Trace): ZIO[R, E, A1] =
    run(ZSink.sum[A1])

  /**
   * Statefully maps over the elements of this stream to produce all
   * intermediate results of type `S` given an initial S.
   */
  def scan[S](s: => S)(f: (S, A) => S)(implicit trace: Trace): ZStream[R, E, S] =
    scanZIO(s)((s, a) => ZIO.succeed(f(s, a)))

  /**
   * Statefully maps over the elements of this stream to produce all
   * intermediate results.
   *
   * See also [[ZStream#scan]].
   */
  def scanReduce[A1 >: A](f: (A1, A) => A1)(implicit trace: Trace): ZStream[R, E, A1] =
    scanReduceZIO[R, E, A1]((curr, next) => ZIO.succeed(f(curr, next)))

  /**
   * Statefully and effectfully maps over the elements of this stream to produce
   * all intermediate results.
   *
   * See also [[ZStream#scanZIO]].
   */
  def scanReduceZIO[R1 <: R, E1 >: E, A1 >: A](
    f: (A1, A) => ZIO[R1, E1, A1]
  )(implicit trace: Trace): ZStream[R1, E1, A1] =
    mapAccumZIO[R1, E1, Option[A1], A1](Option.empty[A1]) {
      case (Some(a1), a) => f(a1, a).map(a2 => Some(a2) -> a2)
      case (None, a)     => ZIO.succeed(Some(a) -> a)
    }

  /**
   * Statefully and effectfully maps over the elements of this stream to produce
   * all intermediate results of type `S` given an initial S.
   */
  def scanZIO[R1 <: R, E1 >: E, S](s: => S)(f: (S, A) => ZIO[R1, E1, S])(implicit
    trace: Trace
  ): ZStream[R1, E1, S] =
    self >>> ZPipeline.scanZIO(s)(f)

  /**
   * Schedules the output of the stream using the provided `schedule` and emits
   * its output at the end (if `schedule` is finite).
   */
  def scheduleEither[R1 <: R, E1 >: E, B](
    schedule: => Schedule[R1, A, B]
  )(implicit trace: Trace): ZStream[R1, E1, Either[B, A]] =
    scheduleWith(schedule)(Right.apply, Left.apply)

  /**
   * Schedules the output of the stream using the provided `schedule`.
   */
  def schedule[R1 <: R](schedule: => Schedule[R1, A, Any])(implicit
    trace: Trace
  ): ZStream[R1, E, A] =
    scheduleEither(schedule).collect { case Right(a) => a }

  /**
   * Schedules the output of the stream using the provided `schedule` and emits
   * its output at the end (if `schedule` is finite). Uses the provided function
   * to align the stream and schedule outputs on the same type.
   */
  def scheduleWith[R1 <: R, E1 >: E, B, C](
    schedule: => Schedule[R1, A, B]
  )(f: A => C, g: B => C)(implicit trace: Trace): ZStream[R1, E1, C] = {

    def loop(
      driver: Schedule.Driver[Any, R1, A, B],
      chunkIterator: Chunk.ChunkIterator[A],
      index: Int
    ): ZChannel[R1, E1, Chunk[A], Any, E1, Chunk[C], Any] =
      if (chunkIterator.hasNextAt(index))
        ZChannel.unwrap {
          val a = chunkIterator.nextAt(index)
          driver
            .next(a)
            .foldZIO(
              _ =>
                driver.last.orDie.map { b =>
                  ZChannel.write(Chunk(f(a), g(b))) *> loop(driver, chunkIterator, index + 1)
                } <* driver.reset,
              _ => ZIO.succeed(ZChannel.write(Chunk.single(f(a))) *> loop(driver, chunkIterator, index + 1))
            )
        }
      else
        ZChannel.readWithCause(
          chunk => loop(driver, chunk.chunkIterator, 0),
          ZChannel.refailCause,
          ZChannel.succeedNow(_)
        )

    new ZStream(ZChannel.fromZIO(schedule.driver).flatMap(self.channel >>> loop(_, Chunk.ChunkIterator.empty, 0)))
  }

  /**
   * Converts an option on values into an option on errors.
   */
  def some[A2](implicit ev: A <:< Option[A2], trace: Trace): ZStream[R, Option[E], A2] =
    self.mapError(Some(_)).someOrFail(None)

  /**
   * Extracts the optional value, or returns the given 'default'. Superseded by
   * `someOrElse` with better type inference. This method was left for binary
   * compatibility.
   */
  protected def someOrElse[A2](default: => A2)(implicit ev: A <:< Option[A2], trace: Trace): ZStream[R, E, A2] =
    map(_.getOrElse(default))

  /**
   * Extracts the optional value, or returns the given 'default'.
   */
  def someOrElse[A2, C](default: => C)(implicit ev0: A <:< Option[A2], ev1: C <:< A2, trace: Trace): ZStream[R, E, A2] =
    map(_.getOrElse(default))

  /**
   * Extracts the optional value, or fails with the given error 'e'.
   */
  def someOrFail[A2, E1 >: E](e: => E1)(implicit ev: A <:< Option[A2], trace: Trace): ZStream[R, E1, A2] =
    self.mapZIO(ev(_).fold[IO[E1, A2]](ZIO.fail(e))(ZIO.succeed(_)))

  /**
   * Emits a sliding window of n elements.
   * {{{
   *   Stream(1, 2, 3, 4).sliding(2).runCollect // Chunk(Chunk(1, 2), Chunk(2, 3), Chunk(3, 4))
   * }}}
   */
  def sliding(chunkSize0: => Int, stepSize0: Int = 1)(implicit trace: Trace): ZStream[R, E, Chunk[A]] =
    ZStream.suspend {
      val chunkSize = chunkSize0
      val stepSize  = stepSize0

      def slidingChunk(chunk: Chunk[A], in: Chunk[A]): (Chunk[A], Chunk[Chunk[A]]) = {
        val updatedChunk = chunk ++ in
        val length       = updatedChunk.length
        if (length >= chunkSize) {
          val array      = new Array[Chunk[A]]((length - chunkSize) / stepSize + 1)
          var arrayIndex = 0
          var chunkIndex = 0
          while (chunkIndex + chunkSize <= length) {
            array(arrayIndex) = updatedChunk.slice(chunkIndex, chunkIndex + chunkSize)
            arrayIndex += 1
            chunkIndex += stepSize
          }
          (updatedChunk.drop(chunkIndex), Chunk.fromArray(array))
        } else (updatedChunk, Chunk.empty)
      }

      def sliding(chunk: Chunk[A], written: Boolean): ZChannel[Any, E, Chunk[A], Any, E, Chunk[Chunk[A]], Any] =
        ZChannel.readWithCause(
          in => {
            val (updatedChunk, out) = slidingChunk(chunk, in)
            if (out.isEmpty) sliding(updatedChunk, written)
            else ZChannel.write(out) *> sliding(updatedChunk, true)
          },
          err => {
            val index = if (written && chunkSize > stepSize) chunkSize - stepSize else 0
            if (index >= chunk.length) ZChannel.refailCause(err)
            else ZChannel.write(Chunk.single(chunk)) *> ZChannel.refailCause(err)
          },
          done => {
            val index = if (written && chunkSize > stepSize) chunkSize - stepSize else 0
            if (index >= chunk.length) ZChannel.succeedNow(done)
            else ZChannel.write(Chunk.single(chunk)) *> ZChannel.succeedNow(done)
          }
        )

      ZStream.fromChannel(self.channel >>> sliding(Chunk.empty, false))
    }

  /**
   * Splits elements based on a predicate.
   * {{{
   *   ZStream.range(1, 10).split(_ % 4 == 0).runCollect // Chunk(Chunk(1, 2, 3), Chunk(5, 6, 7), Chunk(9))
   * }}}
   */
  def split(f: A => Boolean)(implicit trace: Trace): ZStream[R, E, Chunk[A]] = {
    def split(leftovers: Chunk[A])(in: Chunk[A]): ZChannel[R, E, Chunk[A], Any, E, Chunk[Chunk[A]], Any] = {
      val (chunk, remaining) = (leftovers ++ in).splitWhere(f)
      if (chunk.isEmpty || remaining.isEmpty) loop(chunk ++ remaining.drop(1))
      else ZChannel.write(Chunk.single(chunk)) *> split(Chunk.empty)(remaining.drop(1))
    }

    def loop(leftovers: Chunk[A]): ZChannel[R, E, Chunk[A], Any, E, Chunk[Chunk[A]], Any] =
      ZChannel.readWithCause(
        (in: Chunk[A]) => split(leftovers)(in),
        (e: Cause[E]) => ZChannel.refailCause(e),
        (_: Any) => {
          if (leftovers.isEmpty) ZChannel.unit
          else if (leftovers.find(f).isEmpty) ZChannel.write(Chunk.single(leftovers)) *> ZChannel.unit
          else split(Chunk.empty)(leftovers) *> ZChannel.unit
        }
      )

    new ZStream(self.channel >>> loop(Chunk.empty))
  }

  /**
   * Splits elements on a delimiter and transforms the splits into desired
   * output.
   */
  def splitOnChunk[A1 >: A](delimiter: => Chunk[A1])(implicit trace: Trace): ZStream[R, E, Chunk[A]] =
    ZStream.succeed(delimiter).flatMap { delimiter =>
      def next(
        leftover: Option[Chunk[A]],
        delimiterIndex: Int
      ): ZChannel[R, E, Chunk[A], Any, E, Chunk[Chunk[A]], Any] =
        ZChannel.readWithCause(
          inputChunk => {
            var buffer = null.asInstanceOf[collection.mutable.ArrayBuffer[Chunk[A]]]
            inputChunk.foldLeft((leftover getOrElse Chunk.empty, delimiterIndex)) {
              case ((carry, delimiterCursor), a) =>
                val concatenated = carry :+ a
                if (delimiterCursor < delimiter.length && a == delimiter(delimiterCursor)) {
                  if (delimiterCursor + 1 == delimiter.length) {
                    if (buffer eq null) buffer = collection.mutable.ArrayBuffer[Chunk[A]]()
                    buffer += concatenated.take(concatenated.length - delimiter.length)
                    (Chunk.empty, 0)
                  } else (concatenated, delimiterCursor + 1)
                } else (concatenated, if (a == delimiter(0)) 1 else 0)
            } match {
              case (carry, delimiterCursor) =>
                ZChannel.write(
                  if (buffer eq null) Chunk.empty
                  else Chunk.fromArray(buffer.toArray)
                ) *> next(if (carry.nonEmpty) Some(carry) else None, delimiterCursor)
            }
          },
          halt =>
            leftover match {
              case Some(chunk) => ZChannel.write(Chunk.single(chunk)) *> ZChannel.refailCause(halt)
              case None        => ZChannel.refailCause(halt)
            },
          done =>
            leftover match {
              case Some(chunk) => ZChannel.write(Chunk.single(chunk)) *> ZChannel.succeed(done)
              case None        => ZChannel.succeed(done)
            }
        )
      new ZStream(self.channel >>> next(None, 0))
    }

  /**
   * Takes the specified number of elements from this stream.
   */
  def take(n: => Long)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.take(n)

  /**
   * Takes the last specified number of elements from this stream.
   */
  def takeRight(n: => Int)(implicit trace: Trace): ZStream[R, E, A] =
    ZStream.succeed(n).flatMap { n =>
      if (n <= 0) ZStream.empty
      else
        new ZStream(
          ZChannel.unwrap(
            for {
              queue <- ZIO.succeed(SingleThreadedRingBuffer[A](n))
            } yield {
              lazy val reader: ZChannel[Any, E, Chunk[A], Any, E, Chunk[A], Unit] = ZChannel.readWithCause(
                (in: Chunk[A]) => {
                  in.foreach(queue.put)
                  reader
                },
                ZChannel.refailCause,
                (_: Any) => ZChannel.write(queue.toChunk) *> ZChannel.unit
              )

              (self.channel >>> reader)
            }
          )
        )
    }

  /**
   * Takes all elements of the stream until the specified predicate evaluates to
   * `true`.
   */
  def takeUntil(f: A => Boolean)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.takeUntil(f)

  /**
   * Takes all elements of the stream until the specified effectual predicate
   * evaluates to `true`.
   */
  def takeUntilZIO[R1 <: R, E1 >: E](
    f: A => ZIO[R1, E1, Boolean]
  )(implicit trace: Trace): ZStream[R1, E1, A] =
    self >>> ZPipeline.takeUntilZIO(f)

  /**
   * Takes all elements of the stream for as long as the specified predicate
   * evaluates to `true`.
   */
  def takeWhile(f: A => Boolean)(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.takeWhile(f)

  /**
   * Takes all elements of the stream for as long as the specified effectual
   * predicate evaluates to `true`.
   */
  def takeWhileZIO[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Boolean])(implicit trace: Trace): ZStream[R1, E1, A] =
    self >>> ZPipeline.takeWhileZIO(f)

  /**
   * Adds an effect to consumption of every element of the stream.
   */
  def tap[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Any])(implicit trace: Trace): ZStream[R1, E1, A] =
    mapZIO(a => f(a).as(a))

  /**
   * Returns a stream that effectfully "peeks" at the failure and adds an effect
   * to consumption of every element of the stream
   */
  def tapBoth[R1 <: R, E1 >: E](
    f: E => ZIO[R1, E1, Any],
    g: A => ZIO[R1, E1, Any]
  )(implicit ev: CanFail[E], trace: Trace): ZStream[R1, E1, A] =
    tapError(f).tap(g)

  /**
   * Returns a stream that effectfully "peeks" at the failure of the stream.
   */
  def tapError[R1 <: R, E1 >: E](
    f: E => ZIO[R1, E1, Any]
  )(implicit ev: CanFail[E], trace: Trace): ZStream[R1, E1, A] =
    tapErrorCause(_.failureOrCause.fold(f, _ => ZIO.unit))

  /**
   * Returns a stream that effectfully "peeks" at the cause of failure of the
   * stream.
   */
  def tapErrorCause[R1 <: R, E1 >: E](
    f: Cause[E] => ZIO[R1, E1, Any]
  )(implicit ev: CanFail[E], trace: Trace): ZStream[R1, E1, A] = {
    lazy val tapErrorCause: ZChannel[R1, E, Chunk[A], Any, E1, Chunk[A], Any] =
      ZChannel.readWithCause(
        chunk => ZChannel.write(chunk) *> tapErrorCause,
        cause => ZChannel.fromZIO(f(cause)) *> ZChannel.refailCause(cause),
        done => ZChannel.succeedNow(done)
      )

    new ZStream(self.channel.pipeTo(tapErrorCause))
  }

  /**
   * Sends all elements emitted by this stream to the specified sink in addition
   * to emitting them.
   */
  def tapSink[R1 <: R, E1 >: E](
    sink: => ZSink[R1, E1, A, Any, Any]
  )(implicit trace: Trace): ZStream[R1, E1, A] =
    ZStream.fromZIO(Queue.bounded[Take[E1, A]](1) <*> Promise.make[Nothing, Unit]).flatMap { case (queue, promise) =>
      val right = ZStream.fromQueue(queue, 1).flattenTake
      lazy val loop: ZChannel[R1, E, Chunk[A], Any, E1, Chunk[A], Any] =
        ZChannel.readWithCause(
          chunk =>
            ZChannel
              .fromZIO(queue.offer(Take.chunk(chunk)))
              .foldCauseChannel(
                _ => ZChannel.write(chunk) *> ZChannel.identity,
                _ => ZChannel.write(chunk) *> loop
              ),
          cause =>
            ZChannel
              .fromZIO(queue.offer(Take.failCause(cause)))
              .foldCauseChannel(
                _ => ZChannel.refailCause(cause),
                _ => ZChannel.refailCause(cause)
              ),
          _ =>
            ZChannel
              .fromZIO(queue.offer(Take.end))
              .foldCauseChannel(
                _ => ZChannel.unit,
                _ => ZChannel.unit
              )
        )
      new ZStream(
        ZChannel.fromZIO(promise.await) *> self.channel
          .pipeTo(loop)
          .ensuring(queue.offer(Take.end).forkDaemon *> queue.awaitShutdown) *> ZChannel.unit
      )
        .merge(ZStream.execute((promise.succeed(()) *> right.run(sink)).ensuring(queue.shutdown)), HaltStrategy.Both)
    }

  /**
   * Throttles the chunks of this stream according to the given bandwidth
   * parameters using the token bucket algorithm. Allows for burst in the
   * processing of elements by allowing the token bucket to accumulate tokens up
   * to a `units + burst` threshold. Chunks that do not meet the bandwidth
   * constraints are dropped. The weight of each chunk is determined by the
   * `costFn` function.
   */
  def throttleEnforce(units: => Long, duration: => Duration, burst: => Long = 0)(
    costFn: Chunk[A] => Long
  )(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.throttleEnforce(units, duration, burst)(costFn)

  /**
   * Throttles the chunks of this stream according to the given bandwidth
   * parameters using the token bucket algorithm. Allows for burst in the
   * processing of elements by allowing the token bucket to accumulate tokens up
   * to a `units + burst` threshold. Chunks that do not meet the bandwidth
   * constraints are dropped. The weight of each chunk is determined by the
   * `costFn` effectful function.
   */
  def throttleEnforceZIO[R1 <: R, E1 >: E](units: => Long, duration: => Duration, burst: => Long = 0)(
    costFn: Chunk[A] => ZIO[R1, E1, Long]
  )(implicit trace: Trace): ZStream[R1, E1, A] =
    self >>> ZPipeline.throttleEnforceZIO(units, duration, burst)(costFn)

  /**
   * Delays the chunks of this stream according to the given bandwidth
   * parameters using the token bucket algorithm. Allows for burst in the
   * processing of elements by allowing the token bucket to accumulate tokens up
   * to a `units + burst` threshold. The weight of each chunk is determined by
   * the `costFn` function.
   */
  def throttleShape(units: => Long, duration: => Duration, burst: => Long = 0)(
    costFn: Chunk[A] => Long
  )(implicit trace: Trace): ZStream[R, E, A] =
    self >>> ZPipeline.throttleShape(units, duration, burst)(costFn)

  /**
   * Delays the chunks of this stream according to the given bandwidth
   * parameters using the token bucket algorithm. Allows for burst in the
   * processing of elements by allowing the token bucket to accumulate tokens up
   * to a `units + burst` threshold. The weight of each chunk is determined by
   * the `costFn` effectful function.
   */
  def throttleShapeZIO[R1 <: R, E1 >: E](units: => Long, duration: => Duration, burst: => Long = 0)(
    costFn: Chunk[A] => ZIO[R1, E1, Long]
  )(implicit trace: Trace): ZStream[R1, E1, A] =
    self >>> ZPipeline.throttleShapeZIO(units, duration, burst)(costFn)

  /**
   * Ends the stream if it does not produce a value after d duration.
   */
  def timeout(d: => Duration)(implicit trace: Trace): ZStream[R, E, A] =
    ZStream.succeed(d).flatMap { d =>
      ZStream.fromPull[R, E, A] {
        self.toPull.map { pull =>
          // None case on the error channel of pull indicates completion/done.
          // Move it to success channel before calling timeout effect so that it does not log completion as failure.
          // In the end, move None case back to error channel for further stream processing
          pull.unsome.timeoutTo(None)(identity)(d).some
        }
      }
    }

  /**
   * Fails the stream with given error if it does not produce a value after d
   * duration.
   */
  def timeoutFail[E1 >: E](e: => E1)(d: Duration)(implicit
    trace: Trace
  ): ZStream[R, E1, A] =
    self.timeoutTo[R, E1, A](d)(ZStream.fail(e))

  /**
   * Fails the stream with given cause if it does not produce a value after d
   * duration.
   */
  def timeoutFailCause[E1 >: E](
    cause: => Cause[E1]
  )(d: => Duration)(implicit trace: Trace): ZStream[R, E1, A] =
    ZStream.succeed((cause, d)).flatMap { case (cause, d) =>
      ZStream.fromPull[R, E1, A] {
        self.toPull.map { pull =>
          // None case on the error channel of pull indicates completion/done.
          // Move it to success channel before calling timeout effect so that it does not log completion as failure.
          // In the end, move None case back to error channel for further stream processing
          pull.unsome.timeoutFailCause(cause)(d).some
        }
      }
    }

  /**
   * Switches the stream if it does not produce a value after d duration.
   */
  def timeoutTo[R1 <: R, E1 >: E, A2 >: A](
    d: => Duration
  )(that: => ZStream[R1, E1, A2])(implicit trace: Trace): ZStream[R1, E1, A2] = {
    final case class StreamTimeout() extends Throwable
    self.timeoutFailCause(Cause.die(StreamTimeout()))(d).catchSomeCause { case Cause.Die(StreamTimeout(), _) => that }
  }

  /** Converts the stream to its underlying channel */
  def toChannel: ZChannel[R, Any, Any, Any, E, Chunk[A], Any] =
    self.channel

  /**
   * Converts the stream to a scoped hub of chunks. After the scope is closed,
   * the hub will never again produce values and should be discarded.
   */
  def toHub[E1 >: E, A1 >: A](
    capacity: => Int
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Hub[Take[E1, A1]]] =
    for {
      hub <- ZIO.acquireRelease(Hub.bounded[Take[E1, A1]](capacity))(_.shutdown)
      _   <- self.runIntoHubScoped(hub).forkScoped
    } yield hub

  /**
   * Converts this stream of bytes into a `java.io.InputStream` wrapped in a
   * scoped [[ZIO]]. The returned input stream will only be valid within the
   * scope.
   */
  def toInputStream(implicit
    ev0: E <:< Throwable,
    ev1: A <:< Byte,
    trace: Trace
  ): ZIO[R with Scope, E, java.io.InputStream] =
    for {
      runtime <- ZIO.runtime[R]
      pull    <- toPull.asInstanceOf[ZIO[R with Scope, Nothing, ZIO[R, Option[Throwable], Chunk[Byte]]]]
    } yield ZInputStream.fromPull(runtime, pull)

  /**
   * Converts this stream into a `scala.collection.Iterator` wrapped in a scoped
   * [[ZIO]]. The returned iterator will only be valid within the scope.
   */
  def toIterator(implicit trace: Trace): ZIO[R with Scope, Nothing, Iterator[Either[E, A]]] =
    for {
      runtime <- ZIO.runtime[R]
      pull    <- toPull
    } yield {
      def unfoldPull: Iterator[Either[E, A]] =
        runtime.unsafe.run(pull)(trace, Unsafe.unsafe) match {
          case Exit.Success(chunk) => chunk.iterator.map(Right(_)) ++ unfoldPull
          case Exit.Failure(cause) =>
            cause.failureOrCause match {
              case Left(None)    => Iterator.empty
              case Left(Some(e)) => Iterator.single(Left(e))
              case Right(c)      => throw FiberFailure(c)
            }
        }

      unfoldPull
    }

  /**
   * Returns in a scope a ZIO effect that can be used to repeatedly pull chunks
   * from the stream. The pull effect fails with None when the stream is
   * finished, or with Some error if it fails, otherwise it returns a chunk of
   * the stream's output.
   */
  def toPull(implicit trace: Trace): ZIO[R with Scope, Nothing, ZIO[R, Option[E], Chunk[A]]] =
    channel.toPull.map { pull =>
      pull.mapError(error => Some(error)).flatMap {
        case Left(done)  => ZIO.fail(None)
        case Right(elem) => ZIO.succeed(elem)
      }
    }

  /**
   * Converts this stream of chars into a `java.io.Reader` wrapped in a scoped
   * [[ZIO]]. The returned reader will only be valid within the scope.
   */
  def toReader(implicit
    ev0: E <:< Throwable,
    ev1: A <:< Char,
    trace: Trace
  ): ZIO[R with Scope, E, java.io.Reader] =
    for {
      runtime <- ZIO.runtime[R]
      pull    <- toPull.asInstanceOf[ZIO[R with Scope, Nothing, ZIO[R, Option[Throwable], Chunk[Char]]]]
    } yield ZReader.fromPull(runtime, pull)

  /**
   * Converts the stream to a scoped queue of chunks. After the scope is closed,
   * the queue will never again produce values and should be discarded.
   */
  def toQueue(
    capacity: => Int = 2
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Dequeue[Take[E, A]]] =
    for {
      queue <- ZIO.acquireRelease(Queue.bounded[Take[E, A]](capacity))(_.shutdown)
      _     <- self.runIntoQueueScoped(queue).forkScoped
    } yield queue

  /**
   * Converts the stream to a dropping scoped queue of chunks. After the scope
   * is closed, the queue will never again produce values and should be
   * discarded.
   */
  def toQueueDropping(
    capacity: => Int = 2
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Dequeue[Take[E, A]]] =
    for {
      queue <- ZIO.acquireRelease(Queue.dropping[Take[E, A]](capacity))(_.shutdown)
      _     <- self.runIntoQueueScoped(queue).forkScoped
    } yield queue

  /**
   * Converts the stream to a scoped queue of elements. After the scope is
   * closed, the queue will never again produce values and should be discarded.
   */
  def toQueueOfElements(
    capacity: => Int = 2
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Dequeue[Exit[Option[E], A]]] =
    for {
      queue <- ZIO.acquireRelease(Queue.bounded[Exit[Option[E], A]](capacity))(_.shutdown)
      _     <- self.runIntoQueueElementsScoped(queue).forkScoped
    } yield queue

  /**
   * Converts the stream to a sliding scoped queue of chunks. After the scope is
   * closed, the queue will never again produce values and should be discarded.
   */
  def toQueueSliding(
    capacity: => Int = 2
  )(implicit trace: Trace): ZIO[R with Scope, Nothing, Dequeue[Take[E, A]]] =
    for {
      queue <- ZIO.acquireRelease(Queue.sliding[Take[E, A]](capacity))(_.shutdown)
      _     <- self.runIntoQueueScoped(queue).forkScoped
    } yield queue

  /**
   * Converts the stream into an unbounded scoped queue. After the scope is
   * closed, the queue will never again produce values and should be discarded.
   */
  def toQueueUnbounded(implicit trace: Trace): ZIO[R with Scope, Nothing, Dequeue[Take[E, A]]] =
    for {
      queue <- ZIO.acquireRelease(Queue.unbounded[Take[E, A]])(_.shutdown)
      _     <- self.runIntoQueueScoped(queue).forkScoped
    } yield queue

  /**
   * Applies the transducer to the stream and emits its outputs.
   */
  def transduce[R1 <: R, E1 >: E, A1 >: A, Z](
    sink: => ZSink[R1, E1, A1, A1, Z]
  )(implicit trace: Trace): ZStream[R1, E1, Z] =
    self >>> ZPipeline.fromSink(sink)

  /**
   * Updates a service in the environment of this effect.
   */
  def updateService[M] =
    new ZStream.UpdateService[R, E, A, M](self)

  /**
   * Updates a service at the specified key in the environment of this effect.
   */
  def updateServiceAt[Service]: ZStream.UpdateServiceAt[R, E, A, Service] =
    new ZStream.UpdateServiceAt[R, E, A, Service](self)

  /**
   * Threads the stream through a transformation pipeline.
   */
  def via[R1 <: R, E1 >: E, B](pipeline: => ZPipeline[R1, E1, A, B])(implicit
    trace: Trace
  ): ZStream[R1, E1, B] =
    ZStream.suspend(pipeline(self))

  /**
   * Threads the stream through the transformation function `f`.
   */
  def viaFunction[R2, E2, B](f: ZStream[R, E, A] => ZStream[R2, E2, B])(implicit
    trace: Trace
  ): ZStream[R2, E2, B] =
    f(self)

  /**
   * Returns this stream if the specified condition is satisfied, otherwise
   * returns an empty stream.
   */
  def when(b: => Boolean)(implicit trace: Trace): ZStream[R, E, A] =
    ZStream.when(b)(self)

  /**
   * Returns this stream if the specified effectful condition is satisfied,
   * otherwise returns an empty stream.
   */
  def whenZIO[R1 <: R, E1 >: E](b: => ZIO[R1, E1, Boolean])(implicit trace: Trace): ZStream[R1, E1, A] =
    ZStream.whenZIO(b)(self)

  /**
   * Equivalent to [[filter]] but enables the use of filter clauses in
   * for-comprehensions
   */
  def withFilter(predicate: A => Boolean)(implicit trace: Trace): ZStream[R, E, A] =
    filter(predicate)

  /**
   * Zips this stream with another point-wise and emits tuples of elements from
   * both streams.
   *
   * The new stream will end when one of the sides ends.
   */
  def zip[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit
    zippable: Zippable[A, A2],
    trace: Trace
  ): ZStream[R1, E1, zippable.Out] =
    zipWith(that)(zippable.zip(_, _))

  /**
   * Zips this stream with another point-wise, creating a new stream of pairs of
   * elements from both sides.
   *
   * The defaults `defaultLeft` and `defaultRight` will be used if the streams
   * have different lengths and one of the streams has ended before the other.
   */
  def zipAll[R1 <: R, E1 >: E, A1 >: A, A2](
    that: => ZStream[R1, E1, A2]
  )(defaultLeft: => A1, defaultRight: => A2)(implicit trace: Trace): ZStream[R1, E1, (A1, A2)] =
    zipAllWith(that)((_, defaultRight), (defaultLeft, _))((_, _))

  /**
   * Zips this stream with another point-wise, and keeps only elements from this
   * stream.
   *
   * The provided default value will be used if the other stream ends before
   * this one.
   */
  def zipAllLeft[R1 <: R, E1 >: E, A1 >: A, A2](that: => ZStream[R1, E1, A2])(default: => A1)(implicit
    trace: Trace
  ): ZStream[R1, E1, A1] =
    zipAllWith(that)(identity, _ => default)((o, _) => o)

  /**
   * Zips this stream with another point-wise, and keeps only elements from the
   * other stream.
   *
   * The provided default value will be used if this stream ends before the
   * other one.
   */
  def zipAllRight[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(default: => A2)(implicit
    trace: Trace
  ): ZStream[R1, E1, A2] =
    zipAllWith(that)(_ => default, identity)((_, A2) => A2)

  /**
   * Zips this stream with another point-wise. The provided functions will be
   * used to create elements for the composed stream.
   *
   * The functions `left` and `right` will be used if the streams have different
   * lengths and one of the streams has ended before the other.
   */
  def zipAllWith[R1 <: R, E1 >: E, A2, A3](
    that: => ZStream[R1, E1, A2]
  )(left: A => A3, right: A2 => A3)(both: (A, A2) => A3)(implicit trace: Trace): ZStream[R1, E1, A3] = {

    sealed trait State[+A, +A2]
    case object DrainLeft                                extends State[Nothing, Nothing]
    case object DrainRight                               extends State[Nothing, Nothing]
    case object PullBoth                                 extends State[Nothing, Nothing]
    final case class PullLeft[A2](rightChunk: Chunk[A2]) extends State[Nothing, A2]
    final case class PullRight[A](leftChunk: Chunk[A])   extends State[A, Nothing]

    def pull(
      state: State[A, A2],
      pullLeft: ZIO[R, Option[E], Chunk[A]],
      pullRight: ZIO[R1, Option[E1], Chunk[A2]]
    ): ZIO[R1, Nothing, Exit[Option[E1], (Chunk[A3], State[A, A2])]] =
      state match {
        case DrainLeft =>
          pullLeft.foldZIO(
            err => ZIO.succeed(Exit.fail(err)),
            leftChunk => ZIO.succeed(Exit.succeed(leftChunk.map(left) -> DrainLeft))
          )
        case DrainRight =>
          pullRight.foldZIO(
            err => ZIO.succeed(Exit.fail(err)),
            rightChunk => ZIO.succeed(Exit.succeed(rightChunk.map(right) -> DrainRight))
          )
        case PullBoth =>
          pullLeft.unsome
            .zipPar(pullRight.unsome)
            .foldZIO(
              err => ZIO.succeed(Exit.fail(Some(err))),
              {
                case (Some(leftChunk), Some(rightChunk)) =>
                  if (leftChunk.isEmpty && rightChunk.isEmpty)
                    pull(PullBoth, pullLeft, pullRight)
                  else if (leftChunk.isEmpty)
                    pull(PullLeft(rightChunk), pullLeft, pullRight)
                  else if (rightChunk.isEmpty)
                    pull(PullRight(leftChunk), pullLeft, pullRight)
                  else
                    ZIO.succeed(Exit.succeed(zipWithChunks(leftChunk, rightChunk, both)))
                case (Some(leftChunk), None) =>
                  ZIO.succeed(Exit.succeed(leftChunk.map(left) -> DrainLeft))
                case (None, Some(rightChunk)) =>
                  ZIO.succeed(Exit.succeed(rightChunk.map(right) -> DrainRight))
                case (None, None) =>
                  ZIO.succeed(Exit.fail(None))
              }
            )
        case PullLeft(rightChunk) =>
          pullLeft.foldZIO(
            {
              case Some(err) => ZIO.succeed(Exit.fail(Some(err)))
              case None      => ZIO.succeed(Exit.succeed(rightChunk.map(right) -> DrainRight))
            },
            leftChunk =>
              if (leftChunk.isEmpty)
                pull(PullLeft(rightChunk), pullLeft, pullRight)
              else if (rightChunk.isEmpty)
                pull(PullRight(leftChunk), pullLeft, pullRight)
              else
                ZIO.succeed(Exit.succeed(zipWithChunks(leftChunk, rightChunk, both)))
          )
        case PullRight(leftChunk) =>
          pullRight.foldZIO(
            {
              case Some(err) => ZIO.succeed(Exit.fail(Some(err)))
              case None      => ZIO.succeed(Exit.succeed(leftChunk.map(left) -> DrainLeft))
            },
            rightChunk =>
              if (rightChunk.isEmpty)
                pull(PullRight(leftChunk), pullLeft, pullRight)
              else if (leftChunk.isEmpty)
                pull(PullLeft(rightChunk), pullLeft, pullRight)
              else
                ZIO.succeed(Exit.succeed(zipWithChunks(leftChunk, rightChunk, both)))
          )
      }

    def zipWithChunks(
      leftChunk: Chunk[A],
      rightChunk: Chunk[A2],
      f: (A, A2) => A3
    ): (Chunk[A3], State[A, A2]) =
      zipChunks(leftChunk, rightChunk, f) match {
        case (out, Left(leftChunk)) =>
          if (leftChunk.isEmpty)
            out -> PullBoth
          else
            out -> PullRight(leftChunk)
        case (out, Right(rightChunk)) =>
          if (rightChunk.isEmpty)
            out -> PullBoth
          else
            out -> PullLeft(rightChunk)
      }

    self.combineChunks[R1, E1, State[A, A2], A2, A3](that)(PullBoth)(pull)
  }

  /**
   * Zips the two streams so that when a value is emitted by either of the two
   * streams, it is combined with the latest value from the other stream to
   * produce a result.
   *
   * Note: tracking the latest value is done on a per-chunk basis. That means
   * that emitted elements that are not the last value in chunks will never be
   * used for zipping.
   */
  def zipLatest[R1 <: R, E1 >: E, A2, A3](
    that: => ZStream[R1, E1, A2]
  )(implicit zippable: Zippable[A, A2], trace: Trace): ZStream[R1, E1, zippable.Out] =
    self.zipLatestWith(that)(zippable.zip(_, _))

  /**
   * Zips the two streams so that when a value is emitted by either of the two
   * streams, it is combined with the latest value from the other stream to
   * produce a result.
   *
   * Note: tracking the latest value is done on a per-chunk basis. That means
   * that emitted elements that are not the last value in chunks will never be
   * used for zipping.
   */
  def zipLatestWith[R1 <: R, E1 >: E, A2, A3](
    that: => ZStream[R1, E1, A2]
  )(f: (A, A2) => A3)(implicit trace: Trace): ZStream[R1, E1, A3] = {
    def pullNonEmpty[R, E, O](pull: ZIO[R, Option[E], Chunk[O]]): ZIO[R, Option[E], Chunk[O]] =
      pull.flatMap(chunk => if (chunk.isEmpty) pullNonEmpty(pull) else ZIO.succeed(chunk))

    ZStream.fromPull[R1, E1, A3] {
      for {
        left  <- self.toPull.map(pullNonEmpty(_))
        right <- that.toPull.map(pullNonEmpty(_))
        pull <- (ZStream.fromZIOOption {
                  left.raceWith(right)(
                    (leftDone, rightFiber) => ZIO.done(leftDone).zipWith(rightFiber.join)((_, _, true)),
                    (rightDone, leftFiber) => ZIO.done(rightDone).zipWith(leftFiber.join)((r, l) => (l, r, false))
                  )
                }.flatMap { case (l, r, leftFirst) =>
                  ZStream.fromZIO(Ref.make(l(l.size - 1) -> r(r.size - 1))).flatMap { latest =>
                    ZStream.fromChunk(
                      if (leftFirst) r.map(f(l(l.size - 1), _))
                      else l.map(f(_, r(r.size - 1)))
                    ) ++
                      ZStream
                        .repeatZIOOption(left)
                        .mergeEither(ZStream.repeatZIOOption(right))
                        .mapZIO {
                          case Left(leftChunk) =>
                            latest.modify { case (_, rightLatest) =>
                              (leftChunk.map(f(_, rightLatest)), (leftChunk(leftChunk.size - 1), rightLatest))
                            }
                          case Right(rightChunk) =>
                            latest.modify { case (leftLatest, _) =>
                              (rightChunk.map(f(leftLatest, _)), (leftLatest, rightChunk(rightChunk.size - 1)))
                            }
                        }
                        .flatMap(ZStream.fromChunk(_))
                  }
                }).toPull

      } yield pull
    }
  }

  /**
   * Zips this stream with another point-wise, but keeps only the outputs of
   * this stream.
   *
   * The new stream will end when one of the sides ends.
   */
  def zipLeft[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit trace: Trace): ZStream[R1, E1, A] =
    zipWithChunks(that)((cl, cr) =>
      if (cl.size > cr.size)
        (cl.take(cr.size), Left(cl.drop(cr.size)))
      else
        (cl, Right(cr.drop(cl.size)))
    )

  /**
   * Zips this stream with another point-wise, but keeps only the outputs of the
   * other stream.
   *
   * The new stream will end when one of the sides ends.
   */
  def zipRight[R1 <: R, E1 >: E, A2](that: => ZStream[R1, E1, A2])(implicit trace: Trace): ZStream[R1, E1, A2] =
    zipWithChunks(that)((cl, cr) =>
      if (cl.size > cr.size)
        (cr, Left(cl.drop(cr.size)))
      else
        (cr.take(cl.size), Right(cr.drop(cl.size)))
    )

  /**
   * Zips this stream with another point-wise and applies the function to the
   * paired elements.
   *
   * The new stream will end when one of the sides ends.
   */
  def zipWith[R1 <: R, E1 >: E, A2, A3](
    that: => ZStream[R1, E1, A2]
  )(f: (A, A2) => A3)(implicit trace: Trace): ZStream[R1, E1, A3] =
    zipWithChunks(that)(zipChunks(_, _, f))

  /**
   * Zips this stream with another point-wise and applies the function to the
   * paired elements.
   *
   * The new stream will end when one of the sides ends.
   */
  def zipWithChunks[R1 <: R, E1 >: E, A1 >: A, A2, A3](
    that: => ZStream[R1, E1, A2]
  )(
    f: (Chunk[A1], Chunk[A2]) => (Chunk[A3], Either[Chunk[A1], Chunk[A2]])
  )(implicit trace: Trace): ZStream[R1, E1, A3] = {
    sealed trait State[+A1, +A2]
    case object PullBoth                                 extends State[Nothing, Nothing]
    final case class PullLeft[A2](rightChunk: Chunk[A2]) extends State[Nothing, A2]
    final case class PullRight[A1](leftChunk: Chunk[A1]) extends State[A1, Nothing]

    def pull(
      state: State[A1, A2],
      pullLeft: ZIO[R, Option[E], Chunk[A1]],
      pullRight: ZIO[R1, Option[E1], Chunk[A2]]
    ): ZIO[R1, Nothing, Exit[Option[E1], (Chunk[A3], State[A1, A2])]] =
      state match {
        case PullBoth =>
          pullLeft.unsome
            .zipPar(pullRight.unsome)
            .foldZIO(
              err => ZIO.succeed(Exit.fail(Some(err))),
              {
                case (Some(leftChunk), Some(rightChunk)) =>
                  if (leftChunk.isEmpty && rightChunk.isEmpty)
                    pull(PullBoth, pullLeft, pullRight)
                  else if (leftChunk.isEmpty)
                    pull(PullLeft(rightChunk), pullLeft, pullRight)
                  else if (rightChunk.isEmpty)
                    pull(PullRight(leftChunk), pullLeft, pullRight)
                  else
                    ZIO.succeed(Exit.succeed(zipWithChunks(leftChunk, rightChunk)))
                case _ => ZIO.succeed(Exit.fail(None))
              }
            )
        case PullLeft(rightChunk) =>
          pullLeft.foldZIO(
            err => ZIO.succeed(Exit.fail(err)),
            leftChunk =>
              if (leftChunk.isEmpty)
                pull(PullLeft(rightChunk), pullLeft, pullRight)
              else if (rightChunk.isEmpty)
                pull(PullRight(leftChunk), pullLeft, pullRight)
              else
                ZIO.succeed(Exit.succeed(zipWithChunks(leftChunk, rightChunk)))
          )
        case PullRight(leftChunk) =>
          pullRight.foldZIO(
            err => ZIO.succeed(Exit.fail(err)),
            rightChunk =>
              if (rightChunk.isEmpty)
                pull(PullRight(leftChunk), pullLeft, pullRight)
              else if (leftChunk.isEmpty)
                pull(PullLeft(rightChunk), pullLeft, pullRight)
              else
                ZIO.succeed(Exit.succeed(zipWithChunks(leftChunk, rightChunk)))
          )
      }

    def zipWithChunks(
      leftChunk: Chunk[A1],
      rightChunk: Chunk[A2]
    ): (Chunk[A3], State[A1, A2]) =
      f(leftChunk, rightChunk) match {
        case (out, Left(leftChunk)) =>
          if (leftChunk.isEmpty)
            out -> PullBoth
          else
            out -> PullRight(leftChunk)
        case (out, Right(rightChunk)) =>
          if (rightChunk.isEmpty)
            out -> PullBoth
          else
            out -> PullLeft(rightChunk)
      }

    self.combineChunks[R1, E1, State[A1, A2], A2, A3](that)(PullBoth)(pull)
  }

  /**
   * Zips this stream together with the index of elements.
   */
  def zipWithIndex(implicit trace: Trace): ZStream[R, E, (A, Long)] =
    mapAccum(0L)((index, a) => (index + 1, (a, index)))

  /**
   * Zips the two streams so that when a value is emitted by either of the two
   * streams, it is combined with the latest value from the other stream to
   * produce a result.
   *
   * Note: tracking the latest value is done on a per-chunk basis. That means
   * that emitted elements that are not the last value in chunks will never be
   * used for zipping.
   */
  @deprecated("use zipLatestWith", "2.0.3")
  def zipWithLatest[R1 <: R, E1 >: E, A2, A3](
    that: => ZStream[R1, E1, A2]
  )(f: (A, A2) => A3)(implicit trace: Trace): ZStream[R1, E1, A3] =
    zipLatestWith(that)(f)

  /**
   * Zips each element with the next element if present.
   */
  def zipWithNext(implicit trace: Trace): ZStream[R, E, (A, Option[A])] =
    self >>> ZPipeline.zipWithNext[A]

  /**
   * Zips each element with the previous element. Initially accompanied by
   * `None`.
   */
  def zipWithPrevious(implicit trace: Trace): ZStream[R, E, (Option[A], A)] =
    self >>> ZPipeline.zipWithPrevious

  /**
   * Zips each element with both the previous and next element.
   */
  def zipWithPreviousAndNext(implicit trace: Trace): ZStream[R, E, (Option[A], A, Option[A])] =
    self >>> ZPipeline.zipWithPreviousAndNext
}

object ZStream extends ZStreamPlatformSpecificConstructors {

  /**
   * The default chunk size used by the various combinators and constructors of
   * [[ZStream]].
   */
  final val DefaultChunkSize = 4096

  /**
   * Submerges the error case of an `Either` into the `ZStream`.
   */
  def absolve[R, E, O](xs: ZStream[R, E, Either[E, O]])(implicit trace: Trace): ZStream[R, E, O] = {
    lazy val loop: ZChannel[Any, E, Chunk[Either[E, O]], Any, E, Chunk[O], Any] = ZChannel.readWithCause(
      (in: Chunk[Either[E, O]]) => {
        val mapped = in.collectWhile { case Right(o) => o }
        if (mapped.size == in.size) ZChannel.write(mapped) *> loop
        else {
          val firstError = in(mapped.size).asInstanceOf[Left[E, O]]
          ZChannel.write(mapped) *> ZChannel.fail(firstError.value)
        }
      },
      (cause: Cause[E]) => ZChannel.refailCause(cause),
      (_: Any) => ZChannel.unit
    )

    xs.pipeThroughChannel(loop)
  }

  /**
   * Creates a stream from a single value that will get cleaned up after the
   * stream is consumed
   */
  def acquireReleaseWith[R, E, A](acquire: => ZIO[R, E, A])(release: A => URIO[R, Any])(implicit
    trace: Trace
  ): ZStream[R, E, A] =
    scoped[R](ZIO.acquireRelease(acquire)(release))

  /**
   * Creates a stream from a single value that will get cleaned up after the
   * stream is consumed
   */
  def acquireReleaseExitWith[R, E, A](
    acquire: => ZIO[R, E, A]
  )(release: (A, Exit[Any, Any]) => URIO[R, Any])(implicit trace: Trace): ZStream[R, E, A] =
    scoped[R](ZIO.acquireReleaseExit(acquire)(release))

  /**
   * An infinite stream of random alphanumeric characters.
   */
  def alphanumeric(implicit trace: Trace): ZStream[Any, Nothing, Char] =
    ZStream.repeatZIO {
      val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
      Random.nextIntBounded(chars.length).map(chars.charAt)
    }

  /**
   * Creates a pure stream from a variable list of values
   */
  def apply[A](as: A*)(implicit trace: Trace): ZStream[Any, Nothing, A] = fromIterable(as)

  /**
   * Locks the execution of the specified stream to the blocking executor. Any
   * streams that are composed after this one will automatically be shifted back
   * to the previous executor.
   */
  def blocking[R, E, A](stream: => ZStream[R, E, A])(implicit trace: Trace): ZStream[R, E, A] =
    ZStream.fromZIO(ZIO.blockingExecutor).flatMap(stream.onExecutor(_))

  /**
   * Concatenates all of the streams in the chunk to one stream.
   */
  def concatAll[R, E, O](streams: => Chunk[ZStream[R, E, O]])(implicit trace: Trace): ZStream[R, E, O] =
    ZStream.suspend(streams.foldLeft[ZStream[R, E, O]](empty)(_ ++ _))

  /**
   * Prints the specified message to the console for debugging purposes.
   */
  def debug(value: => Any)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.debug(value))

  /**
   * The stream that dies with the `ex`.
   */
  def die(ex: => Throwable)(implicit trace: Trace): ZStream[Any, Nothing, Nothing] =
    fromZIO(ZIO.die(ex))

  /**
   * The stream that dies with an exception described by `msg`.
   */
  def dieMessage(msg: => String)(implicit trace: Trace): ZStream[Any, Nothing, Nothing] =
    fromZIO(ZIO.dieMessage(msg))

  /**
   * The stream that ends with the [[zio.Exit]] value `exit`.
   */
  def done[E, A](exit: => Exit[E, A])(implicit trace: Trace): ZStream[Any, E, A] =
    fromZIO(ZIO.done(exit))

  /**
   * The empty stream
   */
  def empty(implicit trace: Trace): ZStream[Any, Nothing, Nothing] =
    new ZStream(ZChannel.unit)

  /**
   * Accesses the whole environment of the stream.
   */
  def environment[R](implicit trace: Trace): ZStream[R, Nothing, ZEnvironment[R]] =
    fromZIO(ZIO.environment[R])

  /**
   * Accesses the environment of the stream.
   */
  def environmentWith[R]: EnvironmentWithPartiallyApplied[R] =
    new EnvironmentWithPartiallyApplied[R]

  /**
   * Accesses the environment of the stream in the context of an effect.
   */
  def environmentWithZIO[R]: EnvironmentWithZIOPartiallyApplied[R] =
    new EnvironmentWithZIOPartiallyApplied[R]

  /**
   * Accesses the environment of the stream in the context of a stream.
   */
  def environmentWithStream[R]: EnvironmentWithStreamPartiallyApplied[R] =
    new EnvironmentWithStreamPartiallyApplied[R]

  /**
   * Creates a stream that executes the specified effect but emits no elements.
   */
  def execute[R, E](zio: => ZIO[R, E, Any])(implicit trace: Trace): ZStream[R, E, Nothing] =
    ZStream.fromZIO(zio).drain

  /**
   * The stream that always fails with the `error`
   */
  def fail[E](error: => E)(implicit trace: Trace): ZStream[Any, E, Nothing] =
    fromZIO(ZIO.fail(error))

  /**
   * The stream that always fails with `cause`.
   */
  def failCause[E](cause: => Cause[E])(implicit trace: Trace): ZStream[Any, E, Nothing] =
    fromZIO(Exit.failCause(cause))

  /**
   * Creates a one-element stream that never fails and executes the finalizer
   * when it ends.
   */
  def finalizer[R](finalizer: => URIO[R, Any])(implicit trace: Trace): ZStream[R, Nothing, Any] =
    acquireReleaseWith[R, Nothing, Unit](ZIO.unit)(_ => finalizer)

  def from[Input](
    input: => Input
  )(implicit constructor: ZStreamConstructor[Input], trace: Trace): constructor.Out =
    constructor.make(input)

  /**
   * Creates a stream from a [[zio.stream.ZChannel]]
   */
  def fromChannel[R, E, A](channel: ZChannel[R, Any, Any, Any, E, Chunk[A], Any]): ZStream[R, E, A] =
    new ZStream(channel)

  /**
   * Creates a stream from a [[zio.Chunk]] of values
   *
   * @param c
   *   a chunk of values
   * @return
   *   a finite stream of values
   */
  def fromChunk[O](chunk: => Chunk[O])(implicit trace: Trace): ZStream[Any, Nothing, O] =
    new ZStream(
      ZChannel.succeed(chunk).flatMap { chunk =>
        if (chunk.isEmpty) ZChannel.unit else ZChannel.write(chunk)
      }
    )

  /**
   * Creates a stream from a subscription to a hub.
   */
  def fromChunkHub[O](hub: => Hub[Chunk[O]])(implicit
    trace: Trace
  ): ZStream[Any, Nothing, O] =
    scoped(hub.subscribe).flatMap(queue => fromChunkQueue(queue))

  /**
   * Creates a stream from a subscription to a hub in the context of a scoped
   * effect. The scoped effect describes subscribing to receive messages from
   * the hub while the stream describes taking messages from the hub.
   */
  def fromChunkHubScoped[O](
    hub: => Hub[Chunk[O]]
  )(implicit trace: Trace): ZIO[Scope, Nothing, ZStream[Any, Nothing, O]] =
    hub.subscribe.map(queue => fromChunkQueue(queue))

  /**
   * Creates a stream from a subscription to a hub.
   *
   * The hub will be shut down once the stream is closed.
   */
  def fromChunkHubWithShutdown[O](hub: => Hub[Chunk[O]])(implicit
    trace: Trace
  ): ZStream[Any, Nothing, O] =
    fromChunkHub(hub).ensuring(hub.shutdown)

  /**
   * Creates a stream from a subscription to a hub in the context of a scoped
   * effect. The scoped effect describes subscribing to receive messages from
   * the hub while the stream describes taking messages from the hub.
   *
   * The hub will be shut down once the stream is closed.
   */
  def fromChunkHubScopedWithShutdown[O](
    hub: => Hub[Chunk[O]]
  )(implicit trace: Trace): ZIO[Scope, Nothing, ZStream[Any, Nothing, O]] =
    fromChunkHubScoped(hub).map(_.ensuring(hub.shutdown))

  /**
   * Creates a stream from a queue of values
   */
  def fromChunkQueue[O](
    queue: => Dequeue[Chunk[O]]
  )(implicit trace: Trace): ZStream[Any, Nothing, O] =
    repeatZIOChunkOption {
      queue.take
        .catchAllCause(c =>
          queue.isShutdown.flatMap { down =>
            if (down && c.isInterrupted) Pull.end
            else Pull.failCause(c)
          }
        )
    }

  /**
   * Creates a stream from a queue of values. The queue will be shutdown once
   * the stream is closed.
   */
  def fromChunkQueueWithShutdown[O](queue: => Dequeue[Chunk[O]])(implicit
    trace: Trace
  ): ZStream[Any, Nothing, O] =
    fromChunkQueue(queue).ensuring(queue.shutdown)

  /**
   * Creates a stream from an arbitrary number of chunks.
   */
  def fromChunks[O](cs: Chunk[O]*)(implicit trace: Trace): ZStream[Any, Nothing, O] =
    fromIterable(cs).flatMap(fromChunk(_))

  /**
   * Creates a stream from a subscription to a hub.
   */
  def fromHub[A](
    hub: => Hub[A],
    maxChunkSize: => Int = DefaultChunkSize
  )(implicit trace: Trace): ZStream[Any, Nothing, A] =
    scoped(hub.subscribe).flatMap(queue => fromQueue(queue, maxChunkSize))

  /**
   * Creates a stream from a subscription to a hub in the context of a scoped
   * effect. The scoped effect describes subscribing to receive messages from
   * the hub while the stream describes taking messages from the hub.
   */
  def fromHubScoped[A](
    hub: => Hub[A],
    maxChunkSize: => Int = DefaultChunkSize
  )(implicit trace: Trace): ZIO[Scope, Nothing, ZStream[Any, Nothing, A]] =
    ZIO.suspendSucceed(hub.subscribe.map(queue => fromQueueWithShutdown(queue, maxChunkSize)))

  /**
   * Creates a stream from a subscription to a hub.
   *
   * The hub will be shut down once the stream is closed.
   */
  def fromHubWithShutdown[A](
    hub: => Hub[A],
    maxChunkSize: => Int = DefaultChunkSize
  )(implicit trace: Trace): ZStream[Any, Nothing, A] =
    fromHub(hub, maxChunkSize).ensuring(hub.shutdown)

  /**
   * Creates a stream from a subscription to a hub in the context of a scoped
   * effect. The scoped effect describes subscribing to receive messages from
   * the hub while the stream describes taking messages from the hub.
   *
   * The hub will be shut down once the stream is closed.
   */
  def fromHubScopedWithShutdown[A](
    hub: => Hub[A],
    maxChunkSize: => Int = DefaultChunkSize
  )(implicit trace: Trace): ZIO[Scope, Nothing, ZStream[Any, Nothing, A]] =
    fromHubScoped(hub, maxChunkSize).map(_.ensuring(hub.shutdown))

  /**
   * Creates a stream from a `java.io.InputStream`
   */
  def fromInputStream(
    is: => InputStream,
    chunkSize: => Int = ZStream.DefaultChunkSize
  )(implicit trace: Trace): ZStream[Any, IOException, Byte] =
    ZStream.succeed((is, chunkSize)).flatMap { case (is, chunkSize) =>
      ZStream.repeatZIOChunkOption {
        for {
          bufArray  <- ZIO.succeed(Array.ofDim[Byte](chunkSize))
          bytesRead <- ZIO.attemptBlockingIO(is.read(bufArray)).asSomeError
          bytes <- if (bytesRead < 0)
                     ZIO.fail(None)
                   else if (bytesRead == 0)
                     ZIO.succeed(Chunk.empty)
                   else if (bytesRead < chunkSize)
                     ZIO.succeed(Chunk.fromArray(bufArray).take(bytesRead))
                   else
                     ZIO.succeed(Chunk.fromArray(bufArray))
        } yield bytes
      }
    }

  /**
   * Creates a stream from a `java.io.InputStream`. Ensures that the input
   * stream is closed after it is exhausted.
   */
  def fromInputStreamZIO[R](
    is: => ZIO[R, IOException, InputStream],
    chunkSize: => Int = ZStream.DefaultChunkSize
  )(implicit trace: Trace): ZStream[R, IOException, Byte] =
    fromInputStreamScoped[R](ZIO.acquireRelease(is)(is => ZIO.succeed(is.close())), chunkSize)

  /**
   * Creates a stream from a scoped `java.io.InputStream` value.
   */
  def fromInputStreamScoped[R](
    is: => ZIO[Scope with R, IOException, InputStream],
    chunkSize: => Int = ZStream.DefaultChunkSize
  )(implicit trace: Trace): ZStream[R, IOException, Byte] =
    ZStream.scoped[R](is).flatMap(fromInputStream(_, chunkSize))

  /**
   * Creates a stream from an iterable collection of values
   */
  def fromIterable[O](as: => Iterable[O])(implicit trace: Trace): ZStream[Any, Nothing, O] =
    fromIterable(as, DefaultChunkSize)

  /**
   * Creates a stream from an iterable collection of values
   */
  def fromIterable[O](as: => Iterable[O], chunkSize: => Int)(implicit trace: Trace): ZStream[Any, Nothing, O] =
    ZStream.suspend {
      as match {
        case chunk: Chunk[O]       => ZStream.fromChunk(chunk)
        case iterable: Iterable[O] => ZStream.fromIteratorSucceed(iterable.iterator, chunkSize)
      }
    }

  /**
   * Creates a stream from an effect producing a value of type `Iterable[A]`
   */
  def fromIterableZIO[R, E, O](iterable: => ZIO[R, E, Iterable[O]])(implicit trace: Trace): ZStream[R, E, O] =
    fromIterableZIO(iterable, DefaultChunkSize)

  /**
   * Creates a stream from an effect producing a value of type `Iterable[A]`
   */
  def fromIterableZIO[R, E, O](iterable: => ZIO[R, E, Iterable[O]], chunkSize: => Int)(implicit
    trace: Trace
  ): ZStream[R, E, O] =
    ZStream.unwrap(iterable.map(fromIterable(_, chunkSize)))

  /** Creates a stream from an iterator */
  def fromIterator[A](iterator: => Iterator[A], maxChunkSize: => Int = DefaultChunkSize)(implicit
    trace: Trace
  ): ZStream[Any, Throwable, A] =
    ZStream.succeed(maxChunkSize).flatMap { maxChunkSize =>
      if (maxChunkSize == 1) fromIteratorSingle(iterator)
      else {
        object StreamEnd extends Throwable

        ZStream
          .fromZIO(
            FiberRef.currentFatal.get <*> ZIO.attempt(iterator) <*> ZIO.runtime[Any] <*> ZIO
              .succeed(ChunkBuilder.make[A](maxChunkSize))
          )
          .flatMap { case (isFatal, it, rt, builder) =>
            ZStream.repeatZIOChunkOption {
              ZIO.attempt {
                builder.clear()
                var count = 0

                try {
                  while (count < maxChunkSize && it.hasNext) {
                    builder += it.next()
                    count += 1
                  }
                } catch {
                  case e: Throwable if !isFatal(e) =>
                    throw e
                }

                if (count > 0) {
                  builder.result()
                } else {
                  throw StreamEnd
                }
              }.mapError {
                case StreamEnd => None
                case e         => Some(e)
              }
            }
          }
      }
    }

  private def fromIteratorSingle[A](iterator: => Iterator[A])(implicit
    trace: Trace
  ): ZStream[Any, Throwable, A] = {
    object StreamEnd extends Throwable

    ZStream.fromZIO(FiberRef.currentFatal.get <*> ZIO.attempt(iterator) <*> ZIO.runtime[Any]).flatMap {
      case (isFatal, it, rt) =>
        ZStream.repeatZIOOption {
          ZIO.attempt {

            val hasNext: Boolean =
              try it.hasNext
              catch {
                case e: Throwable if !isFatal(e) =>
                  throw e
              }

            if (hasNext) {
              try it.next()
              catch {
                case e: Throwable if !isFatal(e) =>
                  throw e
              }
            } else throw StreamEnd
          }.mapError {
            case StreamEnd => None
            case e         => Some(e)
          }
        }
    }
  }

  /**
   * Creates a stream from a scoped iterator
   */
  def fromIteratorScoped[R, A](
    iterator: => ZIO[Scope with R, Throwable, Iterator[A]],
    maxChunkSize: => Int = DefaultChunkSize
  )(implicit
    trace: Trace
  ): ZStream[R, Throwable, A] =
    scoped[R](iterator).flatMap(fromIterator(_, maxChunkSize))

  /**
   * Creates a stream from an iterator
   */
  def fromIteratorSucceed[A](iterator: => Iterator[A], maxChunkSize: => Int = DefaultChunkSize)(implicit
    trace: Trace
  ): ZStream[Any, Nothing, A] = {

    def writeOneByOne(iterator: Iterator[A]): ZChannel[Any, Any, Any, Any, Nothing, Chunk[A], Any] =
      if (iterator.hasNext)
        ZChannel.write(Chunk.single(iterator.next())) *> writeOneByOne(iterator)
      else
        ZChannel.unit

    def writeChunks(iterator: Iterator[A]): ZChannel[Any, Any, Any, Any, Nothing, Chunk[A], Any] =
      ZChannel.succeed(ChunkBuilder.make[A]()).flatMap { builder =>
        def loop(iterator: Iterator[A]): ZChannel[Any, Any, Any, Any, Nothing, Chunk[A], Any] = {
          builder.clear()
          var count = 0
          while (count < maxChunkSize && iterator.hasNext) {
            builder += iterator.next()
            count += 1
          }
          if (count > 0)
            ZChannel.write(builder.result()) *> loop(iterator)
          else
            ZChannel.unit
        }

        loop(iterator)
      }

    ZStream.fromChannel {
      ZChannel.suspend {
        if (maxChunkSize == 1)
          writeOneByOne(iterator)
        else
          writeChunks(iterator)
      }
    }
  }

  /**
   * Creates a stream from an iterator that may potentially throw exceptions
   */
  def fromIteratorZIO[R, A](iterator: => ZIO[R, Throwable, Iterator[A]])(implicit
    trace: Trace
  ): ZStream[R, Throwable, A] =
    fromIteratorZIO(iterator, DefaultChunkSize)

  /**
   * Creates a stream from an iterator that may potentially throw exceptions
   */
  def fromIteratorZIO[R, A](
    iterator: => ZIO[R, Throwable, Iterator[A]],
    chunkSize: Int
  )(implicit trace: Trace): ZStream[R, Throwable, A] =
    fromZIO(iterator).flatMap(fromIterator(_, chunkSize))

  /**
   * Creates a stream from a Java iterator that may throw exceptions
   */
  def fromJavaIterator[A](iterator: => java.util.Iterator[A])(implicit trace: Trace): ZStream[Any, Throwable, A] =
    fromJavaIterator(iterator, DefaultChunkSize)

  /**
   * Creates a stream from a Java iterator that may throw exceptions
   */
  def fromJavaIterator[A](
    iterator: => java.util.Iterator[A],
    chunkSize: Int
  )(implicit trace: Trace): ZStream[Any, Throwable, A] =
    fromIterator(
      {
        val it = iterator // Scala 2.13 scala.collection.Iterator has `iterator` in local scope
        new Iterator[A] {
          def next(): A        = it.next
          def hasNext: Boolean = it.hasNext
        }
      },
      chunkSize
    )

  /**
   * Creates a stream from a scoped iterator
   */
  def fromJavaIteratorScoped[R, A](iterator: => ZIO[Scope with R, Throwable, java.util.Iterator[A]])(implicit
    trace: Trace
  ): ZStream[R, Throwable, A] =
    fromJavaIteratorScoped[R, A](iterator, DefaultChunkSize)

  /**
   * Creates a stream from a scoped iterator
   */
  def fromJavaIteratorScoped[R, A](
    iterator: => ZIO[Scope with R, Throwable, java.util.Iterator[A]],
    chunkSize: Int
  )(implicit
    trace: Trace
  ): ZStream[R, Throwable, A] =
    scoped[R](iterator).flatMap(fromJavaIterator(_, chunkSize))

  /**
   * Creates a stream from a Java iterator
   */
  def fromJavaIteratorSucceed[A](iterator: => java.util.Iterator[A])(implicit trace: Trace): ZStream[Any, Nothing, A] =
    fromJavaIteratorSucceed(iterator, DefaultChunkSize)

  /**
   * Creates a stream from a Java iterator
   */
  def fromJavaIteratorSucceed[A](
    iterator: => java.util.Iterator[A],
    chunkSize: Int
  )(implicit trace: Trace): ZStream[Any, Nothing, A] =
    fromIteratorSucceed(
      {
        val it = iterator // Scala 2.13 scala.collection.Iterator has `iterator` in local scope
        new Iterator[A] {
          def next(): A        = it.next
          def hasNext: Boolean = it.hasNext
        }
      },
      chunkSize
    )

  /**
   * Creates a stream from a Java iterator that may potentially throw exceptions
   */
  def fromJavaIteratorZIO[R, A](iterator: => ZIO[R, Throwable, java.util.Iterator[A]])(implicit
    trace: Trace
  ): ZStream[R, Throwable, A] =
    fromJavaIteratorZIO(iterator, DefaultChunkSize)

  /**
   * Creates a stream from a Java iterator that may potentially throw exceptions
   */
  def fromJavaIteratorZIO[R, A](
    iterator: => ZIO[R, Throwable, java.util.Iterator[A]],
    chunkSize: Int
  )(implicit trace: Trace): ZStream[R, Throwable, A] =
    fromZIO(iterator).flatMap(fromJavaIterator(_, chunkSize))

  /**
   * Creates a stream from a ZIO effect that pulls elements from another stream.
   * See `toPull` for reference
   */
  def fromPull[R, E, A](zio: ZIO[Scope with R, Nothing, ZIO[R, Option[E], Chunk[A]]])(implicit
    trace: Trace
  ): ZStream[R, E, A] =
    ZStream.unwrapScoped[R](zio.map(pull => repeatZIOChunkOption(pull)))

  /**
   * Creates a stream from a queue of values
   *
   * @param maxChunkSize
   *   Maximum number of queued elements to put in one chunk in the stream
   */
  def fromQueue[O](
    queue: => Dequeue[O],
    maxChunkSize: => Int = DefaultChunkSize
  )(implicit trace: Trace): ZStream[Any, Nothing, O] =
    repeatZIOChunkOption {
      queue
        .takeBetween(1, maxChunkSize)
        .catchAllCause(c =>
          queue.isShutdown.flatMap { down =>
            if (down && c.isInterrupted) Pull.end
            else Pull.failCause(c)
          }
        )
    }

  /**
   * Creates a stream from a queue of values. The queue will be shutdown once
   * the stream is closed.
   *
   * @param maxChunkSize
   *   Maximum number of queued elements to put in one chunk in the stream
   */
  def fromQueueWithShutdown[O](
    queue: => Dequeue[O],
    maxChunkSize: => Int = DefaultChunkSize
  )(implicit trace: Trace): ZStream[Any, Nothing, O] =
    fromQueue(queue, maxChunkSize).ensuring(queue.shutdown)

  /**
   * Creates a stream from a [[zio.Schedule]] that does not require any further
   * input. The stream will emit an element for each value output from the
   * schedule, continuing for as long as the schedule continues.
   */
  def fromSchedule[R, A](schedule: => Schedule[R, Any, A])(implicit
    trace: Trace
  ): ZStream[R, Nothing, A] =
    unwrap(schedule.driver.map(driver => repeatZIOOption(driver.next(()))))

  /**
   * Creates a stream from a [[zio.stm.TPriorityQueue]] of values.
   */
  def fromTPriorityQueue[A](queue: => TPriorityQueue[A])(implicit trace: Trace): ZStream[Any, Nothing, A] =
    repeatZIOChunk(queue.take.map(Chunk.single(_)).commit)

  /**
   * Creates a stream from a [[zio.stm.TQueue]] of values.
   */
  def fromTQueue[A](queue: => TDequeue[A])(implicit trace: Trace): ZStream[Any, Nothing, A] =
    repeatZIOChunk(queue.take.map(Chunk.single(_)).commit)

  /**
   * Creates a stream from an effect producing a value of type `A`
   */
  def fromZIO[R, E, A](fa: => ZIO[R, E, A])(implicit trace: Trace): ZStream[R, E, A] =
    fromZIOOption(fa.mapError(Some(_)))

  /**
   * Creates a stream from an effect producing a value of type `A` or an empty
   * Stream
   */
  def fromZIOOption[R, E, A](fa: => ZIO[R, Option[E], A])(implicit trace: Trace): ZStream[R, E, A] =
    new ZStream(
      ZChannel.unwrap(
        fa.fold(
          {
            case Some(e) => ZChannel.fail(e)
            case None    => ZChannel.unit
          },
          a => ZChannel.write(Chunk.single(a))
        )
      )
    )

  /**
   * The infinite stream of iterative function application: a, f(a), f(f(a)),
   * f(f(f(a))), ...
   */
  def iterate[A](a: => A)(f: A => A)(implicit trace: Trace): ZStream[Any, Nothing, A] =
    unfold(a)(a => Some((a, f(a))))

  /**
   * Logs the specified message at the current log level.
   */
  def log(message: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.log(message))

  /**
   * Annotates each log in streams composed after this with the specified log
   * annotation.
   */
  def logAnnotate(key: => String, value: => String)(implicit
    trace: Trace
  ): ZStream[Any, Nothing, Unit] =
    logAnnotate(LogAnnotation(key, value))

  /**
   * Annotates each log in streams composed after this with the specified log
   * annotation.
   */
  def logAnnotate(annotation: => LogAnnotation, annotations: LogAnnotation*)(implicit
    trace: Trace
  ): ZStream[Any, Nothing, Unit] =
    logAnnotate(Set(annotation) ++ annotations.toSet)

  /**
   * Annotates each log in streams composed after this with the specified log
   * annotation.
   */
  def logAnnotate(annotations: => Set[LogAnnotation])(implicit
    trace: Trace
  ): ZStream[Any, Nothing, Unit] =
    ZStream.scoped(ZIO.logAnnotateScoped(annotations))

  /**
   * Retrieves the log annotations associated with the current scope.
   */
  def logAnnotations(implicit trace: Trace): ZStream[Any, Nothing, Map[String, String]] =
    ZStream.fromZIO(FiberRef.currentLogAnnotations.get)

  /**
   * Logs the specified message at the debug log level.
   */
  def logDebug(message: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.logDebug(message))

  /**
   * Logs the specified message at the error log level.
   */
  def logError(message: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.logError(message))

  /**
   * Logs the specified cause as an error.
   */
  def logErrorCause(cause: => Cause[Any])(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.logErrorCause(cause))

  /**
   * Logs the specified message at the fatal log level.
   */
  def logFatal(message: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.logFatal(message))

  /**
   * Logs the specified message at the informational log level.
   */
  def logInfo(message: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.logInfo(message))

  /**
   * Sets the log level for streams composed after this.
   */
  def logLevel(level: LogLevel)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.scoped(ZIO.logLevelScoped(level))

  /**
   * Adjusts the label for the logging span for streams composed after this.
   */
  def logSpan(label: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.scoped(ZIO.logSpanScoped(label))

  /**
   * Logs the specified message at the trace log level.
   */
  def logTrace(message: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.logTrace(message))

  /**
   * Logs the specified message at the warning log level.
   */
  def logWarning(message: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.fromZIO(ZIO.logWarning(message))

  /**
   * Creates a single-valued stream from a scoped resource
   */
  def scoped[R]: ScopedPartiallyApplied[R] =
    new ScopedPartiallyApplied[R]

  /**
   * Creates a single-valued stream from a scoped resource
   */
  def scopedWith[R, E, A](f: Scope => ZIO[R, E, A])(implicit trace: Trace): ZStream[R, E, A] =
    new ZStream(ZChannel.scopedWith(scope => f(scope).map(Chunk.single)))

  /**
   * Merges a variable list of streams in a non-deterministic fashion. Up to `n`
   * streams may be consumed in parallel and up to `outputBuffer` chunks may be
   * buffered by this operator.
   */
  def mergeAll[R, E, O](n: => Int, outputBuffer: => Int = 16)(
    streams: ZStream[R, E, O]*
  )(implicit trace: Trace): ZStream[R, E, O] =
    fromIterable(streams).flattenPar(n, outputBuffer)

  /**
   * Like [[mergeAll]], but runs all streams concurrently.
   */
  def mergeAllUnbounded[R, E, O](outputBuffer: => Int = 16)(
    streams: ZStream[R, E, O]*
  )(implicit trace: Trace): ZStream[R, E, O] = mergeAll(Int.MaxValue, outputBuffer)(streams: _*)

  /**
   * The stream that never produces any value or fails with any error.
   */
  def never(implicit trace: Trace): ZStream[Any, Nothing, Nothing] =
    ZStream.fromZIO(ZIO.never)

  /**
   * Like [[unfold]], but allows the emission of values to end one step further
   * than the unfolding of the state. This is useful for embedding paginated
   * APIs, hence the name.
   */
  def paginate[R, E, A, S](s: => S)(f: S => (A, Option[S]))(implicit trace: Trace): ZStream[Any, Nothing, A] =
    paginateChunk(s) { s =>
      val page = f(s)
      Chunk.single(page._1) -> page._2
    }

  /**
   * Like [[unfoldChunk]], but allows the emission of values to end one step
   * further than the unfolding of the state. This is useful for embedding
   * paginated APIs, hence the name.
   */
  def paginateChunk[A, S](
    s: => S
  )(f: S => (Chunk[A], Option[S]))(implicit trace: Trace): ZStream[Any, Nothing, A] = {
    def loop(s: S): ZChannel[Any, Any, Any, Any, Nothing, Chunk[A], Any] =
      f(s) match {
        case (as, Some(s)) => ZChannel.write(as) *> loop(s)
        case (as, None)    => ZChannel.write(as) *> ZChannel.unit
      }

    new ZStream(loop(s))
  }

  /**
   * Like [[unfoldChunkZIO]], but allows the emission of values to end one step
   * further than the unfolding of the state. This is useful for embedding
   * paginated APIs, hence the name.
   */
  def paginateChunkZIO[R, E, A, S](
    s: => S
  )(f: S => ZIO[R, E, (Chunk[A], Option[S])])(implicit trace: Trace): ZStream[R, E, A] = {
    def loop(s: S): ZChannel[R, Any, Any, Any, E, Chunk[A], Any] =
      ZChannel.unwrap {
        f(s).map {
          case (as, Some(s)) => ZChannel.write(as) *> loop(s)
          case (as, None)    => ZChannel.write(as) *> ZChannel.unit
        }
      }

    new ZStream(ZChannel.suspend(loop(s)))
  }

  def provideLayer[RIn, E, ROut, RIn2, ROut2](layer: ZLayer[RIn, E, ROut])(
    stream: => ZStream[ROut with RIn2, E, ROut2]
  )(implicit
    ev: EnvironmentTag[RIn2],
    tag: EnvironmentTag[ROut],
    trace: Trace
  ): ZStream[RIn with RIn2, E, ROut2] =
    ZStream.suspend(stream.provideSomeLayer[RIn with RIn2](ZLayer.environment[RIn2] ++ layer))

  /**
   * Like [[unfoldZIO]], but allows the emission of values to end one step
   * further than the unfolding of the state. This is useful for embedding
   * paginated APIs, hence the name.
   */
  def paginateZIO[R, E, A, S](s: => S)(f: S => ZIO[R, E, (A, Option[S])])(implicit
    trace: Trace
  ): ZStream[R, E, A] =
    paginateChunkZIO(s)(f(_).map { case (a, s) => Chunk.single(a) -> s })

  /**
   * Constructs a stream from a range of integers (lower bound included, upper
   * bound not included)
   */
  def range(min: => Int, max: => Int, chunkSize: => Int = DefaultChunkSize)(implicit
    trace: Trace
  ): ZStream[Any, Nothing, Int] =
    ZStream.suspend {
      def go(min: Int, max: Int, chunkSize: Int): ZChannel[Any, Any, Any, Any, Nothing, Chunk[Int], Any] = {
        val remaining = max - min

        if (remaining > chunkSize)
          ZChannel.write(Chunk.fromArray(Array.range(min, min + chunkSize))) *> go(min + chunkSize, max, chunkSize)
        else {
          ZChannel.write(Chunk.fromArray(Array.range(min, min + remaining)))
        }
      }

      new ZStream(go(min, max, chunkSize))
    }

  /**
   * Repeats the provided value infinitely.
   */
  def repeat[A](a: => A)(implicit trace: Trace): ZStream[Any, Nothing, A] =
    new ZStream(ZChannel.succeed(a).flatMap(a => ZChannel.write(Chunk.single(a)).repeated))

  /**
   * Repeats the value using the provided schedule.
   */
  def repeatWithSchedule[R, A](a: => A, schedule: => Schedule[R, A, _])(implicit
    trace: Trace
  ): ZStream[R, Nothing, A] =
    repeatZIOWithSchedule(ZIO.succeed(a), schedule)

  /**
   * Creates a stream from an effect producing a value of type `A` which repeats
   * forever.
   */
  def repeatZIO[R, E, A](fa: => ZIO[R, E, A])(implicit trace: Trace): ZStream[R, E, A] =
    repeatZIOOption(fa.mapError(Some(_)))

  /**
   * Creates a stream from an effect producing chunks of `A` values which
   * repeats forever.
   */
  def repeatZIOChunk[R, E, A](fa: => ZIO[R, E, Chunk[A]])(implicit trace: Trace): ZStream[R, E, A] =
    repeatZIOChunkOption(fa.mapError(Some(_)))

  /**
   * Creates a stream from an effect producing chunks of `A` values until it
   * fails with None.
   */
  def repeatZIOChunkOption[R, E, A](
    fa: => ZIO[R, Option[E], Chunk[A]]
  )(implicit trace: Trace): ZStream[R, E, A] =
    unfoldChunkZIO(fa)(fa =>
      fa.map(chunk => Some((chunk, fa))).catchAll {
        case None    => Exit.none
        case Some(e) => Exit.fail(e)
      }
    )

  /**
   * Creates a stream from an effect producing values of type `A` until it fails
   * with None.
   */
  def repeatZIOOption[R, E, A](fa: => ZIO[R, Option[E], A])(implicit trace: Trace): ZStream[R, E, A] =
    repeatZIOChunkOption(fa.map(Chunk.single))

  /**
   * Creates a stream from an effect producing a value of type `A`, which is
   * repeated using the specified schedule.
   */
  def repeatZIOWithSchedule[R, E, A](
    effect: => ZIO[R, E, A],
    schedule: => Schedule[R, A, Any]
  )(implicit trace: Trace): ZStream[R, E, A] =
    ZStream((effect, schedule)).flatMap { case (effect, schedule) =>
      ZStream.fromZIO(effect zip schedule.driver).flatMap { case (a, driver) =>
        ZStream.succeed(a) ++
          ZStream.unfoldZIO(a)(driver.next(_).foldZIO(ZIO.succeed(_), _ => effect.map(nextA => Some(nextA -> nextA))))
      }
    }

  /**
   * Accesses the specified service in the environment of the effect.
   */
  def service[A: Tag](implicit trace: Trace): ZStream[A, Nothing, A] =
    ZStream.serviceWith(identity)

  /**
   * Accesses the service corresponding to the specified key in the environment.
   */
  def serviceAt[Service]: ZStream.ServiceAtPartiallyApplied[Service] =
    new ZStream.ServiceAtPartiallyApplied[Service]

  /**
   * Accesses the specified service in the environment of the stream.
   */
  def serviceWith[Service]: ServiceWithPartiallyApplied[Service] =
    new ServiceWithPartiallyApplied[Service]

  /**
   * Accesses the specified service in the environment of the stream in the
   * context of an effect.
   */
  def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
    new ServiceWithZIOPartiallyApplied[Service]

  /**
   * Accesses the specified service in the environment of the stream in the
   * context of a stream.
   */
  def serviceWithStream[Service]: ServiceWithStreamPartiallyApplied[Service] =
    new ServiceWithStreamPartiallyApplied[Service]

  /**
   * Creates a single-valued pure stream
   */
  def succeed[A](a: => A)(implicit trace: Trace): ZStream[Any, Nothing, A] =
    fromChunk(Chunk.single(a))

  /**
   * Returns a lazily constructed stream.
   */
  def suspend[R, E, A](stream: => ZStream[R, E, A]): ZStream[R, E, A] =
    new ZStream(ZChannel.suspend(stream.channel))

  /**
   * Annotates each metric in streams composed after this with the specified
   * tag.
   */
  def tagged(key: => String, value: => String)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    tagged(Set(MetricLabel(key, value)))

  /**
   * Annotates each metric in streams composed after this with the specified
   * tag.
   */
  def tagged(tag: => MetricLabel, tags: MetricLabel*)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    tagged(Set(tag) ++ tags.toSet)

  /**
   * Annotates each metric in streams composed after this with the specified
   * tag.
   */
  def tagged(tags: => Set[MetricLabel])(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    ZStream.scoped(ZIO.taggedScoped(tags))

  /**
   * Retrieves the metric tags associated with the current scope.
   */
  def tags(implicit trace: Trace): ZStream[Any, Nothing, Set[MetricLabel]] =
    ZStream.fromZIO(ZIO.tags)

  /**
   * A stream that emits Unit values spaced by the specified duration.
   */
  def tick(interval: => Duration)(implicit trace: Trace): ZStream[Any, Nothing, Unit] =
    repeatWithSchedule((), Schedule.spaced(interval))

  /**
   * A stream that contains a single `Unit` value.
   */
  val unit: ZStream[Any, Nothing, Unit] =
    succeed(())(Trace.empty)

  /**
   * Creates a stream by peeling off the "layers" of a value of type `S`
   */
  def unfold[S, A](s: => S)(f: S => Option[(A, S)])(implicit trace: Trace): ZStream[Any, Nothing, A] =
    unfoldChunk(s)(f(_).map { case (a, s) => Chunk.single(a) -> s })

  /**
   * Creates a stream by peeling off the "layers" of a value of type `S`.
   */
  def unfoldChunk[S, A](
    s: => S
  )(f: S => Option[(Chunk[A], S)])(implicit trace: Trace): ZStream[Any, Nothing, A] = {
    def loop(s: S): ZChannel[Any, Any, Any, Any, Nothing, Chunk[A], Any] =
      f(s) match {
        case Some((as, s)) => ZChannel.write(as) *> loop(s)
        case None          => ZChannel.unit
      }

    new ZStream(ZChannel.suspend(loop(s)))
  }

  /**
   * Creates a stream by effectfully peeling off the "layers" of a value of type
   * `S`
   */
  def unfoldChunkZIO[R, E, A, S](
    s: => S
  )(f: S => ZIO[R, E, Option[(Chunk[A], S)]])(implicit trace: Trace): ZStream[R, E, A] = {
    def loop(s: S): ZChannel[R, Any, Any, Any, E, Chunk[A], Any] =
      ZChannel.unwrap {
        f(s).map {
          case Some((as, s)) => ZChannel.write(as) *> loop(s)
          case None          => ZChannel.unit
        }
      }

    new ZStream(loop(s))
  }

  /**
   * Creates a stream by effectfully peeling off the "layers" of a value of type
   * `S`
   */
  def unfoldZIO[R, E, A, S](s: => S)(f: S => ZIO[R, E, Option[(A, S)]])(implicit
    trace: Trace
  ): ZStream[R, E, A] =
    unfoldChunkZIO(s)(f(_).map(_.map { case (a, s) => Chunk.single(a) -> s }))

  /**
   * Creates a stream produced from an effect
   */
  def unwrap[R, E, A](fa: => ZIO[R, E, ZStream[R, E, A]])(implicit trace: Trace): ZStream[R, E, A] =
    fromZIO(fa).flatten

  /**
   * Creates a stream produced from a scoped [[ZIO]]
   */
  def unwrapScoped[R]: UnwrapScopedPartiallyApplied[R] =
    new UnwrapScopedPartiallyApplied[R]

  /**
   * Creates a stream produced from a scoped [[ZIO]]
   */
  def unwrapScopedWith[R, E, A](f: Scope => ZIO[R, E, ZStream[R, E, A]])(implicit trace: Trace): ZStream[R, E, A] =
    scopedWith(scope => f(scope)).flatten

  /**
   * Returns the specified stream if the given condition is satisfied, otherwise
   * returns an empty stream.
   */
  def when[R, E, O](b: => Boolean)(zStream: => ZStream[R, E, O])(implicit trace: Trace): ZStream[R, E, O] =
    whenZIO(ZIO.succeed(b))(zStream)

  /**
   * Returns the resulting stream when the given `PartialFunction` is defined
   * for the given value, otherwise returns an empty stream.
   */
  def whenCase[R, E, A, O](a: => A)(pf: PartialFunction[A, ZStream[R, E, O]])(implicit
    trace: Trace
  ): ZStream[R, E, O] =
    whenCaseZIO(ZIO.succeed(a))(pf)

  /**
   * Returns the resulting stream when the given `PartialFunction` is defined
   * for the given effectful value, otherwise returns an empty stream.
   */
  def whenCaseZIO[R, E, A](a: => ZIO[R, E, A]): WhenCaseZIO[R, E, A] =
    new WhenCaseZIO(() => a)

  /**
   * Returns the specified stream if the given effectful condition is satisfied,
   * otherwise returns an empty stream.
   */
  def whenZIO[R, E](b: => ZIO[R, E, Boolean]) =
    new WhenZIO(() => b)

  final class EnvironmentWithPartiallyApplied[R](private val dummy: Boolean = true) extends AnyVal {
    def apply[A](f: ZEnvironment[R] => A)(implicit trace: Trace): ZStream[R, Nothing, A] =
      ZStream.environment[R].map(f)
  }

  final class EnvironmentWithZIOPartiallyApplied[R](private val dummy: Boolean = true) extends AnyVal {
    def apply[R1 <: R, E, A](f: ZEnvironment[R] => ZIO[R1, E, A])(implicit
      trace: Trace
    ): ZStream[R with R1, E, A] =
      ZStream.environment[R].mapZIO(f)
  }

  final class EnvironmentWithStreamPartiallyApplied[R](private val dummy: Boolean = true) extends AnyVal {
    def apply[R1 <: R, E, A](f: ZEnvironment[R] => ZStream[R1, E, A])(implicit
      trace: Trace
    ): ZStream[R with R1, E, A] =
      ZStream.environment[R].flatMap(f)
  }

  final class ScopedPartiallyApplied[R](private val dummy: Boolean = true) extends AnyVal {
    def apply[E, A](zio: => ZIO[Scope with R, E, A])(implicit trace: Trace): ZStream[R, E, A] =
      new ZStream(ZChannel.scoped[R](zio.map(Chunk.single)))
  }

  final class ServiceAtPartiallyApplied[Service](private val dummy: Boolean = true) extends AnyVal {
    def apply[Key](
      key: => Key
    )(implicit
      tag: EnvironmentTag[Map[Key, Service]],
      trace: Trace
    ): ZStream[Map[Key, Service], Nothing, Option[Service]] =
      ZStream.environmentWith(_.getAt(key))
  }

  final class ServiceWithPartiallyApplied[Service](private val dummy: Boolean = true) extends AnyVal {
    def apply[A](f: Service => A)(implicit
      tag: Tag[Service],
      trace: Trace
    ): ZStream[Service, Nothing, A] =
      ZStream.fromZIO(ZIO.serviceWith[Service](f))
  }

  final class ServiceWithZIOPartiallyApplied[Service](private val dummy: Boolean = true) extends AnyVal {
    def apply[R <: Service, E, A](f: Service => ZIO[R, E, A])(implicit
      tag: Tag[Service],
      trace: Trace
    ): ZStream[R with Service, E, A] =
      ZStream.fromZIO(ZIO.serviceWithZIO[Service](f))
  }

  final class ServiceWithStreamPartiallyApplied[Service](private val dummy: Boolean = true) extends AnyVal {
    def apply[R <: Service, E, A](f: Service => ZStream[R, E, A])(implicit
      tag: Tag[Service],
      trace: Trace
    ): ZStream[R with Service, E, A] =
      ZStream.service[Service].flatMap(f)
  }

  final class UnwrapScopedPartiallyApplied[R](private val dummy: Boolean = true) extends AnyVal {
    def apply[E, A](fa: => ZIO[Scope with R, E, ZStream[R, E, A]])(implicit
      trace: Trace
    ): ZStream[R, E, A] =
      scoped[R](fa).flatten
  }

  /**
   * Representation of a grouped stream. This allows to filter which groups will
   * be processed. Once this is applied all groups will be processed in parallel
   * and the results will be merged in arbitrary order.
   */
  sealed trait GroupBy[-R, +E, +K, +V] { self =>

    protected def grouped(implicit trace: Trace): ZStream[R, E, (K, Dequeue[Take[E, V]])]

    /**
     * Run the function across all groups, collecting the results in an
     * arbitrary order.
     */
    def apply[R1 <: R, E1 >: E, A](f: (K, ZStream[Any, E, V]) => ZStream[R1, E1, A], buffer: => Int = 16)(implicit
      trace: Trace
    ): ZStream[R1, E1, A] =
      grouped.flatMapPar[R1, E1, A](Int.MaxValue, buffer) { case (k, q) =>
        f(k, ZStream.fromQueueWithShutdown(q).flattenTake)
      }

    /**
     * Only consider the first n groups found in the stream.
     */
    def first(n: => Int): GroupBy[R, E, K, V] =
      new GroupBy[R, E, K, V] {
        override def grouped(implicit trace: Trace): ZStream[R, E, (K, Dequeue[Take[E, V]])] =
          self.grouped.zipWithIndex.filterZIO { case elem @ ((_, q), i) =>
            if (i < n) ZIO.succeed(elem).as(true)
            else q.shutdown.as(false)
          }.map(_._1)
      }

    /**
     * Filter the groups to be processed.
     */
    def filter(f: K => Boolean): GroupBy[R, E, K, V] =
      new GroupBy[R, E, K, V] {
        override def grouped(implicit trace: Trace): ZStream[R, E, (K, Dequeue[Take[E, V]])] =
          self.grouped.filterZIO { case elem @ (k, q) =>
            if (f(k)) ZIO.succeed(elem).as(true)
            else q.shutdown.as(false)
          }
      }
  }

  final class ProvideSomeLayer[R0, -R, +E, +A](private val self: ZStream[R, E, A]) extends AnyVal {
    def apply[E1 >: E, R1](
      layer: => ZLayer[R0, E1, R1]
    )(implicit
      ev: R0 with R1 <:< R,
      tagged: EnvironmentTag[R1],
      trace: Trace
    ): ZStream[R0, E1, A] =
      self.asInstanceOf[ZStream[R0 with R1, E, A]].provideLayer(ZLayer.environment[R0] ++ layer)
  }

  final class UpdateService[-R, +E, +A, M](private val self: ZStream[R, E, A]) extends AnyVal {
    def apply[R1 <: R with M](
      f: M => M
    )(implicit tag: Tag[M], trace: Trace): ZStream[R1, E, A] =
      self.provideSomeEnvironment(_.update(f))
  }

  final class UpdateServiceAt[-R, +E, +A, Service](private val self: ZStream[R, E, A]) extends AnyVal {
    def apply[R1 <: R with Map[Key, Service], Key](key: => Key)(
      f: Service => Service
    )(implicit tag: Tag[Map[Key, Service]], trace: Trace): ZStream[R1, E, A] =
      self.provideSomeEnvironment(_.updateAt(key)(f))
  }

  /**
   * A `ZStreamConstructor[Input]` knows how to construct a `ZStream` value from
   * an input of type `Input`. This allows the type of the `ZStream` value
   * constructed to depend on `Input`.
   */
  trait ZStreamConstructor[Input] {

    /**
     * The type of the `ZStream` value.
     */
    type Out

    /**
     * Constructs a `ZStream` value from the specified input.
     */
    def make(input: => Input)(implicit trace: Trace): Out
  }

  object ZStreamConstructor extends ZStreamConstructorPlatformSpecific {

    /**
     * Constructs a `ZStream[R, E, A]` from a [[zio.stream.ZChannel]]
     */
    implicit def ChannelConstructor[R, E, A]: WithOut[ZChannel[R, Any, Any, Any, E, Chunk[A], Any], ZStream[R, E, A]] =
      new ZStreamConstructor[ZChannel[R, Any, Any, Any, E, Chunk[A], Any]] {
        type Out = ZStream[R, E, A]
        def make(input: => ZChannel[R, Any, Any, Any, E, Chunk[A], Any])(implicit trace: Trace): ZStream[R, E, A] =
          ZStream.fromChannel(input)
      }

    /**
     * Constructs a `ZStream[Any, Nothing, A]` from a `Hub[Chunk[A]]`.
     */
    implicit def ChunkHubConstructor[A]: WithOut[Hub[Chunk[A]], ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[Hub[Chunk[A]]] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => Hub[Chunk[A]])(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.fromChunkHub(input)
      }

    /**
     * Constructs a `ZStream[Any, Nothing, A]` from a Queue[Chunk[A]]`.
     */
    implicit def ChunkQueueConstructor[A]: WithOut[Queue[Chunk[A]], ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[Queue[Chunk[A]]] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => Queue[Chunk[A]])(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.fromChunkQueue(input)
      }

    /**
     * Constructs a `ZStream[Any, Nothing, A]` from an `Iterable[Chunk[A]]`.
     */
    implicit def ChunksConstructor[A, Collection[Element] <: Iterable[Element]]
      : WithOut[Collection[Chunk[A]], ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[Collection[Chunk[A]]] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => Collection[Chunk[A]])(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.fromIterable(input).flatMap(ZStream.fromChunk(_))
      }

    /**
     * Constructs a `ZStream[R, E, A]` from a `ZIO[R, E, Iterable[A]]`.
     */
    implicit def IterableZIOConstructor[R, E, A, Collection[Element] <: Iterable[Element]]
      : WithOut[ZIO[R, E, Collection[A]], ZStream[R, E, A]] =
      new ZStreamConstructor[ZIO[R, E, Collection[A]]] {
        type Out = ZStream[R, E, A]
        def make(input: => ZIO[R, E, Collection[A]])(implicit trace: Trace): ZStream[R, E, A] =
          ZStream.fromIterableZIO(input)
      }

    /**
     * Constructs a `ZStream[Any, Throwable, A]` from an `Iterator[A]`.
     */
    implicit def IteratorConstructor[A, IteratorLike[Element] <: Iterator[Element]]
      : WithOut[IteratorLike[A], ZStream[Any, Throwable, A]] =
      new ZStreamConstructor[IteratorLike[A]] {
        type Out = ZStream[Any, Throwable, A]
        def make(input: => IteratorLike[A])(implicit trace: Trace): ZStream[Any, Throwable, A] =
          ZStream.fromIterator(input)
      }

    /**
     * Constructs a `ZStream[R, Throwable, A]` from a `ZIO[R with Scope,
     * Throwable, Iterator[A]]`.
     */
    implicit def IteratorScopedConstructor[R, E <: Throwable, A, IteratorLike[Element] <: Iterator[Element]]
      : WithOut[ZIO[Scope with R, E, IteratorLike[A]], ZStream[R, Throwable, A]] =
      new ZStreamConstructor[ZIO[Scope with R, E, IteratorLike[A]]] {
        type Out = ZStream[R, Throwable, A]
        def make(input: => ZIO[Scope with R, E, IteratorLike[A]])(implicit
          trace: Trace
        ): ZStream[R, Throwable, A] =
          ZStream.fromIteratorScoped[R, A](input)
      }

    /**
     * Constructs a `ZStream[R, Throwable, A]` from a `ZIO[R, Throwable,
     * Iterator[A]]`.
     */
    implicit def IteratorZIOConstructor[R, E <: Throwable, A, IteratorLike[Element] <: Iterator[Element]]
      : WithOut[ZIO[R, E, IteratorLike[A]], ZStream[R, Throwable, A]] =
      new ZStreamConstructor[ZIO[R, E, IteratorLike[A]]] {
        type Out = ZStream[R, Throwable, A]
        def make(input: => ZIO[R, E, IteratorLike[A]])(implicit trace: Trace): ZStream[R, Throwable, A] =
          ZStream.fromIteratorZIO(input)
      }

    /**
     * Constructs a `ZStream[Any, Throwable, A]` from a `java.util.Iterator[A]`.
     */
    implicit def JavaIteratorConstructor[A, JaveIteratorLike[Element] <: java.util.Iterator[Element]]
      : WithOut[JaveIteratorLike[A], ZStream[Any, Throwable, A]] =
      new ZStreamConstructor[JaveIteratorLike[A]] {
        type Out = ZStream[Any, Throwable, A]
        def make(input: => JaveIteratorLike[A])(implicit trace: Trace): ZStream[Any, Throwable, A] =
          ZStream.fromJavaIterator(input)
      }

    /**
     * Constructs a `ZStream[R, Throwable, A]` from a `ZIO[R with Scope,
     * Throwable, java.util.Iterator[A]]`.
     */
    implicit def JavaIteratorScopedConstructor[R, E <: Throwable, A, JaveIteratorLike[Element] <: java.util.Iterator[
      Element
    ]]: WithOut[ZIO[Scope with R, E, JaveIteratorLike[A]], ZStream[R, Throwable, A]] =
      new ZStreamConstructor[ZIO[Scope with R, E, JaveIteratorLike[A]]] {
        type Out = ZStream[R, Throwable, A]
        def make(input: => ZIO[Scope with R, E, JaveIteratorLike[A]])(implicit
          trace: Trace
        ): ZStream[R, Throwable, A] =
          ZStream.fromJavaIteratorScoped[R, A](input)
      }

    /**
     * Constructs a `ZStream[R, Throwable, A]` from a `ZIO[R, Throwable,
     * java.util.Iterator[A]]`.
     */
    implicit def JavaIteratorZIOConstructor[R, E <: Throwable, A, JaveIteratorLike[Element] <: java.util.Iterator[
      Element
    ]]: WithOut[ZIO[R, E, JaveIteratorLike[A]], ZStream[R, Throwable, A]] =
      new ZStreamConstructor[ZIO[R, E, JaveIteratorLike[A]]] {
        type Out = ZStream[R, Throwable, A]
        def make(input: => ZIO[R, E, JaveIteratorLike[A]])(implicit trace: Trace): ZStream[R, Throwable, A] =
          ZStream.fromJavaIteratorZIO(input)
      }

    /**
     * Constructs a `ZStream[R, Nothing, A]` from a `Schedule[R, Any, A]`.
     */
    implicit def ScheduleConstructor[R, A]: WithOut[Schedule[R, Any, A], ZStream[R, Nothing, A]] =
      new ZStreamConstructor[Schedule[R, Any, A]] {
        type Out = ZStream[R, Nothing, A]
        def make(input: => Schedule[R, Any, A])(implicit trace: Trace): ZStream[R, Nothing, A] =
          ZStream.fromSchedule(input)
      }

    /**
     * Constructs a `ZStream[Any, Nothing, A]` from a `TQueue[A]`.
     */
    implicit def TQueueConstructor[A]: WithOut[TQueue[A], ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[TQueue[A]] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => TQueue[A])(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.fromTQueue(input)
      }
  }

  trait ZStreamConstructorLowPriority1 extends ZStreamConstructorLowPriority2 {

    /**
     * Constructs a `ZStream[Any, Nothing, A]` from a `Chunk[A]`.
     */
    implicit def ChunkConstructor[A]: WithOut[Chunk[A], ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[Chunk[A]] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => Chunk[A])(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.fromChunk(input)
      }

    /**
     * Constructs a `ZStream[Any, Nothing, A]` from a `Hub[A]`.
     */
    implicit def HubConstructor[A]: WithOut[Hub[A], ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[Hub[A]] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => Hub[A])(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.fromHub(input)
      }

    /**
     * Constructs a `ZStream[Any, Nothing, A]` from a `Iterable[A]`.
     */
    implicit def IterableConstructor[A, Collection[Element] <: Iterable[Element]]
      : WithOut[Collection[A], ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[Collection[A]] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => Collection[A])(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.fromIterable(input)
      }

    /**
     * Constructs a `ZStream[Any, Nothing, A]` from a `Queue[A]`.
     */
    implicit def QueueConstructor[A]: WithOut[Queue[A], ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[Queue[A]] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => Queue[A])(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.fromQueue(input)
      }

    /**
     * Construct a `ZStream[R, E, A]` from a `ZIO[R, Option[E], A]`.
     */
    implicit def ZIOOptionConstructor[R, E, A]: WithOut[ZIO[R, Option[E], A], ZStream[R, E, A]] =
      new ZStreamConstructor[ZIO[R, Option[E], A]] {
        type Out = ZStream[R, E, A]
        def make(input: => ZIO[R, Option[E], A])(implicit trace: Trace): ZStream[R, E, A] =
          ZStream.fromZIOOption(input)
      }

    /**
     * Construct a `ZStream[R, E, A]` from a `ZIO[R, Option[E], A]`.
     */
    implicit def ZIOOptionNoneConstructor[R, A]: WithOut[ZIO[R, None.type, A], ZStream[R, Nothing, A]] =
      new ZStreamConstructor[ZIO[R, None.type, A]] {
        type Out = ZStream[R, Nothing, A]
        def make(input: => ZIO[R, None.type, A])(implicit trace: Trace): ZStream[R, Nothing, A] =
          ZStream.fromZIOOption(input)
      }

    /**
     * Construct a `ZStream[R, E, A]` from a `ZIO[R, Option[E], A]`.
     */
    implicit def ZIOOptionSomeConstructor[R, E, A]: WithOut[ZIO[R, Some[E], A], ZStream[R, E, A]] =
      new ZStreamConstructor[ZIO[R, Some[E], A]] {
        type Out = ZStream[R, E, A]
        def make(input: => ZIO[R, Some[E], A])(implicit trace: Trace): ZStream[R, E, A] =
          ZStream.fromZIOOption(input)
      }
  }

  trait ZStreamConstructorLowPriority2 extends ZStreamConstructorLowPriority3 {

    /**
     * Construct a `ZStream[R, E, A]` from a `ZIO[R, E, A]`.
     */
    implicit def ZIOConstructor[R, E, A]: WithOut[ZIO[R, E, A], ZStream[R, E, A]] =
      new ZStreamConstructor[ZIO[R, E, A]] {
        type Out = ZStream[R, E, A]
        def make(input: => ZIO[R, E, A])(implicit trace: Trace): ZStream[R, E, A] =
          ZStream.fromZIO(input)
      }
  }

  trait ZStreamConstructorLowPriority3 {

    /**
     * The type of the `ZStreamConstructor` with the type of the `ZStream`
     * value.
     */
    type WithOut[In, Out0] = ZStreamConstructor[In] { type Out = Out0 }

    /**
     * Construct a `ZStream[R, E, A]` from a `ZIO[R, E, A]`.
     */
    implicit def SucceedConstructor[A]: WithOut[A, ZStream[Any, Nothing, A]] =
      new ZStreamConstructor[A] {
        type Out = ZStream[Any, Nothing, A]
        def make(input: => A)(implicit trace: Trace): ZStream[Any, Nothing, A] =
          ZStream.succeed(input)
      }
  }

  type Pull[-R, +E, +A] = ZIO[R, Option[E], Chunk[A]]

  private[zio] object Pull {
    def emit[A](a: A)(implicit trace: Trace): IO[Nothing, Chunk[A]]         = ZIO.succeed(Chunk.single(a))
    def emit[A](as: Chunk[A])(implicit trace: Trace): IO[Nothing, Chunk[A]] = ZIO.succeed(as)
    def fromDequeue[E, A](d: Dequeue[stream.Take[E, A]])(implicit trace: Trace): IO[Option[E], Chunk[A]] =
      d.take.flatMap(_.done)
    def fail[E](e: E)(implicit trace: Trace): IO[Option[E], Nothing] = ZIO.fail(Some(e))
    def failCause[E](c: Cause[E])(implicit trace: Trace): IO[Option[E], Nothing] =
      Exit.failCause(c).mapError(Some(_))
    def empty[A](implicit trace: Trace): IO[Nothing, Chunk[A]]   = ZIO.succeed(Chunk.empty)
    def end(implicit trace: Trace): IO[Option[Nothing], Nothing] = Exit.failNone
  }

  private[zio] case class BufferedPull[R, E, A](
    upstream: ZIO[R, Option[E], Chunk[A]],
    done: Ref[Boolean],
    cursor: Ref[(Chunk[A], Int)]
  ) {
    def ifNotDone[R1, E1, A1](fa: ZIO[R1, Option[E1], A1])(implicit trace: Trace): ZIO[R1, Option[E1], A1] =
      done.get.flatMap(
        if (_) Pull.end
        else fa
      )

    def update(implicit trace: Trace): ZIO[R, Option[E], Unit] =
      ifNotDone {
        upstream.foldZIO(
          {
            case None    => done.set(true) *> Pull.end
            case Some(e) => Pull.fail(e)
          },
          chunk => cursor.set(chunk -> 0)
        )
      }

    def pullElement(implicit trace: Trace): ZIO[R, Option[E], A] =
      ifNotDone {
        cursor.modify { case (chunk, idx) =>
          if (idx >= chunk.size) (update *> pullElement, (Chunk.empty, 0))
          else (ZIO.succeed(chunk(idx)), (chunk, idx + 1))
        }.flatten
      }

    def pullChunk(implicit trace: Trace): ZIO[R, Option[E], Chunk[A]] =
      ifNotDone {
        cursor.modify { case (chunk, idx) =>
          if (idx >= chunk.size) (update *> pullChunk, (Chunk.empty, 0))
          else (ZIO.succeed(chunk.drop(idx)), (Chunk.empty, 0))
        }.flatten
      }

  }

  private[zio] object BufferedPull {
    def make[R, E, A](
      pull: ZIO[R, Option[E], Chunk[A]]
    )(implicit trace: Trace): ZIO[R, Nothing, BufferedPull[R, E, A]] =
      for {
        done   <- Ref.make(false)
        cursor <- Ref.make[(Chunk[A], Int)](Chunk.empty -> 0)
      } yield BufferedPull(pull, done, cursor)
  }

  /**
   * A synchronous queue-like abstraction that allows a producer to offer an
   * element and wait for it to be taken, and allows a consumer to wait for an
   * element to be available.
   */
  private[zio] class Handoff[A](ref: Ref[Handoff.State[A]]) {
    def offer(a: A)(implicit trace: Trace): UIO[Unit] =
      Promise.make[Nothing, Unit].flatMap { p =>
        ref.modify {
          case s @ Handoff.State.Full(_, notifyProducer) => (notifyProducer.await *> offer(a), s)
          case Handoff.State.Empty(notifyConsumer)       => (notifyConsumer.succeed(()) *> p.await, Handoff.State.Full(a, p))
        }.flatten
      }

    def take(implicit trace: Trace): UIO[A] =
      Promise.make[Nothing, Unit].flatMap { p =>
        ref.modify {
          case Handoff.State.Full(a, notifyProducer)   => (notifyProducer.succeed(()).as(a), Handoff.State.Empty(p))
          case s @ Handoff.State.Empty(notifyConsumer) => (notifyConsumer.await *> take, s)
        }.flatten
      }

    def poll(implicit trace: Trace): UIO[Option[A]] =
      Promise.make[Nothing, Unit].flatMap { p =>
        ref.modify {
          case Handoff.State.Full(a, notifyProducer) => (notifyProducer.succeed(()).as(Some(a)), Handoff.State.Empty(p))
          case s @ Handoff.State.Empty(_)            => (ZIO.succeed(None), s)
        }.flatten
      }
  }

  private[zio] object Handoff {
    def make[A](implicit trace: Trace): UIO[Handoff[A]] =
      Promise
        .make[Nothing, Unit]
        .flatMap(p => Ref.make[State[A]](State.Empty(p)))
        .map(new Handoff(_))

    sealed trait State[+A]
    object State {
      case class Empty(notifyConsumer: Promise[Nothing, Unit])          extends State[Nothing]
      case class Full[+A](a: A, notifyProducer: Promise[Nothing, Unit]) extends State[A]
    }
  }

  final class WhenZIO[R, E](private val b: () => ZIO[R, E, Boolean]) extends AnyVal {
    def apply[R1 <: R, E1 >: E, O](zStream: ZStream[R1, E1, O])(implicit trace: Trace): ZStream[R1, E1, O] =
      fromZIO(b()).flatMap(if (_) zStream else ZStream.empty)
  }

  final class WhenCaseZIO[R, E, A](private val a: () => ZIO[R, E, A]) extends AnyVal {
    def apply[R1 <: R, E1 >: E, O](pf: PartialFunction[A, ZStream[R1, E1, O]])(implicit
      trace: Trace
    ): ZStream[R1, E1, O] =
      fromZIO(a()).flatMap(pf.applyOrElse(_, (_: A) => ZStream.empty))
  }

  sealed trait HaltStrategy
  object HaltStrategy {
    case object Left   extends HaltStrategy
    case object Right  extends HaltStrategy
    case object Both   extends HaltStrategy
    case object Either extends HaltStrategy
  }

  implicit final class RefineToOrDieOps[R, E <: Throwable, A](private val self: ZStream[R, E, A]) extends AnyVal {

    /**
     * Keeps some of the errors, and terminates the fiber with the rest.
     */
    def refineToOrDie[E1 <: E: ClassTag](implicit ev: CanFail[E], trace: Trace): ZStream[R, E1, A] =
      self.refineOrDie { case e: E1 => e }
  }

  implicit final class SyntaxOps[-R, +E, O](self: ZStream[R, E, O]) {
    /*
     * Collect elements of the given type flowing through the stream, and filters out others.
     */
    def collectType[O1 <: O](implicit tag: ClassTag[O1], trace: Trace): ZStream[R, E, O1] =
      self.collect { case o if tag.runtimeClass.isInstance(o) => o.asInstanceOf[O1] }
  }

  private[zio] class Rechunker[A](n: Int) {
    private var buffer: Chunk[A]              = Chunk.empty[A]
    private var chunkBuilder: ChunkBuilder[A] = null
    private var pos: Int                      = 0

    def isEmpty: Boolean = pos == 0

    final def rechunk(
      chunk: Chunk[A]
    )(implicit trace: Trace): ZChannel[Any, ZNothing, Any, Any, ZNothing, Chunk[A], Any] = {
      val chunkSize = chunk.size
      if (chunkSize == 0) {
        null
      } else if (isEmpty && chunkSize == n) {
        ZChannel.write(chunk)
      } else if (n == 1) {
        rechunk1(chunk, chunkSize)
      } else if (chunkSize < 25) {
        // Optimized for small chunks. The limit 25 comes from testing with benchmarks
        var i = 0

        var channel: ZChannel[Any, ZNothing, Any, Any, ZNothing, Chunk[A], Any] = null

        if (chunkBuilder == null) {
          chunkBuilder = ChunkBuilder.make(n)
        }

        while (i < chunkSize) {
          chunkBuilder.addOne(chunk(i))
          i += 1
          pos += 1
          if (pos == n) {
            buffer ++= chunkBuilder.result()
            chunkBuilder.clear()

            // Flush buffer
            val result = ZChannel.write(buffer)
            pos = 0
            buffer = Chunk.empty[A]
            if (channel == null) channel = result
            else channel = channel *> result
          }
        }

        channel
      } else {
        // Optimized for large chunks
        var channel: ZChannel[Any, ZNothing, Any, Any, ZNothing, Chunk[A], Any] = null
        var chunkOffset                                                         = 0

        if (chunkBuilder != null) {
          // Last chunk might have been small and written to chunkBuilder,
          // so add to buffer before continuing with large chunk
          buffer ++= chunkBuilder.result()
          chunkBuilder.clear()
        }

        while (chunkOffset < chunkSize) {
          val needed    = n - pos
          val available = chunkSize - chunkOffset
          val size      = math.min(needed, available)
          buffer ++= chunk.slice(chunkOffset, chunkOffset + size)
          pos += size
          chunkOffset += size

          if (size == needed) {
            // Flush buffer
            val result = ZChannel.write(buffer)
            pos = 0
            buffer = Chunk.empty[A]
            if (channel == null) channel = result
            else channel = channel *> result
          }
        }

        channel
      }
    }

    private def rechunk1(chunk: Chunk[A], len: Int)(implicit
      trace: Trace
    ): ZChannel[Any, ZNothing, Any, Any, ZNothing, Chunk[A], Any] = {
      var channel: ZChannel[Any, ZNothing, Any, Any, ZNothing, Chunk[A], Any] = ZChannel.write(Chunk.single(chunk.head))

      var i = 1
      while (i < len) {
        val c = Chunk.single(chunk(i))
        channel = channel *> ZChannel.write(c)
        i += 1
      }

      channel
    }

    def done()(implicit trace: Trace): ZChannel[Any, ZNothing, Any, Any, ZNothing, Chunk[A], Any] = {
      // Last chunk might have been small and written to chunkBuilder,
      // so add to buffer before last flush
      if (chunkBuilder != null) {
        buffer ++= chunkBuilder.result()
        chunkBuilder.clear()
      }

      if (isEmpty) ZChannel.unit
      else ZChannel.write(buffer)
    }

  }

  private[zio] sealed trait SinkEndReason
  private[zio] object SinkEndReason {
    case object ScheduleEnd extends SinkEndReason
    case object UpstreamEnd extends SinkEndReason
  }

  private[zio] sealed trait HandoffSignal[E, A]
  private[zio] object HandoffSignal {
    case class Emit[E, A](els: Chunk[A])        extends HandoffSignal[E, A]
    case class Halt[E, A](error: Cause[E])      extends HandoffSignal[E, A]
    case class End[E, A](reason: SinkEndReason) extends HandoffSignal[E, A]
  }

  private[zio] sealed trait DebounceState[+E, +A]
  private[zio] object DebounceState {
    case object NotStarted                                         extends DebounceState[Nothing, Nothing]
    case class Previous[A](fiber: Fiber[Nothing, Chunk[A]])        extends DebounceState[Nothing, A]
    case class Current[E, A](fiber: Fiber[E, HandoffSignal[E, A]]) extends DebounceState[E, A]
  }

  /**
   * An `Emit[R, E, A, B]` represents an asynchronous callback that can be
   * called multiple times. The callback can be called with a value of type
   * `ZIO[R, Option[E], Chunk[A]]`, where succeeding with a `Chunk[A]` indicates
   * to emit those elements, failing with `Some[E]` indicates to terminate with
   * that error, and failing with `None` indicates to terminate with an end of
   * stream signal.
   */
  trait Emit[+R, -E, -A, +B] extends (ZIO[R, Option[E], Chunk[A]] => B) {

    def apply(v1: ZIO[R, Option[E], Chunk[A]]): B

    /**
     * Emits a chunk containing the specified values.
     */
    def chunk(as: Chunk[A])(implicit trace: Trace): B =
      apply(ZIO.succeed(as))

    /**
     * Terminates with a cause that dies with the specified `Throwable`.
     */
    def die(t: Throwable)(implicit trace: Trace): B =
      apply(ZIO.die(t))

    /**
     * Terminates with a cause that dies with a `Throwable` with the specified
     * message.
     */
    def dieMessage(message: String)(implicit trace: Trace): B =
      apply(ZIO.dieMessage(message))

    /**
     * Either emits the specified value if this `Exit` is a `Success` or else
     * terminates with the specified cause if this `Exit` is a `Failure`.
     */
    def done(exit: Exit[E, A])(implicit trace: Trace): B =
      apply(ZIO.done(exit.mapBothExit(e => Some(e), a => Chunk.single(a))))

    /**
     * Terminates with an end of stream signal.
     */
    def end(implicit trace: Trace): B =
      apply(Exit.failNone)

    /**
     * Terminates with the specified error.
     */
    def fail(e: E)(implicit trace: Trace): B =
      apply(ZIO.fail(Some(e)))

    /**
     * Either emits the success value of this effect or terminates the stream
     * with the failure value of this effect.
     */
    def fromEffect(zio: ZIO[R, E, A])(implicit trace: Trace): B =
      apply(zio.mapBoth(e => Some(e), a => Chunk.single(a)))

    /**
     * Either emits the success value of this effect or terminates the stream
     * with the failure value of this effect.
     */
    def fromEffectChunk(zio: ZIO[R, E, Chunk[A]])(implicit trace: Trace): B =
      apply(zio.mapError(e => Some(e)))

    /**
     * Terminates the stream with the specified cause.
     */
    def halt(cause: Cause[E])(implicit trace: Trace): B =
      apply(Exit.failCause(cause.map(e => Some(e))))

    /**
     * Emits a chunk containing the specified value.
     */
    def single(a: A)(implicit trace: Trace): B =
      apply(ZIO.succeed(Chunk.single(a)))
  }

  /**
   * Provides extension methods for streams that are sorted by distinct keys.
   */
  implicit final class SortedByKey[R, E, K, A](private val self: ZStream[R, E, (K, A)]) {

    /**
     * Zips this stream that is sorted by distinct keys and the specified stream
     * that is sorted by distinct keys to produce a new stream that is sorted by
     * distinct keys. Combines values associated with each key into a tuple,
     * using the specified values `defaultLeft` and `defaultRight` to fill in
     * missing values.
     *
     * This allows zipping potentially unbounded streams of data by key in
     * constant space but the caller is responsible for ensuring that the
     * streams are sorted by distinct keys.
     */
    def zipAllSortedByKey[R1 <: R, E1 >: E, B](
      that: => ZStream[R1, E1, (K, B)]
    )(defaultLeft: => A, defaultRight: => B)(implicit
      ord: Ordering[K],
      trace: Trace
    ): ZStream[R1, E1, (K, (A, B))] =
      zipAllSortedByKeyWith(that)((_, defaultRight), (defaultLeft, _))((_, _))

    /**
     * Zips this stream that is sorted by distinct keys and the specified stream
     * that is sorted by distinct keys to produce a new stream that is sorted by
     * distinct keys. Keeps only values from this stream, using the specified
     * value `default` to fill in missing values.
     *
     * This allows zipping potentially unbounded streams of data by key in
     * constant space but the caller is responsible for ensuring that the
     * streams are sorted by distinct keys.
     */
    def zipAllSortedByKeyLeft[R1 <: R, E1 >: E, B](
      that: => ZStream[R1, E1, (K, B)]
    )(default: => A)(implicit ord: Ordering[K], trace: Trace): ZStream[R1, E1, (K, A)] =
      zipAllSortedByKeyWith(that)(identity, _ => default)((a, _) => a)

    /**
     * Zips this stream that is sorted by distinct keys and the specified stream
     * that is sorted by distinct keys to produce a new stream that is sorted by
     * distinct keys. Keeps only values from that stream, using the specified
     * value `default` to fill in missing values.
     *
     * This allows zipping potentially unbounded streams of data by key in
     * constant space but the caller is responsible for ensuring that the
     * streams are sorted by distinct keys.
     */
    def zipAllSortedByKeyRight[R1 <: R, E1 >: E, B](
      that: => ZStream[R1, E1, (K, B)]
    )(default: => B)(implicit ord: Ordering[K], trace: Trace): ZStream[R1, E1, (K, B)] =
      zipAllSortedByKeyWith(that)(_ => default, identity)((_, b) => b)

    /**
     * Zips this stream that is sorted by distinct keys and the specified stream
     * that is sorted by distinct keys to produce a new stream that is sorted by
     * distinct keys. Uses the functions `left`, `right`, and `both` to handle
     * the cases where a key and value exist in this stream, that stream, or
     * both streams.
     *
     * This allows zipping potentially unbounded streams of data by key in
     * constant space but the caller is responsible for ensuring that the
     * streams are sorted by distinct keys.
     */
    def zipAllSortedByKeyWith[R1 <: R, E1 >: E, B, C](
      that: => ZStream[R1, E1, (K, B)]
    )(left: A => C, right: B => C)(
      both: (A, B) => C
    )(implicit ord: Ordering[K], trace: Trace): ZStream[R1, E1, (K, C)] = {
      sealed trait State
      case object DrainLeft                                extends State
      case object DrainRight                               extends State
      case object PullBoth                                 extends State
      final case class PullLeft(rightChunk: Chunk[(K, B)]) extends State
      final case class PullRight(leftChunk: Chunk[(K, A)]) extends State

      def pull(
        state: State,
        pullLeft: ZIO[R, Option[E], Chunk[(K, A)]],
        pullRight: ZIO[R1, Option[E1], Chunk[(K, B)]]
      ): ZIO[R1, Nothing, Exit[Option[E1], (Chunk[(K, C)], State)]] =
        state match {
          case DrainLeft =>
            pullLeft.fold(
              e => Exit.fail(e),
              leftChunk => Exit.succeed(leftChunk.map { case (k, a) => (k, left(a)) } -> DrainLeft)
            )
          case DrainRight =>
            pullRight.fold(
              e => Exit.fail(e),
              rightChunk => Exit.succeed(rightChunk.map { case (k, b) => (k, right(b)) } -> DrainRight)
            )
          case PullBoth =>
            pullLeft.unsome
              .zipPar(pullRight.unsome)
              .foldZIO(
                e => ZIO.succeed(Exit.fail(Some(e))),
                {
                  case (Some(leftChunk), Some(rightChunk)) =>
                    if (leftChunk.isEmpty && rightChunk.isEmpty) pull(PullBoth, pullLeft, pullRight)
                    else if (leftChunk.isEmpty) pull(PullLeft(rightChunk), pullLeft, pullRight)
                    else if (rightChunk.isEmpty) pull(PullRight(leftChunk), pullLeft, pullRight)
                    else ZIO.succeed(Exit.succeed(mergeSortedByKeyChunk(leftChunk, rightChunk)))
                  case (Some(leftChunk), None) =>
                    if (leftChunk.isEmpty) pull(DrainLeft, pullLeft, pullRight)
                    else ZIO.succeed(Exit.succeed(leftChunk.map { case (k, a) => (k, left(a)) } -> DrainLeft))
                  case (None, Some(rightChunk)) =>
                    if (rightChunk.isEmpty) pull(DrainRight, pullLeft, pullRight)
                    else
                      ZIO.succeed(Exit.succeed(rightChunk.map { case (k, b) => (k, right(b)) } -> DrainRight))
                  case (None, None) => ZIO.succeed(Exit.fail(None))
                }
              )
          case PullLeft(rightChunk) =>
            pullLeft.foldZIO(
              {
                case Some(e) => ZIO.succeed(Exit.fail(Some(e)))
                case None =>
                  ZIO.succeed(Exit.succeed(rightChunk.map { case (k, b) => (k, right(b)) } -> DrainRight))
              },
              leftChunk =>
                if (leftChunk.isEmpty) pull(PullLeft(rightChunk), pullLeft, pullRight)
                else ZIO.succeed(Exit.succeed(mergeSortedByKeyChunk(leftChunk, rightChunk)))
            )
          case PullRight(leftChunk) =>
            pullRight.foldZIO(
              {
                case Some(e) => ZIO.succeed(Exit.fail(Some(e)))
                case None    => ZIO.succeed(Exit.succeed(leftChunk.map { case (k, a) => (k, left(a)) } -> DrainLeft))
              },
              rightChunk =>
                if (rightChunk.isEmpty) pull(PullRight(leftChunk), pullLeft, pullRight)
                else ZIO.succeed(Exit.succeed(mergeSortedByKeyChunk(leftChunk, rightChunk)))
            )
        }

      def mergeSortedByKeyChunk(
        leftChunk: Chunk[(K, A)],
        rightChunk: Chunk[(K, B)]
      ): (Chunk[(K, C)], State) = {
        val builder       = ChunkBuilder.make[(K, C)]()
        var state         = null.asInstanceOf[State]
        val leftIterator  = leftChunk.iterator
        val rightIterator = rightChunk.iterator
        var leftTuple     = leftIterator.next()
        var rightTuple    = rightIterator.next()
        var k1            = leftTuple._1
        var a             = leftTuple._2
        var k2            = rightTuple._1
        var b             = rightTuple._2
        var loop          = true
        while (loop) {
          val compare = ord.compare(k1, k2)
          if (compare == 0) {
            builder += k1 -> both(a, b)
            if (leftIterator.hasNext && rightIterator.hasNext) {
              leftTuple = leftIterator.next()
              rightTuple = rightIterator.next()
              k1 = leftTuple._1
              a = leftTuple._2
              k2 = rightTuple._1
              b = rightTuple._2
            } else if (leftIterator.hasNext) {
              state = PullRight(Chunk.fromIterator(leftIterator))
              loop = false
            } else if (rightIterator.hasNext) {
              state = PullLeft(Chunk.fromIterator(rightIterator))
              loop = false
            } else {
              state = PullBoth
              loop = false
            }
          } else if (compare < 0) {
            builder += k1 -> left(a)
            if (leftIterator.hasNext) {
              leftTuple = leftIterator.next()
              k1 = leftTuple._1
              a = leftTuple._2
            } else {
              val rightBuilder = ChunkBuilder.make[(K, B)]()
              rightBuilder += rightTuple
              rightBuilder ++= rightIterator
              state = PullLeft(rightBuilder.result())
              loop = false
            }
          } else {
            builder += k2 -> right(b)
            if (rightIterator.hasNext) {
              rightTuple = rightIterator.next()
              k2 = rightTuple._1
              b = rightTuple._2
            } else {
              val leftBuilder = ChunkBuilder.make[(K, A)]()
              leftBuilder += leftTuple
              leftBuilder ++= leftIterator
              state = PullRight(leftBuilder.result())
              loop = false
            }
          }
        }
        (builder.result(), state)
      }

      self.combineChunks[R1, E1, State, (K, B), (K, C)](that)(PullBoth)(pull)
    }
  }

  private def mapDequeue[A, B](dequeue: Dequeue[A])(f: A => B): Dequeue[B] =
    new Dequeue[B] {
      def awaitShutdown(implicit trace: Trace): UIO[Unit] =
        dequeue.awaitShutdown
      def capacity: Int =
        dequeue.capacity
      def isShutdown(implicit trace: Trace): UIO[Boolean] =
        dequeue.isShutdown
      def shutdown(implicit trace: Trace): UIO[Unit] =
        dequeue.shutdown
      def size(implicit trace: Trace): UIO[Int] =
        dequeue.size
      def take(implicit trace: Trace): UIO[B] =
        dequeue.take.map(f)
      def takeAll(implicit trace: Trace): UIO[Chunk[B]] =
        dequeue.takeAll.map(_.map(f))
      def takeUpTo(max: Int)(implicit trace: Trace): UIO[Chunk[B]] =
        dequeue.takeUpTo(max).map(_.map(f))
    }

  /**
   * A variant of `groupBy` that retains the insertion order of keys.
   */
  private def groupBy[K, V](values: Iterable[V])(f: V => K): Chunk[(K, Chunk[V])] = {

    class GroupedBuilder extends Function1[K, ChunkBuilder[V]] { self =>
      val builder = ChunkBuilder.make[(K, ChunkBuilder[V])]()
      def apply(key: K): ChunkBuilder[V] = {
        val builder = ChunkBuilder.make[V]()
        self.builder += key -> builder
        builder
      }
      def result(): Chunk[(K, Chunk[V])] = {
        val chunk = builder.result()
        chunk.map { case (key, builder) => key -> builder.result() }
      }
    }

    val groupedBuilder = new GroupedBuilder
    val iterator       = values.iterator
    val map            = mutable.Map.empty[K, ChunkBuilder[V]]
    while (iterator.hasNext) {
      val value   = iterator.next()
      val key     = f(value)
      val builder = map.getOrElseUpdate(key, groupedBuilder(key))
      builder += value
    }
    groupedBuilder.result()
  }

  private def zipChunks[A, B, C](cl: Chunk[A], cr: Chunk[B], f: (A, B) => C): (Chunk[C], Either[Chunk[A], Chunk[B]]) =
    if (cl.size > cr.size)
      (cl.take(cr.size).zipWith(cr)(f), Left(cl.drop(cr.size)))
    else
      (cl.zipWith(cr.take(cl.size))(f), Right(cr.drop(cl.size)))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy