{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

#if MTL
{-# OPTIONS_GHC -fno-warn-orphans #-}
#endif

module Control.Effect.Writer (
    EffectWriter, Writer, runWriter,
    tell, listen, listens, pass, censor,
    stateWriter
) where

import Control.Monad.Effect
import Control.Arrow (second)
#if !MIN_VERSION_base(4, 8, 0)
import Data.Monoid (Monoid (..))
#endif
import Control.Effect.State

#ifdef MTL
import Data.Type.Row
import qualified Control.Monad.Writer.Class as W

instance (Monoid w, Member (Writer w) l, Writer w ~ InstanceOf Writer l) => W.MonadWriter w (Effect l) where
    tell = tell
    listen = listen
    pass = pass
#endif

-- | An effect that allows accumulating output.
data Writer w a where
    Tell :: w -> Writer w ()

type instance Is Writer f = IsWriter f

type family IsWriter f where
    IsWriter (Writer w) = 'True
    IsWriter f = 'False

class (Monoid w, MemberEffect Writer (Writer w) l) => EffectWriter w l
instance (Monoid w, MemberEffect Writer (Writer w) l) => EffectWriter w l

-- | Writes a value to the output.
tell :: EffectWriter w l => w -> Effect l ()
tell = send . Tell

-- | Executes a computation, and obtains the writer output.
-- The writer output of the inner computation is still
-- written to the writer output of the outer computation.
listen :: EffectWriter w l => Effect l a -> Effect l (a, w)
listen effect = do
    value@(_, output) <- intercept point bind effect
    tell output
    return value

-- | Like `listen`, but the writer output is run through a function.
listens :: EffectWriter w l => (w -> b) -> Effect l a -> Effect l (a, b)
listens f = fmap (second f) . listen

-- | Runs a computation that returns a value and a function,
-- applies the function to the writer output, and then returns the value.
pass :: EffectWriter w l => Effect l (a, w -> w) -> Effect l a
pass effect = do
    ((x, f), l) <- listen effect
    tell (f l)
    return x

-- | Applies a function to the writer output of a computation.
censor :: EffectWriter w l => (w -> w) -> Effect l a -> Effect l a
censor f effect = pass $ do
    a <- effect
    return (a, f)

-- | Executes a writer computation which sends its output to a state effect.
stateWriter :: (Monoid s, EffectState s l) => Effect (Writer s ':+ l) a -> Effect l a
stateWriter = eliminate return (\(Tell l) k -> modify (mappend l) >> k ())

-- | Completely handles a writer effect. The writer value must be a `Monoid`.
-- `mempty` is used as an initial value, and `mappend` is used to combine values.
-- Returns the result of the computation and the final output value.
runWriter :: Monoid w => Effect (Writer w ':+ l) a -> Effect l (a, w)
runWriter = eliminate point bind

point :: Monoid w => a -> Effect l (a, w)
point x = return (x, mempty)

bind :: Monoid w => Writer w a -> (a -> Effect l (b, w)) -> Effect l (b, w)
bind (Tell l) k = second (mappend l) `fmap` k ()
