{-# LANGUAGE TemplateHaskell, DeriveDataTypeable #-}

-- | This module defines placeholders that you can use while coding to
-- allow incomplete code to compile. They work just like 'undefined',
-- but with improved error messages and compile-time warnings.
module Development.Placeholders (
    -- * Example
    -- $example

    -- * Placeholders
    notImplemented,
    todo,
    placeholder,
    placeholderNoWarning,

    -- * Exceptions
    PlaceholderException(..)
) where

import Control.Exception (Exception, throw)
import Data.Typeable (Typeable)
import Language.Haskell.TH (Q, Exp, Loc(..), litE, stringL, location, report)

-- | Thrown when attempting to evaluate a placeholder at runtime.
data PlaceholderException = PlaceholderException String
    deriving (Show, Typeable)

instance Exception PlaceholderException

-- | Indicates that this piece of code has not yet been implemented.
--
-- @$notImplemented = $(placeholder \"Unimplemented feature\")@
notImplemented :: Q Exp
notImplemented = placeholder "Unimplemented feature"

-- | Indicates unimplemented code or a known bug with a custom message.
--
-- @$(todo msg) = $(placeholder (\"TODO: \" ++ msg))@
todo :: String -> Q Exp
todo msg = placeholder $ "TODO: " ++ msg

-- | Generates an expression of any type that, if evaluated at runtime will
-- throw a 'PlaceholderException'. It is therefore similar to 'error', except
-- that the source location is automatically included. Also, a warning is
-- generated at compile time so you won't forget to replace placeholders
-- before packaging your code.
placeholder :: String -> Q Exp
placeholder msg = do
    emitWarning msg
    placeholderNoWarning msg

-- | Similar to 'placeholder', but does not generate a compiler warning. Use
-- with care!
placeholderNoWarning :: String -> Q Exp
placeholderNoWarning msg = do
    runtimeMsg <- formatMessage msg `fmap` location
    [| throw $ PlaceholderException $(litE $ stringL runtimeMsg) |]

emitWarning :: String -> Q ()
emitWarning msg = report False $ msg

formatMessage :: String -> Loc -> String
formatMessage msg loc = msg ++ " at " ++ formatLoc loc

formatLoc :: Loc -> String
formatLoc loc = let file = loc_filename loc
                    (line, col) = loc_start loc
                in concat [file, ":", show line, ":", show col]

-- $example
-- 
-- > {-# LANGUAGE TemplateHaskell #-}
-- >
-- > import Development.Placeholders
-- >
-- > theUltimateAnswer :: Int
-- > theUltimateAnswer = $notImplemented
-- >
-- > main = do
-- >     putStrLn "The ultimate answer:"
-- >     print theUltimateAnswer
--
-- This will compile with a warning about the unimplemented function:
--
-- @
-- $ ghc --make Simple.hs
-- ...
-- Simple.hs:6:21: Unimplemented feature
-- ...
-- @
--
-- At runtime, an exception will be thrown when the placeholder is evaluated,
-- indicating the location of the placeholder.
--
-- @
-- $ .\/Simple
-- The ultimate answer:
-- Simple: PlaceholderExcption \"Unimplemented feature at Simple.hs:6:21\"
-- @
--
-- If compiled with the GHC flag @-Werror@, the warning will get turned into
-- an error and compilation will fail. @-Werror@ can therefore be used to
-- verify that you haven't left any unintended placeholders behind.