{-# LANGUAGE BangPatterns, MagicHash, ForeignFunctionInterface, JavaScriptFFI,
             UnliftedFFITypes
  #-}
module Data.JSString.Internal.Fusion ( -- * Types
                                       Stream(..)
                                     , Step(..)

                                       -- * Creation and elimination
                                     , stream
                                     , unstream
                                     , reverseStream

                                     , length

                                       -- * Transformations
                                     , reverse

                                       -- * Construction
                                       -- ** Scans
                                     , reverseScanr

                                       -- ** Accumulating maps
                                     , mapAccumL

                                       -- ** Generation and unfolding
                                     , unfoldrN

                                       -- * Indexing
                                     , index
                                     , findIndex
                                     , countChar
                                     ) where

import           GHC.Exts (Char(..), Int(..), chr#, Int#, isTrue#, (-#), (+#), (>=#))

import           Prelude hiding (length, reverse)
import           Data.Char

import           Data.JSString.Internal.Type (JSString(..))
import qualified Data.JSString.Internal.Type          as I
import           Data.JSString.Internal.Fusion.Types
import qualified Data.JSString.Internal.Fusion.Common as S

import           System.IO.Unsafe

import           GHCJS.Prim

default (Int)

-- | /O(n)/ Convert a 'JSString' into a 'Stream Char'.
stream :: JSString -> Stream Char
stream x =
  let next i = case js_index i x of
                 -1#  -> Done
                 ch   -> let !i' = i + if isTrue# (ch >=# 0x10000#)
                                       then 2
                                       else 1
                         in  Yield (C# (chr# ch)) i'
  in Stream next 0
{-# INLINE [0] stream #-}

-- | /O(n)/ Convert a 'JSString' into a 'Stream Char', but iterate
-- backwards.
reverseStream :: JSString -> Stream Char
reverseStream x =
  let l = js_length x
      {-# INLINE next #-}
      next i = case js_indexR i x of
                  -1#  -> Done
                  ch   -> let !i' = i - if isTrue# (ch >=# 0x10000#)
                                        then 2
                                        else 1
                          in  Yield (C# (chr# ch)) i'
  in Stream next (I# (l -# 1#))
{-# INLINE [0] reverseStream #-}

-- | /O(n)/ Convert a 'Stream Char' into a 'JSString'.
unstream :: Stream Char -> JSString
unstream (Stream next s) = runJSString $ \done ->
  let go !s0 = case next s0 of
                 Done       -> done I.empty
                 Skip s1    -> go s1
                 Yield x s1 -> js_newSingletonArray x >>= loop 1 s1
      loop !i !s0 a = case next s0 of
                        Done       -> js_packString a >>= done
                        Skip s1    -> loop i s1 a
                        Yield x s1 -> js_writeArray x i a >> loop (i+1) s1 a
  in go s
{-# INLINE [0] unstream #-}
{-# RULES "STREAM stream/unstream fusion" forall s. stream (unstream s) = s #-}


-- ----------------------------------------------------------------------------
-- * Basic stream functions

runJSString :: ((a -> IO a) -> IO a) -> a
runJSString f = unsafePerformIO (f pure)

length :: Stream Char -> Int
length = S.lengthI
{-# INLINE[0] length #-}

-- | /O(n)/ Reverse the characters of a string.
reverse :: Stream Char -> JSString
reverse (Stream next s) = runJSString $ \done ->
  let go !s0 = case next s0 of
                 Done       -> done I.empty
                 Skip s1    -> go s1
                 Yield x s1 -> js_newSingletonArray x >>= loop 1 s1
      loop !i !s0 a = case next s0 of
                        Done       -> js_packReverse a >>= done
                        Skip s1    -> loop i s1 a
                        Yield x s1 -> js_writeArray x i a >> loop (i+1) s1 a
  in go s
{-# INLINE [0] reverse #-}

-- | /O(n)/ Perform the equivalent of 'scanr' over a list, only with
-- the input and result reversed.
reverseScanr :: (Char -> Char -> Char) -> Char -> Stream Char -> Stream Char
reverseScanr f z0 (Stream next0 s0) = Stream next (S1 :*: z0 :*: s0)
  where
    {-# INLINE next #-}
    next (S1 :*: z :*: s) = Yield z (S2 :*: z :*: s)
    next (S2 :*: z :*: s) = case next0 s of
                              Yield x s' -> let !x' = f x z
                                            in Yield x' (S2 :*: x' :*: s')
                              Skip s'    -> Skip (S2 :*: z :*: s')
                              Done       -> Done
{-# INLINE reverseScanr #-}

-- | /O(n)/ Like 'unfoldr', 'unfoldrN' builds a stream from a seed
-- value. However, the length of the result is limited by the
-- first argument to 'unfoldrN'. This function is more efficient than
-- 'unfoldr' when the length of the result is known.
unfoldrN :: Int -> (a -> Maybe (Char,a)) -> a -> Stream Char
unfoldrN n = S.unfoldrNI n
{-# INLINE [0] unfoldrN #-}

-------------------------------------------------------------------------------
-- ** Indexing streams

-- | /O(n)/ stream index (subscript) operator, starting from 0.
index :: Stream Char -> Int -> Char
index = S.indexI
{-# INLINE [0] index #-}

-- | The 'findIndex' function takes a predicate and a stream and
-- returns the index of the first element in the stream
-- satisfying the predicate.
findIndex :: (Char -> Bool) -> Stream Char -> Maybe Int
findIndex = S.findIndexI
{-# INLINE [0] findIndex #-}

-- | /O(n)/ The 'count' function returns the number of times the query
-- element appears in the given stream.
countChar :: Char -> Stream Char -> Int
countChar = S.countCharI
{-# INLINE [0] countChar #-}

-- | /O(n)/ Like a combination of 'map' and 'foldl''. Applies a
-- function to each element of a 'Text', passing an accumulating
-- parameter from left to right, and returns a final 'JSString'.
mapAccumL :: (a -> Char -> (a, Char)) -> a -> Stream Char -> (a, JSString)
mapAccumL f z0 (Stream next s0) = runJSString $ \done ->
  let go !s1 = case next s1 of
                 Done        -> done (z0, I.empty)
                 Skip s2     -> go s2
                 Yield ch s2 -> let (z1, ch1) = f z0 ch
                                in  js_newSingletonArray ch1 >>= loop 1 s2 z1
      loop !i !s1 !z1 a = case next s1 of
        Done         -> js_packString a >>= \s -> done (z1, s)
        Skip s2      -> loop i s2 z1 a
        Yield ch1 s2 -> let (z2, ch2) = f z1 ch1
                        in  js_writeArray ch2 i a >> loop (i+1) s2 z2 a
  in go s0
{-# INLINE [0] mapAccumL #-}

-------------------------------------------------------------------------------

-- returns -1 for end of stream
foreign import javascript unsafe
  "h$jsstringIndex" js_index :: Int -> JSString -> Int#
foreign import javascript unsafe
  "h$jsstringIndexR" js_indexR :: Int -> JSString -> Int#
foreign import javascript unsafe
  "$1.length" js_length :: JSString -> Int#
foreign import javascript unsafe
  "$r = [$1];" js_newSingletonArray :: Char -> IO JSVal
foreign import javascript unsafe
  "$3[$2] = $1;" js_writeArray :: Char -> Int -> JSVal -> IO ()
foreign import javascript unsafe
  "h$jsstringPackArray" js_packString :: JSVal -> IO JSString
foreign import javascript unsafe
  "h$jsstringPackArrayReverse" js_packReverse :: JSVal -> IO JSString