{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}

-- |
-- Module: Main
-- Copyright: Copyright © 2019 Lars Kuhtz <lakuhtz@gmail.com>
-- License: BSD3
-- Maintainer: Lars Kuhtz <lakuhtz@gmail.com>
-- Stability: experimental
--
-- Test functions
--
module Main
( main

-- * Testing
, test0
, test1
, test2
) where

import Control.StopWatch

import qualified Crypto.Hash as C

import Data.Bool
import qualified Data.ByteArray as BA
import qualified Data.ByteArray.Pack as BA hiding (pack)
import qualified Data.ByteString as B
import Data.Either
import Data.Hashable

import Foreign

import Numeric.Natural

import System.IO.Unsafe
import System.Random.Internal

-- internal modules

import Data.Cuckoo
import Data.Cuckoo.Internal

-- -------------------------------------------------------------------------- --
-- Main

main :: IO ()
main = do
    putStrLn "fill with random Int values up to first insert failure"
    stopWatch (test0 @Int n) >>= p "Int"

    putStrLn ""
    putStrLn "fill up to first insert failure"
    stopWatch (test1 @Int n) >>= p "Int (default instance)"
    stopWatch (test1 @Double n) >>= p "Double (default instance)"

    putStrLn ""
    putStrLn "[ByteString] fill up to first insert failure"
    stopWatch (test2 @HashablePkg n) >>= p "Hashable Package"
    stopWatch (test2 @Fnv1aSip n) >>= p "Fnv1a+Sip"
    stopWatch (test2 @Crypto n) >>= p "Blake2b_256"

    putStrLn ""
    putStrLn "[ByteString] fill to 95%"
    stopWatch (test3 @HashablePkg n) >>= p "Hashable Package"
    stopWatch (test3 @Fnv1aSip n) >>= p "Fnv1a+Sip"
    stopWatch (test3 @Crypto n) >>= p "Blake2b_256"
  where
    p l (r, t) = putStrLn $ show t <> " - " <> l <> " - " <> show r
    n = 500000

-- -------------------------------------------------------------------------- --
-- Orphans

instance CuckooFilterHash Int
instance CuckooFilterHash Double

-- -------------------------------------------------------------------------- --
-- Hashable (I think, this uses SIP hash)

newtype HashablePkg = HashablePkg B.ByteString
    deriving (Show, Eq, Ord)
    deriving newtype (BA.ByteArrayAccess, BA.ByteArray, Semigroup, Monoid, Hashable)

