-- | Provides error handling functions for checking parser output.
--   The functions and data types of this module are mostly tiny, little
--   helpers used by all parser modules.

module Language.Haskell.FreeTheorems.Frontend.Error where



import Control.Monad (foldM)
import Control.Monad.Error (Error(..), throwError)
import Control.Monad.Writer (Writer, runWriter, tell)
import Data.List (intersperse)
import Text.PrettyPrint (Doc, empty, text, fsep, ($$), nest)

import Language.Haskell.FreeTheorems.Syntax
    (Declaration, getDeclarationName, unpackIdent)



-- | A wrapper type for a @Writer@ which stores pretty-printable documents along
--   with checked values.

type Checked a = Writer [Doc] a



-- | A wrapper type for @Writer@ which stores pretty-printable documents along
--   with parsed values.
--   This type is provided as standard return type for parsers.

type Parsed a = Writer [Doc] a





-- | The error type is just a synonym for @Either@ where errors are represented
--   by a pretty-printable @Doc@.

type ErrorOr a = Either Doc a

-- needed to make 'ErrorOr' a monad
instance Error Doc where
  noMsg    = empty
  strMsg s = text s



-- | A wrapper function for @runWriter@.

runChecks :: Checked a -> (a, [Doc])
runChecks = runWriter



-- | Applies a checking function (the first argument) element-wise to a list of
--   values (the second argument). The result list contains only those elements
--   for which the checking function does not yield an error.

foldChecks :: (a -> ErrorOr b) -> [a] -> Checked [a]
foldChecks check = foldM doCheck []
  where
    doCheck xs x = 
      case getError (check x) of
        Nothing -> return (xs ++ [x])
        Just e  -> tell [e] >> return xs



-- | Checks if the argument contains an error.

isError :: ErrorOr a -> Bool
isError = either (const True) (const False)



-- | Returns the error message stored in the argument or @Nothing@ if there is 
--   no error message in the argument.

getError :: ErrorOr a -> Maybe Doc
getError = either Just (const Nothing)



-- | If the first argument is True, then the second argument is taken as an
--   error message. Otherwise () is returned as a non-error message.

errorIf :: Bool -> Doc -> ErrorOr ()
errorIf False = return . const ()
errorIf True  = throwError



-- | Transforms a string into a pretty-printed document by splitting the string
--   into words and forming a pretty paragraph of all words.

pp :: String -> Doc
pp = fsep . map text . words



-- | Checks a declaration for errors.
--   If the second argument is an error, this function extends the error 
--   message to make clear it belongs to a declaration.

inDecl :: Declaration -> ErrorOr a -> ErrorOr a
inDecl d e = case getError e of
  Nothing  -> e
  Just doc -> throwError $
                pp ("In the declaration of " 
                    ++ unpackIdent (getDeclarationName d) ++ ":")
                $$ nest 2 doc
    


-- | Used to extend error messages by a list of items violating a certain rule. 

violating :: String -> [String] -> String
violating name xs =
  let text = if length xs == 1
               then " The following " ++ name ++ " violates this rule: "
               else " The following " ++ name ++ "s violate this rule: "
   in text ++ (concat . intersperse ", " $ xs)