{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}

-- | This module contains some utility functions to work with different levels
-- of Pandoc filters. For example, for the conversion from @'Inline' ->
-- ['Inline']@ to @'Pandoc' -> 'Pandoc'@ filter.
--
-- If you don't need to compose filters and only want to convert between Pandoc
-- filter functions, just use 'convertFilter' and 'convertFilterM'.
--
-- However, if you are working with multiple Pandoc filters of different type
-- and want to compose them, this module also provides a monoid wrapper type
-- 'PartialFilterM', which I call a "wrapped filter", and a few functions to
-- apply, compose, and convert them.
module Text.Pandoc.Filter.Utils (
  -- * Filter function conversion
  convertFilter,
  convertFilterM,
  -- * Wrapped filter definitions
  PartialFilter,
  PandocFilter,
  PartialFilterM,
  PandocFilterM,
  -- * Filter function → Wrapped filter
  ToPartialFilter (..),
  mkConcatedFilter,
  -- * Wrapped filter application/composition
  applyFilter,
  applyFilters,
  applyFilterM,
  applyFiltersM,
  -- * Wrapped filter → filter function
  getFilter,
  getConcatedFilter,
  getFilterM,
  getConcatedFilterM,
  -- * Wrapped filter conversion
  toFilterM,
  ) where

import Control.Monad          ((>=>))
import Data.Foldable          (fold)
import Data.Functor.Identity  (Identity (..))
import Text.Pandoc.Definition
import Text.Pandoc.Walk

-- | @PartialFilterM m p@ is a wrapper for any monadic @p -> m p@ Pandoc
-- filters acting on a subnode (e.g. 'Inline' or 'Block') of the 'Pandoc'
-- abstract syntax tree. On this page, we will call it a "wrapped" filter to
-- distinguish it from filter functions @a -> m b@.
--
-- * @m@: a monad.
-- * @p@: the type of a subnode of 'Pandoc' (e.g. 'Inline').
newtype PartialFilterM m p =
  PartialFilterM
    { -- | Apply a monadic filter to @p@.
      applyFilterM :: p -> m p
    }

-- | @PartialFilter p@ is a wrapper for ordinary @p -> p@ Pandoc filters acting
-- on a subnode (e.g. 'Inline' or 'Block') of the 'Pandoc' abstract syntax
-- tree. On this page, we will call it a "wrapped" filter to distinguish it
-- from filter functions @a -> b@.
--
-- * @p@: the type of a subnode of 'Pandoc' (e.g. 'Inline').
type PartialFilter = PartialFilterM Identity

-- | An alias for @PartialFilter Pandoc@. It encapsulates a monadic
-- filter @'Pandoc' -> m 'Pandoc'@ acting directly on 'Pandoc'.
--
-- * @m@: a monad.
type PandocFilter = PartialFilter Pandoc

-- | An alias for @PartialFilterM m Pandoc@, a monadic version of
-- 'PandocFilter'.
--
-- * @m@: a monad.
type PandocFilterM m = PartialFilterM m Pandoc

-- | Apply a wrapped filter to @p@, which returns @p@ directly.
applyFilter
  :: PartialFilter p -- ^ A wrapped filter.
  -> p               -- ^ 'Pandoc' AST node.
  -> p               -- ^ Transformed node.
applyFilter = (runIdentity .) . applyFilterM

-- | It is mostly the same as 'applyFilterM', which converts a wrapped monadic
-- filter to a monadic filter function, but it should be used when you don't
-- need to apply the filter immediately. There is a slight difference in that
-- it will perform an implicit conversion if the requested filter function is
-- of a different type.
--
-- For example, it can be used to convert a wrapped monadic filter
-- @'PartialFilterM' 'IO' 'Inline'@ to monadic filter function @'Block' -> 'IO'
-- 'Block'@.
getFilterM
  :: (Monad m, Walkable a b)
  => PartialFilterM m a -- ^ A wrapped filter on @a@.
  -> (b -> m b)         -- ^ Filter function that can be directly applied to @b@.
getFilterM = applyFilterM . mkFilter

-- | It is mostly the same as 'applyFilter', which converts a wrapped filter to
-- a filter function, but it should be used when you don't need to apply the
-- filter immediately. There is a slight difference in that it will perform an
-- implicit conversion if the requested filter function is of a different type.
--
-- For example, it can be used to convert a wrapped filter @'PartialFilter'
-- 'Inline'@ to filter function @'Block' -> 'Block'@.
getFilter
  :: (Walkable a b)
  => PartialFilter a -- ^ A wrapped partial filter on @a@.
  -> (b -> b)        -- ^ Filter function that can be directly applied to @b@.
getFilter = applyFilter . mkFilter

-- | The 'Semigroup' instance of `PartialFilterM`. @f1 <> f2@ will apply @f1@
-- first followed by @f2@.
instance (Monad m) => Semigroup (PartialFilterM m p) where
  f1 <> f2 = PartialFilterM (applyFilterM f1 >=> applyFilterM f2)

-- | The 'Monoid' instance of `PartialFilterM`.
instance (Monad m) => Monoid (PartialFilterM m p) where
  mempty = PartialFilterM return

-- | A helper typeclass used as a polymorphic constructor of 'PartialFilterM'.
class ToPartialFilter m f p where
  -- | The actual constructor of 'PartialFilterM'. It takes an ordinary filter
  -- function @a -> b@ and wraps it as a wrapped filter 'PartialFilterM'. It
  -- can also be used to convert between different types of @'PartialFilterM'
  -- m@.
  mkFilter
    :: f                  -- ^ A filter function, usually @a -> a@ for some @'Walkable' a p@.
    -> PartialFilterM m p -- ^ Wrapped Pandoc filter.

