package ch_02

import cats.effect.*
import utils.*
import java.util.concurrent.Executors
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

val threadPool         = Executors.newFixedThreadPool(8)
given ExecutionContext = ExecutionContext.fromExecutorService(threadPool)

type CallBack[A] = Either[Throwable, A] => Unit

// async is a foreign interface which lifts async computation to an IO
// CE blocks (semantically) until 'cb' is invoked (by some other thread)
def async[A](f: () => Either[Throwable, A]): IO[A] = IO.async_ { (cb: CallBack[A]) =>
  threadPool.execute { () =>
    val result: Either[Throwable, A] = f()
    cb(result) // CE is notified of the result
  }
}

// ex. lift lambda to IO
def asyncToIO[A](f: () => A): IO[A] = IO.async_ { (cb: CallBack[A]) =>
  threadPool.execute { () =>
    val result: Either[Throwable, A] = Try(f()).toEither
    cb(result)
  }
}

// ex. lift Future to IO (a.k.a IO.fromFuture)
def asyncFuture[A](future: => Future[A]): IO[A] = IO.async_ { (cb: CallBack[A]) =>
  future.onComplete {
    case Failure(exception) => cb(Left(exception))
    case Success(value)     => cb(Right(value))
  }
}

// ex. implement a never ending IO (a.k.a IO.never)
def neverEndingIO: IO[Unit] = IO.async_ { _ => () }

// in case the async computation gets canceled, a finalizer is also needed
def asyncWithFinalizer[A](f: () => Either[Throwable, A]): IO[A] = IO.async { (cb: CallBack[A]) =>
  // finalizer in case the computation gets canceled  => IO[Unit]
  // finalizer might not be present                   => Option[IO[Unit]]
  IO {
    threadPool.execute { () =>
      val result = f()
      cb(result)
    }
  }.as(Some(IO("canceled").inspect.void)) // onCancel
}