{-# LANGUAGE DataKinds       #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators   #-}


{-|
Module      : Language.Haskell.Instance.Ban
Description : Declare that a typeclass instance
Copyright   : (c) 2017, Commonwealth Scientific and Industrial Research Organisation
License     : BSD3
Maintainer  : jack.kelly@data61.csiro.au
Stability   : experimental
Portability : Non-Portable
-}

module Language.Haskell.Instance.Ban (banInstance) where

import Data.Maybe                 (mapMaybe)
import GHC.TypeLits
import Language.Haskell.TH.Lib
import Language.Haskell.TH.Ppr
import Language.Haskell.TH.Syntax

-- TODO: Mark instances as deprecated so haddock sees them.
-- TODO: Overlappable instances?
-- | Ban an instance of a typeclass; code which tries to use the
-- banned instance will fail at compile time. This works by generating
-- an instance that depends on a custom type error:
--
-- @
-- instance TypeError (..) => ToJSON Foo where
--   ...
-- @
--
-- Use it like this:
--
-- @
-- \$(banInstance [t|ToJSON Foo|] "why ToJSON Foo should never be defined")
-- @
banInstance
  :: TypeQ
     -- ^ The instance you want to ban.
     -- Most easily written with a type-quote: @[t|ToJSON Foo|]@
  -> String -- ^ The reason that this instance is banned.
  -> DecsQ
banInstance constraintQ message = do
  loc <- qLocation
  ClassI (ClassD _ _ _ _ classDecs) _ <- className <$> constraintQ >>= reify
  let context :: CxtQ
      context = cxt [[t|TypeError ('Text "Attempt to use banned instance (" ':<>: 'ShowType $(constraintQ) ':<>: 'Text ")"
                                  ':$$: 'Text "Reason for banning: " ':<>: 'Text $(symbol message)
                                  ':$$: 'Text "Instance banned at " ':<>: 'Text $(symbol $ formatLocation loc)
                                  ':$$: 'Text ""
                                  )|]]
  pure <$> instanceD context constraintQ (convertClassDecs classDecs)

symbol :: String -> TypeQ
symbol = litT . strTyLit

formatLocation :: Loc -> String
formatLocation Loc{..} = concat ["[", loc_package, ":", loc_module, "] ", loc_filename, ":",  show $ fst loc_start]

className :: Type -> Name
className topTy = go topTy where
  go (AppT ty _) = className ty
  go (ConT name) = name
  go _ = error $ "Cannot determine class name for type: " ++ pprint topTy

convertClassDecs :: [Dec] -> [DecQ]
convertClassDecs = mapMaybe go where
  -- TODO: Support type/data families?
  go (SigD name _) = Just $ funD name [clause [] (normalB [|undefined|]) []]
  go DefaultSigD{} = Nothing
  go _ = error "Banning instances only supported for classes \
               \that contain only functions. Patches welcome."