-- | Representation of probabilities and random computations.
module Game.LambdaHack.Core.Random
  ( -- * The @Rng@ monad
    Rnd
    -- * Random operations
  , randomR, randomR0, nextRandom, randomWord32
  , oneOf, shuffle, invalidInformationCode, shuffleExcept, frequency
    -- * Fractional chance
  , Chance, chance
    -- * Casting dice scaled with level
  , castDice, oddsDice, castDiceXY
    -- * Specialized monadic folds
  , foldrM, foldlM'
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , rollFreq
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Control.Monad.Trans.State.Strict as St
import           Data.Int (Int32)
import           Data.Ratio
import qualified Data.Vector.Unboxed as U
import           Data.Word (Word16, Word32)
import qualified System.Random.SplitMix32 as SM

import qualified Game.LambdaHack.Core.Dice as Dice
import           Game.LambdaHack.Core.Frequency

-- | The monad of computations with random generator state.
type Rnd a = St.State SM.SMGen a

-- | Get a random object within a (inclusive) range with a uniform distribution.
randomR :: (Integral a) => (a, a) -> Rnd a
{-# INLINE randomR #-}
randomR (0, h) = randomR0 h
randomR (l, h) | l > h = error "randomR: empty range"
randomR (l, h) = St.state $ \g ->
  let (x, g') = nextRandom (h - l) g
  in (x + l, g')

-- | Generate random 'Integral' in @[0, x]@ range.
randomR0 :: (Integral a) => a -> Rnd a
{-# INLINE randomR0 #-}
randomR0 h = St.state $ nextRandom h

-- | Generate a random integral value in @[0, x]@ range, where @x@ is within
-- @Int32@.
--
-- The limitation to @Int32@ values is needed to keep it working on signed
-- types. In package @random@, a much more complex scheme is used
-- to keep it working for arbitrary fixed number of bits.
nextRandom :: forall a. (Integral a) => a -> SM.SMGen -> (a, SM.SMGen)
{-# INLINE nextRandom #-}
nextRandom 0 g = (0, g)
nextRandom h g = assert (h > 0 && toInteger h
                                  <= (toInteger :: Int32 -> Integer) maxBound) $
  let (w32, g') = SM.bitmaskWithRejection32'
                    ((fromIntegralWrap :: a -> Word32) h) g
      -- `fromIntegralWrap` is fine here, because wrapping is OK.
      x = (fromIntegralWrap :: Word32 -> a) w32
  in if x > h
     then error $ "nextRandom internal error"
                  `showFailure` (toInteger x, toInteger h, w32)
     else (x, g')

-- | Get a random 'Word32' using full range.
randomWord32 :: Rnd Word32
{-# INLINE randomWord32 #-}
randomWord32 = St.state SM.nextWord32

-- | Get any element of a list with equal probability.
oneOf :: [a] -> Rnd a
oneOf [] = error $ "oneOf []" `showFailure` ()
oneOf [x] = return x
oneOf xs = do
  r <- randomR0 (length xs - 1)
  return $! xs !! r

-- | Generates a random permutation. Naive, but good enough for small inputs.
shuffle :: Eq a => [a] -> Rnd [a]
shuffle [] = return []
shuffle l = do
  x <- oneOf l
  (x :) <$> shuffle (delete x l)

-- | Code that means the information (e.g., flavour or hidden kind index)
-- should be regenerated, because it could not be transferred from
-- previous playthrough (it's random in each playthrough or there was
-- no previous playthrough).
invalidInformationCode :: Word16
invalidInformationCode = maxBound

-- | Generates a random permutation, except for the existing mapping.
shuffleExcept :: U.Vector Word16 -> Int -> [Word16] -> Rnd [Word16]
shuffleExcept v len l0 = assert (len == length l0) $
  shuffleE 0 (l0 \\ filter (/= invalidInformationCode) (U.toList v))
 where
  shuffleE :: Int -> [Word16] -> Rnd [Word16]
  shuffleE i _ | i == len = return []
  shuffleE i l = do
    let a0 = v U.! i
    if a0 == invalidInformationCode then do
      a <- oneOf l
      (a :) <$> shuffleE (succ i) (delete a l)
    else
      (a0 :) <$> shuffleE (succ i) l

-- | Gen an element according to a frequency distribution.
frequency :: Show a => Frequency a -> Rnd a
{-# INLINE frequency #-}
frequency = St.state . rollFreq

-- | Randomly choose an item according to the distribution.
rollFreq :: Show a => Frequency a -> SM.SMGen -> (a, SM.SMGen)
rollFreq fr g = case runFrequency fr of
  [] -> error $ "choice from an empty frequency"
                `showFailure` nameFrequency fr
  [(n, x)] | n <= 0 -> error $ "singleton void frequency"
                               `showFailure` (nameFrequency fr, n, x)
  [(_, x)] -> (x, g)  -- speedup
  fs -> let sumf = foldl' (\ !acc (!n, _) -> acc + n) 0 fs
            (r, ng) = nextRandom (pred sumf) g
            frec :: Int -> [(Int, a)] -> a
            frec !m [] = error $ "impossible roll"
                                 `showFailure` (nameFrequency fr, fs, m)
            frec m ((n, x) : _) | m < n = x
            frec m ((n, _) : xs) = frec (m - n) xs
        in assert (sumf > 0 `blame` "frequency with nothing to pick"
                            `swith` (nameFrequency fr, fs))
             (frec r fs, ng)

-- | Fractional chance.
type Chance = Rational

-- | Give @True@, with probability determined by the fraction.
chance :: Chance -> Rnd Bool
chance r = do
  let n = numerator r
      d = denominator r
  k <- randomR (1, d)
  return (k <= n)

-- | Cast dice scaled with current level depth.
castDice :: Dice.AbsDepth -> Dice.AbsDepth -> Dice.Dice -> Rnd Int
castDice = Dice.castDice randomR

-- | Cast dice scaled with current level depth and return @True@
-- if the results is greater than 50.
oddsDice :: Dice.AbsDepth -> Dice.AbsDepth -> Dice.Dice -> Rnd Bool
oddsDice ldepth totalDepth dice = do
  c <- castDice ldepth totalDepth dice
  return $! c > 50

-- | Cast dice, scaled with current level depth, for coordinates.
castDiceXY :: Dice.AbsDepth -> Dice.AbsDepth -> Dice.DiceXY -> Rnd (Int, Int)
castDiceXY ldepth totalDepth (Dice.DiceXY dx dy) = do
  x <- castDice ldepth totalDepth dx
  y <- castDice ldepth totalDepth dy
  return (x, y)

foldrM :: Foldable t => (a -> b -> Rnd b) -> b -> t a -> Rnd b
foldrM f z0 xs = let f' x (z, g) = St.runState (f x z) g
                 in St.state $ \g -> foldr f' (z0, g) xs

foldlM' :: Foldable t => (b -> a -> Rnd b) -> b -> t a -> Rnd b
foldlM' f z0 xs = let f' (z, g) x = St.runState (f z x) g
                  in St.state $ \g -> foldl' f' (z0, g) xs
