-- |
-- Module      : Basement.Block.Builder
-- License     : BSD-style
-- Maintainer  : Foundation
--
-- Block builder

{-# LANGUAGE Rank2Types #-}

module Basement.Block.Builder
    ( Builder
    , run

    -- * Emit functions
    , emit
    , emitPrim
    , emitString
    , emitUTF8Char

    -- * unsafe
    , unsafeRunString
    ) where

import qualified Basement.Alg.UTF8 as UTF8
import           Basement.UTF8.Helper          (charToBytes)
import           Basement.Numerical.Conversion (charToInt)
import           Basement.Block.Base (Block(..), MutableBlock(..))
import qualified Basement.Block.Base as B
import           Basement.Cast
import           Basement.Compat.Base
import           Basement.Compat.Semigroup
import           Basement.Monad
import           Basement.FinalPtr (FinalPtr, withFinalPtr)
import           Basement.Numerical.Additive
import           Basement.String                (String(..))
import qualified Basement.String as S
import           Basement.Types.OffsetSize
import           Basement.PrimType (PrimType(..), primMbaWrite)
import           Basement.UArray.Base (UArray(..))
import qualified Basement.UArray.Base as A

import           GHC.ST
import           Data.Proxy

newtype Action = Action
    { runAction_ :: forall prim . PrimMonad prim
                 => MutableBlock Word8 (PrimState prim)
                 -> Offset Word8
                 -> prim (Offset Word8)
    }

data Builder = Builder {-# UNPACK #-} !(CountOf Word8)
                                      !Action

instance Semigroup Builder where
    (<>) = append
    {-# INLINABLE (<>) #-}
instance Monoid Builder where
    mempty = empty
    {-# INLINE mempty #-}
    mappend = append
    {-# INLINABLE mappend #-}
    mconcat = concat
    {-# INLINABLE mconcat #-}

-- | create an empty builder
--
-- this does nothing, build nothing, take no space (in the resulted block)
empty :: Builder
empty = Builder 0 (Action $ \_ !off -> pure off)
{-# INLINE empty #-}

-- | concatenate the 2 given bulider
append :: Builder -> Builder -> Builder
append (Builder size1 (Action action1)) (Builder size2 (Action action2)) =
    Builder size action
  where
    action = Action $ \arr off -> do
      off' <- action1 arr off
      action2 arr off'
    size = size1 + size2
{-# INLINABLE append #-}

-- | concatenate the list of builder
concat :: [Builder] -> Builder
concat = loop 0 (Action $ \_ !off -> pure off)
  where
    loop !sz acc          []                              = Builder sz acc
    loop !sz (Action acc) (Builder !s (Action action):xs) =
       loop (sz + s) (Action $ \arr off -> acc arr off >>= action arr) xs
{-# INLINABLE concat #-}

-- | run the given builder and return the generated block
run :: PrimMonad prim => Builder -> prim (Block Word8)
run (Builder sz action) = do
    mb <- B.new sz
    off <- runAction_ action mb 0
    B.unsafeShrink mb (offsetAsSize off) >>= B.unsafeFreeze

-- | run the given builder and return a UTF8String
--
-- this action is unsafe as there is no guarantee upon the validity of the
-- content of the built block.
unsafeRunString :: PrimMonad prim => Builder -> prim String
unsafeRunString b = do
    str <- run b
    pure $ String $ A.UArray 0 (B.length str) (A.UArrayBA str)

-- | add a Block in the builder
emit :: Block a -> Builder
emit b = Builder size $ Action $ \arr off ->
    B.unsafeCopyBytesRO arr off b' 0 size *> pure (off + sizeAsOffset size)
  where
    b' :: Block Word8
    b' = cast b
    size :: CountOf Word8
    size = B.length b'

emitPrim :: (PrimType ty, ty ~ Word8) => ty -> Builder
emitPrim a = Builder size $ Action $ \(MutableBlock arr) off ->
    primMbaWrite arr off a *> pure (off + sizeAsOffset size)
  where
    size = getSize Proxy a
    getSize :: PrimType ty => Proxy ty -> ty -> CountOf Word8
    getSize p _ = primSizeInBytes p

-- | add a string in the builder
emitString :: String -> Builder
emitString (String str) = Builder size $ Action $ \arr off ->
    A.onBackendPrim (onBA arr off) (onAddr arr off) str *> pure (off + sizeAsOffset size)
  where
    size = A.length str
    onBA :: PrimMonad prim
         => MutableBlock Word8 (PrimState prim)
         -> Offset Word8
         -> Block Word8
         -> prim ()
    onBA   arr off ba   = B.unsafeCopyBytesRO arr off ba 0 size
    onAddr :: PrimMonad prim
           => MutableBlock Word8 (PrimState prim)
           -> Offset Word8
           -> FinalPtr Word8
           -> prim ()
    onAddr arr off fptr = withFinalPtr fptr $ \ptr -> B.unsafeCopyBytesPtr arr off ptr size

-- | emit a UTF8 char in the builder
--
-- this function may be replaced by `emit :: Encoding -> Char -> Builder`
emitUTF8Char :: Char -> Builder
emitUTF8Char c = Builder (charToBytes $ charToInt c) $ Action $ \block@(MutableBlock !_) off ->
    UTF8.writeUTF8 block off c