type source = int * string

exception Unseeded_generator

exception No_default_generator

let setup_rng =
  "\nPlease setup your default random number generator. On Unix, the best \
   path is to call [Mirage_crypto_rng_unix.use_default ()].\
   \nBut you can use Fortuna (or any other RNG) and setup the seeding \
   (done by default in MirageOS): \
   \n\
   \nTo initialize the RNG with a default generator, and set up entropy \
   collection and periodic reseeding as a background task, do the \
   following:\
   \n  If you are using MirageOS, use the random device in config.ml: \
   `let main = Mirage.main \"Unikernel.Main\" (random @-> job)`, \
   and `let () = register \"my_unikernel\" [main $ default_random]`. \
   \n  If you are using miou, execute \
   `Mirage_crypto_rng_mkernel.(initialize (module Pfortuna))` \
   at startup."

let () = Printexc.register_printer (function
    | Unseeded_generator ->
      Some ("The RNG has not been seeded." ^ setup_rng)
    | No_default_generator ->
      Some ("The default generator is not yet initialized. " ^ setup_rng)
    | _ -> None)

module type Generator = sig
  type g
  val block : int
  val create : ?time:(unit -> int64) -> unit -> g
  val generate_into : g:g -> bytes -> off:int -> int -> unit
  [@@alert unsafe "Does not do bounds checks. Use Mirage_crypto_rng.generate_into instead."]
  val reseed : g:g -> string -> unit
  val accumulate : g:g -> source -> [`Acc of string -> unit]
  val seeded : g:g -> bool
  val pools : int
end

type 'a generator = (module Generator with type g = 'a)
type g = Generator : ('a * bool * 'a generator) -> g

let create (type a) ?g ?seed ?(strict=false) ?time (m : a generator) =
  let module M = (val m) in
  let g = Option.value g ~default:(M.create ?time ()) in
  Option.iter (M.reseed ~g) seed;
  Generator (g, strict, m)

let _default_generator = Atomic.make None

let set_default_generator g = Atomic.set _default_generator (Some g)

let unset_default_generator () = Atomic.set _default_generator None

let default_generator () =
  match Atomic.get _default_generator with
  | None -> raise No_default_generator
  | Some g -> g

let get = function Some g -> g | None -> default_generator ()

let generate_into ?(g = default_generator ()) b ?(off = 0) n =
  let Generator (g, _, m) = g in
  let module M = (val m) in
  if off < 0 || n < 0 then
    invalid_arg ("negative offset " ^ string_of_int off ^ " or length " ^
                 string_of_int n);
  if Bytes.length b - off < n then
    invalid_arg "buffer too short";
  begin[@alert "-unsafe"]
    M.generate_into ~g b ~off n
  end

let generate ?g n =
  let data = Bytes.create n in
  generate_into ?g data ~off:0 n;
  Bytes.unsafe_to_string data

let reseed ?(g = default_generator ()) cs =
  let Generator (g, _, m) = g in let module M = (val m) in M.reseed ~g cs

let accumulate g source =
  let Generator (g, _, m) = get g in
  let module M = (val m) in
  M.accumulate ~g source

let seeded g =
  let Generator (g, _, m) = get g in let module M = (val m) in M.seeded ~g

let block g =
  let Generator (_, _, m) = get g in let module M = (val m) in M.block

let pools g =
  let Generator (_, _, m) = get g in let module M = (val m) in M.pools

let strict g =
  let Generator (_, s, _) = get g in s
