package gs.std.v0.effect

import cats.Applicative
import cats.effect.Sync
import cats.syntax.all.*
import fs2.Chunk
import fs2.Stream
import gs.std.v0.core.Blob
import gs.std.v0.core.ByteCount
import java.security.SecureRandom
import java.util.Random

/** Random number generator.
  */
trait Rng[F[_]]:

  /** Explicitly set or update the seed using the given 8 bytes.
    *
    * @param seed
    *   The seed value to assign or augment.
    * @return
    *   This [[Rng]] instance.
    */
  def updateSeed(seed: Long): F[Rng[F]]

  /** @return
    *   The next (unbounded) random `Int`.
    */
  def nextInt(): F[Int]

  /** Calculate a number bounded inclusively within 0 and some bound.
    *
    * @param bound
    *   The bound.
    * @return
    *   The calculated number.
    */
  def nextInt(bound: Int): F[Int]

  /** Calculate a number bounded inclusively within some origin and some bound.
    *
    * @param origin
    *   The origin.
    * @param bound
    *   The bound.
    * @return
    *   The calculated number.
    */
  def nextInt(
    origin: Int,
    bound: Int
  ): F[Int]

  /** @return
    *   The next (unbounded) random `Long`.
    */
  def nextLong(): F[Long]

  /** Calculate a number bounded inclusively within 0 and some bound.
    *
    * @param bound
    *   The bound.
    * @return
    *   The calculated number.
    */
  def nextLong(bound: Long): F[Long]

  /** Calculate a number bounded inclusively within some origin and some bound.
    *
    * @param origin
    *   The origin.
    * @param bound
    *   The bound.
    * @return
    *   The calculated number.
    */
  def nextLong(
    origin: Long,
    bound: Long
  ): F[Long]

  /** @return
    *   The next (unbounded) random `Double`.
    */
  def nextDouble(): F[Double]

  /** Calculate a number bounded inclusively within 0 and some bound.
    *
    * @param bound
    *   The bound.
    * @return
    *   The calculated number.
    */
  def nextDouble(bound: Double): F[Double]

  /** Calculate a number bounded inclusively within some origin and some bound.
    *
    * @param origin
    *   The origin.
    * @param bound
    *   The bound.
    * @return
    *   The calculated number.
    */
  def nextDouble(
    origin: Double,
    bound: Double
  ): F[Double]

  /** @return
    *   The next randomly selected `true` or `false` value.
    */
  def nextBoolean(): F[Boolean]

  /** Produce an infinite stream of `Int` bounded inclusively within some origin
    * and some bound.
    *
    * @param origin
    *   The origin.
    * @param bound
    *   The bound.
    * @return
    *   The infinite stream of values.
    */
  def nextInts(
    origin: Int,
    bound: Int
  ): Stream[F, Int]

  /** Produce an infinite stream of `Long` bounded inclusively within some
    * origin and some bound.
    *
    * @param origin
    *   The origin.
    * @param bound
    *   The bound.
    * @return
    *   The infinite stream of values.
    */
  def nextLongs(
    origin: Long,
    bound: Long
  ): Stream[F, Long]

  /** Produce an infinite stream of `Double` bounded inclusively within some
    * origin and some bound.
    *
    * @param origin
    *   The origin.
    * @param bound
    *   The bound.
    * @return
    *   The infinite stream of values.
    */
  def nextDoubles(
    origin: Double,
    bound: Double
  ): Stream[F, Double]

  /** Produce an array of random bytes.
    *
    * @param count
    *   The number of bytes to produce.
    * @return
    *   The array of randomly generated bytes.
    */
  def nextByteArray(count: Int): F[Array[Byte]]

  /** @return
    *   Infinite stream of random bytes.
    */
  def nextBytes(): Stream[F, Byte]

  /** Produce a blob of random bytes.
    *
    * @param size
    *   The number of bytes to produce.
    * @return
    *   The blob of randomly generated bytes.
    */
  def nextBlob(size: Int): F[Blob]

