module Jobs.Handler.Intervals.Utils
  ( mkIntervals, mkIntervalsCached
  , getCurrentInterval
  , currentIntervalCached
  ) where

import Import hiding (init, maximumBy, cached)

import Control.Monad.Random.Lazy (evalRand, mkStdGen)
import System.Random.Shuffle (shuffleM)
import Data.List ((!!), unfoldr, maximumBy, init, genericLength)
import qualified Data.ByteString as ByteString
import Data.Bits (Bits(shiftR))

import qualified Data.Map.Strict as Map
  

mkIntervals :: forall a. Integer -> (ByteString -> Maybe a) -> Natural -> [(Maybe a, Maybe a)]
mkIntervals bytes fromBS numIterations = zip (Nothing : init intervals') intervals'
  where
    bits = bytes * 8
    base :: Integer
    base = 2 ^ bits

    -- | Exclusive upper bounds
    intervals
      | numIterations <= 0 = pure base
      | otherwise = go protoIntervals ^.. folded . _1
      where
        go [] = []
        go ints
          | maximumOf (folded . _1) ints == Just base = ints
          | otherwise = go $ lts ++ over _1 succ (over _2 (subtract $ toInteger numIterations) closest) : map (over _1 succ) gts
          where
            closest = maximumBy (comparing $ view _2) ints
            (lts, geqs) = partition (((>) `on` view _1) closest) ints
            gts = filter (((<) `on` view _1) closest) geqs
        -- | Exclusive upper bounds
        protoIntervals :: [(Integer, Integer)]
        protoIntervals = [ over _1 (i *) $ base `divMod` toInteger numIterations
                         | i <- [1 .. toInteger numIterations]
                         ]

    intervals' = map (fromBS' <=< assertM' (> 0)) intervals

    fromBS' :: Integer -> Maybe a
    fromBS' = fromBS . pad . ByteString.pack . reverse . unfoldr step
      where step i
              | i <= 0 || i >= base = Nothing
              | otherwise = Just (fromIntegral i, i `shiftR` 8)
            pad bs
              | toInteger (ByteString.length bs) >= bytes = bs
              | otherwise = pad $ ByteString.cons 0 bs

getCurrentInterval :: forall a. Natural -> Natural -> [a] -> a
getCurrentInterval epoch iteration intervals = permIntervals !! fromIntegral (toInteger iteration `mod` genericLength permIntervals)
  where permIntervals = shuffleM intervals `evalRand` mkStdGen (hash epoch)


mkIntervalsCached :: forall m a. (NFData a, MonadIO m)
                  => TVar (Map Natural [(Maybe a, Maybe a)])
                  -> Integer -> (ByteString -> Maybe a) -> Natural -> m [(Maybe a, Maybe a)]
mkIntervalsCached cacheTVar bytes fromBS numIterations = atomically $ do
    cached <- readTVar cacheTVar
    case Map.lookup numIterations cached of
      Just c -> return c
      Nothing -> do
        modifyTVar' cacheTVar $ force . Map.insert numIterations intervals'
        return intervals'
  where intervals' = mkIntervals bytes fromBS numIterations

currentIntervalCached :: forall m a. (NFData a, MonadIO m)
                      => TVar (Map Natural [(Maybe a, Maybe a)])
                      -> Integer -> (ByteString -> Maybe a)
                      -> Natural -> Natural -> Natural -> m (Maybe a, Maybe a)
currentIntervalCached cacheTVar bytes fromBS numIterations epoch iteration
  = getCurrentInterval epoch iteration <$> mkIntervalsCached cacheTVar bytes fromBS numIterations
