{-# language ViewPatterns #-}
{-# language LambdaCase #-}

-- | This library is based on the notion of a predicate transformer, the below
-- type @PT a b@, which is a function from @a@ to predicates on @b@.
-- They act as a sort of compositional "matcher language".
-- Composing these predicate transformers is meant to be analogous to composing optics
-- and there are utilities for using predicate transformers with (`lens`-style) optics.
--
-- Some predicate transformers provided by other libraries:
-- `Data.Foldable.all`, `Data.Foldable.any` (base)
-- `either` (base)
-- `Control.Lens.allOf` (lens)

module PredicateTransformers where

import Control.DeepSeq(NFData, force)
import Control.Exception(SomeException, evaluate, throwIO, try)
import Control.Lens hiding (index, zoom)
import Control.Monad.Writer(execWriter, tell)
import Data.Foldable(toList)
import Data.Functor.Rep(Representable(..))
import Data.Semigroup(All(..), Any(..))
import Debug.Trace
import System.IO.Unsafe

-- |A convenient alias for predicates.
type Pred a = a -> Bool

-- |Predicate transformers form a category where composition is ordinary function composition.
-- Forms a category with `.` and `id`.
-- Multiple are already provided by the standard library,
-- for instance `Data.Foldable.all` and `Data.Foldable.any`.
type PT a b = Pred a -> Pred b

-- |Operate on the target of a prism, or fail.
match :: APrism s t a b -> PT a s
match p pred = either (const False) pred . matching p

-- |Invert a predicate.
nay :: PT a a
nay = (not .)

-- |Operate on the `Just` branch of a `Maybe`, or fail.
just :: PT a (Maybe a)
just = match _Just

-- |Operate on the `Left` branch of an `Either`, or fail.
left :: PT e (Either e a)
left = match _Left

-- |Operate on the `Right` branch of an `Either`, or fail.
right :: PT a (Either e a)
right = match _Right

-- |Operate on the last value in a foldable, or fail if it's not present.
endingWith :: Foldable f => PT a (f a)
endingWith _ (toList -> []) = False
endingWith p (toList -> xs) = p $ last xs
{-# inlinable endingWith #-}

-- |Operate on the first value in a foldable, or fail if it's not present.
startingWith :: Foldable f => PT a (f a)
startingWith p (toList -> (x:_)) = p x
startingWith _ (toList -> []) = False
{-# inlinable startingWith #-}

-- |Require that a foldable has a single element, and operate on that element.
only :: Foldable f => PT a (f a)
only p (toList -> [x]) = p x
only _ _ = False
{-# inlinable only #-}

-- |Only test the @k@th element of a foldable.
kth :: Foldable f => Int -> PT a (f a)
kth k p = startingWith p . drop k . toList
{-# inlinable kth #-}

-- |Given a list of predicates and a list of values, ensure that each predicate holds for each respective value.
-- Fails if the two lists have different lengths.
list :: [Pred a] -> Pred [a]
list (p:ps) (x:xs) = p x && dist ps xs
list [] [] = True
list _ _ = False

-- |Given a functor-full of predicates, and a functor-full of values, ensure that the structures
-- of the two functors match and apply all of the predicates to all of the values.
-- Generalized version of `list`.
dist ::
    (Eq (f ()), Functor f, Foldable f) =>
    f (Pred a) -> Pred (f a)
dist preds values =
    (() <$ preds) == (() <$ values) &&
    list (toList preds) (toList values)
{-# inlinable dist #-}

-- |Given a representable functor-full of predicates, and a functor-full of values,
-- yield a representable functor-full of booleans. Similar to `dist`.
distRep :: Representable f =>
    f (a -> Bool) -> f a -> f Bool
distRep pr fa = tabulate (\r -> index pr r $ index fa r)
{-# inlinable distRep #-}

-- |Test all predicates against one value.
allTrue :: [Pred a] -> Pred a
allTrue ps a = all ($ a) ps

-- |Check that a predicate is true for all values behind a generalized getter
-- and that there's at least one value for which it's true.
allOf1 :: Getting (All, Any) s a -> PT a s
allOf1 g p (foldMapOf g (\x -> (All $ p x, Any $ p x)) -> (All a, Any y)) = a && y

-- |Sugar for tupling.
(==>) :: a -> b -> (a, b)
(==>) = (,)

pair :: Pred a -> Pred b -> Pred (a,b)
pair f s (a,b) = f a && s b

-- |Flipped function composition; @f !@ for a function @f@ is a predicate transformer.
(!) :: (b -> a) -> (a -> c) -> b -> c
(!) = flip (.)

-- |Prints the input of a predicate, for debugging.
traced :: Show a => (a -> c) -> a -> c
traced p a = traceShow a (p a)

-- |Prints the input of a predicate, if the predicate fails.
traceFail :: (a -> String) -> PT a a
traceFail s p a = unsafePerformIO $ do
  try (evaluate (p a)) >>= \case
    Left ex -> do
      traceIO (s a)
      throwIO (ex :: SomeException)
    Right True ->
      pure True
    Right False -> do
      traceIO ("\n" ++ s a)
      pure False

traceFailShow :: Show a => PT a a
traceFailShow = traceFail show

-- |Predicate which always succeeds.
something :: Pred a
something = const True

-- |Predicate which triggers full evaluation of its input.
-- Useful for testing that an exception isn't thrown.
forced :: NFData a => a -> Bool
forced a = force a `seq` True