{-# LANGUAGE ConstraintKinds, DataKinds #-}
{-# LANGUAGE NoMonomorphismRestriction, GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}

{-| This module defines two effects ('Clipboard' and 'OpenUrl'),
and then composes them ('Workflow') "on-the-fly".

For each effect, we:

* Define a functor (e.g. 'ClipboardF').
* (optionally, for convenience, define aliases for the constraint
(e.g. 'MonadClipboard') and effect-set (e.g. 'Clipboard')).
* Define overloaded constructors (e.g. 'getClipboard', 'setClipboard'). TODO th.
* Define a handler (e.g. @('handleClipboard' :: 'ClipboardF' ('IO' a) -> 'IO' a)@),
which involves minimal boilerplate. (if you've used the @free@ package,
you know how it's done).
Then, wrap that handler (e.g. with the shape @'ClipboardF' a -> a@)
in an 'Interpreter', for /extensibility/.

You can use these effects extensibly, "@mtl@-style". e.g.
Since 'getClipboard' and 'openUrl' are overloaded, they can both be used in
'openFromClipboard'.

@
'openFromClipboard' = do   -- :: ('MonadClipboard' m, 'MonadOpenUrl' m) => m ()
  s \<- 'getClipboard'      -- :: ('MonadClipboard' m                ) => m String
  'openUrl' s              -- :: (                  'MonadOpenUrl' m) => m ()
@

Note:

* the type of @openFromClipboard@ is inferred.
* the constraints are aliases; you don't need to write a new class
for each new effect type.
* for compositions of effects (like 'MonadWorkflow'),
you don't even need to write a new type.
Just append the interpreters you want (with `appendInterpreters`).

-}
module Vinyl.Effects.Example
 ( main

 -- * Effect #1: Clipboard
 ,MonadClipboard
 ,Clipboard
 ,ClipboardF(..)
 -- ** overloaded constructors
 ,getClipboard
 ,setClipboard
 -- ** e.g. reverseClipboard
 ,reverseClipboard
 -- ** the interpreter
 ,runClipboard
 ,interpretClipboard
 ,interpretClipboard2
 ,handleClipboard
 -- ** the implementation
 ,sh_GetClipboard
 ,sh_SetClipboard

 -- * Effect #2: Clipboard
 ,MonadOpenUrl
 ,OpenUrl
 ,OpenUrlF
 -- ** overloaded constructors
 ,openUrl
 -- ** the interpreter
 ,runOpenUrl
 ,interpretOpenUrl
 ,handleOpenUrl
 -- ** the implementation
 ,sh_OpenUrl

 -- * Workflow: \#1 + \#2
 , MonadWorkflow
 , Workflow
 , runWorkflow
 , interpretWorkflow1
 , interpretWorkflow2
 ,interpretOpenUrl2

 -- ** e.g. openFromClipboard
 , openFromClipboard
 , openFromClipboard_nothingSpecialized
 , openFromClipboard_monadSpecialized
 , openFromClipboard_effectsSpecialized
 , openFromClipboard_bothSpecialized

 -- * Reader, as an effect
 ,MonadReader
 ,Reader
 ,ReaderF
 ,ask

 -- * Writer, as an effect
 ,MonadWriter
 ,Writer
 ,WriterF
 ,tell

 -- * State, as an effect
 ,MonadState
 ,State
 ,StateF
 ,get
 ,put

 -- ** instance MonadLanguge RWS
 , RWS(..)
 , runRWS
 , liftRWS
 , liftReaderF
 , liftWriterF
 , liftStateF
 , exampleRWS
 ,exampleRWS_specializedLanguage
 ,exampleRWS_specializedRWS

 ) where
import Vinyl.Effects
-- import Vinyl.Effects.Interpreter.Cofree as I

-- import Data.Vinyl (rget)
-- import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.Trans.Reader (ReaderT(..))
import Control.Monad.Trans.Writer (WriterT(..))
import Control.Monad.Trans.State (StateT(..))
import Data.Functor.Identity (Identity(..))

import Data.Proxy (Proxy(..))
-- import Data.Function ((&))
import Control.Arrow ((>>>))
import System.Process (CreateProcess(..), StdStream(..), createProcess, waitForProcess, proc, shell)
import GHC.IO.Handle (hGetContents)

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

{-| run with:

@
stack build && stack exec example-vinyl-effects
@

(read the source too).

-}
main :: IO ()
main = do
 putStrLn ""

 -- runOpenUrl $ openUrl "http://google.com"

 -- runClipboard $ setClipboard "'" -- { echo '''' | pbcopy } would fail, unless propertly escaped

 -- runClipboard $ setClipboard "http://google.com"
 -- runWorkflow $ openFromClipboard
 --
 -- runClipboard $ reverseClipboard
 -- print =<< runClipboard getClipboard

 runClipboard $ do
   setClipboard "http://google.com"   -- `setClipboard` as a `Clipboard`

 contents <- runWorkflow $ do
  --  openFromClipboard
   reverseClipboard                   -- `setClipboard` as a `Workflow`
   getClipboard

 print contents

 let ((a::Int), (w::[String]), s) = runRWS False (1::Int) $ do --TODO why (a::Int) when (1::Int) and (m i)?
       -- _ <- exampleRWS --Err No instance for (Num t0) arising from a use of ‘exampleRWS’
       (_::Int) <- exampleRWS --TODO Can it be inferred? Constraint trick?
       exampleRWS
 print (a,w,s)

{-old

let ((a::Int), (w::[String]), s) = runRWS False (1::Int) $ exampleRWS >> exampleRWS
print (a,w,s)

let ((a::Int), (w::[String]), (s::Int)) = runRWS False 1 $ do

-}

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

{- | an effect to visit the url that's currently in the clipboard.

uses two distinct effects, i.e. it's a 'Workflow' action.

@
openFromClipboard = do
  s <- 'getClipboard'
  'openUrl' s
@

Inferred (with @NoMonomorphismRestriction@):

@
 :: ( 'MonadClipboard' m effects
    , 'MonadOpenUrl' m effects
    )
 => m ()
@

(the same, without aliases)

@
 :: ( 'MonadLanguage' m effects
    , 'RElem' ClipboardF effects ('RIndex' ClipboardF effects)
    , 'RElem' OpenUrlF   effects ('RIndex' OpenUrlF   effects)
    )
 => m ()
@

(which is what haddock displays, unformatted).

i.e. "any monad, that supports any set of effects that have at least 'ClipboardF' and 'OpenUrlF'".

you can specialize the effects:

@
openFromClipboard
 :: ('MonadLanguage' m [ClipboardF, OpenUrlF])
 => m ()
@

i.e. "any monad, that supports exactly two effects, 'ClipboardF' and 'OpenUrlF'".

or the monad:

@
openFromClipboard
  :: (ClipboardF ∈ effects, OpenUrlF ∈ effects)
  => 'Language' effects ()
@

or both:

@
openFromClipboard
 () =>
 :: 'Language' [ClipboardF, OpenUrlF] ()
@

-}
openFromClipboard = do
  s <- getClipboard
  openUrl s

-- | @= 'openFromClipboard'@
openFromClipboard_nothingSpecialized :: (MonadWorkflow m effects) => m ()
openFromClipboard_nothingSpecialized = openFromClipboard

-- | @= 'openFromClipboard'@
openFromClipboard_effectsSpecialized
 :: (MonadLanguage m [ClipboardF, OpenUrlF])
 => m ()
openFromClipboard_effectsSpecialized = openFromClipboard

-- | @= 'openFromClipboard'@
openFromClipboard_monadSpecialized
  :: (ClipboardF  effects, OpenUrlF  effects)
  => Language effects ()
openFromClipboard_monadSpecialized = openFromClipboard
--old   :: ([ClipboardF, OpenUrlF] ⊆ effects)

-- | @= 'openFromClipboard'@
openFromClipboard_bothSpecialized
 :: ()
 => Language [ClipboardF, OpenUrlF] ()
openFromClipboard_bothSpecialized = openFromClipboard


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

-- | a constraint (with @-XConstraintKinds@).
type MonadWorkflow m effects =
   ( MonadClipboard m effects
   , MonadOpenUrl   m effects
   )

-- | a set of two effects.
type Workflow = '[ClipboardF,OpenUrlF]

{-| run an ad-hoc grouping of two effects.

@
runWorkflow = 'interpretLanguage' interpretWorkflow1
@

can run any action of type:

@('MonadWorkflow' m effects) => m a@

-}
runWorkflow :: Language Workflow :~> IO
runWorkflow = interpretLanguage interpretWorkflow1

{-old
runWorkflow :: Language Workflow a -> IO a
-}

{- | definition #1:  compose interpreters by appending vinyl records.

@
interpretWorkflow = 'appendInterpreters' 'interpretClipboard' 'interpretOpenUrl'
@

no new @Either@-like @data@types needed,
the @type@-aliases are only for clarity.

-}
interpretWorkflow1 :: Interpreter IO Workflow
interpretWorkflow1 = interpretClipboard `appendInterpreters` interpretOpenUrl

{- | definition #2: Construct an interpreter directly, via handlers.

@
'Interpreter'
  $ 'HandlerM' 'handleClipboard'
 :& 'HandlerM' 'handleOpenUrl'
 :& RNil
@

-}
interpretWorkflow2 :: Interpreter IO Workflow
interpretWorkflow2 = Interpreter
  $ HandlerM handleClipboard
 :& HandlerM handleOpenUrl
 :& RNil

{-| If we can handle an effect, plus some others;
then we can handle that effect, alone.

@
'interpretOpenUrl2' = 'downcastInterpreter' 'interpretWorkflow1'
@

This casts @\'['ClipboardF','OpenUrlF']@ down to @\'['OpenUrlF']@.

(For example, some library exports only a single interpreter
that handles five effects.
We can reconstruct an interpreter that handles only three
of those effects with a one-liner).

-}
interpretOpenUrl2 :: Interpreter IO OpenUrl
interpretOpenUrl2 = downcastInterpreter interpretWorkflow1

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

-- | the constraint
type MonadClipboard m effects =
  ( MonadLanguage m effects
  , ClipboardF  effects
  )

-- | the set of effects (one)
type Clipboard = '[ClipboardF]

-- | the functor
data ClipboardF k
 = GetClipboard (String -> k)
 | SetClipboard String k
 deriving Functor

-- | @getClipboard = 'liftL' $ 'GetClipboard' id@
getClipboard :: (MonadClipboard m effects) => m String
getClipboard = liftL $ GetClipboard id
   -- GetClipboard id :: ClipboardF String

-- | @setClipboard s = 'liftL' $ 'SetClipboard' s ()@
setClipboard :: (MonadClipboard m effects) => String -> m ()
setClipboard s = liftL $ SetClipboard s ()
  -- SetClipboard s () :: ClipboardF ()

-- | derived from the two primitves.
reverseClipboard :: (MonadClipboard m effects) => m ()
reverseClipboard = getClipboard >>= (reverse >>> setClipboard)

{- | calls 'interpretLanguage'.

when using free monads directly, you would:

@
runClipboard = 'iterTM' handleClipboard
@

-}
runClipboard :: Language '[ClipboardF] :~> IO
runClipboard = interpretLanguage interpretClipboard

{- | definition #1:
"inject" a  handler into an interpreter with 'singletonInterpreter'.

@
'singletonInterpreter' 'handleClipboard'
@

-}
interpretClipboard :: Interpreter IO '[ClipboardF]
interpretClipboard = singletonInterpreter handleClipboard

{- | definition #2:
 constructed and interpreted directly from single handler.

@
= 'Interpreter'
  $ 'HandlerM' 'handleClipboard'
  ':&' 'RNil'
@

-}
interpretClipboard2 :: Interpreter IO '[ClipboardF]
interpretClipboard2 = Interpreter
   $ HandlerM handleClipboard
  :& RNil

{- | glue the functor to its effects.

@
handleClipboard = \\case
  'GetClipboard' f   -> 'sh_GetClipboard' '>>=' f
  'SetClipboard' s k -> 'sh_SetClipboard' s '>>' k
@

-}
handleClipboard :: AnAlgebra ClipboardF (IO a)
handleClipboard = \case
  GetClipboard f -> sh_GetClipboard >>= f
  SetClipboard s k -> sh_SetClipboard s >> k

-- | shells out (@$ pbpaste@), works only on OSX.
sh_GetClipboard :: IO String
sh_GetClipboard = do
  -- TODO readProcess
  (_in, Just _out, _err, _process) <- createProcess  --NOTE safe
      (proc "pbpaste" [])
      { std_out = CreatePipe }
  out <- hGetContents _out
  let s = init out -- strip trailing \n  --TODO NOTE safe
  return s

-- | shells out (@$ ... | pbcopy@), works only on OSX. blocking.
sh_SetClipboard :: String -> IO ()
sh_SetClipboard s = do
  (_in, _out, _err, _process) <- createProcess $
      (shell $ "echo '"++s++"' | pbcopy") -- lol. TODO escape?
  _ <- waitForProcess _process
  return ()

--------------------------------------------------------------------------------
--
-- -- | the set of handlers (one)
-- type CoClipboard = '[CoClipboardF]
--
-- {- | the dual functor:
--
-- * the sum (@ data ... = ... | ...@) becomes
-- a product (@ data ... = ... { ..., ... }@)
-- * @(->)@ becomes @(,)@ and vice versa
--
-- because as 'ClipboardF' "produces" values,
-- so 'CoClipboardF' "consumes" them.
--
-- -}
-- data CoClipboardF k = CoClipboardF
--  { _getClipboard :: (String, k)  -- TODO {String} should be {m String}; unlike _setClipboard, {k ~ m ()} wont work
--  , _setClipboard :: String -> k
--  }
--  deriving Functor
--
-- -- pairClipboardT :: Pairing CoClipboardT ClipboardT
-- -- pairClipboardT = pairClipboardF
--
-- pairClipboardF :: Pairing CoClipboardF ClipboardF
-- pairClipboardF = Pairing go
--  where
--  go :: (a -> b -> r) -> (CoClipboardF a -> ClipboardF b -> r)
--  go p CoClipboardF{..} = \case
--    GetClipboard   f -> let (s,a) = _getClipboard
--                            b = f s
--                        in  p a b
--    SetClipboard s b -> let a = _setClipboard s
--                        in  p a b

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

-- | the constraint
type MonadOpenUrl m effects =
  ( MonadLanguage m effects
  , OpenUrlF  effects
  )

-- | the set of effects (one)
type OpenUrl = '[OpenUrlF]

-- | the functor
data OpenUrlF k -- TODO name OpenFile, works for any file
 = OpenUrl String k
 deriving Functor

-- | @openUrl s = 'liftL' $ 'OpenUrl' s ()@
openUrl :: (MonadOpenUrl m effects) => String -> m ()
openUrl s = liftL $ OpenUrl s ()
   -- OpenUrl s :: OpenUrlF ()

{- |

@
runOpenUrl = interpretLanguage interpretOpenUrl
@

-}
runOpenUrl :: Language '[OpenUrlF] :~> IO
runOpenUrl = interpretLanguage interpretOpenUrl

{- |

@
interpretOpenUrl = 'singletonInterpreter' $ \case
  'OpenUrl' s k -> 'sh_OpenUrl' s >> k
@

can extract the "co-algebra" with

@
handleOpenUrl = 'fromSingletonInterpreter' interpretOpenUrl
@

-}
interpretOpenUrl :: Interpreter IO '[OpenUrlF]
interpretOpenUrl = singletonInterpreter handleOpenUrl

{- | glue the functor to its effects.

@
handleOpenUrl = \\case
  'OpenUrl' s k -> 'sh_OpenUrl' s '>>' k
@

-}
handleOpenUrl :: AnAlgebra OpenUrlF (IO a)
handleOpenUrl = \case
  OpenUrl s k -> sh_OpenUrl s >> k

-- | shells out (@$ open ...@), should work cross-platform. blocking.
sh_OpenUrl :: String -> IO ()
sh_OpenUrl s = do
  (_in, _out, _err, _process) <- createProcess
      (proc "open" [s])
  _ <- waitForProcess _process
  return ()

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

-- | the constraint
type MonadReader r m effects =
  ( MonadLanguage m effects
  , ReaderF r  effects
  )

-- | the set of effects (one)
type Reader r = '[ReaderF r]

-- | the functor
data ReaderF r k
 = Ask (r -> k)
 deriving Functor

-- | @ask = 'liftL' $ 'Ask' id@
ask :: (MonadReader r m effects) => m r
ask = liftL $ Ask id

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

-- | the constraint
type MonadWriter w m effects =
  ( MonadLanguage m effects
  , WriterF w  effects
  )

-- | the set of effects (one)
type Writer w = '[WriterF w]

-- | the functor
data WriterF w k
 = Tell w k
 deriving Functor

-- | @tell w = 'liftL' $ 'Tell' w ()@
tell :: (MonadWriter w m effects) => w -> m ()
tell w = liftL $ Tell w ()

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

-- | the constraint
type MonadState s m effects =
  ( MonadLanguage m effects
  , StateF s  effects
  )

-- | the set of effects (one)
type State s = '[StateF s]

-- | the functor
data StateF s k
 = Get (s -> k)
 | Put s k
 deriving Functor

-- | @get = 'liftL' $ 'Get' id@
get :: (MonadState s m effects) => m s
get = liftL $ Get id

-- | @put s = 'liftL' $ 'Put' s ()@
put :: (MonadState s m effects) => s -> m ()
put s = liftL $ Put s ()

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

{-| since 'MonadLanguage is a class,
even though 'RWS' is a custom monad (not a 'Language'),
you can still provide an instance.

The instance declares that @RWS@ has three effects:
reading an environment, logging, and state access.
Which means that the functors ('ReaderF', 'WriterF', 'StateF')
can be injected into our concrete/custom monad stack.

Thus 'exampleRWS',
which is built with overloaded procedures
('ask','tell','get','put' are from this module, not the @mtl@ package),
can be specialized to both a 'Language':

@
-- 'exampleRWS_specializedLanguage'
exampleRWS
 :: (Num i, Show i)
 => Language [ReaderF Bool, WriterF [String], StateF i] i
@

and the more familiar 'RWS':

@
-- 'exampleRWS_specializedRWS'
exampleRWS
 :: (Num i, Show i)
 => RWS Bool [String] i i
@

To "lift the effect", we define a record of liftings
for **each** possible effect:

@
'liftRWS'
   = OpNaturalTransformation 'liftReaderF'
  :& OpNaturalTransformation 'liftWriterF'
  :& OpNaturalTransformation 'liftStateF'
  :& RNil

  -- (the 'OpNaturalTransformation' is boilerplate)
@

and then perform a record lookup for **the** particular effect
given at runtime:

@
instance ('Monoid' w) => 'MonadLanguage' (RWS r w s) ['ReaderF' r, 'WriterF' w, 'StateF' s] where
  'liftL' effect = 'getOpNaturalTransformation' ('rget' 'Proxy' 'liftRWS') effect
@

For example, when the @effect@ is @'ask'@:

@
liftL ask
=
liftL ask
=
getOpNaturalTransformation (rget Proxy liftRWS) ask
=
getOpNaturalTransformation (OpNaturalTransformation liftReaderF) ask
=
liftReaderF ask
=
liftReaderF (Ask id)
=
RWS $ ReaderT $ \r -> do
  return (id r)
=
RWS $ ReaderT $ \r -> do
  return r
=
RWS $ ReaderT return
@

-}
newtype RWS r w s a = RWS { getRWS ::
 ReaderT r (WriterT w (StateT s Identity)) a
 }
 deriving (Functor,Applicative,Monad)

-- |
runRWS :: (Monoid w) => r -> s -> RWS r w s a -> (a,w,s)
runRWS r s
    = getRWS
  >>> flip runReaderT r
  >>> runWriterT
  >>> flip runStateT s
  >>> runIdentity
  >>> (\((_a,_w),_s) -> (_a,_w,_s))

{-|
-}
instance (Monoid w) => MonadLanguage (RWS r w s) [ReaderF r, WriterF w, StateF s] where
  -- liftL :: (f ∈ effects, Functor f) => f a -> m a
  liftL = getOpNaturalTransformation (rget Proxy liftRWS)

{-old
liftL = getOpNaturalTransformation . rget Proxy liftRWS
liftL effect = getOpNaturalTransformation (rget effect liftRWS) effect
-}

{- |

-}
liftRWS
 :: (Monoid w)
 => Rec (OpNaturalTransformation (RWS r w s)) [ReaderF r, WriterF w, StateF s]
liftRWS
   = OpNaturalTransformation liftReaderF
  :& OpNaturalTransformation liftWriterF
  :& OpNaturalTransformation liftStateF
  :& RNil

-- |
liftReaderF :: (Monoid w) => ReaderF r a -> RWS r w s a
liftReaderF = \case
 Ask f -> RWS $ ReaderT $ \r -> do
   return (f r)
 -- ReaderT r m a = r -> m a

-- |
liftWriterF :: (Monoid w) => WriterF w a -> RWS r w s a
liftWriterF = \case
 Tell w k -> RWS $ ReaderT $ \_ -> WriterT $ do
   return (k,w)
 -- WriterT w m a = m (a, w)

-- |
liftStateF :: (Monoid w) => StateF s a -> RWS r w s a
liftStateF = \case
 Get f   -> RWS $ ReaderT $ \_ -> WriterT $ StateT $ \s -> do
   return ((f s, mempty), s) --TODO?
 Put s k -> RWS $ ReaderT $ \_ -> WriterT $ StateT $ \_ -> do
   return ((k,mempty), s)
 -- StateT s m a = s -> m (a,s)

{-NOTE

ReaderT r (WriterT w (StateT s Identity)) a
~
r -> WriterT w (StateT s Identity) a
~
r -> StateT s Identity (a,w)
~
r -> Identity ((a,w),s)
~
r -> ((a,w),s)

-}

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

-- | uses all four effectful operations ('ask','tell','get','put').
exampleRWS
 :: ( MonadReader Bool     m effects
    , MonadWriter [String] m effects
    , MonadState  i        m effects
    , Num i, Show i
    )
 => m i
exampleRWS = do
  b <- ask
  i <- get

  let f = if b then id else negate
  let j = f i
  let k = i + 1

  tell $ [show j]
  put k
  return j

-- | @= 'exampleRWS'@
exampleRWS_specializedLanguage
 :: (Num i, Show i)
 => Language [ReaderF Bool, WriterF [String], StateF i] i
exampleRWS_specializedLanguage = exampleRWS

-- | @= 'exampleRWS'@
exampleRWS_specializedRWS
 :: (Num i, Show i)
 => RWS Bool [String] i i
exampleRWS_specializedRWS = exampleRWS

exampleRWS_mono
 :: ( MonadReader Bool     m effects
    , MonadWriter [String] m effects
    , MonadState  Int      m effects
    )
 => m Int
exampleRWS_mono = exampleRWS

exampleRWS_monoTwice = do
  exampleRWS_mono
  exampleRWS_mono

exampleRWS_typeEquality
 :: ( MonadReader Bool     m effects
    , MonadWriter [String] m effects
    , MonadState  i        m effects
    , i ~ Int
    )
 => m i
exampleRWS_typeEquality = exampleRWS

exampleRWS_typeEqualityTwice = do
  exampleRWS_typeEquality
  exampleRWS_typeEquality

{-TODO no inference

exampleRWS_twice
 :: ( MonadReader Bool     m effects
    , MonadWriter [String] m effects
    , MonadState  i        m effects
    , Num i, Show i
    )
 => m i
exampleRWS_twice = do
  _ <- exampleRWS   <====== ambiguity
  exampleRWS

  Could not deduce (Num a0) arising from a use of ‘exampleRWS’
  from the context (MonadReader Bool m effects,
                    MonadWriter [String] m effects,
                    MonadState i m effects,
                    Num i,
                    Show i)

-}

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