{-# LANGUAGE OverloadedStrings #-}
module Foundation.Conduit.Textual
    ( lines
    , words
    , fromBytes
    , toBytes
    ) where

import           Basement.Imports hiding (throw)
import           Basement.UArray (UArray)
import           Foundation.String (String)
import           Foundation.Collection
import qualified Basement.String as S
import           Foundation.Conduit.Internal
import           Foundation.Monad
import           Data.Char (isSpace)

-- | Split conduit of string to its lines
--
-- This is very similar to Prelude lines except
-- it work directly on Conduit
--
-- Note that if the newline character is not ever appearing in the stream,
-- this function will keep accumulating data until OOM
--
-- TODO: make a size-limited function
lines :: Monad m => Conduit String String m ()
lines = await >>= maybe (finish []) (go False [])
  where
    mconcatRev = mconcat . reverse

    finish l = if null l then return () else yield (mconcatRev l)

    go prevCR prevs nextBuf = do
        case S.breakLine nextBuf of
            Right (line, next)
                | S.null line && prevCR -> yield (mconcatRev (line : stripCRFromHead prevs)) >> go False mempty next
                | otherwise             -> yield (mconcatRev (line : prevs)) >> go False mempty next
            Left lastCR ->
                let nextCurrent = nextBuf : prevs
                 in await >>= maybe (finish nextCurrent) (go lastCR nextCurrent)
    stripCRFromHead []     = []
    stripCRFromHead (x:xs) = S.revDrop 1 x:xs

words :: Monad m => Conduit String String m ()
words = await >>= maybe (finish []) (go [])
  where
    mconcatRev = mconcat . reverse

    finish l = if null l then return () else yield (mconcatRev l)

    go prevs nextBuf =
        case S.dropWhile isSpace next' of
            rest'
                | null rest' ->
                    let nextCurrent = nextBuf : prevs
                     in await >>= maybe (finish nextCurrent) (go nextCurrent)
                | otherwise  -> yield (mconcatRev (line : prevs)) >> go mempty rest'
      where (line, next') = S.break isSpace nextBuf

fromBytes :: MonadThrow m => S.Encoding -> Conduit (UArray Word8) String m ()
fromBytes encoding = loop mempty
  where
    loop r = await >>= maybe (finish r) (go r)
    finish buf | null buf  = return ()
               | otherwise = case S.fromBytes encoding buf of
                                    (s, Nothing, _)  -> yield s
                                    (_, Just err, _) -> throw err
    go current nextBuf =
        case S.fromBytes encoding (current `mappend` nextBuf) of
            (s, Nothing           , r) -> yield s >> loop r
            (s, Just S.MissingByte, r) -> yield s >> loop r
            (_, Just err          , _) -> throw err

toBytes :: Monad m => S.Encoding -> Conduit String (UArray Word8) m ()
toBytes encoding = awaitForever $ \a -> pure (S.toBytes encoding a) >>= yield