{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}  -- GeneralizedNewtypeDeriving failed.
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE TypeFamilies #-}

#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
{-# LANGUAGE DeriveDataTypeable #-}
#endif

#if MIN_VERSION_transformers(0,5,0) || MIN_VERSION_base(4,9,0)
#define HAVE_FUNCTOR_CLASSES
#endif

-- |
-- Module:       $HEADER$
-- Description:  ApplyEndo provides easier endomorphism evaluation in cases
--               when there is an "obvious" default value.
-- Copyright:    (c) 2015-2016, Peter Trško
-- License:      BSD3
--
-- Maintainer:   peter.trsko@gmail.com
-- Stability:    experimental
-- Portability:  CPP, DeriveFoldable, DeriveFunctor, DeriveDataTypeable,
--               DeriveGeneric, DeriveTraversable, FlexibleInstances,
--               NoImplicitPrelude, TypeFamilies
--
-- 'ApplyEndo' provides easier endomorphism evaluation in cases when there is an
-- \"obvious\" default value.
module Data.Monoid.Endo.Apply
    (
    -- * ApplyEndo
      ApplyEndo(..)
    , apply
    , applyF

    -- ** ApplyEndo Mempty
    , Mempty
    , applyMempty
    , applyMempty_
    , joinApplyMempty

    -- ** ApplyEndo Def
    --
    -- $applyEndoDef
    , Def
    , applyDef
    , applyDef_
    , joinApplyDef

    -- ** ApplyEndo Reader
    , Reader
    , applyReader
    , applyReaderWith
    , joinApplyReader

    -- ** ApplyEndo Modify
    , Modify
    , applyModify
    , joinApplyModify

    -- ** ApplyEndo Modify'
    , Modify'
    , applyModify'
    , joinApplyModify'

    )
  where

import Prelude (seq)

import Control.Applicative (Applicative(pure))
import Control.Monad
    ( Monad((>>=))
#ifdef HAVE_APPLICATIVE_MONAD
    , void
#else
    , liftM
#endif
    )
import Data.Foldable (Foldable)
import Data.Function
    ( (.)
    , ($)
#ifndef HAVE_APPLICATIVE_MONAD
    , const
#endif
    )
import Data.Functor (Functor, (<$>))
#ifdef HAVE_FUNCTOR_CLASSES
import Data.Functor.Classes
    ( Eq1
    , Ord1
    , Read1(liftReadsPrec)
    , Show1(liftShowsPrec)
    , readsData
    , readsUnaryWith
    , showsUnaryWith
    )
#endif
import Data.Functor.Identity (Identity(runIdentity))
import Data.Monoid (Endo(Endo, appEndo), Monoid(mempty))
import Data.Traversable (Traversable)
import GHC.Generics (Generic, Generic1)

#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
import Data.Data (Data, Typeable)
#endif

import Control.Monad.Reader.Class (MonadReader)
import qualified Control.Monad.Reader.Class as MonadReader (asks)
import Control.Monad.State.Class (MonadState)
import qualified Control.Monad.State.Class as MonadState (state)

import Data.Default.Class (Default(def))

import Data.Monoid.Endo.FromEndo (FromEndo(..))


-- | There are cases when it is \"obvious\" what is the default value, which
-- should be modified by the endomorphism. This type is a result of such
-- endomorphism application and it uses phantom type @t@ as distinguishing
-- property, which decides what is the correct \"default value\".
newtype ApplyEndo t f a = ApplyEndo {applyEndo :: f a}
  deriving
    ( Applicative
    , Foldable
    , Functor
    , Generic
    , Generic1
    , Monad
    , Traversable
#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
    , Data
    , Typeable
#endif
#ifdef HAVE_FUNCTOR_CLASSES
    , Eq1
    , Ord1
#endif
    )

#ifdef HAVE_FUNCTOR_CLASSES
instance Read1 f => Read1 (ApplyEndo t f) where
    liftReadsPrec rp rl =
        readsData $ readsUnaryWith (liftReadsPrec rp rl) "ApplyEndo" ApplyEndo

instance Show1 f => Show1 (ApplyEndo t f) where
    liftShowsPrec sp sl d (ApplyEndo x) =
        showsUnaryWith (liftShowsPrec sp sl) "ApplyEndo" d x
#endif
    -- HAVE_FUNCTOR_CLASSES

-- | Apply endomorphism using provided \"default\" value.
apply :: Applicative f => a -> Endo a -> ApplyEndo t f a
apply defaultValue (Endo f) = ApplyEndo . pure $ f defaultValue
{-# INLINE apply #-}

-- | Similar as 'apply', but expects 'Endo' to be wrapped by a 'Functor'.
applyF :: Functor f => a -> f (Endo a) -> ApplyEndo t f a
applyF defaultValue endo = ApplyEndo $ (`appEndo` defaultValue) <$> endo

-- {{{ ApplyEndo Mempty -------------------------------------------------------

-- | Type tag identifying usage of 'mempty' from 'Monoid'.
data Mempty
  deriving
    ( Generic
#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
    , Typeable
#endif
    )

instance (Applicative f, Monoid a) => FromEndo (ApplyEndo Mempty f a) where
    type EndoOperatedOn (ApplyEndo Mempty f a) = a

    fromEndo = apply mempty

-- | Constrained version of 'applyEndo'. Usage example:
--
-- @
-- applyMempty . fromEndo :: ('Applicative' f, 'Monoid' a) => 'Endo' a -> f a
-- @
applyMempty :: Monoid a => ApplyEndo Mempty f a -> f a
applyMempty = applyEndo
{-# INLINE applyMempty #-}

-- | Same as 'applyMempty', but 'Applicative' functor is specialized to
-- 'Identity' functor and evaluated.
--
-- Examples:
--
-- >>> fromEndoWith applyMempty_ $ foldEndo (+1) [(*10), (+42)] :: Int
-- 421
-- >>> fromEndoWith applyMempty_ $ dualFoldEndo (+1) [(*10), (+42)] :: Int
-- 52
applyMempty_ :: Monoid a => ApplyEndo Mempty Identity a -> a
applyMempty_ = runIdentity . applyMempty
{-# INLINE applyMempty_ #-}

-- | Evaluates 'ApplyEndo' in a 'Monad' by joining it with the monad it
-- contains. It can be also viewed as a variant of 'applyMempty' defined as:
--
-- @
-- 'joinApplyMempty' = ('>>=' 'applyMempty')
-- @
joinApplyMempty
    ::  ( Monad m
        , Monoid a
#ifndef HAVE_APPLICATIVE_MONAD
        , Applicative m
#endif
        )
    => m (ApplyEndo Mempty m a) -> m a
joinApplyMempty = (>>= applyMempty)
{-# INLINE joinApplyMempty #-}

-- }}} ApplyEndo Mempty -------------------------------------------------------

-- {{{ ApplyEndo Def ----------------------------------------------------------

-- $applyEndoDef
--
-- Apply endomorphism to a default value 'def' from 'Default'. See also
-- following packages:
--
-- * <https://hackage.haskell.org/package/data-default-extra data-default-extra>
--
-- * <https://hackage.haskell.org/package/data-default data-default>
--
-- Both of those packages provide additional instances to 'Default' type
-- class.

-- | Type tag identifying usage of 'def' from 'Default'.
data Def
  deriving
    ( Generic
#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
    , Typeable
#endif
    )

instance (Applicative f, Default a) => FromEndo (ApplyEndo Def f a) where
    type EndoOperatedOn (ApplyEndo Def f a) = a

    fromEndo = apply def

-- | Constrained version of 'applyEndo'. Usage example:
--
-- @
-- applyDef . fromEndo :: ('Applicative' f, 'Default' a) => 'Endo' a -> f a
-- @
applyDef :: (Applicative f, Default a) => ApplyEndo Def f a -> f a
applyDef = applyEndo
{-# INLINE applyDef #-}

-- | Same as 'applyDef', but 'Applicative' functor is specialized to 'Identity'
-- functor and evaluated.
--
-- Examples:
--
-- >>> fromEndoWith applyDef_ $ foldEndo (+1) [(*10), (+42)] :: Int
-- 421
-- >>> fromEndoWith applyDef_ $ dualFoldEndo (+1) [(*10), (+42)] :: Int
-- 52
applyDef_ :: Default a => ApplyEndo Def Identity a -> a
applyDef_ = runIdentity . applyDef
{-# INLINE applyDef_ #-}

-- | Evaluates 'ApplyEndo' in a 'Monad' by joining it with the monad it
-- contains. It can be also viewed as a variant of 'applyDef' defined as:
--
-- @
-- 'joinApplyDef' = ('>>=' 'applyDef')
-- @
joinApplyDef
    ::  ( Monad m
        , Default a
#ifndef HAVE_APPLICATIVE_MONAD
        , Applicative m
#endif
        )
    => m (ApplyEndo Def m a) -> m a
joinApplyDef = (>>= applyDef)
{-# INLINE joinApplyDef #-}

-- }}} ApplyEndo Def ----------------------------------------------------------

-- {{{ ApplyEndo Reader -------------------------------------------------------

-- | Type tag identifying usage of 'MonadReader.asks' operation in 'FromEndo'
-- instance of 'ApplyEndo'.
data Reader
  deriving
    ( Generic
#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
    , Typeable
#endif
    )

-- | Evaluates 'ApplyEndo' in terms of 'MonadReader.asks' operation:
--
-- @
-- 'fromEndo' = 'ApplyEndo' . 'MonadReader.asks' . 'appEndo'
-- @
instance MonadReader r m => FromEndo (ApplyEndo Reader m r) where
    type EndoOperatedOn (ApplyEndo Reader m r) = r

    fromEndo = ApplyEndo . MonadReader.asks . appEndo

-- | Evaluates 'ApplyEndo' in terms of 'MonadReader.asks' operation.
--
-- This @(->) r@ is a valid 'MonadReader' instance, therefore, this is a valid
-- use case:
--
-- >>> (applyReader . fromEndo $ foldEndo (*10) (+1)) 0 :: Int
-- 10
applyReader :: MonadReader r m => ApplyEndo Reader m r -> m r
applyReader = applyEndo

-- | Evaluates 'ApplyEndo' in terms of 'MonadReader.asks' operation and then
-- evaluates the resalt using provided function.
--
-- This @(->) r@ is a valid 'MonadReader' instance, therefore, this is a valid
-- use case:
--
-- >>> applyReaderWith ($ 0) . fromEndo $ foldEndo (*10) (+1) :: Int
-- 10
applyReaderWith :: MonadReader r m => (m r -> a) -> ApplyEndo Reader m r -> a
applyReaderWith = (. applyEndo)

-- | Evaluates 'ApplyEndo' in a 'Monad' by joining it with the monad it
-- contains. It can be also viewed as a variant of 'applyReader' defined as:
--
-- @
-- 'joinApplyReader' = ('>>=' 'applyReader')
-- @
joinApplyReader :: MonadReader r m => m (ApplyEndo Reader m r) -> m r
joinApplyReader = (>>= applyEndo)

-- }}} ApplyEndo Reader -------------------------------------------------------

-- {{{ ApplyEndo Modify -------------------------------------------------------

-- | Type tag identifying usage of 'MonadState.state' operation in 'FromEndo'
-- instance of 'ApplyEndo'.
data Modify
  deriving
    ( Generic
#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
    , Typeable
#endif
    )

-- | Evaluates 'ApplyEndo' in terms of 'MonadState.state' operation:
--
-- @
-- 'fromEndo' e = 'ApplyEndo' . 'MonadState.state' '$' \\s ->
--     let s' = 'appEndo' e s in (s', s')
-- @
instance MonadState s m => FromEndo (ApplyEndo Modify m s) where
    type EndoOperatedOn (ApplyEndo Modify m s) = s

    fromEndo e =
        ApplyEndo . MonadState.state $ \s -> let s' = appEndo e s in (s', s')

-- | Evaluates 'ApplyEndo' in terms of 'MonadState.state' operation.
applyModify :: MonadState s m => ApplyEndo Modify m s -> m s
applyModify = applyEndo

-- | Evaluates 'ApplyEndo' in a 'Monad' by joining it with the monad it
-- contains. It can be also viewed as a variant of 'applyModify' defined as:
--
-- @
-- 'joinApplyModify' = ('>>=' 'applyModify')
-- @
joinApplyModify :: MonadState s m => m (ApplyEndo Modify m s) -> m s
joinApplyModify = (>>= applyEndo)

-- | Same as 'Modify', but strictness is implied.
data Modify'
  deriving
    ( Generic
#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
    , Typeable
#endif
    )

-- | Evaluates 'ApplyEndo' in terms of 'MonadState.state' operation:
--
-- @
-- 'fromEndo' ('Endo' f) = 'ApplyEndo' . 'MonadState.state' $ \\s ->
--     let s' = f s in s' \`seq\` (s', s')
-- @
instance MonadState s m => FromEndo (ApplyEndo Modify' m s) where
    type EndoOperatedOn (ApplyEndo Modify' m s) = s

    fromEndo (Endo f) =
        ApplyEndo . MonadState.state $ \s -> let s' = f s in s' `seq` (s', s')

-- | Evaluates 'ApplyEndo' in terms of 'MonadState.state' operation.
applyModify' :: MonadState r m => ApplyEndo Modify' m () -> m ()
applyModify' = void . applyEndo

-- | Evaluates 'ApplyEndo' in a 'Monad' by joining it with the monad it
-- contains. It can be also viewed as a variant of 'applyModify'' defined as:
--
-- @
-- 'joinApplyModify'' = ('>>=' 'applyModify'')
-- @
joinApplyModify' :: MonadState r m => m (ApplyEndo Modify' m r) -> m r
joinApplyModify' = (>>= applyEndo)

-- }}} ApplyEndo Modify -------------------------------------------------------

-- {{{ Helper functions (not exported) ----------------------------------------

#ifndef HAVE_APPLICATIVE_MONAD
void :: Monad m => m a -> m ()
void = liftM $ const ()
#endif

-- {{{ Helper functions (not exported) ----------------------------------------