{-# language CPP #-}
{-# language FlexibleInstances #-}
{-# language DeriveFunctor #-}
{-# language DeriveFoldable #-}
{-# language UndecidableInstances #-}
{-# language RankNTypes #-}
{-# language FunctionalDependencies #-}
{-# language StandaloneDeriving #-}
module Data.Functor.Selection
  ( -- * Selection
  Selection(..)
  -- ** Selecting/Deselecting
  -- | Most selection combinators require that both the selected and unselected types
  -- be equal (i.e. Selection f a a); this is necessary since items will switch
  -- their selection status. Your selected and unselected types may diverge, but
  -- you'll need to unify them in order to extract your underlying functor.
  , newSelection
  , forgetSelection
  , select
  , include
  , exclude
  , selectAll
  , deselectAll
  , invertSelection
  , mapSelected
  , mapUnselected
  , getSelected
  , getUnselected
  , unify
  , trans

  -- * Comonad Combinators
  , selectWithContext
  ) where

#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif
import Control.Monad (ap)
import Control.Comonad (Comonad(..))
import Data.Bifoldable (Bifoldable(..))
import Data.Bifunctor (Bifunctor(..))
import Data.Bitraversable (Bitraversable(..))


-- | You can make any type selectable if it contains a functor of (Either b a)
class Functor f => Selectable s f | s -> f where
  -- Modify the underlying representation of a selection
  modifySelection :: (f (Either b a) -> f (Either d c)) -> s b a -> s d c
  modifySelection f = wrapSelection . f . unwrapSelection

  -- | Lift a functor into the Selectable
  wrapSelection :: f (Either b a) -> s b a

  -- | Extract the underlying functor from the Selectable
  unwrapSelection :: s b a -> f (Either b a)

-- | The simplest selection type
newtype Selection f b a = Selection {
  -- | Expose the underlying representation of a Selection
  runSelection :: f (Either b a)
  } deriving (Functor, Foldable)

deriving instance (Show (f (Either b a))) => Show (Selection f b a)
deriving instance (Eq (f (Either b a))) => Eq (Selection f b a)

instance Monad m => Applicative (Selection m b) where
  pure = Selection . pure . Right
  (<*>) = ap

-- | Selection is a monad over selected items when the underlying m is a Monad
instance (Monad m) => Monad (Selection m b) where
  return = pure
  Selection m >>= k =
    Selection $ m >>= either (return . Left) (runSelection . k)

-- | Bifunctor over unselected ('first') and selected ('second') values
instance (Functor f) => Bifunctor (Selection f) where
  first f = Selection . fmap (first f) . runSelection
  second = fmap

-- | Bifoldable over unselected and selected values respectively
instance (Foldable f) => Bifoldable (Selection f) where
  bifoldMap l r = foldMap (bifoldMap l r) . runSelection

-- | Bitraversable over unselected and selected values respectively
instance (Traversable f) => Bitraversable (Selection f) where
  bitraverse l r = fmap Selection . traverse (bitraverse l r) . runSelection

instance (Functor f) => Selectable (Selection f) f where
  -- modifySelection f (Selection s) = Selection $ f s
  wrapSelection = Selection
  unwrapSelection = runSelection

-- | Create a selection from a functor by selecting all values
newSelection :: (Selectable s f) => f a -> s b a
newSelection = wrapSelection . fmap Right

-- | Drops selection from your functor returning all values (selected or not).
--
-- @'forgetSelection' . 'newSelection' = id@
--
-- @'forgetSelection' = 'unify' id id@
forgetSelection :: (Selectable s f) => s a a -> f a
forgetSelection = unify id id

-- | Clear the selection then select only items which match a predicate.
--
-- @'select' f = 'include' f . 'deselectAll'@
select :: (Selectable s f) => (a -> Bool) -> s a a -> s a a
select f = include f . deselectAll

-- | Add items which match a predicate to the current selection
--
-- @'include' f . 'select' g = 'select' (\a -> f a || g a)@
include :: (Selectable s f) => (a -> Bool) -> s a a -> s a a
include f = modifySelection (fmap (either (choose f) Right))

-- | Remove items which match a predicate to the current selection
--
-- @'exclude' f . 'select' g = 'select' (\a -> f a && not (g a))@
exclude :: (Selectable s f) => (a -> Bool) -> s a a -> s a a
exclude f = modifySelection (fmap (either Left (switch . choose f)))

-- | Select all items in the container
--
-- @'selectAll' = 'include' (const True)@
selectAll :: (Selectable s f) => s a a -> s a a
selectAll = include (const True)

-- | Deselect all items in the container
--
-- @'deselectAll' = 'exclude' (const True)@
deselectAll :: (Selectable s f) => s a a -> s a a
deselectAll = exclude (const True)

-- | Flip the selection, all selected are now unselected and vice versa.
invertSelection :: (Selectable s f) => s b a -> s a b
invertSelection = modifySelection (fmap switch)

-- | Map over selected values
--
-- @'onSelected' = fmap@
mapSelected :: (Selectable s f) => (a -> c) -> s b a -> s b c
mapSelected f = modifySelection (fmap (second f))

-- | Map over unselected values
--
-- @'onSelected' f = 'modifySelection' (fmap ('first' f))@
mapUnselected :: (Selectable s f) => (b -> c) -> s b a -> s c a
mapUnselected f = modifySelection (fmap (first f))

-- | Collect all selected values into a list. For more complex operations use
-- foldMap.
--
-- @'getSelected' = foldMap (:[])@
getSelected :: (Selectable s f, Foldable f) => s b a -> [a]
getSelected = foldMap (bifoldMap (const []) pure) . unwrapSelection

-- | Collect all unselected values into a list. For more complex operations use
-- operations from Bifoldable.
--
-- @'getUnselected' = 'getSelected' . 'invertSelection'@
getUnselected :: (Selectable s f, Foldable f) => s b a -> [b]
getUnselected = getSelected . invertSelection

-- | Unify selected and unselected and forget the selection
--
-- @'unify' f g == 'forgetSelection' . 'onUnselected' f . 'onSelected' g@
unify :: (Selectable s f) => (b -> c) -> (a -> c) -> s b a -> f c
unify l r = fmap (either l r) . unwrapSelection

-- | Perform a natural transformation over the underlying container of a selectable
trans :: (Selectable s f, Selectable t g) => (forall c. f c -> g c) -> s b a -> t b a
trans f = wrapSelection . f . unwrapSelection

-- Comonad combinators

-- | Select values based on their context within a comonad. This combinator makes
-- its selection by running the predicate using extend.
selectWithContext :: (Selectable s w, Comonad w) => (w a -> Bool) -> s a a -> s a a
selectWithContext f = modifySelection (extend (choose' extract f) .  fmap (either id id))


-- Helpers
choose' :: (a -> b) -> (a -> Bool) -> a -> Either b b
choose' f p a = if p a then Right (f a)
                      else Left (f a)

choose :: (a -> Bool) -> a -> Either a a
choose = choose' id

switch :: Either a b -> Either b a
switch = either Right Left