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

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

#if !MIN_VERSION_base(4,9,0) && MIN_VERSION_transformers(0,5,0)
-- Definitions in Data.Functor.Classes from transformers >=0.5 are compatible
-- with those in base >=4.9, therefore we can enable them.
#define HAVE_FUNCTOR_CLASSES
#endif

-- |
-- Module:       $HEADER$
-- Description:  Conversion of values in to endomorphisms.
-- Copyright:    (c) 2014-2016, Peter Trško
-- License:      BSD3
--
-- Maintainer:   peter.trsko@gmail.com
-- Stability:    experimental
-- Portability:  CPP, DeriveDataTypeable, DeriveGeneric, FlexibleInstances,
--               NoImplicitPrelude, TypeFamilies
--
-- Conversion of values in to endomorphisms.
module Data.Monoid.Endo.AnEndo
    (
    -- * Conversion Into Endo
    --
    -- | Various types can be interpreted as an encoding of an endomorphism. In
    -- example, enum can be viewed as family of endomorphisms where each sets a
    -- specific field of a record to a specific enum value, i.e. data
    -- constructor. Type class 'AnEndo' provides generic way to convert values
    -- in to an endomorphism using 'anEndo' and 'aDualEndo' functions.
      AnEndo(..)

    -- ** WrappedFoldable
    --
    -- $wrappedFoldable
    , WrappedFoldable(..)

    -- * Utility Functions and Types
    , embedEndoWith
    , embedDualEndoWith
    )
  where

import Control.Applicative (Applicative)
import Control.Monad (Monad)
import Data.Foldable (Foldable(foldMap))
import Data.Function (($), (.), id)
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(Identity))
import Data.Maybe (Maybe(Just, Nothing))
import Data.Monoid
    ( Dual(Dual, getDual)
    , Endo(Endo)
    , Monoid(mempty, mconcat)
    , (<>)
    )
#ifdef HAVE_SEMIGROUPS
import Data.Semigroup (Option(Option))
#endif
import Data.Traversable (Traversable)
import GHC.Generics (Generic, Generic1)
import Text.Read (Read)
import Text.Show (Show)

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

#ifdef HAVE_PROXY
import Data.Proxy (Proxy(Proxy))
#endif

import Data.Functor.Reverse (Reverse)


