{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE OverloadedStrings      #-}
{-# LANGUAGE RankNTypes             #-}
{-# LANGUAGE TupleSections          #-}
module Boots.Random(
    RD(..)
  , HasRandom(..)
  , MonadRandom(..)
  , newRD
  , RDType(..)
  , makeRD
  , makeRD0
  , forkRD
  , hex32
  , hex64
  , nextWord64
  , splitSMGen
  ) where

import           Boots.App.Internal
import           Boots.Prelude
import           Control.Concurrent.MVar
import           Control.Monad.Factory
import           Data.IORef
import           Data.Text               (toLower, unpack)
import           Data.Tuple
import           Foreign
import           Numeric                 (showHex)
import           Salak
import           System.Random.SplitMix

-- | Random value generator.
data RD = RD { unRD :: forall a. (SMGen -> (a, SMGen)) -> IO a }

-- | Seed container type.
data RDType = RDIORef | RDMVar

instance FromProp m RDType where
  fromProp = readEnum (go . toLower)
    where
      {-# INLINE go #-}
      go "ioref" = Right RDIORef
      go "mvar"  = Right RDMVar
      go v       = Left $ "unknown <" <> unpack v <> ">"

-- | Environment values with `RD`.
class HasRandom env where
  askRandom :: Lens' env RD

instance HasRandom RD where
  askRandom = id
  {-# INLINE askRandom #-}

-- | Create a new random value generator.
{-# INLINE newRD #-}
newRD :: RDType -> IO RD
newRD RDIORef = initSMGen >>= newIORef >>= \ref -> return (RD $ \f -> atomicModifyIORef' ref (swap.f))
newRD _       = initSMGen >>= makeRD

-- | Create random value generator with a seed.
{-# INLINE makeRD #-}
makeRD :: SMGen -> IO RD
makeRD seed = newMVar seed >>= \ref -> return (RD $ \f -> modifyMVar ref (return . swap . f))

-- | Create a thread unsafe `RD`, which should be used only in single thread.
-- It is faster than `RD` generated by `newRD` and `makeRD`.
{-# INLINE makeRD0 #-}
makeRD0 :: SMGen -> (RD -> IO a) -> IO a
makeRD0 smg f = do
  let (seed, gamma) = unseedSMGen smg
  allocaArray 2 $ \ps -> do
    pokeArray ps [seed,gamma]
    f $ RD $ \func -> do
      [s0,g0] <- peekArray 2 ps
      let (a, smg2) = func $ seedSMGen s0 g0
          (s1,g1)   = unseedSMGen smg2
      pokeArray ps [s1,g1]
      return a

-- | Fork a new `RD` from old `RD`.
{-# INLINE forkRD #-}
forkRD :: RD -> IO RD
forkRD (RD f) = f splitSMGen >>= makeRD

class Monad m => MonadRandom env m | m -> env where
  nextW64   :: m Word64

-- | Convert `Word64` into 64 bit hex.
hex64 :: IsString a => Word64 -> a
hex64 i = fromString $ let x = showHex i "" in replicate (16 - length x) '0' ++ x
{-# INLINE hex64 #-}

-- | Convert `Word64` into 32 bit hex.
hex32 :: IsString a => Word64 -> a
hex32 i = fromString $ let x = showHex i "" in drop 8 $ replicate (16 - length x) '0' ++ x
{-# INLINE hex32 #-}

instance (HasRandom env, MonadMask n, MonadIO n) => MonadRandom env (Factory n env) where
  nextW64 = do
    vr <- asksEnv (view askRandom)
    liftIO $ unRD vr nextWord64
  {-# INLINE nextW64 #-}

instance (HasRandom env, MonadIO n) => MonadRandom env (AppT env n) where
  nextW64 = do
    vr <- asks (view askRandom)
    liftIO $ unRD vr nextWord64
  {-# INLINE nextW64 #-}