{-# LANGUAGE BangPatterns, CPP, GeneralizedNewtypeDeriving, RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}

module Data.Hashable.SipHash
    (
      LE64
    , Sip
    , fromWord64
    , fullBlock
    , lastBlock
    , finalize
    , hashByteString
    ) where

#include "MachDeps.h"

import Data.Bits ((.|.), (.&.), rotateL, shiftL, xor)
#if MIN_VERSION_base(4,5,0)
import Data.Bits (unsafeShiftL)
#endif
import Data.Word (Word8, Word64)
import Foreign.ForeignPtr (withForeignPtr)
import Foreign.Ptr (Ptr, castPtr, plusPtr)
import Data.ByteString.Internal (ByteString(PS), inlinePerformIO)
import Foreign.Storable (peek)
import Numeric (showHex)

newtype LE64 = LE64 { fromLE64 :: Word64 }
    deriving (Eq)

instance Show LE64 where
    show (LE64 !v) = let s = showHex v ""
                     in "0x" ++ replicate (16 - length s) '0' ++ s

data Sip = Sip {
      v0 :: {-# UNPACK #-} !Word64, v1 :: {-# UNPACK #-} !Word64
    , v2 :: {-# UNPACK #-} !Word64, v3 :: {-# UNPACK #-} !Word64
    }

fromWord64 :: Word64 -> LE64
#ifndef WORDS_BIGENDIAN
fromWord64 = LE64
#else
#error big endian support TBD
#endif

initState :: (Sip -> r) -> Word64 -> Word64 -> r
initState k k0 k1 = k (Sip s0 s1 s2 s3)
    where !s0 = (k0 `xor` 0x736f6d6570736575)
          !s1 = (k1 `xor` 0x646f72616e646f6d)
          !s2 = (k0 `xor` 0x6c7967656e657261)
          !s3 = (k1 `xor` 0x7465646279746573)

sipRound :: (Sip -> r) -> Sip -> r
sipRound k Sip{..} = k (Sip v0_c v1_d v2_c v3_d)
  where v0_a = v0 + v1
        v2_a = v2 + v3
        v1_a = v1 `rotateL` 13
        v3_a = v3 `rotateL` 16
        v1_b = v1_a `xor` v0_a
        v3_b = v3_a `xor` v2_a
        v0_b = v0_a `rotateL` 32
        v2_b = v2_a + v1_b
        v0_c = v0_b + v3_b
        v1_c = v1_b `rotateL` 17
        v3_c = v3_b `rotateL` 21
        v1_d = v1_c `xor` v2_b
        v3_d = v3_c `xor` v0_c
        v2_c = v2_b `rotateL` 32

fullBlock :: Int -> LE64 -> (Sip -> r) -> Sip -> r
fullBlock c m k st@Sip{..}
    | c == 2    = sipRound (sipRound k') st'
    | otherwise = runRounds c k' st'
  where k' st1@Sip{..} = k st1{ v0 = v0 `xor` fromLE64 m }
        st'           = st{ v3 = v3 `xor` fromLE64 m }
{-# INLINE fullBlock #-}

runRounds :: Int -> (Sip -> r) -> Sip -> r
runRounds c k = go 0
  where go i st
            | i < c     = sipRound (go (i+1)) st
            | otherwise = k st
{-# INLINE runRounds #-}

lastBlock :: Int -> Int -> LE64 -> (Sip -> r) -> Sip -> r
lastBlock !c !len !m k st =
#ifndef WORDS_BIGENDIAN
    fullBlock c (LE64 m') k st
#else
#error big endian support TBD
#endif
  where m' = fromLE64 m .|. ((fromIntegral len .&. 0xff) `shiftL` 56)
{-# INLINE lastBlock #-}

finalize :: Int -> (Word64 -> r) -> Sip -> r
finalize d k st@Sip{..}
    | d == 4    = sipRound (sipRound (sipRound (sipRound k'))) st'
    | otherwise = runRounds d k' st'
  where k' Sip{..} = k $! v0 `xor` v1 `xor` v2 `xor` v3
        st'        = st{ v2 = v2 `xor` 0xff }
{-# INLINE finalize #-}

hashByteString :: Int -> Int -> Word64 -> Word64 -> ByteString -> Word64
hashByteString !c !d k0 k1 (PS fp off len) =
  inlinePerformIO . withForeignPtr fp $ \basePtr ->
    let ptr0 = basePtr `plusPtr` off
        scant = len .&. 7
        endBlocks = ptr0 `plusPtr` (len - scant)
        go !ptr st
            | ptr == endBlocks = readLast ptr
            | otherwise = do
                m <- peekLE64 ptr
                fullBlock c m (go (ptr `plusPtr` 8)) st
          where
            zero !m _ _ = lastBlock c len (LE64 m) (finalize d return) st
            one k m p s = do
              w <- fromIntegral `fmap` peekByte p
              k (m .|. (w `unsafeShiftL` s)) (p `plusPtr` 1) (s+8)
            readLast p =
              case scant of
                0 -> zero 0 p (0::Int)
                1 -> one zero 0 p 0
                2 -> one (one zero) 0 p 0
                3 -> one (one (one zero)) 0 p 0
                4 -> one (one (one (one zero))) 0 p 0
                5 -> one (one (one (one (one zero)))) 0 p 0
                6 -> one (one (one (one (one (one zero))))) 0 p 0
                _ -> one (one (one (one (one (one (one zero)))))) 0 p 0
    in initState (go ptr0) k0 k1

peekByte :: Ptr Word8 -> IO Word8
peekByte = peek

peekLE64 :: Ptr Word8 -> IO LE64
#if defined(x86_64_HOST_ARCH) || defined(i386_HOST_ARCH)
-- platforms on which unaligned loads are legal and usually fast
peekLE64 p = LE64 `fmap` peek (castPtr p)
#else
peekLE64 p = do
  let peek8 d = fromIntegral `fmap` peekByte (p `plusPtr` d)
  b0 <- peek8 0
  b1 <- peek8 1
  b2 <- peek8 2
  b3 <- peek8 3
  b4 <- peek8 4
  b5 <- peek8 5
  b6 <- peek8 6
  b7 <- peek8 7
  let !w = (b7 `shiftL` 56) .|. (b6 `shiftL` 48) .|. (b5 `shiftL` 40) .|.
           (b4 `shiftL` 32) .|. (b3 `shiftL` 24) .|. (b2 `shiftL` 16) .|.
           (b1 `shiftL` 8) .|. b0
  return (fromWord64 w)
#endif

#if !MIN_VERSION_base(4,5,0)
unsafeShiftL :: Word64 -> Int -> Word64
unsafeShiftL = shiftL
#endif
