package tofu.concurrent
import cats.effect.concurrent.MVar
import cats.effect.{Concurrent, Sync}
import cats.Applicative
import tofu.Guarantee
import tofu.concurrent.QVar.QVarByMVar
import tofu.higherKind.{RepresentableK, derived}
import tofu.syntax.monadic._

/** a middleground between cats.concurrent.MVar and zio.Queue.bounded(1) */
trait QVar[+F[_], A] {

  /**
    * Returns `true` if the `MVar` is empty and can receive a `put`, or
    * `false` otherwise.
    *
    * Note that due to concurrent tasks, logic built in terms of `isEmpty`
    * is problematic.
    */
  def isEmpty: F[Boolean]

  /**
    * Fills the `MVar` if it is empty, or blocks (asynchronously)
    * if the `MVar` is full, until the given value is next in
    * line to be consumed on [[take]].
    *
    * This operation is atomic.
    *
    * @return a task that on evaluation will complete when the
    *         `put` operation succeeds in filling the `MVar`,
    *         with the given value being next in line to
    *         be consumed
    */
  def put(a: A): F[Unit]

  /**
    * Empties the `MVar` if full, returning the contained value,
    * or blocks (asynchronously) until a value is available.
    *
    * This operation is atomic.
    *
    * @return a task that on evaluation will be completed after
    *         a value was retrieved
    */
  def take: F[A]

  /**
    * Tries reading the current value, or blocks (asynchronously)
    * until there is a value available.
    *
    * This operation is atomic.
    *
    * @return a task that on evaluation will be completed after
    *         a value has been read
    */
  def read: F[A]
}

object QVar {
  implicit def representableK[A]: RepresentableK[QVar[*[_], A]] = derived.genRepresentableK[QVar[*[_], A]]

  final implicit class QVarOps[F[_], A](private val self: QVar[F, A]) extends AnyVal {
    def toAtom(implicit F: Applicative[F], FG: Guarantee[F]): Atom[F, A] = Atom.QAtom(self)
  }

  final case class QVarByMVar[F[_], A](mvar: MVar[F, A]) extends QVar[F, A] {
    override def isEmpty: F[Boolean] = mvar.isEmpty
    override def put(a: A): F[Unit]  = mvar.put(a)
    override def take: F[A]          = mvar.take
    override def read: F[A]          = mvar.read
  }
}

trait MakeQVar[I[_], F[_]] {
  def qvarOf[A](a: A): I[QVar[F, A]]
  def qvarEmpty[A]: I[QVar[F, A]]
}

object QVars {
  def apply[F[_]](implicit qvars: QVars[F]): MakeQVar.Applier[F, F] = new MakeQVar.Applier(qvars)
}

object MakeQVar {
  def apply[I[_], F[_]](implicit mkvar: MakeQVar[I, F]) = new Applier[I, F](mkvar)

  final class Applier[I[_], F[_]](private val makeMVar: MakeQVar[I, F]) extends AnyVal {
    def empty[A]: I[QVar[F, A]]    = makeMVar.qvarEmpty[A]
    def of[A](a: A): I[QVar[F, A]] = makeMVar.qvarOf(a)
  }

  implicit def concurrentMakeMVar[I[_]: Sync, F[_]: Concurrent]: MakeQVar[I, F] = new MakeQVar[I, F] {
    def qvarOf[A](a: A): I[QVar[F, A]] = MVar.in[I, F, A](a).map(QVarByMVar(_))
    def qvarEmpty[A]: I[QVar[F, A]]    = MVar.emptyIn[I, F, A].map(QVarByMVar(_))
  }
}