module Jsonifier
(
  -- * ByteString
  toByteString,
  -- * Json
  Json,
  -- ** Primitives
  null,
  bool,
  -- ** Numbers
  intNumber,
  wordNumber,
  doubleNumber,
  scientificNumber,
  -- ** Strings
  textString,
  scientificString,
  -- ** Composites
  array,
  object,
)
where

import Jsonifier.Prelude hiding (null, bool)
import PtrPoker.Poke (Poke)
import PtrPoker.Write (Write)
import qualified Jsonifier.Allocation as Allocation
import qualified Jsonifier.Poke as Poke
import qualified Jsonifier.Write as Write
import qualified PtrPoker.Poke as Poke
import qualified PtrPoker.Write as Write
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Internal as ByteString


{-|
Render a JSON value into strict bytestring.
-}
{-# INLINE toByteString #-}
toByteString :: Json -> ByteString
toByteString =
  Write.writeToByteString . coerce


-- * Json
-------------------------

{-|
Specification of how to render a JSON value to 'ByteString'.
A sort of a JSON-specialized string 'ByteString' builder.

You can construct it by using the specialized
conversion functions from Haskell types.
After constructing, you can convert to strict 'ByteString'
using the 'toByteString' function.
-}
newtype Json =
  Json Write.Write

{-# INLINE write #-}
write :: Int -> Poke.Poke -> Json
write size poke =
  Json (Write.Write size poke)

{-|
JSON Null literal.
-}
{-# INLINE null #-}
null :: Json
null =
  write 4 Poke.null

{-|
JSON Boolean literal.
-}
{-# INLINE bool #-}
bool :: Bool -> Json
bool =
  \ case
    True ->
      write 4 Poke.true
    False ->
      write 5 Poke.false

{-|
JSON Number literal from @Int@.
-}
{-# INLINE intNumber #-}
intNumber :: Int -> Json
intNumber =
  Json . Write.intAsciiDec

{-|
JSON Number literal from @Word@.
-}
{-# INLINE wordNumber #-}
wordNumber :: Word -> Json
wordNumber =
  Json . Write.wordAsciiDec

{-|
JSON Number literal from @Double@.

Since JSON doesn\'t have support for them,
non-real values like @NaN@, @Infinity@, @-Infinity@ get rendered as @0@.
-}
{-# INLINE doubleNumber #-}
doubleNumber :: Double -> Json
doubleNumber =
  Json . Write.zeroNonRealDoubleAsciiDec

{-|
JSON Number literal from @Scientific@.
-}
{-# INLINE scientificNumber #-}
scientificNumber :: Scientific -> Json
scientificNumber =
  Json . Write.scientificAsciiDec

{-|
JSON String literal from @Text@.
-}
{-# INLINE textString #-}
textString :: Text -> Json
textString text =
  let
    allocation =
      2 + Allocation.stringBody text
    poke =
      Poke.string text
    in write allocation poke

{-|
JSON String literal from @Scientific@.

You may need this when the reader of your JSON
cannot handle large number literals.
-}
{-# INLINE scientificString #-}
scientificString :: Scientific -> Json
scientificString =
  Json . Write.scientificString

{-|
JSON Array literal from a foldable over element literals.

Don\'t be afraid to use 'fmap' to map the elements of the input datastructure,
it will all be optimized away.
-}
{-# INLINE array #-}
array :: Foldable f => f Json -> Json
array foldable =
  write size poke
  where
    size =
      foldr step finalize foldable 0 0
      where
        step (Json (Write.Write writeSize _)) next !count !size =
          next (succ count) (writeSize + size)
        finalize count size =
          Allocation.array count size
    poke =
      Poke.Poke $
        Poke.pokePtr Poke.openingSquareBracket >=>
        foldr step finalize foldable True
      where
        step (Json (Write.Write _ poke)) next first =
          if first
            then
              Poke.pokePtr poke >=>
              next False
            else
              Poke.pokePtr Poke.comma >=>
              Poke.pokePtr poke >=>
              next False
        finalize _ =
          Poke.pokePtr Poke.closingSquareBracket

{-|
JSON Array literal from a foldable over pairs of key to value literal.

Don\'t be afraid to use 'fmap' to map the elements of the input datastructure,
it will all be optimized away.
-}
{-# INLINE object #-}
object :: Foldable f => f (Text, Json) -> Json
object f =
  foldr step finalize f True 0 0 mempty
  where
    step (key, Json (Write.Write{..})) next first !size !allocation !poke =
      if first
        then
          next False 1 rowAllocation rowPoke
        else
          next False (succ size) (allocation + rowAllocation)
            (poke <> Poke.comma <> rowPoke)
      where
        rowAllocation =
          Allocation.stringBody key +
          writeSize
        rowPoke =
          Poke.objectRow key writePoke
    finalize _ size contentsAllocation bodyPoke =
      write allocation poke
      where
        allocation =
          Allocation.object size contentsAllocation
        poke =
          Poke.openingCurlyBracket <> bodyPoke <> Poke.closingCurlyBracket