{-|
Module      : Database.Immutable
Copyright   : (c) Philip Kamenarsky, 2018
License     : MIT
Maintainer  : p.kamenarsky@gmail.com
Stability   : experimental
Portability : POSIX

This package contains a thin wrapper over a continous memory region combined
with an efficient reverse index implementation for fast and type-safe indexed
lookups.

It is aimed at storing, loading and querying big immutable datasets. Once
written, a database can not be modified further.

The underlying storage is pinned and thus ensures efficient garbage
collection without ever reading the structure contents, since no pointers
live inside the dataset that point outside it.

-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}

module Database.Immutable
  (
  -- * Database
    DB

  -- * Index types
  , ByteStringIndex
  , Word32Index

  , Id(Id)
  , Limit(Limit)
  , length
  , zeroId
  , incId
  , addLimit
  , subIds

  -- * Querying
  , (!)
  , slice
  , lookup
  ) where

import qualified Data.ByteString as B
import           Data.Either (either)
import qualified Data.Vector.Storable as V
import qualified Data.Serialize as S

import           Database.Immutable.Internal

import           System.IO.Unsafe (unsafePerformIO)

import           Prelude hiding (length, lookup)

-- | /O(1)/ Return number of records contained in the database.
length :: DB indexes a -> Limit a
length (DB _ _ offsets) = Limit (fromIntegral $ V.length offsets)

-- | /O(1)/ Return the database element at the specified position.
(!) :: S.Serialize a => DB indexes a -> Id a -> Maybe a
(!) (DB _ contents offsets) (Id index)
  | Just count' <- count
  , Just offset' <- offset = either (const Nothing) Just $ S.decode
      $ B.take (fromIntegral count')
      $ B.drop (fromIntegral offset') contents
  | otherwise = Nothing
  where
    count = offsets V.!? fromIntegral index
    offset
      | index == 0 = Just 0
      | otherwise = offsets V.!? (fromIntegral index - 1)

unsafeIndex :: S.Serialize a => DB indexes a -> Id a -> a
unsafeIndex (DB _ contents offsets) (Id index)
  = either (error "unsafeIndex") id $ S.decode
      $ B.take (fromIntegral count)
      $ B.drop (fromIntegral offset) contents
  where
    count = offsets V.! fromIntegral index
    offset
      | index == 0 = 0
      | otherwise = offsets V.! (fromIntegral index - 1)

-- | /O(n)/ Return a slice of the database. The database must contain
-- at least i+n elements.
slice :: S.Serialize a => Id a -> Limit a -> DB indexes a -> [a]
slice (Id index) (Limit limit) db@(DB _ _ offsets) = map
  ((db `unsafeIndex`) . Id . fromIntegral)
  [index'..max index' (min (V.length offsets) (index' + fromIntegral limit)) - 1]
  where
    index' = fromIntegral $ max index 0

-- | /O(n)/ Lookup by index, @n@ being the count of returned elements.
--
-- Example:
--
-- > lookup personsDB #nameIndex "Phil" -- Return all elements named "Phil"
lookup
  :: forall indexes s v a
  . S.Serialize a
  => LookupIndex indexes s v a
  => Name s             -- ^ Index name
  -> v                  -- ^ Index value
  -> DB indexes a       -- ^ Database
  -> [a]                -- ^ Resulting items
lookup name t db@(DB indexes _ _)
  = map ((db `unsafeIndex`) . Id . fromIntegral) is
  where
    is = unsafePerformIO
       $ lookupIndex (Indexes' indexes :: Indexes' indexes a) name t