-- {{{ AnEndo Type Class ------------------------------------------------------

-- | Class that represents various endomorphism representation. In other words
-- anything that encodes @(a -> a)@ can be instance of this class.
--
-- Here are some important instances with not so obvious definitions.
--
-- @
-- instance 'AnEndo' ('Proxy' a) where
--     type 'EndoOperatesOn' ('Proxy' a) = a
--
--     'anEndo'    _ = 'mempty' -- = Endo 'id'
--     'aDualEndo' _ = 'mempty'
-- @
--
-- It got quite common to use 'Proxy' data type as an explicit way to pass
-- types around. Above instance allows you to restrict type of result of
-- endomorphism folding, to some extent.
--
-- @
-- instance 'AnEndo' a => 'AnEndo' ('Maybe' a) where
--     type 'EndoOperatesOn' ('Maybe' a) = 'EndoOperatesOn' a
--
--     'anEndo' 'Nothing'  = 'mempty' -- = Endo 'id'
--     'anEndo' ('Just' e) = 'anEndo' e
--
--     -- Definition of 'aDualEndo' is analogous.
-- @
--
-- Instance for @Maybe@ lets us conditionally inject endomorphism in to a
-- folding chain.
--
-- @
-- instance 'AnEndo' a => 'AnEndo' ('Identity' a) where
--     type 'EndoOperatesOn' ('Identity' a) = 'EndoOperatesOn' a
--
--     'anEndo' ('Identity' e) = 'anEndo' e
--     'aDualEndo' ('Identity' e) = 'aDualEndo' e
-- @
--
-- Above instance allows us to discard 'Identity' wrapper, which is commonly
-- used in data types that are parametrized by functor or monad.
class AnEndo a where
    -- | Extract type on which endomorphism operates, e.g. for
    -- @('Endo' a)@ it would be @a@.
    type EndoOperatesOn a

    -- | Convert value encoding @(a -> a)@ in to 'Endo'. Default
    -- implementation:
    --
    -- @
    -- 'anEndo' = 'getDual' . 'aDualEndo'
    -- @
    anEndo :: a -> Endo (EndoOperatesOn a)
    anEndo = getDual . aDualEndo

    -- | Dual to 'anEndo'. Default implementation:
    --
    -- @
    -- 'aDualEndo' = 'Dual' . 'anEndo'
    -- @
    aDualEndo :: a -> Dual (Endo (EndoOperatesOn a))
    aDualEndo = Dual . anEndo

#if HAVE_MINIMAL_PRAGMA
    {-# MINIMAL anEndo | aDualEndo #-}
#endif

instance AnEndo (Endo a) where
    type EndoOperatesOn (Endo a) = a
    anEndo = id

instance AnEndo (a -> a) where
    type EndoOperatesOn (a -> a) = a
    anEndo = Endo

instance AnEndo a => AnEndo (Identity a) where
    type EndoOperatesOn (Identity a) = EndoOperatesOn a

    anEndo (Identity e) = anEndo e
    aDualEndo (Identity e) = aDualEndo e

instance AnEndo a => AnEndo (Maybe a) where
    type EndoOperatesOn (Maybe a) = EndoOperatesOn a

    anEndo Nothing  = mempty
    anEndo (Just e) = anEndo e

    aDualEndo Nothing  = mempty
    aDualEndo (Just e) = aDualEndo e

#ifdef HAVE_PROXY
-- | Constructs identity endomorphism for specified phantom type.
instance AnEndo (Proxy a) where
    type EndoOperatesOn (Proxy a) = a

    anEndo    Proxy = mempty
    aDualEndo Proxy = mempty
#endif

#ifdef HAVE_SEMIGROUPS
-- | Has same semantics as 'Maybe' and it is actually defined in terms of
-- 'AnEndo' instance for 'Maybe'.
instance AnEndo a => AnEndo (Option a) where
    type EndoOperatesOn (Option a) = EndoOperatesOn a

    anEndo (Option maybe) = anEndo maybe
    aDualEndo (Option maybe) = aDualEndo maybe
#endif

-- {{{ Foldable Instances -----------------------------------------------------

-- | Wrapper for 'Foldable' types. Used to provide instances that work for all
-- 'Foldable' types without the need for @OverlappingInstances@ language
-- extension.
newtype WrappedFoldable f a = WrapFoldable {getFoldable :: f a}
  deriving
    ( Applicative
    , Foldable
    , Functor
    , Generic
    , Generic1
    , Monad
    , Read
    , Show
    , Traversable
#ifdef HAVE_KIND_POLYMORPHIC_TYPEABLE
    , Data
    , Typeable
#endif
#ifdef HAVE_FUNCTOR_CLASSES
    , Eq1
    , Ord1
#endif
    )

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

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

instance (Foldable f, AnEndo a) => AnEndo (WrappedFoldable f a) where
    type EndoOperatesOn (WrappedFoldable f a) = EndoOperatesOn a
    anEndo    (WrapFoldable fa) = foldMap anEndo    fa
    aDualEndo (WrapFoldable fa) = foldMap aDualEndo fa

instance AnEndo a => AnEndo [a] where
    type EndoOperatesOn [a] = EndoOperatesOn a
    anEndo    = anEndo    . WrapFoldable
    aDualEndo = aDualEndo . WrapFoldable

-- {{{ Transformers -----------------------------------------------------------

-- | Fold in reverese order.
instance (Foldable f, AnEndo a) => AnEndo (Reverse f a) where
    type EndoOperatesOn (Reverse f a) = EndoOperatesOn a
    anEndo    = anEndo    . WrapFoldable
    aDualEndo = aDualEndo . WrapFoldable

-- }}} Transformers -----------------------------------------------------------

-- }}} Foldable Instances -----------------------------------------------------

-- {{{ Instances For Tuples ---------------------------------------------------

instance
    ( AnEndo a
    , AnEndo b
    , EndoOperatesOn a ~ EndoOperatesOn b
    ) => AnEndo (a, b)
  where
    type EndoOperatesOn (a, b) = EndoOperatesOn a
    anEndo    (a, b) = anEndo    a <> anEndo    b
    aDualEndo (a, b) = aDualEndo a <> aDualEndo b

instance
    ( AnEndo a
    , AnEndo b
    , AnEndo c
    , EndoOperatesOn a ~ EndoOperatesOn b
    , EndoOperatesOn a ~ EndoOperatesOn c
    ) => AnEndo (a, b, c)
  where
    type EndoOperatesOn (a, b, c) = EndoOperatesOn a
    anEndo    (a, b, c) = anEndo    a <> anEndo    b <> anEndo    c
    aDualEndo (a, b, c) = aDualEndo a <> aDualEndo b <> aDualEndo c

instance
    ( AnEndo a1
    , AnEndo a2
    , AnEndo a3
    , AnEndo a4
    , EndoOperatesOn a1 ~ EndoOperatesOn a2
    , EndoOperatesOn a1 ~ EndoOperatesOn a3
    , EndoOperatesOn a1 ~ EndoOperatesOn a4
    ) => AnEndo (a1, a2, a3, a4)
  where
    type EndoOperatesOn (a1, a2, a3, a4) = EndoOperatesOn a1
    anEndo (a1, a2, a3, a4) = mconcat
        [ anEndo a1
        , anEndo a2
        , anEndo a3
        , anEndo a4
        ]

    aDualEndo (a1, a2, a3, a4) = mconcat
        [ aDualEndo a1
        , aDualEndo a2
        , aDualEndo a3
        , aDualEndo a4
        ]

instance
    ( AnEndo a1
    , AnEndo a2
    , AnEndo a3
    , AnEndo a4
    , AnEndo a5
    , EndoOperatesOn a1 ~ EndoOperatesOn a2
    , EndoOperatesOn a1 ~ EndoOperatesOn a3
    , EndoOperatesOn a1 ~ EndoOperatesOn a4
    , EndoOperatesOn a1 ~ EndoOperatesOn a5
    ) => AnEndo (a1, a2, a3, a4, a5)
  where
    type EndoOperatesOn (a1, a2, a3, a4, a5) = EndoOperatesOn a1

    anEndo (a1, a2, a3, a4, a5) = mconcat
        [ anEndo a1
        , anEndo a2
        , anEndo a3
        , anEndo a4
        , anEndo a5
        ]

    aDualEndo (a1, a2, a3, a4, a5) = mconcat
        [ aDualEndo a1
        , aDualEndo a2
        , aDualEndo a3
        , aDualEndo a4
        , aDualEndo a5
        ]

instance
    ( AnEndo a1
    , AnEndo a2
    , AnEndo a3
    , AnEndo a4
    , AnEndo a5
    , AnEndo a6
    , EndoOperatesOn a1 ~ EndoOperatesOn a2
    , EndoOperatesOn a1 ~ EndoOperatesOn a3
    , EndoOperatesOn a1 ~ EndoOperatesOn a4
    , EndoOperatesOn a1 ~ EndoOperatesOn a5
    , EndoOperatesOn a1 ~ EndoOperatesOn a6
    ) => AnEndo (a1, a2, a3, a4, a5, a6)
  where
    type EndoOperatesOn (a1, a2, a3, a4, a5, a6) = EndoOperatesOn a1

    anEndo (a1, a2, a3, a4, a5, a6) = mconcat
        [ anEndo a1
        , anEndo a2
        , anEndo a3
        , anEndo a4
        , anEndo a5
        , anEndo a6
        ]

    aDualEndo (a1, a2, a3, a4, a5, a6) = mconcat
        [ aDualEndo a1
        , aDualEndo a2
        , aDualEndo a3
        , aDualEndo a4
        , aDualEndo a5
        , aDualEndo a6
        ]

instance
    ( AnEndo a1
    , AnEndo a2
    , AnEndo a3
    , AnEndo a4
    , AnEndo a5
    , AnEndo a6
    , AnEndo a7
    , EndoOperatesOn a1 ~ EndoOperatesOn a2
    , EndoOperatesOn a1 ~ EndoOperatesOn a3
    , EndoOperatesOn a1 ~ EndoOperatesOn a4
    , EndoOperatesOn a1 ~ EndoOperatesOn a5
    , EndoOperatesOn a1 ~ EndoOperatesOn a6
    , EndoOperatesOn a1 ~ EndoOperatesOn a7
    ) => AnEndo (a1, a2, a3, a4, a5, a6, a7)
  where
    type EndoOperatesOn (a1, a2, a3, a4, a5, a6, a7) = EndoOperatesOn a1

    anEndo (a1, a2, a3, a4, a5, a6, a7) = mconcat
        [ anEndo a1
        , anEndo a2
        , anEndo a3
        , anEndo a4
        , anEndo a5
        , anEndo a6
        , anEndo a7
        ]

    aDualEndo (a1, a2, a3, a4, a5, a6, a7) = mconcat
        [ aDualEndo a1
        , aDualEndo a2
        , aDualEndo a3
        , aDualEndo a4
        , aDualEndo a5
        , aDualEndo a6
        , aDualEndo a7
        ]

instance
    ( AnEndo a1
    , AnEndo a2
    , AnEndo a3
    , AnEndo a4
    , AnEndo a5
    , AnEndo a6
    , AnEndo a7
    , AnEndo a8
    , EndoOperatesOn a1 ~ EndoOperatesOn a2
    , EndoOperatesOn a1 ~ EndoOperatesOn a3
    , EndoOperatesOn a1 ~ EndoOperatesOn a4
    , EndoOperatesOn a1 ~ EndoOperatesOn a5
    , EndoOperatesOn a1 ~ EndoOperatesOn a6
    , EndoOperatesOn a1 ~ EndoOperatesOn a7
    , EndoOperatesOn a1 ~ EndoOperatesOn a8
    ) => AnEndo (a1, a2, a3, a4, a5, a6, a7, a8)
  where
    type EndoOperatesOn (a1, a2, a3, a4, a5, a6, a7, a8) = EndoOperatesOn a1

    anEndo (a1, a2, a3, a4, a5, a6, a7, a8) = mconcat
        [ anEndo a1
        , anEndo a2
        , anEndo a3
        , anEndo a4
        , anEndo a5
        , anEndo a6
        , anEndo a7
        , anEndo a8
        ]

    aDualEndo (a1, a2, a3, a4, a5, a6, a7, a8) = mconcat
        [ aDualEndo a1
        , aDualEndo a2
        , aDualEndo a3
        , aDualEndo a4
        , aDualEndo a5
        , aDualEndo a6
        , aDualEndo a7
        , aDualEndo a8
        ]

instance
    ( AnEndo a1
    , AnEndo a2
    , AnEndo a3
    , AnEndo a4
    , AnEndo a5
    , AnEndo a6
    , AnEndo a7
    , AnEndo a8
    , AnEndo a9
    , EndoOperatesOn a1 ~ EndoOperatesOn a2
    , EndoOperatesOn a1 ~ EndoOperatesOn a3
    , EndoOperatesOn a1 ~ EndoOperatesOn a4
    , EndoOperatesOn a1 ~ EndoOperatesOn a5
    , EndoOperatesOn a1 ~ EndoOperatesOn a6
    , EndoOperatesOn a1 ~ EndoOperatesOn a7
    , EndoOperatesOn a1 ~ EndoOperatesOn a8
    , EndoOperatesOn a1 ~ EndoOperatesOn a9
    ) => AnEndo (a1, a2, a3, a4, a5, a6, a7, a8, a9)
  where
    type EndoOperatesOn (a1, a2, a3, a4, a5, a6, a7, a8, a9) = EndoOperatesOn a1

    anEndo (a1, a2, a3, a4, a5, a6, a7, a8, a9) = mconcat
        [ anEndo a1
        , anEndo a2
        , anEndo a3
        , anEndo a4
        , anEndo a5
        , anEndo a6
        , anEndo a7
        , anEndo a8
        , anEndo a9
        ]

    aDualEndo (a1, a2, a3, a4, a5, a6, a7, a8, a9) = mconcat
        [ aDualEndo a1
        , aDualEndo a2
        , aDualEndo a3
        , aDualEndo a4
        , aDualEndo a5
        , aDualEndo a6
        , aDualEndo a7
        , aDualEndo a8
        , aDualEndo a9
        ]

instance
    ( AnEndo a1
    , AnEndo a2
    , AnEndo a3
    , AnEndo a4
    , AnEndo a5
    , AnEndo a6
    , AnEndo a7
    , AnEndo a8
    , AnEndo a9
    , AnEndo a10
    , EndoOperatesOn a1 ~ EndoOperatesOn a2
    , EndoOperatesOn a1 ~ EndoOperatesOn a3
    , EndoOperatesOn a1 ~ EndoOperatesOn a4
    , EndoOperatesOn a1 ~ EndoOperatesOn a5
    , EndoOperatesOn a1 ~ EndoOperatesOn a6
    , EndoOperatesOn a1 ~ EndoOperatesOn a7
    , EndoOperatesOn a1 ~ EndoOperatesOn a8
    , EndoOperatesOn a1 ~ EndoOperatesOn a9
    , EndoOperatesOn a1 ~ EndoOperatesOn a10
    ) => AnEndo (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)
  where
    type EndoOperatesOn (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) = EndoOperatesOn a1

    anEndo (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) = mconcat
        [ anEndo a1
        , anEndo a2
        , anEndo a3
        , anEndo a4
        , anEndo a5
        , anEndo a6
        , anEndo a7
        , anEndo a8
        , anEndo a9
        , anEndo a10
        ]

    aDualEndo (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) = mconcat
        [ aDualEndo a1
        , aDualEndo a2
        , aDualEndo a3
        , aDualEndo a4
        , aDualEndo a5
        , aDualEndo a6
        , aDualEndo a7
        , aDualEndo a8
        , aDualEndo a9
        , aDualEndo a10
        ]

-- }}} Instances For Tuples ---------------------------------------------------
-- }}} AnEndo Type Class ------------------------------------------------------

-- {{{ Utility Functions and Types --------------------------------------------

-- | Use 'Endo' (possibly result of 'Data.Endo.Fold.foldEndo') and use it to
-- create value of different type.
--
-- Examples:
--
-- @
-- 'embedEndoWith' 'Control.Monad.Trans.Writer.Lazy.tell'
--     :: (Monad m, 'AnEndo' e, w ~ 'EndoOperatesOn' e)
--     => e
--     -> 'Control.Monad.Trans.Writer.Lazy.WriterT' ('Endo' w) m ()
--
-- 'embedEndoWith' ('Control.Monad.Trans.State.Lazy.modify' . 'Data.Monoid.appEndo')
--     :: (Monad m, 'AnEndo' e, s ~ 'EndoOperatesOn' e)
--     => e
--     -> 'Control.Monad.Trans.State.Lazy.StateT' s m ()
-- @
--
-- See also 'embedDualEndoWith'.
embedEndoWith :: (AnEndo e, EndoOperatesOn e ~ a)
    => (Endo a -> b)
    -- ^ Embedding function.
    -> e -> b
embedEndoWith = (. anEndo)

-- | Dual to 'embedEndoWith', which uses 'aDualEndo' instead of 'anEndo'.
embedDualEndoWith
    :: (AnEndo e, EndoOperatesOn e ~ a)
    => (Dual (Endo a) -> b)
    -- ^ Embedding function.
    -> e -> b
embedDualEndoWith = (. aDualEndo)

-- }}} Utility Functions and Types --------------------------------------------

-- $wrappedFoldable
--
-- Newtype 'WrappedFoldable' allows us to use 'anEndo', 'aDualEndo',
-- 'Data.Endo.Fold.foldEndo', and 'Data.Endo.Fold.dualFoldEndo' for any
-- 'Foldable' instance without the need to create specific instance for that
-- specific 'Foldable' type and reduces. It would be possible to create
-- 'AnEndo' instance for all 'Foldable' types, but that would require
-- @OverlappingInstances@ language extension.
--
-- Usage examples:
--
-- @
-- \\vectorOfEndos -> 'anEndo' ('WrappedFoldable' vectorOfEndos)
--     :: Vector ('Data.Monoid.Endo.E' a) -> 'Endo' a
-- @
--
-- @
-- \\vectorOfEndos -> 'Data.Monoid.Endo.Fold.foldEndo' ('WrappedFoldable' vectorOfEndos)
--     :: 'Data.Monoid.Endo.Fold.FoldEndoArgs' => Vector ('Data.Monoid.Endo.E' a) -> args
-- @
--
-- Note that the @Vector@ is just one of possible 'Foldable' data types that
-- may be used here. Also, @('Data.Monoid.Endo.E' a)@ is just an example of
-- endomorphism representation, any 'AnEndo' instance can be used.