{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE UnboxedTuples #-}

module Data.Builder
  ( -- * Builder
    Builder (..)
  , cons
  , singleton
  , doubleton
  , tripleton

    -- * Run
  , run
  ) where

import Compat (unsafeShrinkAndFreeze#)
import Data.Chunks (Chunks (ChunksCons, ChunksNil))
import Data.Primitive (SmallArray (SmallArray))
import GHC.Exts (Int#, SmallMutableArray#, State#, newSmallArray#, runRW#, unsafeFreezeSmallArray#, writeSmallArray#, (*#), (+#), (-#), (>#))

import qualified Data.Chunks as C

-- | Builder for an array of boxed elements.
newtype Builder a
  = Builder
      -- The chunks being built up are in reverse order.
      -- Consequently, functions that run a builder must
      -- reverse the chunks at the end.
      ( forall s.
        SmallMutableArray# s a ->
        Int# ->
        Int# ->
        Chunks a ->
        State# s ->
        (# State# s, SmallMutableArray# s a, Int#, Int#, Chunks a #)
      )

run :: Builder a -> Chunks a
run (Builder f) = case runRW#
  -- The initial size of 16 elements is chosen somewhat
  -- arbitrarily. It is more than enough to saturate a
  -- cache line.
  ( \s0 -> case newSmallArray# 16# errorThunk s0 of
      (# s1, marr0 #) -> case f marr0 0# 16# ChunksNil s1 of
        (# s2, marr, off, _, cs #) ->
          -- Recall that freezeSmallArray copies a slice.
          -- If resize functions ever become available for
          -- SmallArray, we should use that instead.
          case unsafeShrinkAndFreeze# marr off s2 of
            (# s3, arr #) ->
              let !r =
                    C.reverseOnto
                      (ChunksCons (SmallArray arr) ChunksNil)
                      cs
               in (# s3, r #)
  ) of
  (# _, cs #) -> cs

errorThunk :: a
{-# NOINLINE errorThunk #-}
errorThunk = error "array-builder:Data.Builder: error"

instance Monoid (Builder a) where
  {-# INLINE mempty #-}
  mempty =
    Builder
      ( \marr0 off0 len0 cs0 s0 ->
          (# s0, marr0, off0, len0, cs0 #)
      )

instance Semigroup (Builder a) where
  {-# INLINE (<>) #-}
  Builder f <> Builder g =
    Builder
      ( \marr0 off0 len0 cs0 s0 -> case f marr0 off0 len0 cs0 s0 of
          (# s1, marr1, off1, len1, cs1 #) ->
            g marr1 off1 len1 cs1 s1
      )

cons :: a -> Builder a -> Builder a
{-# INLINE cons #-}
cons a b = singleton a <> b

-- | A builder with one element.
singleton :: a -> Builder a
{-# NOINLINE singleton #-}
singleton a =
  Builder
    ( \marr off len cs s0 -> case len ># 0# of
        1# -> case writeSmallArray# marr off a s0 of
          s1 -> (# s1, marr, off +# 1#, len -# 1#, cs #)
        _ -> case unsafeFreezeSmallArray# marr s0 of
          (# s1, arr #) ->
            let !lenNew = nextLength off
             in -- Since we feed the element to newSmallArray#, we do not
                -- need to write it to the 0 index.
                case newSmallArray# lenNew a s1 of
                  (# s2, marrNew #) ->
                    let !csNew = ChunksCons (SmallArray arr) cs
                     in (# s2, marrNew, 1#, lenNew -# 1#, csNew #)
    )

{- | A builder with two elements.

@since 0.1.1.0
-}
doubleton :: a -> a -> Builder a
{-# NOINLINE doubleton #-}
doubleton a b =
  Builder
    ( \marr off len cs s0 -> case len ># 1# of
        1# -> case writeSmallArray# marr off a s0 of
          s1 -> case writeSmallArray# marr (off +# 1#) b s1 of
            s2 -> (# s2, marr, off +# 2#, len -# 2#, cs #)
        _ -> case unsafeShrinkAndFreeze# marr off s0 of
          (# s1, arr #) ->
            let !lenNew = nextLength off
             in -- Since we feed the element to newSmallArray#, we do not
                -- need to write element a to the 0 index.
                case newSmallArray# lenNew a s1 of
                  (# s2, marrNew #) -> case writeSmallArray# marrNew 1# b s2 of
                    s3 ->
                      let !csNew = ChunksCons (SmallArray arr) cs
                       in (# s3, marrNew, 2#, lenNew -# 2#, csNew #)
    )

{- | A builder with three elements.

@since 0.1.1.0
-}
tripleton :: a -> a -> a -> Builder a
{-# NOINLINE tripleton #-}
tripleton a b c =
  Builder
    ( \marr off len cs s0 -> case len ># 1# of
        1# -> case writeSmallArray# marr off a s0 of
          s1 -> case writeSmallArray# marr (off +# 1#) b s1 of
            s2 -> case writeSmallArray# marr (off +# 2#) c s2 of
              s3 -> (# s3, marr, off +# 3#, len -# 3#, cs #)
        _ -> case unsafeShrinkAndFreeze# marr off s0 of
          (# s1, arr #) ->
            let !lenNew = nextLength off
             in -- Since we feed the element to newSmallArray#, we do not
                -- need to write element a to the 0 index.
                case newSmallArray# lenNew a s1 of
                  (# s2, marrNew #) -> case writeSmallArray# marrNew 1# b s2 of
                    s3 -> case writeSmallArray# marrNew 2# c s3 of
                      s4 ->
                        let !csNew = ChunksCons (SmallArray arr) cs
                         in (# s4, marrNew, 3#, lenNew -# 3#, csNew #)
    )

nextLength :: Int# -> Int#
{-# INLINE nextLength #-}
nextLength i = i *# 2#