instance (Monad m, Walkable a p) => ToPartialFilter m (a -> a) p where
  mkFilter = PartialFilterM . (return .) . walk

instance (Monad m, Walkable a p) => ToPartialFilter m (a -> m a) p where
  mkFilter = PartialFilterM . walkM

instance (Monad m, Walkable [a] p) => ToPartialFilter m (a -> [a]) p where
  mkFilter = PartialFilterM . (return .) . walk . concatMap

instance (Monad m, Walkable [a] p) => ToPartialFilter m (a -> m [a]) p where
  mkFilter = PartialFilterM . walkM . (fmap concat .) . mapM

-- | This instance can be used to convert @'PartialFilterM' m a@ to
-- @'PartialFilterM' m b@.
instance (Monad m, Walkable a b) => ToPartialFilter m (PartialFilterM m a) b where
  mkFilter = mkFilter . applyFilterM

-- | Construct a wrapped filter 'PartialFilterM' from a list of filter
-- functions of the same type. The final filter is concatenated from left to
-- right such that the first element in the list will be applied first and the
-- last element will be applied at the end.
--
-- For example, it can be used to convert an list of filter functions
-- @['Inline' -> ['Inline']]@ to a wrapped filter @'PandocFilter'@.
mkConcatedFilter
  :: (Monad m, ToPartialFilter m f p, Foldable t)
  => t f                -- ^ A list of filter functions of the same type.
  -> PartialFilterM m p -- ^ Concatenated filter.
mkConcatedFilter = foldMap mkFilter

-- | Convert an ordinary wrapped filter 'PartialFilter' to the monadic version
-- 'PartialFilterM'.
--
-- For example, it can be used to convert an ordinary wrapped filter
-- @'PartialFilter' 'Inline'@ to monadic wrapped filter @'PartialFilterM' 'IO'
-- 'Inline'@.
toFilterM
  :: (Monad m)
  => PartialFilter p    -- ^ An ordinary filter.
  -> PartialFilterM m p -- ^ The monadic version.
toFilterM = PartialFilterM . (return .) . applyFilter

-- | Apply a list of monadic wrapped filters sequentially, from left to right,
-- i.e. the first element in the list will be applied first and the last
-- element will be applied at the end.
applyFiltersM
  :: (Foldable t, Monad m)
  => t (PartialFilterM m p) -- ^ A list of monadic wrapped filters.
  -> p                      -- ^ 'Pandoc' AST node.
  -> m p                    -- ^ Transformed node.
applyFiltersM = applyFilterM . fold

-- | Apply a list of wrapped filters sequentially, from left to right, i.e.
-- the first element in the list will be applied first and the last element
-- will be applied at the end.
applyFilters
  :: (Foldable t)
  => t (PartialFilter p) -- ^ A list of wrapped filter.
  -> p                   -- ^ 'Pandoc' AST node.
  -> p                   -- ^ Transformed node.
applyFilters = applyFilter . fold

-- | It is mostly the same as 'applyFiltersM', which converts a list of wrapped
-- monadic filter to a monadic filter function, but it should be used when you
-- don't need to apply the filter immediately. There is a slight difference in
-- that it will perform an implicit conversion if the requested filter function
-- is of a different type.
--
-- For example, it can be used to convert a list of wrapped monadic filter
-- @['PartialFilterM' 'IO' 'Inline']@ to a filter function @'Block' -> 'IO'
-- 'Block'@.
getConcatedFilterM
  :: (Foldable t, Monad m, Walkable a b)
  => t (PartialFilterM m a) -- ^ A list of monadic partial filters on @a@.
  -> (b -> m b)             -- ^ Monadic filter function applicable to @b@ directly.
getConcatedFilterM = applyFilterM . mkConcatedFilter

-- | It is mostly the same as 'applyFilters', which converts a list of wrapped
-- filter to a filter function, but it should be used when you don't need to
-- apply the filter immediately. There is a slight difference in that it will
-- perform an implicit conversion if the requested filter function is of a
-- different type.
--
-- For example, it can be used to convert a list of wrapped filter
-- @['PartialFilter' 'Inline']@ to a filter function @'Block' -> 'Block'@.
getConcatedFilter
  :: (Foldable t, Walkable a b)
  => t (PartialFilter a) -- ^ A list of wrapped filter acting on @a@
  -> (b -> b)            -- ^ Filter function applicable to @b@ directly.
getConcatedFilter = applyFilter . mkConcatedFilter

-- | Conversion between monadic filter functions, e.g. from @'Inline' ->
-- 'IO' ['Inline']@ filter to @'Pandoc' -> 'IO' 'Pandoc'@ filter.
convertFilterM
  :: (Monad m, ToPartialFilter m f p)
  => f                -- ^ A monadic filter function.
  -> (p -> m p)       -- ^ Monadic filter function acting on @p@.
convertFilterM = applyFilterM . mkFilter

-- | Conversion between filter functions, e.g. from @'Inline' -> ['Inline']@
-- filter to @'Pandoc' -> 'Pandoc'@ filter.
convertFilter
  :: (ToPartialFilter Identity f p)
  => f        -- ^ A filter function.
  -> (p -> p) -- ^ Filter function acting on @p@.
convertFilter = applyFilter . mkFilter