    {-# LANGUAGE Trustworthy #-}
    {-# LANGUAGE BangPatterns, NoImplicitPrelude, RecordWildCards #-}
    {-# OPTIONS_GHC -fno-warn-name-shadowing #-}

    module GHC.Event.IntTable
        (
          IntTable
        , new
        , lookup
        , insertWith
        , reset
        , delete
        , updateWith
        ) where

    import Data.Bits ((.&.), shiftL, shiftR)
    import Data.IORef (IORef, newIORef, readIORef, writeIORef)
    import Data.Maybe (Maybe(..), isJust, isNothing)
    import Foreign.ForeignPtr (ForeignPtr, mallocForeignPtr, withForeignPtr)
    import Foreign.Storable (peek, poke)
    import GHC.Base (Monad(..), (=<<), ($), const, liftM, otherwise, when)
    import GHC.Classes (Eq(..), Ord(..))
    import GHC.Event.Arr (Arr)
    import GHC.Num (Num(..))
    import GHC.Prim (seq)
    import GHC.Types (Bool(..), IO(..), Int(..))
    import qualified GHC.Event.Arr as Arr

    -- A very simple chained integer-keyed mutable hash table. We use
    -- power-of-two sizing, grow at a load factor of 0.75, and never
    -- shrink. The "hash function" is the identity function.

    newtype IntTable a = IntTable (IORef (IT a))

    data IT a = IT {
          tabArr  :: {-# UNPACK #-} !(Arr (Bucket a))
        , tabSize :: {-# UNPACK #-} !(ForeignPtr Int)
        }

    data Bucket a = Empty
                  | Bucket {
          bucketKey   :: {-# UNPACK #-} !Int
        , bucketValue :: a
        , bucketNext  :: Bucket a
        }

    lookup :: Int -> IntTable a -> IO (Maybe a)
    lookup k (IntTable ref) = do
      let go Bucket{..}
            | bucketKey == k = return (Just bucketValue)
            | otherwise      = go bucketNext
          go _ = return Nothing
      it@IT{..} <- readIORef ref
      go =<< Arr.read tabArr (indexOf k it)

    new :: Int -> IO (IntTable a)
    new capacity = IntTable `liftM` (newIORef =<< new_ capacity)

    new_ :: Int -> IO (IT a)
    new_ capacity = do
      arr <- Arr.new Empty capacity
      size <- mallocForeignPtr
      withForeignPtr size $ \ptr -> poke ptr 0
      return IT { tabArr = arr
                , tabSize = size
                }

    grow :: IT a -> IORef (IT a) -> Int -> IO ()
    grow oldit ref size = do
      newit <- new_ (Arr.size (tabArr oldit) `shiftL` 1)
      let copySlot n !i
            | n == size = return ()
            | otherwise = do
              let copyBucket !m Empty          = copySlot m (i+1)
                  copyBucket  m bkt@Bucket{..} = do
                    let idx = indexOf bucketKey newit
                    next <- Arr.read (tabArr newit) idx
                    Arr.write (tabArr newit) idx bkt { bucketNext = next }
                    copyBucket (m+1) bucketNext
              copyBucket n =<< Arr.read (tabArr oldit) i
      copySlot 0 0
      withForeignPtr (tabSize newit) $ \ptr -> poke ptr size
      writeIORef ref newit

    -- | @insertWith f k v table@ inserts @k@ into @table@ with value @v@.
    -- If @k@ already appears in @table@ with value @v0@, the value is updated
    -- to @f v0 v@ and @Just v0@ is returned.
    insertWith :: (a -> a -> a) -> Int -> a -> IntTable a -> IO (Maybe a)
    insertWith f k v inttable@(IntTable ref) = do
      it@IT{..} <- readIORef ref
      let idx = indexOf k it
          go seen bkt@Bucket{..}
            | bucketKey == k = do
              let !v' = f v bucketValue
                  !next = seen <> bucketNext
                  Empty        <> bs = bs
                  b@Bucket{..} <> bs = b { bucketNext = bucketNext <> bs }
              Arr.write tabArr idx (Bucket k v' next)
              return (Just bucketValue)
            | otherwise = go bkt { bucketNext = seen } bucketNext
          go seen _ = withForeignPtr tabSize $ \ptr -> do
            size <- peek ptr
            if size + 1 >= Arr.size tabArr - (Arr.size tabArr `shiftR` 2)
              then grow it ref size >> insertWith f k v inttable
              else do
                v `seq` Arr.write tabArr idx (Bucket k v seen)
                poke ptr (size + 1)
                return Nothing
      go Empty =<< Arr.read tabArr idx
    {-# INLINABLE insertWith #-}

    -- | Used to undo the effect of a prior insertWith.
    reset :: Int -> Maybe a -> IntTable a -> IO ()
    reset k (Just v) tbl = insertWith const k v tbl >> return ()
    reset k Nothing  tbl = delete k tbl >> return ()

    indexOf :: Int -> IT a -> Int
    indexOf k IT{..} = k .&. (Arr.size tabArr - 1)

    -- | Remove the given key from the table and return its associated value.
    delete :: Int -> IntTable a -> IO (Maybe a)
    delete k t = updateWith (const Nothing) k t

    updateWith :: (a -> Maybe a) -> Int -> IntTable a -> IO (Maybe a)
    updateWith f k (IntTable ref) = do
      it@IT{..} <- readIORef ref
      let idx = indexOf k it
          go changed bkt@Bucket{..}
            | bucketKey == k =
                let fbv = f bucketValue
                    !nb = case fbv of
                            Just val -> bkt { bucketValue = val }
                            Nothing  -> bucketNext
                in (fbv, Just bucketValue, nb)
            | otherwise = case go changed bucketNext of
                            (fbv, ov, nb) -> (fbv, ov, bkt { bucketNext = nb })
          go _ e = (Nothing, Nothing, e)
      (fbv, oldVal, newBucket) <- go False `liftM` Arr.read tabArr idx
      when (isJust oldVal) $ do
        Arr.write tabArr idx newBucket
        when (isNothing fbv) $
          withForeignPtr tabSize $ \ptr -> do
            size <- peek ptr
            poke ptr (size - 1)
      return oldVal