instance CuckooFilterHash HashablePkg where
    cuckooHash (Salt s) a = fromIntegral $! hashWithSalt s a
    cuckooFingerprint (Salt s) a = fromIntegral $! hashWithSalt (s + 23) a
    {-# INLINE cuckooHash #-}
    {-# INLINE cuckooFingerprint #-}

-- -------------------------------------------------------------------------- --
-- Fnv1a Hashes

newtype Fnv1aSip = Fnv1aSip B.ByteString
    deriving (Show, Eq, Ord)
    deriving newtype (BA.ByteArrayAccess, BA.ByteArray, Semigroup, Monoid)

instance CuckooFilterHash Fnv1aSip where
    cuckooHash (Salt s) a = fnv1a_bytes s a
    cuckooFingerprint (Salt s) a = sip_bytes s a
    {-# INLINE cuckooHash #-}
    {-# INLINE cuckooFingerprint #-}

-- -------------------------------------------------------------------------- --
-- ByteStrings with cryptographic cuckoo filter hash functions

newtype Crypto = Crypto B.ByteString
    deriving (Show, Eq, Ord)
    deriving newtype (BA.ByteArrayAccess, BA.ByteArray, Semigroup, Monoid)

instance CuckooFilterHash Crypto where
    -- cuckooHash _ a = unsafePerformIO $ BA.withByteArray (C.hash @_ @C.Blake2b_256 a) $ peek
    cuckooHash (Salt s) a = unsafePerformIO $ flip BA.withByteArray peek
        $ C.hash @BA.Bytes @C.Blake2b_256
        $ fromRight (error "must not happen")
        $ BA.fill (BA.length a + 8) (BA.putStorable s >> BA.putBytes a)
    cuckooFingerprint s a = int $ cuckooHash (s + 23) a
    {-# INLINE cuckooHash #-}
    {-# INLINE cuckooFingerprint #-}

-- -------------------------------------------------------------------------- --
-- Test

-- | Fill with random items until first insert failure
--
test0 :: forall a . Variate a => CuckooFilterHash a => Natural -> IO TestResult
test0 n = do
    rng <- initialize 0
    s <- Salt <$> uniform rng
    f <- newCuckooFilter @4 @10 @a s n
    let go i fp = do
            x <- uniform rng
            fp' <- bool fp (succ fp) <$> member f x
            insert f x >>= \case
                True -> go (succ i) fp'
                False -> return (i, fp')
    (a, b) <- go 0 0
    c <- itemCount f
    return $! TestResult a 1 b c
        (int b / int a * 100)
        (int c / int (capacityInItems f) * 100)

-- | Fill up to first insert failure
--
test1 :: forall a . CuckooFilterHash a => Num a => Natural -> IO TestResult
test1 n = do
    rng <- initialize 0
    s <- Salt <$> uniform rng
    f <- newCuckooFilter @4 @10 @a s n
    let go i fp = do
            let x = int i
            fp' <- bool fp (succ fp) <$> member f x
            insert f x >>= \case
                True -> go (succ i) fp'
                False -> return (i, fp')
    (a, b) <- go 0 0
    c <- itemCount f
    return $! TestResult a 1 b c
        (int b / int a * 100)
        (int c / int (capacityInItems f) * 100)

-- | Fill up to first insert failure
--
test2 :: forall a . CuckooFilterHash a => BA.ByteArray a => Natural -> IO TestResult
test2 n = do
    rng <- initialize 0
    s <- Salt <$> uniform rng
    f <- newCuckooFilter @4 @10 @a s n
    let go i fp = do
            let x = BA.pack (castEnum <$> show i)
            fp' <- bool fp (succ fp) <$> member f x
            insert f x >>= \case
                True -> go (succ i) fp'
                False -> return (i, fp')
    (a, b) <- go 0 0
    c <- itemCount f
    return $! TestResult a 1 b c
        (int b / int a * 100)
        (int c / int (capacityInItems f) * 100)

-- | Fill 90% of the filter
--
test3 :: forall a . CuckooFilterHash a => BA.ByteArray a => Natural -> IO TestResult
test3 n = do
    rng <- initialize 0
    s <- Salt <$> uniform rng
    f <- newCuckooFilter @4 @10 @a s n
    let go i x fp
            | int i >= int @_ @Double n * 95 / 100 = return (i, x, fp)
        go i x fp = do
            let bytes = BA.pack (castEnum <$> show i)
            fp' <- bool fp (succ fp) <$> member f bytes

            -- unless (fp == fp') $ do
            --     print $ itemHashes f bytes

            insert f bytes >>= \case
                True -> go (succ i) x fp'
                False -> go (succ i) (succ x) fp'
    (i, x, b) <- go 0 0 0
    c <- itemCount f
    return $! TestResult i x b c
        (int b / int i * 100)
        (int c / int (capacityInItems f) * 100)

data TestResult = TestResult
    { _testInsertCount :: !Int
    , _testInsertFailures :: !Int
    , _testFalsePositiveCount :: !Int
    , _testItemCount :: !Int
    , _testFalsePositiveRate :: !Double
    , _testLoadFactor :: !Double
    }
    deriving (Show, Eq, Ord)

castEnum :: Enum a => Enum b => a -> b
castEnum = toEnum . fromEnum
{-# INLINE castEnum #-}
