package io.catbird.util.effect

import cats.effect.ContextShift
import com.twitter.util.{ Future, FuturePool, Promise }
import io.catbird.util.Rerunnable

import scala.Unit
import java.lang.Runnable
import java.util.concurrent.ExecutorService

import scala.concurrent.{ ExecutionContext, ExecutionContextExecutorService }

/**
 * The goal here is to provide an implicit instance for `ContextShift[Rerunnable]`, so you can use libraries like
 * `fs2` in a finagle-based application without converting between `Future` and `IO` everywhere.
 *
 * Usage:
 * {{{
 *   implicit val rerunnableCS: ContextShift[Rerunnable] = RerunnableContextShift.global
 * }}}
 */
object RerunnableContextShift {

  final def fromExecutionContext(ec: ExecutionContext): ContextShift[Rerunnable] =
    new RerunnableContextShift(ec)

  final def fromExecutorService(es: ExecutorService): ContextShift[Rerunnable] =
    fromExecutionContext(ExecutionContext.fromExecutorService(es))

  final def fromExecutionContextExecutorService(eces: ExecutionContextExecutorService): ContextShift[Rerunnable] =
    fromExecutorService(eces)

  final lazy val global: ContextShift[Rerunnable] =
    fromExecutionContext(scala.concurrent.ExecutionContext.global)

  /**
   * Mirrors the api of `scala.concurrent.ExecutionContext.Implicit.global`.
   *
   * Usage:
   * {{{
   *   import io.catbird.util.effect.RerunnableContextShift.Implicits.global
   * }}}
   */
  object Implicits {
    final implicit def global: ContextShift[Rerunnable] = RerunnableContextShift.global
  }
}

final private[effect] class RerunnableContextShift private (ec: ExecutionContext) extends ContextShift[Rerunnable] {
  private final lazy val futurePool = FuturePool.interruptible(ec.asInstanceOf[ExecutionContextExecutorService])

  override def shift: Rerunnable[Unit] =
    Rerunnable.withFuturePool(futurePool)(()) // This is a bit of a hack, but it will have to do

  override def evalOn[A](targetEc: ExecutionContext)(fa: Rerunnable[A]): Rerunnable[A] =
    for {
      r <- executeOn(targetEc)(fa).liftToTry
      _ <- shift
      a <- Rerunnable.fromFuture(Future.value(r).lowerFromTry)
    } yield a

  private def executeOn[A](targetEc: ExecutionContext)(fa: Rerunnable[A]): Rerunnable[A] =
    Rerunnable.fromFuture {
      val p = Promise[A]()

      targetEc.execute(new Runnable {
        override def run(): Unit =
          fa.run.proxyTo[A](p)
      })

      p
    }
}