{-# LANGUAGE CPP                        #-}
{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeInType                 #-}
{-# LANGUAGE TypeOperators              #-}
{-# OPTIONS_HADDOCK not-home            #-}

-- |
-- Module      : Data.Conduino.Internal
-- Copyright   : (c) Justin Le 2019
-- License     : BSD3
--
-- Maintainer  : justin@jle.im
-- Stability   : experimental
-- Portability : non-portable
--
-- Internal module exposing the internals of 'Pipe', including its
-- underlying representation and base functor.
--
module Data.Conduino.Internal (
    Pipe(..)
  , PipeF(..)
  , awaitEither
  , yield
  , trimapPipe, mapInput, mapOutput, mapUpRes
  , hoistPipe
  , RecPipe
  , toRecPipe, fromRecPipe
  ) where

import           Control.Monad.Except
import           Control.Monad.Free.Class
import           Control.Monad.Free.TH
import           Control.Monad.RWS
import           Control.Monad.Trans.Free        (FreeT(..))
import           Control.Monad.Trans.Free.Church

#if !MIN_VERSION_base(4,13,0)
import           Control.Monad.Fail
#endif

-- | Base functor of 'Pipe'.
--
-- A pipe fundamentally has the ability to await and the ability to yield.
-- The other functionality are implemented.
--
-- *  Lifting effects is implemented by the 'MonadTrans' and 'MonadIO'
--    instances that 'FT' gives.
-- *  /Ending/ with a result is implemented by the 'Applicative' instance's
--   'pure' that 'FT' gives.
-- *  Applicative and monadic sequenceing "after a pipe is done" is
--    implemented by the 'Applicative' and 'Monad' instances that 'FT'
--    gives.
--
-- On top of these we implement 'Data.Conduino..|' and other combinators
-- based on the structure that 'FT' gives.  For some functions, it can be
-- easier to use an alternative encoding, 'RecPipe', which is the same
-- thing but explicitly recursive.
data PipeF i o u a =
      PAwaitF (u -> a) (i -> a)
    | PYieldF o a
  deriving Functor

makeFree ''PipeF

-- | Similar to a conduit from the /conduit/ package.
--
-- For a @'Pipe' i o u m a@, you have:
--
-- *  @i@: Type of input stream (the things you can 'Data.Conduino.await')
-- *  @o@: Type of output stream (the things you 'yield')
-- *  @u@: Type of the /result/ of the upstream pipe (Outputted when
--    upstream pipe terminates)
-- *  @m@: Underlying monad (the things you can 'lift')
-- *  @a@: Result type when pipe terminates (outputted when finished, with
--    'pure' or 'return')
--
-- Some specializations:
--
-- *  If @i@ is @()@, the pipe is a /source/ --- it doesn't need anything
--    to produce items.  It will pump out items on its own, for pipes
--    downstream to receive and process.
--
-- *  If @o@ is 'Void', the pipe is a /sink/ --- it will never 'yield'
--    anything downstream.  It will consume items from things upstream, and
--    produce a result (@a@) if and when it terminates.
--
-- *  If @u@ is 'Void', then the pipe's upstream is limitless, and never
--    terminates.  This means that you can use 'Data.Condunio.awaitSurely'
--    instead of 'Data.Conduino.await', to get await a value that is
--    guaranteed to come.  You'll get an @i@ instead of a @'Maybe' i@.
--
-- *  If @a@ is 'Void', then the pipe never terminates --- it will keep on
--    consuming and/or producing values forever.  If this is a sink, it
--    means that the sink will never terminate, and so
--    'Data.Condunio.runPipe' will also never terminate.  If it is
--    a source, it means that if you chain something downstream with
--    'Data.Condunio..|', that downstream pipe can use 'awaitSurely' to
--    guarantee something being passed down.
--
-- Applicative and Monadic sequencing of pipes chains by exhaustion.
--
-- @
-- do pipeX
--    pipeY
--    pipeZ
-- @
--
-- is a pipe itself, that behaves like @pipeX@ until it terminates, then
-- @pipeY@ until it terminates, then @pipeZ@ until it terminates.  The
-- 'Monad' instance allows you to choose "which pipe to behave like next"
-- based on the terminating result of a previous pipe.
--
-- @
-- do x <- pipeX
--    pipeBasedOn x
-- @
--
-- Usually you would use it by chaining together pipes with
-- 'Data.Condunio..|' and then running the result with
-- 'Data.Condunio.runPipe'.
--
-- @
-- 'Data.Conduino.runPipe' $ someSource
--        'Data.Conduino..|' somePipe
--        .| someOtherPipe
--        .| someSink
-- @
--
-- See 'Data.Condunio..|' and 'Data.Condunio.runPipe' for more information
-- on usage.
--
-- For a "prelude" of commonly used 'Pipe's, see
-- "Data.Condunio.Combinators".
--
newtype Pipe i o u m a = Pipe { pipeFree :: FT (PipeF i o u) m a }
  deriving
    ( Functor
    , Applicative
    , Monad
    , MonadTrans
    , MonadFree (PipeF i o u)
    , MonadIO
    , MonadState s
    , MonadReader r
    , MonadWriter w
    , MonadError e
    , MonadRWS r w s
    )

instance MonadFail m => MonadFail (Pipe i o u m) where
#if MIN_VERSION_base(4,13,0)
    fail = lift . fail
#else
    fail = lift . Control.Monad.Fail.fail
#endif

-- | Await on upstream output.  Will block until it receives an @i@
-- (expected input type) or a @u@ if the upstream pipe terminates.
awaitEither :: Pipe i o u m (Either u i)
awaitEither = pAwaitF

-- | Send output downstream.
yield :: o -> Pipe i o u m ()
yield = pYieldF

-- | Map over the input type, output type, and upstream result type.
--
-- If you want to map over the result type, use 'fmap'.
trimapPipe
    :: (i -> j)
    -> (p -> o)
    -> (u -> v)
    -> Pipe j p v m a
    -> Pipe i o u m a
trimapPipe f g h = Pipe . transFT go . pipeFree
  where
    go = \case
      PAwaitF a b -> PAwaitF (a . h) (b . f)
      PYieldF a x -> PYieldF (g a) x

-- | Transform the underlying monad of a pipe.
hoistPipe
    :: (Monad m, Monad n)
    => (forall x. m x -> n x)
    -> Pipe i o u m a
    -> Pipe i o u n a
hoistPipe f = Pipe . hoistFT f . pipeFree

-- | (Contravariantly) map over the expected input type.
mapInput :: (i -> j) -> Pipe j o u m a -> Pipe i o u m a
mapInput f = trimapPipe f id id

-- | Map over the downstream output type.
--
-- If you want to map over the result type, use 'fmap'.
mapOutput :: (p -> o) -> Pipe i p u m a -> Pipe i o u m a
mapOutput f = trimapPipe id f id

-- | (Contravariantly) map over the upstream result type.
mapUpRes :: (u -> v) -> Pipe i o v m a -> Pipe i o u m a
mapUpRes = trimapPipe id id

-- | A version of 'Pipe' that uses explicit, concrete recursion instead of
-- church-encoding like 'Pipe'.  Some functions --- especially ones that
-- combine multiple pipes into one --- are easier to implement in this
-- form.
type RecPipe i o u = FreeT (PipeF i o u)

-- | Convert from a 'Pipe' to a 'RecPipe'.  While most of this library is
-- defined in terms of 'Pipe', it can be easier to write certain low-level
-- pipe combining functions in terms of 'RecPipe' than 'Pipe'.
toRecPipe :: Monad m => Pipe i o u m a -> RecPipe i o u m a
toRecPipe = fromFT . pipeFree

-- | Convert a 'RecPipe' back into a 'Pipe'.
fromRecPipe :: Monad m => RecPipe i o u m a -> Pipe i o u m a
fromRecPipe = Pipe . toFT