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

module Database.Immutable.Read
  (
  -- * Construction / reading
    createDB
  , readDB

  -- * Indexes
  , I.Name
  , I.Indexes

  , unindexed
  , byteStringIndex
  , word32Index
  ) where

import qualified Data.ByteString as B
import           Data.Maybe (maybe)
import           Data.Monoid ((<>))
import qualified Data.Serialize as S
import           Data.Word

import qualified Data.Vector.Storable as VS
import qualified Data.Vector.Storable.Mutable as VSM

import qualified Multimap.ByteString as MMB
import qualified Multimap.Word32 as MMW

import           GHC.TypeLits

import qualified Database.Immutable.Internal as I

-- | Default 'I.Indexes' description, specifying an unindexed database.
unindexed :: I.Indexes '[] a
unindexed = I.Indexes (pure ())

-- | Add a 'Word32' index to an 'I.Indexes' description to be built when
-- reading a database.
word32Index
  :: KnownSymbol s

  => I.Name s
  -- ^ Index name

  -> (a -> b)
  -- ^ Projecting function

  -> (b -> Word32)
  -- ^ Index computing function

  -> I.Indexes indexes a
  -- ^ 'Indexes' description

  -> I.Indexes ('(s, I.Word32Index b):indexes) a
  -- ^ Resulting 'I.Indexes' desctiption, inlcuding the new 'Word32' index
word32Index _ p f (I.Indexes indexes') = I.Indexes $ do
  indexes <- indexes'
  mm <- MMW.new
  pure (mm, p, f, indexes)

-- | Add a 'B.ByteString' index to an 'I.Indexes' description to be built when
-- reading a database.
byteStringIndex
  :: KnownSymbol s

  => I.Name s
  -- ^ Index name

  -> (a -> b)
  -- ^ Projecting function

  -> (b -> B.ByteString)
  -- ^ Index computing function

  -> I.Indexes indexes a
  -- ^ 'Indexes' description

  -> I.Indexes ('(s, I.ByteStringIndex b):indexes) a
  -- ^ Resulting 'I.Indexes' desctiption, inlcuding the new 'B.ByteString'
  -- index
byteStringIndex _ p f (I.Indexes indexes') = I.Indexes $ do
  indexes <- indexes'
  mm <- MMB.new
  pure (mm, p, f, indexes)

-- | Create a database from a 'B.ByteString' and build up
-- in-memory indexes according to the 'I.Indexes' description.
createDB
  :: forall indexes a
  . S.Serialize a
  => I.InsertIndex indexes a

  => B.ByteString
  -- ^ Serialised elements

  -> Int
  -- ^ Element count

  -> (Maybe (Int -> Int -> IO ()))
  -- ^ Progress indicating function, called with the number of elements
  -- currently read and the total count of elements

  -> I.Indexes indexes a
  -- ^ 'Indexes' description

  -> IO (Either String (I.DB indexes a))
  -- ^ Resulting database or a parse/deserializiation error
createDB contents count progress (I.Indexes indexes') = do
  indexes <- indexes'
  offsets <- VSM.new (fromIntegral count)

  let go bs (!x) f
        | not (B.null bs) = do
          case f bs of
            S.Fail e _   -> pure $ Left e
            S.Partial _  -> pure $ Left "Unexpected Partial"
            S.Done a bs' -> do
              VSM.write offsets x (fromIntegral (B.length contents - B.length bs'))
              I.insertIndex
                (I.Indexes' indexes :: I.Indexes' indexes a)
                (fromIntegral x)
                a

              maybe (pure ()) (\f' -> f' x (fromIntegral count)) progress

              go bs' (x + 1) (S.runGetPartial S.get)
        | otherwise = pure $ Right ()

  r <- go contents 0 (S.runGetPartial S.get)

  case r of
    Left e  -> pure $ Left e
    Right () -> do
      voffsets <- VS.unsafeFreeze offsets
      pure $ Right (I.DB indexes contents voffsets)

-- | Read a database from a file path and build up in-memory indexes
-- according to the 'I.Indexes' description.
readDB
  :: forall indexes a
  . S.Serialize a
  => I.InsertIndex indexes a

  => FilePath
  -- ^ File path to database

  -> (Maybe (Int -> Int -> IO ()))
  -- ^ Progress indicating function, called with the number of elements
  -- currently read and the total count of elements

  -> I.Indexes indexes a
  -- ^ 'Indexes' description

  -> IO (Either String (I.DB indexes a))
  -- ^ Resulting database or a parse/deserializiation error
readDB path progress indexes = do
  meta <- B.readFile (path <> ".meta")

  case S.decode meta of
    Left e -> pure $ Left e
    Right (count :: Word32) -> do
      contents <- B.readFile path
      createDB contents (fromIntegral count) progress indexes