{-# LANGUAGE TypeApplications #-}

-- | General-purpose utility functions

module Serokell.Util.Verify
       ( VerificationRes (..)

       -- * Helpers
       , isVerFailure
       , isVerSuccess
       , verResToMonadError

       -- * Verification
       , verifyGeneric

       -- * Prety printing
       , buildVerResFull
       , buildVerResSingle
       , formatAllErrors
       , formatFirstError
       , verResFullF
       , verResSingleF
       ) where

import           Control.Monad.Except   (MonadError, throwError)
import           Data.Semigroup         (Semigroup)
import qualified Data.Semigroup         as Semigroup
import           Data.Text              (Text)
import qualified Data.Text              as T
import qualified Data.Text.Lazy.Builder as B
import           Formatting             (Format, later, sformat)

import           Serokell.Util.Text     (listBuilder)

data VerificationRes
    = VerSuccess
    | VerFailure ![T.Text]
    deriving (Eq, Show)

isVerSuccess :: VerificationRes -> Bool
isVerSuccess VerSuccess = True
isVerSuccess _          = False

isVerFailure :: VerificationRes -> Bool
isVerFailure (VerFailure _) = True
isVerFailure _              = False

instance Semigroup VerificationRes where
    VerSuccess <> a = a
    VerFailure xs <> a =
        VerFailure $
        xs ++
        case a of
            VerSuccess    -> []
            VerFailure ys -> ys

instance Monoid VerificationRes where
    mempty = VerSuccess
    mappend = (Semigroup.<>)

-- | This function takes list of (predicate, message) pairs and checks
-- each predicate.  If predicate is False it's considered an error.
-- If there is at least one error this function returns VerFailure,
-- otherwise VerSuccess is returned.  It's useful to verify some data
-- before using it.
-- Example usage: `verifyGeneric [(checkA, "A is bad"), (checkB, "B is bad")]`
verifyGeneric :: [(Bool, T.Text)] -> VerificationRes
verifyGeneric errors
    | null messages = VerSuccess
    | otherwise = VerFailure messages
  where
    messages = map snd . filter (not . fst) $ errors

----------------------------------------------------------------------------
-- Pretty printing
----------------------------------------------------------------------------

-- | Format VerificationRes in a pretty way using all errors messages
-- for VerFailure.
buildVerResFull :: VerificationRes -> B.Builder
buildVerResFull VerSuccess          = buildVerResImpl Nothing
buildVerResFull (VerFailure errors) = buildVerResImpl $ Just errors

-- | Format VerificationRes in a pretty way using only first message
-- for VerFailure.
buildVerResSingle :: VerificationRes -> B.Builder
buildVerResSingle VerSuccess          = buildVerResImpl Nothing
-- [SU-1] Use NonEmpty instead of unsafe head.
buildVerResSingle (VerFailure errors) = buildVerResImpl $ Just $ [head errors]

buildVerResImpl :: Maybe [T.Text] -> B.Builder
buildVerResImpl Nothing       = "success"
buildVerResImpl (Just errors) =
    "failure: " `mappend` listBuilder @Text @Text @Text "[" "; " "]" errors

-- | Formatter based on buildVerResFull.
verResFullF :: Format r (VerificationRes -> r)
verResFullF = later buildVerResFull

-- | Formatter based on buildVerResSingle.
verResSingleF :: Format r (VerificationRes -> r)
verResSingleF = later buildVerResSingle

-- These two functions can have more general type.

-- | Pretty printer for errors from VerFailure, all errors are printed.
formatAllErrors :: [Text] -> Text
formatAllErrors = sformat verResFullF . VerFailure

-- | Pretty printer for errors from VerFailure, only first error is printed.
formatFirstError :: [Text] -> Text
formatFirstError = sformat verResSingleF . VerFailure

----------------------------------------------------------------------------
-- Conversion to MonadError (including Either)
----------------------------------------------------------------------------

verResToMonadError
    :: MonadError e m
    => ([Text] -> e) -> VerificationRes -> m ()
verResToMonadError _ VerSuccess          = pure ()
verResToMonadError f (VerFailure errors) = throwError $ f errors