{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MultiWayIf #-}

{-| This is a builder optimized for concatenating short
    variable-length strings whose length has a known upper
    bound. In these cases, this can be up to ten times faster
    than the builder provided by the @text@ library. However,
    data whose textual encoding has no known upper bound cannot
    be encoded by the builder provided here. For example, it
    is possible to provide decimal builders for types like 'Int8' and
    'Word16', whose lengths are respectively bounded by
    4 and 5. However, this is not possible for 'Integer', since
    its decimal representation could be arbitrarily long.
-}
module Data.Text.Builder.Variable
  ( Builder
  , run
  , contramap
  , charBmp
  , staticCharBmp
  , word8
  ) where

import Data.Monoid
import Data.Word
import Data.Text (Text)
import Text.Printf (printf)
import Control.Monad.ST
import Data.Char (ord)
import Data.Vector (Vector)
import Data.Function (on)
import Data.Maybe (fromMaybe)
import qualified Data.Vector as Vector
import qualified Data.Text as Text
import qualified Data.Text.Array as A
import qualified Data.Text.Builder.Common.Internal as I
import qualified Data.Text.Internal as TI

data Builder a
  = Builder
      {-# UNPACK #-} !Int -- the maximum length, not a character count
      !(forall s. Int -> A.MArray s -> a -> ST s Int)

instance Monoid (Builder a) where
  {-# INLINE mempty #-}
  mempty = Builder 0 (\i _ _ -> return i)
  {-# INLINE mappend #-}
  mappend (Builder len1 f) (Builder len2 g) =
    Builder (len1 + len2) $ \ix1 marr a -> do
      ix2 <- f ix1 marr a
      g ix2 marr a

run :: Builder a -> a -> Text
run (Builder maxLen f) = \a ->
  let (outArr,len) = A.run2 $ do
        marr <- A.new maxLen
        finalIx <- f 0 marr a
        return (marr,finalIx)
   in TI.text outArr 0 len
{-# INLINE run #-}

contramap :: (b -> a) -> Builder a -> Builder b
contramap f (Builder len g) = Builder len $ \i marr b ->
  g i marr (f b)
{-# INLINE contramap #-}

-- finish writing this. it's important for completeness
-- char :: Builder a
-- char = Builder 2 $ \

-- | A character in the basic multilingual plane.
charBmp :: Builder Char
charBmp = Builder 1 $ \i marr c -> do
  A.unsafeWrite marr i (fromIntegral (ord c))
  return (i + 1)
{-# INLINE charBmp #-}

staticCharBmp :: Char -> Builder a
staticCharBmp c = Builder 1 $ \i marr _ -> do
  A.unsafeWrite marr i (fromIntegral (ord c))
  return (i + 1)
{-# INLINE staticCharBmp #-}

word8 :: Builder Word8
word8 = Builder 3 $ \pos marr w -> if
  | w < 10 -> do
      A.unsafeWrite marr pos (i2w w)
      return (pos + 1)
  | w < 100 -> do
      let wInt = fromIntegral w
          ix = wInt + wInt
      A.unsafeWrite marr pos (A.unsafeIndex I.twoDecimalDigits ix)
      A.unsafeWrite marr (pos + 1) (A.unsafeIndex I.twoDecimalDigits (ix + 1))
      return (pos + 2)
  | otherwise -> do
      let wInt = fromIntegral w
          ix = wInt + wInt + wInt
      A.unsafeWrite marr pos (A.unsafeIndex I.threeDecimalDigits ix)
      A.unsafeWrite marr (pos + 1) (A.unsafeIndex I.threeDecimalDigits (ix + 1))
      A.unsafeWrite marr (pos + 2) (A.unsafeIndex I.threeDecimalDigits (ix + 2))
      return (pos + 3)
{-# INLINE word8 #-}

-- This has not yet been tested.
vector ::
     Text -- ^ Default, used when index is out of range
  -> Vector Text -- ^ Texts to index into
  -> Builder Int
vector tDef v =
  let xs = Vector.map I.portableUntext v
      xDef = I.portableUntext tDef
   in Builder
        (Vector.maximum $ Vector.map I.portableTextLength $ Vector.cons tDef v)
        $ \pos marr i -> do
          let (arr,len) = fromMaybe xDef (xs Vector.!? i)
              finalIx = i + len
          A.copyI marr i arr 0 finalIx
          return finalIx
{-# INLINE vector #-}

i2w :: Integral a => a -> Word16
i2w v = asciiZero + fromIntegral v
{-# INLINE i2w #-}

asciiZero :: Word16
asciiZero = 48
{-# INLINE asciiZero #-}