{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE RankNTypes #-}
module Streaming.Bracketed.Internal where

import           Data.Foldable
import           Data.Bifunctor
import           Data.IORef
import           Control.Exception

import           Streaming
import qualified Streaming.Prelude             as S

import           System.IO

newtype Bracketed a r =
    Bracketed { runBracketed :: IORef Finstack -> Stream (Of a) IO r }
    deriving Functor

-- | `first` maps over the yielded elements.
instance Bifunctor Bracketed where
    first f (Bracketed b) = Bracketed (S.map f . b)
    second = fmap

instance Applicative (Bracketed a) where
    pure = Bracketed . const . pure
    Bracketed b <*> Bracketed b' = Bracketed (\finref ->
        b finref <*> b' finref)

instance Monad (Bracketed a) where
    return = pure
    Bracketed b >>= f = Bracketed (\finref ->
        do r <- b finref
           let Bracketed b' = f r
           b' finref)

instance MonadIO (Bracketed a) where
    liftIO action = Bracketed (\_ -> liftIO action)

-- | A stack of finalizers, accompanied by its length.
--
--   Finalizers at the head of the list correspond to deeper levels of nesting.
data Finstack = Finstack !Int [IO ()]

-- | Lift a `Stream` that doesn't perform allocation to a `Bracketed`.
clear :: Stream (Of x) IO r -> Bracketed x r
clear stream = Bracketed (const stream)

-- | Lift a `Stream` that requires resource allocation to a `Bracketed`.
bracketed :: IO a -> (a -> IO ()) -> (a -> Stream (Of x) IO r) -> Bracketed x r
bracketed allocate finalize stream = Bracketed (\finref ->
    let open = do
            a <- allocate
            Finstack size0 fins <- readIORef finref
            writeIORef finref (Finstack (succ size0) (finalize a : fins))
            pure (size0,a)
    in do (size0,a) <- liftIO (mask (\_ -> open))
          r <- stream a
          liftIO (mask (\_ -> reset size0 finref))
          pure r)

-- | Consume a `Bracketed` stream, exhausting it.
with :: Bracketed a r -> (forall x. Stream (Of a) IO x -> IO (Of b x)) -> IO (Of b r)
with (Bracketed b) f =
    Control.Exception.bracket (newIORef (Finstack 0 []))
                              (reset 0)
                              (f . b)

-- | Consume a `Bracketed` stream, possibly wihout exhausting it.
--   
--   Finalizers lying in unconsumed parts of the stream will not be executed
--   until the callback returns, so better not tarry too long if you want
--   prompt finalization.
with_ :: Bracketed a r -> (Stream (Of a) IO r -> IO b) -> IO b
with_ (Bracketed b) f =
    Control.Exception.bracket (newIORef (Finstack 0 []))
                              (reset 0)
                              (f . b)

-- | Apply to the underlying stream a transformation that preserves the return value.
over :: (forall x. Stream (Of a) IO x -> Stream (Of b) IO x) -> Bracketed a r -> Bracketed b r
over transform (Bracketed b) = Bracketed (transform . b)

-- | Apply to the underlying stream a transformation that might not preserve
--   the return value.
over_ :: (Stream (Of a) IO r -> Stream (Of b) IO r') -> Bracketed a r -> Bracketed b r'
over_ transform (Bracketed b) = Bracketed (\finref ->
    let level = do
            Finstack size _ <- readIORef finref
            pure size
    in do size0 <- liftIO level
          r <- transform (b finref)
          liftIO (mask (\_ -> reset size0 finref))
          pure r)

-- | Replaces each element of a stream with an associated stream. 
--
--   Can be useful for traversing hierachical structures.
for :: Bracketed a r -> (a -> Bracketed b x) -> Bracketed b r
for (Bracketed b) f =
    Bracketed (\fins -> S.for (b fins) (flip (runBracketed . f) fins))

-- | Executes all finalizers that lie above a certain level.
reset :: Int -> IORef Finstack -> IO ()
reset size0 finref =
    do Finstack size fins <- readIORef finref
       let (pending,fins') = splitAt (size - size0) fins
       writeIORef finref (Finstack size0 fins')
       foldr finally (pure ()) pending

-- | A bracketed stream of all the lines in a text file.
--
--   This is adequate for simple use cases. For more advanced ones where
--   efficiency and memory usage are important, it's better to use a packed
--   text representation like the one provided by the @text@ package.
linesFromFile :: TextEncoding -> NewlineMode -> FilePath -> Bracketed String ()
linesFromFile encoding newlineMode path = bracketed
  (openFile path ReadMode)
  hClose
  (\h -> do
    liftIO (hSetEncoding h encoding)
    liftIO (hSetNewlineMode h newlineMode)
    S.untilRight
      (do
        eof <- hIsEOF h
        if eof then Right <$> pure () else Left <$> hGetLine h
      )
  )

-- | Given a list of text files and line ranges, create a stream of lines
--   belonging to the concatenated ranges.
concatRanges
  :: TextEncoding
  -> NewlineMode
  -> [(FilePath, Int, Int)]
  -> Bracketed String ()
concatRanges encoding newlineMode ranges =
  let streamRange (path, start, end) =
        over_ (S.take (end - start)) . over (S.drop start) $ linesFromFile
          encoding
          newlineMode
          path
  in  traverse_ streamRange ranges