{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Data.Functor.Partitioner
  ( Partitioner (..),
    partitionList,
    part,
    part',

    -- * Working safely with 'Data.Map.Map's
    type WithKey,
    partitionMap,
    partWithKey,
    traversePartWithKey,
  )
where

import Data.Coerce
import Data.Map qualified as M
import Protolude

-- TODO: Profunctor
data Partitioner a b
  = forall m.
  (Monoid m) =>
  Partitioner
  { ingest :: a -> Maybe m,
    digest :: m -> b
  }

partitionList :: Partitioner a b -> [a] -> b
partitionList (Partitioner ing dig) = dig . fold . mapMaybe ing

part :: (a -> Maybe b) -> Partitioner a [b]
part f = Partitioner {ingest = fmap (: []) . f, digest = identity}

-- | Like 'part' but allows returning multiple entries in the result
part' :: (a -> Maybe [b]) -> Partitioner a [b]
part' f = Partitioner {ingest = f, digest = identity}

instance Functor (Partitioner a) where
  fmap f (Partitioner ing dig) = (Partitioner ing (f . dig))

instance Applicative (Partitioner a) where
  pure a =
    Partitioner {ingest = const (Nothing :: Maybe ()), digest = pure a}

  (Partitioner ingp digp) <*> (Partitioner ingq digq) =
    Partitioner
      { ingest = \a -> case ingp a of
          Just mp -> Just (mp, mempty)
          Nothing -> (mempty,) <$> ingq a,
        digest = \(mp, mq) -> digp mp (digq mq)
      }

-- instance Profunctor Partitioner
-- ...

-- | Encapsulates a 'Data.Map.Map''s key, in order to maintain the invariant all
-- keys and values are preserved when returning @Just@ from 'partWithKey'.
--
-- The problem with Maps is that if you change the keys in a non-injective way,
-- you may accidentally overwrite values. If you do need to change the keys,
-- this module can not guarantee the correctness of your code. You can munge a
-- Map in arbitrary ways, by using 'partitionList' after 'Data.Map.toList', but
-- make sure that your key mapping is injective, or use a newtype wrapper for
-- Map that uses the inner type @a@'s 'Semigroup' instance.
newtype WithKey k v = WithKey (k, v)

partWithKey ::
  (Ord k) =>
  (k -> v -> Maybe a) ->
  Partitioner (WithKey k v) (Map k a)
partWithKey f =
  Partitioner
    { ingest = \(WithKey (k, v)) -> M.singleton k <$> f k v,
      digest = identity
    }

traversePartWithKey ::
  (Ord k, Applicative f) =>
  (k -> v -> Maybe (f a)) ->
  Partitioner (WithKey k v) (f (Map k a))
traversePartWithKey f =
  Partitioner
    { ingest = \(WithKey (k, v)) -> do
        -- Maybe
        x <- f k v
        pure $ Ap $ (M.singleton k <$> x),
      digest = getAp
    }

-- | Use with 'partWithKey' to match on the key.
--
-- It uses 'WithKey' to maintain the invariant that all keys and values are
-- preserved when returning @Just@ from 'partWithKey'.
partitionMap :: Partitioner (WithKey k a) b -> M.Map k a -> b
partitionMap p = partitionList p . coerce . M.toAscList
