{-# 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
  , withRecPipe
  , runStateP
  , pAwaitF, pYieldF
  ) where

import           Control.Applicative
import           Control.Monad.Catch
import           Control.Monad.Except
import           Control.Monad.Free.Class
import           Control.Monad.Free.TH
import           Control.Monad.RWS
import           Control.Monad.Trans.Free        (FreeT(..), FreeF(..))
import           Control.Monad.Trans.Free.Church
import           Control.Monad.Trans.State
import           Data.Functor

#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 'Data.Void.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 'Data.Void.Void', then the pipe's upstream is limitless,
--    and never terminates.  This means that you can use
--    'Data.Conduino.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 'Data.Void.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.Conduino.runPipe' will also never terminate.  If it is
--    a source, it means that if you chain something downstream with
--    'Data.Conduino..|', that downstream pipe can use
--    'Data.Conduino.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.Conduino..|' and then running the result with
-- 'Data.Conduino.runPipe'.
--
-- @
-- 'Data.Conduino.runPipe' $ someSource
--        'Data.Conduino..|' somePipe
--        .| someOtherPipe
--        .| someSink
-- @
--
-- See 'Data.Conduino..|' and 'Data.Conduino.runPipe' for more information
-- on usage.
--
-- For a "prelude" of commonly used 'Pipe's, see
-- "Data.Conduino.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
    , Alternative
    , MonadPlus
    , MonadThrow
    , MonadCatch
    )

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.
--
-- Note that if you are trying to work with monad transformers, this is
-- probably not what you want.  See "Data.Conduino.Lift" for tools for
-- working with underlying monad transformers.
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

-- | Convenint wrapper over 'toRecPipe' and 'fromRecPipe'.
--
-- @since 0.2.1.0
withRecPipe
    :: (Monad m, Monad n)
    => (RecPipe i o u m a -> RecPipe j p v n b)
    -> Pipe i o u m a
    -> Pipe j p v n b
withRecPipe f = fromRecPipe . f . toRecPipe

-- | Turn a 'Pipe' that runs over 'StateT' into a "state-modifying 'Pipe'",
-- that returns the final state when it terminates.
--
-- The main usage of this is to "isolate" the state from other pipes in the
-- same chain.  For example, of @p@, @q@, and @r@ are all pipes under
-- 'StateT', then:
--
-- @
--     p
--  .| q
--  .| r
-- @
--
-- will all share underlying state, and each can modify the state that they
-- all three share.  We essentially have global state.
--
-- However, if you use 'runStateP', you can all have them use different
-- encapsulated states.
--
-- @
--     void (runStateP s0 p)
--  .| void (runStateP s1 q)
--  .| runStateP s2 r
-- @
--
-- In this case, each of those three chained pipes will use their own
-- internal states, without sharing.
--
-- This is also useful if you want to chain a pipe over 'StateT' with
-- pipes that don't use state at all: for example if @a@ and @b@ are
-- "non-stateful" pipes (/not/ over 'StateT'), you can do:
--
-- @
--     a
--  .| void (runStateP s1 q)
--  .| b
-- @
--
-- And @a@ and @b@ will be none the wiser to the fact that @q@ uses
-- 'StateT' internally.
--
-- Note to avoid the usage of 'void', 'Data.Conduino.Lift.evalStateP' might
-- be more useful.
--
-- @since 0.2.1.0
runStateP
    :: Monad m
    => s
    -> Pipe i o u (StateT s m) a
    -> Pipe i o u m (a, s)
runStateP = withRecPipe . go
  where
    go s (FreeT p) = FreeT $ runStateT p s <&> \(q, s') ->
      case q of
        Pure x -> Pure (x, s')
        Free l -> Free $ go s' <$> l