{-# LANGUAGE OverloadedStrings #-}
module Prednote.Comparisons
  ( -- * Comparisions that do not run in a context
    compareBy
  , compare
  , equalBy
  , equal
  , compareByMaybe
  , greater
  , less
  , greaterEq
  , lessEq
  , notEq
  , greaterBy
  , lessBy
  , greaterEqBy
  , lessEqBy
  , notEqBy

  -- * Comparisions that run in a context
  , compareByM
  , equalByM
  , compareByMaybeM
  , greaterByM
  , lessByM
  , greaterEqByM
  , lessEqByM
  , notEqByM

  -- * Parsing comparers
  , parseComparer
  ) where

import Prednote.Core
import Prelude hiding (compare, not)
import qualified Prelude
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as X
import Rainbow

-- | Build a Pred that compares items.  The idea is that the item on
-- the right hand side is baked into the 'Pred' and that the 'Pred'
-- compares this single right-hand side to each left-hand side item.
compareByM
  :: (Show a, Functor f)
  => Text
  -- ^ Description of the right-hand side

  -> (a -> f Ordering)
  -- ^ How to compare the left-hand side to the right-hand side.
  -- Return LT if the item is less than the right hand side; GT if
  -- greater; EQ if equal to the right hand side.

  -> Ordering
  -- ^ When subjects are compared, this ordering must be the result in
  -- order for the Predbox to be True; otherwise it is False. The subject
  -- will be on the left hand side.

  -> PredM f a

compareByM rhsDesc get tgt = predicateM f
  where
    f a = fmap mkTup (get a)
      where
        mkTup ord = (bl, val, cond)
          where
            val = Value [chunk . X.pack . show $ a]
            cond = Condition [chunk condTxt]
            condTxt = "is" <+> ordDesc <+> rhsDesc
            ordDesc = case ord of
              EQ -> "equal to"
              LT -> "less than"
              GT -> "greater than"
            bl = ord == tgt

-- | Build a Pred that compares items.  The idea is that the item on
-- the right hand side is baked into the 'Pred' and that the 'Pred'
-- compares this single right-hand side to each left-hand side item.
compareBy
  :: Show a
  => Text
  -- ^ Description of the right-hand side

  -> (a -> Ordering)
  -- ^ How to compare the left-hand side to the right-hand side.
  -- Return LT if the item is less than the right hand side; GT if
  -- greater; EQ if equal to the right hand side.

  -> Ordering
  -- ^ When subjects are compared, this ordering must be the result in
  -- order for the Predbox to be True; otherwise it is False. The subject
  -- will be on the left hand side.

  -> Pred a

compareBy rhsDesc get ord = compareByM rhsDesc (fmap return get) ord

-- | Overloaded version of 'compareBy'.

compare
  :: (Show a, Ord a)
  => a
  -- ^ Right-hand side

  -> Ordering
  -- ^ When subjects are compared, this ordering must be the result in
  -- order for the Predbox to be True; otherwise it is False. The subject
  -- will be on the left hand side.

  -> Pred a
compare rhs ord =
  compareBy (X.pack . show $ rhs) (`Prelude.compare` rhs) ord

-- | Builds a 'Pred' that tests items for equality.

equalByM
  :: (Show a, Functor f)

  => Text
  -- ^ Description of the right-hand side

  -> (a -> f Bool)
  -- ^ How to compare an item against the right hand side.  Return
  -- 'True' if the items are equal; 'False' otherwise.

  -> PredM f a
equalByM rhsDesc get = predicateM f
  where
    f a = fmap mkTup (get a)
      where
        mkTup bl = (bl, Value [chunk . X.pack . show $ a],
          Condition [chunk $ "is equal to" <+> rhsDesc])

-- | Builds a 'Pred' that tests items for equality.

equalBy
  :: Show a

  => Text
  -- ^ Description of the right-hand side

  -> (a -> Bool)
  -- ^ How to compare an item against the right hand side.  Return
  -- 'True' if the items are equal; 'False' otherwise.

  -> Pred a
equalBy rhsDesc f = equalByM rhsDesc (fmap return f)

-- | Overloaded version of 'equalBy'.

equal
  :: (Eq a, Show a)
  => a
  -- ^ Right-hand side

  -> Pred a
equal rhs = equalBy (X.pack . show $ rhs) (== rhs)


-- | Builds a 'Pred' for items that might fail to return a comparison.
compareByMaybeM
  :: (Functor f, Show a)
  => Text
  -- ^ Description of the right-hand side

  -> (a -> f (Maybe Ordering))
  -- ^ How to compare an item against the right hand side. Return LT if
  -- the item is less than the right hand side; GT if greater; EQ if
  -- equal to the right hand side.

  -> Ordering
  -- ^ When subjects are compared, this ordering must be the result in
  -- order for the Predbox to be True; otherwise it is False. The subject
  -- will be on the left hand side.

  -> PredM f a

compareByMaybeM rhsDesc get ord = predicateM f
  where
    f a = fmap mkTup (get a)
      where
        mkTup mayOrd = (bl, val, cond)
          where
            val = Value [chunk . X.pack . show $ a]
            cond = Condition [chunk $ "is" <+> ordDesc <+> rhsDesc]
            ordDesc = case ord of
              EQ -> "equal to"
              LT -> "less than"
              GT -> "greater than"
            bl = case mayOrd of
              Nothing -> False
              Just o -> o == ord


-- | Builds a 'Pred' for items that might fail to return a comparison.
compareByMaybe
  :: Show a
  => Text
  -- ^ Description of the right-hand side

  -> (a -> Maybe Ordering)
  -- ^ How to compare an item against the right hand side. Return LT if
  -- the item is less than the right hand side; GT if greater; EQ if
  -- equal to the right hand side.

  -> Ordering
  -- ^ When subjects are compared, this ordering must be the result in
  -- order for the Predbox to be True; otherwise it is False. The subject
  -- will be on the left hand side.

  -> Pred a

compareByMaybe rhsDesc get ord = compareByMaybeM rhsDesc (fmap return get) ord

greater
  :: (Show a, Ord a)

  => a
  -- ^ Right-hand side

  -> Pred a
greater rhs = compare rhs GT

less
  :: (Show a, Ord a)

  => a
  -- ^ Right-hand side

  -> Pred a
less rhs = compare rhs LT

greaterEq
  :: (Show a, Ord a)
  => a
  -- ^ Right-hand side

  -> Pred a
greaterEq r = greater r ||| equal r

lessEq
  :: (Show a, Ord a)
  => a
  -- ^ Right-hand side

  -> Pred a
lessEq r = less r ||| equal r

notEq
  :: (Show a, Eq a)
  => a
  -- ^ Right-hand side

  -> Pred a
notEq = not . equal

greaterByM
  :: (Show a, Functor f)
  => Text
  -- ^ Description of right-hand side

  -> (a -> f Ordering)
  -- ^ How to compare an item against the right hand side. Return LT
  -- if the item is less than the right hand side; GT if greater; EQ
  -- if equal to the right hand side.

  -> PredM f a
greaterByM desc get = compareByM desc get GT

greaterBy
  :: Show a
  => Text
  -- ^ Description of right-hand side

  -> (a -> Ordering)
  -- ^ How to compare an item against the right hand side. Return LT
  -- if the item is less than the right hand side; GT if greater; EQ
  -- if equal to the right hand side.

  -> Pred a
greaterBy desc get = greaterByM desc (fmap return get)


lessByM
  :: (Show a, Functor f)
  => Text
  -- ^ Description of right-hand side

  -> (a -> f Ordering)
  -- ^ How to compare an item against the right hand side. Return LT
  -- if the item is less than the right hand side; GT if greater; EQ
  -- if equal to the right hand side.

  -> PredM f a
lessByM desc get = compareByM desc get LT

lessBy
  :: Show a
  => Text
  -- ^ Description of right-hand side

  -> (a -> Ordering)
  -- ^ How to compare an item against the right hand side. Return LT
  -- if the item is less than the right hand side; GT if greater; EQ
  -- if equal to the right hand side.

  -> Pred a
lessBy desc get = lessByM desc (fmap return get)

greaterEqByM
  :: (Functor f, Monad f, Show a)
  => Text
  -- ^ Description of right-hand side

  -> (a -> f Ordering)
  -- ^ How to compare an item against the right hand side. Return LT
  -- if the item is less than the right hand side; GT if greater; EQ
  -- if equal to the right hand side.

  -> PredM f a
greaterEqByM desc get = greaterByM desc get ||| equalByM desc f'
  where
    f' = fmap (fmap (== EQ)) get

greaterEqBy
  :: Show a
  => Text
  -- ^ Description of right-hand side

  -> (a -> Ordering)
  -- ^ How to compare an item against the right hand side. Return LT
  -- if the item is less than the right hand side; GT if greater; EQ
  -- if equal to the right hand side.

  -> Pred a
greaterEqBy desc get = greaterEqByM desc (fmap return get)

lessEqByM
  :: (Functor f, Monad f, Show a)
  => Text
  -- ^ Description of right-hand side

  -> (a -> f Ordering)
  -- ^ How to compare an item against the right hand side. Return LT
  -- if the item is less than the right hand side; GT if greater; EQ
  -- if equal to the right hand side.

  -> PredM f a
lessEqByM desc get = lessByM desc get ||| equalByM desc f'
  where
    f' = fmap (fmap (== EQ)) get

lessEqBy
  :: Show a
  => Text
  -- ^ Description of right-hand side

  -> (a -> Ordering)
  -- ^ How to compare an item against the right hand side. Return LT
  -- if the item is less than the right hand side; GT if greater; EQ
  -- if equal to the right hand side.

  -> Pred a
lessEqBy desc get = lessEqByM desc (fmap return get)

notEqByM
  :: (Functor f, Show a)
  => Text
  -- ^ Description of right-hand side

  -> (a -> f Bool)
  -- ^ How to compare an item against the right hand side.  Return
  -- 'True' if equal; 'False' otherwise.

  -> PredM f a
notEqByM desc = not . equalByM desc

notEqBy
  :: Show a
  => Text
  -- ^ Description of right-hand side

  -> (a -> Bool)
  -- ^ How to compare an item against the right hand side.  Return
  -- 'True' if equal; 'False' otherwise.

  -> Pred a
notEqBy desc f = notEqByM desc (fmap return f)

-- | Parses a string that contains text, such as @>=@, which indicates
-- which comparer to use.  Returns the comparer.
parseComparer
  :: (Monad f, Functor f)
  => Text
  -- ^ The string with the comparer to be parsed

  -> (Ordering -> PredM f a)
  -- ^ A function that, when given an ordering, returns a 'Pred'.
  -- Typically you will get this by partial application of 'compare',
  -- 'compareBy', or 'compareByMaybe'.

  -> Maybe (PredM f a)
  -- ^ If an invalid comparer string is given, Nothing; otherwise, the
  -- 'Pred'.
parseComparer t f
  | t == ">" = Just (f GT)
  | t == "<" = Just (f LT)
  | t == "=" = Just (f EQ)
  | t == "==" = Just (f EQ)
  | t == ">=" = Just (f GT ||| f EQ)
  | t == "<=" = Just (f LT ||| f EQ)
  | t == "/=" = Just (not $ f EQ)
  | t == "!=" = Just (not $ f EQ)
  | otherwise = Nothing

-- | Append two 'X.Text', with an intervening space if both 'X.Text'
-- are not empty.
(<+>) :: Text -> Text -> Text
l <+> r
  | full l && full r = l <> " " <> r
  | otherwise = l <> r
  where
    full = Prelude.not . X.null