object Rng:

  /** Instantiate [[Rng]] using a new `java.util.Random` instance.
    *
    * For secure random number generation, please refer to the `secure[F]`
    * function.
    *
    * @param random
    *   The `java.util.Random` instance.
    * @return
    *   The new [[Rng]].
    */
  def default[F[_]: Sync]: Rng[F] =
    default[F](new Random())

  /** Instantiate [[Rng]] using the given random number generator.
    *
    * @param random
    *   The `java.util.Random` instance.
    * @return
    *   The new [[Rng]].
    */
  def default[F[_]: Sync](random: Random): Rng[F] =
    new JavaRandom[F](random)

  /** Instantiate [[Rng]] using a new `java.security.SecureRandom` instance.
    *
    * @param random
    *   The `java.security.SecureRandom` instance.
    * @return
    *   The new [[Rng]].
    */
  def secure[F[_]: Sync]: Rng[F] =
    secure[F](new SecureRandom())

  /** Instantiate [[Rng]] using the given secure random number generator.
    *
    * @param random
    *   The `java.security.SecureRandom` instance.
    * @return
    *   The new [[Rng]].
    */
  def secure[F[_]: Sync](random: SecureRandom): Rng[F] =
    new JavaRandom[F](random)

  /** @return
    *   New instance of [[Rng]] that always returns `0` or `false`.
    */
  def zero[F[_]: Applicative]: Rng[F] =
    new Zero[F]

  /** Implementation of [[Rng]] based on any given `java.util.Random` instance.
    *
    * @param random
    *   The underlying random number generator.
    */
  final class JavaRandom[F[_]: Sync](random: Random) extends Rng[F]:

    /** @inheritDocs
      */
    override def nextBytes(): Stream[F, Byte] =
      Stream
        .evalUnChunk[F, Byte](Sync[F].delay {
          // Allocate space for the random data. Generate 1kb at once.
          val bytes = new Array[Byte](ByteCount.IntSizes.OneKilobyte)
          val _     = random.nextBytes(bytes)
          // This chunk is _backed by_ the array -- the data isn't copied, so we
          // allocate a new array each time and cede it to the chunk.
          Chunk.array(bytes)
        })
        .repeat

    /** @inheritDocs
      */
    override def nextByteArray(count: Int): F[Array[Byte]] =
      if count < 0 then
        Sync[F].raiseError(
          new IllegalArgumentException(
            "Cannot generate a negative number of bytes."
          )
        )
      else if count == 0 then Sync[F].pure(Array.empty)
      else
        Sync[F].delay {
          val bytes = new Array[Byte](count)
          val _     = random.nextBytes(bytes)
          bytes
        }

    /** @inheritDocs
      */
    override def nextBlob(size: Int): F[Blob] =
      nextByteArray(size).map(new Blob(_))

    /** @inheritDocs
      */
    override def updateSeed(seed: Long): F[Rng[F]] =
      Sync[F].delay(random.setSeed(seed)).as(this)

    /** @inheritDocs
      */
    override def nextInt(): F[Int] =
      Sync[F].delay(random.nextInt())

    /** @inheritDocs
      */
    override def nextInt(bound: Int): F[Int] =
      Sync[F].delay(random.nextInt(bound))

    /** @inheritDocs
      */
    override def nextInt(
      origin: Int,
      bound: Int
    ): F[Int] =
      Sync[F].delay(random.nextInt(origin, bound))

    /** @inheritDocs
      */
    override def nextLong(): F[Long] =
      Sync[F].delay(random.nextLong())

    /** @inheritDocs
      */
    override def nextLong(bound: Long): F[Long] =
      Sync[F].delay(random.nextLong(bound))

    /** @inheritDocs
      */
    override def nextLong(
      origin: Long,
      bound: Long
    ): F[Long] =
      Sync[F].delay(random.nextLong(origin, bound))

    /** @inheritDocs
      */
    override def nextDouble(): F[Double] =
      Sync[F].delay(random.nextDouble())

    /** @inheritDocs
      */
    override def nextDouble(bound: Double): F[Double] =
      Sync[F].delay(random.nextDouble(bound))

    /** @inheritDocs
      */
    override def nextDouble(
      origin: Double,
      bound: Double
    ): F[Double] =
      Sync[F].delay(random.nextDouble(origin, bound))

    /** @inheritDocs
      */
    override def nextBoolean(): F[Boolean] =
      Sync[F].delay(random.nextBoolean())

    /** @inheritDocs
      */
    override def nextInts(
      origin: Int,
      bound: Int
    ): Stream[F, Int] =
      Stream.repeatEval(nextInt(origin, bound))

    /** @inheritDocs
      */
    override def nextLongs(
      origin: Long,
      bound: Long
    ): Stream[F, Long] =
      Stream.repeatEval(nextLong(origin, bound))

    /** @inheritDocs
      */
    override def nextDoubles(
      origin: Double,
      bound: Double
    ): Stream[F, Double] =
      Stream.repeatEval(nextDouble(origin, bound))

  end JavaRandom

  /** Implementation of [[Rng]] that always returns `0` or `false`.
    */
  final class Zero[F[_]: Applicative] extends Rng[F]:

    /** @inheritDocs
      */
    override def nextByteArray(count: Int): F[Array[Byte]] =
      Applicative[F].pure(Array.fill[Byte](count)(0))

    /** @inheritDocs
      */
    override def nextBytes(): Stream[F, Byte] =
      Stream[F, Byte](0).repeat

    /** @inheritDocs
      */
    override def nextBlob(size: Int): F[Blob] =
      nextByteArray(size).map(new Blob(_))

    /** @inheritDocs
      */
    override def updateSeed(seed: Long): F[Rng[F]] = Applicative[F].pure(this)

    /** @inheritDocs
      */
    override def nextInt(): F[Int] = Applicative[F].pure(0)

    /** @inheritDocs
      */
    override def nextInt(bound: Int): F[Int] = Applicative[F].pure(0)

    /** @inheritDocs
      */
    override def nextInt(
      origin: Int,
      bound: Int
    ): F[Int] = Applicative[F].pure(0)

    /** @inheritDocs
      */
    override def nextLong(): F[Long] = Applicative[F].pure(0)

    /** @inheritDocs
      */
    override def nextLong(bound: Long): F[Long] = Applicative[F].pure(0)

    /** @inheritDocs
      */
    override def nextLong(
      origin: Long,
      bound: Long
    ): F[Long] = Applicative[F].pure(0)

    /** @inheritDocs
      */
    override def nextDouble(): F[Double] = Applicative[F].pure(0.0)

    /** @inheritDocs
      */
    override def nextDouble(bound: Double): F[Double] = Applicative[F].pure(0.0)

    /** @inheritDocs
      */
    override def nextDouble(
      origin: Double,
      bound: Double
    ): F[Double] = Applicative[F].pure(0.0)

    /** @inheritDocs
      */
    override def nextBoolean(): F[Boolean] = Applicative[F].pure(false)

    /** @inheritDocs
      */
    override def nextInts(
      origin: Int,
      bound: Int
    ): Stream[F, Int] =
      Stream.repeatEval(Applicative[F].pure(0))

    /** @inheritDocs
      */
    override def nextLongs(
      origin: Long,
      bound: Long
    ): Stream[F, Long] =
      Stream.repeatEval(Applicative[F].pure(0))

    /** @inheritDocs
      */
    override def nextDoubles(
      origin: Double,
      bound: Double
    ): Stream[F, Double] =
      Stream.repeatEval(Applicative[F].pure(0.0))

  end Zero

end Rng
