{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE TypeFamilies #-}

module Main (main) where

import qualified Data.Vector.Storable            as V
import qualified Data.Vector.Storable.Mutable    as VM
import qualified Data.Vector.Mutable             as BV
import           Control.Monad.Primitive         (PrimMonad(PrimState))
import qualified Data.HashTable.IO               as H

import           Criterion.Main                  (bench, bgroup, defaultMain, nfIO)
import           Criterion                       (Benchmark)

import qualified Data.Vector.Hashtables.Internal as VH

vh :: Int -> IO (VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int)
vh n = do
    ht <- VH.initialize n :: IO (VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int)
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0
    return ht

fvh :: Int -> IO (VH.FrozenDictionary V.Vector Int V.Vector Int)
fvh n = do
    h <- vh n
    c <- VH.clone h
    VH.unsafeFreeze c

bh :: Int -> IO (H.BasicHashTable Int Int)
bh n = do
    ht <- H.newSized n :: IO (H.BasicHashTable Int Int)
    let go !i | i <= n = H.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0
    return ht

fl :: Int -> [(Int, Int)]
fl n = mkPair <$> [0 .. n]
  where
    mkPair !x = (x, x)

vhfind :: Int -> VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int -> IO Int
vhfind n ht = do
    let go !i !s | i <= n = do
                                x <- VH.findEntry ht i
                                go (i + 1) (s + x)
                 | otherwise = return s
    go 0 0

fvhfind :: Int -> VH.FrozenDictionary V.Vector Int V.Vector Int -> IO Int
fvhfind n ht = return $ go 0 0 where
    go !i !s | i <= n = go (i + 1) (s + VH.findElem ht i)
             | otherwise = s

bhfind :: Int -> H.BasicHashTable Int Int -> IO Int
bhfind n ht = do
    let go !i !s | i <= n = do
                                Just x <- H.lookup ht i
                                go (i + 1) (s + x)
                 | otherwise = return s
    go 0 0

htb :: Int -> IO ()
htb n = do
    ht <- H.newSized n :: IO (H.BasicHashTable Int Int)
    let go !i | i <= n = H.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0


vht :: Int -> IO ()
vht n = do
    ht <- VH.initialize n :: IO (VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int)
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

vhtd :: Int -> IO ()
vhtd n = do
    ht <- VH.initialize n :: IO (VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int)
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0
    let go1 !i | i <= n = VH.delete ht i >> go1 (i + 1)
               | otherwise = return ()
    go1 0

htbd :: Int -> IO ()
htbd n = do
    ht <- H.newSized n :: IO (H.BasicHashTable Int Int)
    let go !i | i <= n = H.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0
    let go1 !i | i <= n = H.delete ht i >> go1 (i + 1)
               | otherwise = return ()
    go1 0

vhtb :: Int -> IO ()
vhtb n = do
    ht <- VH.initialize n :: IO (VH.Dictionary (PrimState IO) BV.MVector Int BV.MVector Int)
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

vhtk :: Int -> IO ()
vhtk n = do
    ht <- VH.initialize n :: IO (VH.Dictionary (PrimState IO) VM.MVector Int BV.MVector Int)
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

htbg :: Int -> IO ()
htbg n = do
    ht <- H.newSized 1 :: IO (H.BasicHashTable Int Int)
    let go !i | i <= n = H.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

vhtg :: Int -> IO ()
vhtg n = do
    ht <- VH.initialize 1 :: IO (VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int)
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

vhtbg :: Int -> IO ()
vhtbg n = do
    ht <- VH.initialize 1 :: IO (VH.Dictionary (PrimState IO) BV.MVector Int BV.MVector Int)
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

vhtkg :: Int -> IO ()
vhtkg n = do
    ht <- VH.initialize 1 :: IO (VH.Dictionary (PrimState IO) VM.MVector Int BV.MVector Int)
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

mvb :: Int -> IO ()
mvb n = do
    ht <- BV.new (n+1)
    let go !i | i <= n = BV.write ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

mv :: Int -> IO ()
mv n = do
    ht <- VM.new (n+1)
    let go !i | i <= n = VM.write ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

bhfromList l = do
  _bht <- H.fromList l :: IO (H.BasicHashTable Int Int)
  return ()

vhfromList l = do
  _ht <- VH.fromList l :: IO (VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int)
  return ()

bhlookupIndex :: Int -> H.BasicHashTable Int Int -> IO Int
bhlookupIndex n ht = do
    let go !i !s | i <= n = do
                                Just x <- H.lookupIndex ht i
                                go (i + 1) (s + fromIntegral x)
                 | otherwise = return s
    go 0 0

vhlookupIndex :: Int -> VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int -> IO Int
vhlookupIndex n ht = do
    let go !i !s | i <= n = do
                                Just x <- VH.lookupIndex ht i
                                go (i + 1) (s + x)
                 | otherwise = return s
    go 0 0

bhtoList = H.toList

vhtoList = VH.toList

bgc :: Int -> IO Benchmark
bgc n = do
    h <- vh n
    h2 <- bh n
    fh <- fvh n
    let l = fl n
    return $ bgroup (show n)
        [ bgroup "insert" 
            [ bench "hashtables basic"  $ nfIO (htb n)
            , bench "vector-hashtables boxed" $ nfIO (vhtb n)
            , bench "vector-hashtables unboxed keys" $ nfIO (vhtk n)
            , bench "vector-hashtables" $ nfIO (vht n) 
            , bench "mutable vector boxed" $ nfIO (mvb n)
            , bench "mutable vector" $ nfIO (mv n) ]
        , bgroup "insert (resize)"
            [ bench "hashtables basic"  $ nfIO (htbg n)
            , bench "vector-hashtables boxed" $ nfIO (vhtbg n)
            , bench "vector-hashtables unboxed keys" $ nfIO (vhtkg n)
            , bench "vector-hashtables" $ nfIO (vhtg n) ]
        , bgroup "insert, delete"
            [ bench "hashtables basic"  $ nfIO (htbd n)
            , bench "vector-hashtables" $ nfIO (vhtd n) ]
        , bgroup "find"
            [ bench "hashtables basic" $ nfIO (bhfind n h2)
            , bench "vector-hashtables" $ nfIO (vhfind n h)
            , bench "vector-hashtables (frozen)" $ nfIO (fvhfind n fh) ]
        , bgroup "lookupIndex"
            [ bench "hashtables basic" $ nfIO (bhlookupIndex n h2)
            , bench "vector-hashtables" $ nfIO (vhlookupIndex n h) ]
        , bgroup "fromList"
            [ bench "hashtables basic" $ nfIO (bhfromList l)
            , bench "vector-hashtables" $ nfIO (vhfromList l) ]
        , bgroup "toList"
            [ bench "hashtables basic" $ nfIO (bhtoList h2)
            , bench "vector-hashtables" $ nfIO (vhtoList h) ]]

main :: IO ()
main = do
    let inputs = [1000,10000,100000,1000000]
    comparisonBench <- mapM bgc inputs
    utilitiesBench  <- mapM utilities inputs
    defaultMain
        [ bgroup "Comparison" comparisonBench
        , bgroup "Utilities"  utilitiesBench ]

-- ** Utilities benchmark

utilities n = do
    -- utilities input data
    hAt' <- vh n
    hInsert <- vh n
    hDelete <- vh n
    hLookup <- vh n
    hLookup' <- vh n
    hLookupIndex <- vh n
    hNull <- vh n
    hLength <- vh n
    hSize <- vh n
    hMember <- vh n
    hFindWithDefault <- vh n
    hUpsert <- vh n
    hAlter <- vh n
    hAlterM <- vh n
    hUnion1 <- vh n
    hUnion2 <- vh n
    hDifference1 <- vh n
    hDifference2 <- vh n
    hIntersection1 <- vh n
    hIntersection2 <- vh n
    hFromList <- VH.toList =<< vh n
    hToList <- vh n

    return $ bgroup (show n)
        [ bench "at'" $ nfIO (bhuat' n hAt')
        , bench "insert" $ nfIO (bhuinsert n hInsert)
        , bench "delete" $ nfIO (bhudelete n hDelete)
        , bench "lookup" $ nfIO (bhulookup n hLookup)
        , bench "lookup'" $ nfIO (bhulookup' n hLookup')
        , bench "lookupIndex" $ nfIO (bhulookupIndex n hLookupIndex)
        , bench "null" $ nfIO (bhunull hNull)
        , bench "length" $ nfIO (bhulength hLength)
        , bench "size" $ nfIO (bhusize hSize)
        , bench "member" $ nfIO (bhumember n hMember)
        , bench "findWithDefault" $ nfIO (bhufindWithDefault n hFindWithDefault)
        , bench "upsert" $ nfIO (bhuupsert n hUpsert)
        , bench "alter" $ nfIO (bhualter n hAlter)
        , bench "alterM" $ nfIO (bhualterM n hAlterM)
        , bench "union" $ nfIO (bhuunion hUnion1 hUnion2)
        , bench "difference" $ nfIO (bhudifference hDifference1 hDifference2)
        , bench "intersection" $ nfIO (bhuintersection hIntersection1 hIntersection2)
        , bench "fromList" $ nfIO (bhufromList hFromList)
        , bench "toList" $ nfIO (VH.toList hToList) ]

bhuat' n ht = do
    let go !i | i <= n = VH.at' ht i >> go (i + 1)
              | otherwise = return ()
    go 0

bhuinsert n ht = do
    let go !i | i <= n = VH.insert ht i i >> go (i + 1)
              | otherwise = return ()
    go 0

bhudelete n ht = do
    let go !i | i <= n = VH.delete ht i >> go (i + 1)
              | otherwise = return ()
    go 0

bhulookup n ht = do
    let go !i | i <= n = VH.lookup ht i >> go (i + 1)
              | otherwise = return ()
    go 0

bhulookup' n ht = do
    let go !i | i <= n = VH.lookup' ht i >> go (i + 1)
              | otherwise = return ()
    go 0

bhulookupIndex n ht = do
    let go !i | i <= n = VH.lookupIndex ht i >> go (i + 1)
              | otherwise = return ()
    go 0

bhunull = VH.null

bhulength = VH.length

bhusize = VH.size

bhumember n ht = do
    let go !i | i <= n = VH.member ht i >> go (i + 1)
              | otherwise = return ()
    go 0

bhufindWithDefault n ht = do
    let go !i | i <= n = VH.findWithDefault ht 0 i >> go (i + 1)
              | otherwise = return ()
    go 0

bhuupsert n ht = do
    let go !i | i <= n = VH.upsert ht (maybe minBound succ) i >> go (i + 1)
              | otherwise = return ()
    go 0

bhualter n ht = do
    let go !i | i <= n = VH.alter ht (fmap succ) i >> go (i + 1)
              | otherwise = return ()
    go 0

bhualterM :: Int -> VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int -> IO ()
bhualterM n ht = do
    let f = return . fmap succ
        go !i | i <= n = VH.alterM ht f i >> go (i + 1)
              | otherwise = return ()
    go 0

bhuunion ht1 ht2 = VH.union ht1 ht2 >> return ()

bhudifference ht1 ht2 = VH.difference ht1 ht2 >> return ()

bhuintersection ht1 ht2 = VH.intersection ht1 ht2 >> return ()

bhufromList htlist = do
  ht <- VH.fromList htlist :: IO (VH.Dictionary (PrimState IO) VM.MVector Int VM.MVector Int)
  return ()

bhutoList ht = VH.toList ht >> return ()
