{- |
This module defines the 'Proof' type, some proofs, and some helper functions.

A 'Proof' does three things:

 - verifies that the input value meets some criteria
 - optionally transforms the input value to another value while preserving that criteria
 - puts the proof name in type-signature where the type-checker can use it
-}
module Text.Reform.Proof where

import Control.Applicative.Indexed (IndexedFunctor(imap))
import Control.Monad.Trans   (lift)
import Numeric               (readDec, readFloat, readSigned)
import Text.Reform.Result (FormRange, Result(..))
import Text.Reform.Core   (Form(..), Proved(..))

-- | A 'Proof' attempts to prove something about a value.
--
-- If successful, it can also transform the value to a new value. The
-- proof should hold for the new value as well.
--
-- Generally, each 'Proof' has a unique data-type associated with it
-- which names the proof, such as:
--
-- > data NotNull = NotNull
--
data Proof m error proof a b
    = Proof { proofName     :: proof                   -- ^ name of the thing to prove
            , proofFunction :: a -> m (Either error b) -- ^ function which provides the proof
            }

-- | apply a 'Proof' to a 'Form'
prove :: (Monad m) =>
         Form m input error view q a
      -> Proof m error proof a b
      -> Form m input error view proof b
prove (Form frm) (Proof p f) =
    Form $ do (xml, mval) <- frm
              val <- lift $ lift $ mval
              case val of
                (Error errs)            -> return (xml, return $ Error errs)
                (Ok (Proved _ pos a)) ->
                    do r <- lift $ lift $ f a
                       case r of
                         (Left err) -> return (xml, return $ Error [(pos, err)])
                         (Right b)  ->
                             return (xml, return $ Ok (Proved { proofs   = p
                                                              , pos      = pos
                                                              , unProved = b
                                                              }))

-- * transformations (proofs minus the proof).

-- | transform a 'Form' using a 'Proof', and the replace the proof with @()@.
--
-- This is useful when you want just want classic digestive-functors behaviour.
transform :: (Monad m) =>
             Form m input error view anyProof a
          -> Proof m error proof a b
          -> Form m input error view () b
transform frm proof = imap (const ()) id (frm `prove` proof)

-- | transform the 'Form' result using a monadic 'Either' function.
transformEitherM :: (Monad m) => Form m input error view anyProof a
                 -> (a -> m (Either error b))
                 -> Form m input error view () b
transformEitherM frm func = frm `transform` (Proof () func)

-- | transform the 'Form' result using an 'Either' function.
transformEither :: (Monad m) =>
                   Form m input error view anyProof a
                -> (a -> Either error b)
                -> Form m input error view () b
transformEither frm func = transformEitherM frm (return . func)

-- * Various Proofs

-- | proof that a list is not empty
data NotNull = NotNull

-- | prove that a list is not empty
notNullProof :: (Monad m) => error -> Proof m error NotNull [a] [a]
notNullProof errorMsg = Proof NotNull (return . check)
    where
    check list =
        if null list
          then (Left errorMsg)
          else (Right list)

-- | proof that a 'String' is a decimal number
data Decimal        = Decimal
-- | proof that a 'String' is a Real/Fractional number
data RealFractional = RealFractional
-- | proof that a number is also (allowed to be) signed
data Signed a       = Signed a

-- | read an unsigned number in decimal notation
decimal :: (Monad m, Eq i, Num i) =>
           (String -> error) -- ^ create an error message ('String' is the value that did not parse)
        -> Proof m error Decimal String i
decimal mkError = Proof Decimal (return . toDecimal)
    where
      toDecimal str =
          case readDec str of
            [(d,[])] -> (Right d)
            _        -> (Left $ mkError str)

-- | read signed decimal number
signedDecimal :: (Monad m, Eq i, Real i) => (String -> error) -> Proof m error (Signed Decimal) String i
signedDecimal mkError = Proof (Signed Decimal) (return . toDecimal)
    where
      toDecimal str =
          case (readSigned readDec) str of
            [(d,[])] -> (Right d)
            _        -> (Left $ mkError str)

-- | read 'RealFrac' number
realFrac :: (Monad m, RealFrac a) => (String -> error) -> Proof m error RealFractional String a
realFrac mkError = Proof RealFractional (return . toRealFrac)
    where
      toRealFrac str =
          case readFloat str of
            [(f,[])] -> (Right f)
            _        -> (Left $ mkError str)

-- | read a signed 'RealFrac' number
realFracSigned :: (Monad m, RealFrac a) => (String -> error) -> Proof m error (Signed RealFractional) String a
realFracSigned mkError = Proof (Signed RealFractional) (return . toRealFrac)
    where
      toRealFrac str =
          case (readSigned readFloat) str of
            [(f,[])] -> (Right f)
            _        -> (Left $ mkError str)