{-# LANGUAGE EmptyDataDecls, TemplateHaskell #-}
-- | 
-- The orphan instance problem is well-known in Haskell. This module
-- by no means purports to solve the problem, but provides a workaround
-- that may be significantly less awful than the status quo in some
-- cases.
--
-- Say I think that the 'Name' type should have an 'IsString' instance.
-- But I don't control either the class or the type, so if I define the
-- instance, and then the template-haskell package defines one, my code
-- is going to break.
--
-- 'safeInstance' can help me to solve this problem:
--
-- > safeInstance ''IsString [t| Name |] [d|
-- >   fromString = mkName |]
--
-- This will declare an instance only if one doesn't already exist.
-- Now anyone importing your module is guaranteed to get an instance
-- one way or the other.
--
-- This module is still highly experimental. I suspect that some things
-- like recursion still won't work, because of how the names are
-- mangled. Let me know how you get on!
module NotCPP.OrphanEvasion (
  MultiParams,
  safeInstance,
  safeInstance',
 ) where

import Control.Applicative

import Language.Haskell.TH
import Language.Haskell.TH.Syntax

import NotCPP.ScopeLookup

-- | An empty type used only to signify a multiparameter typeclass in
-- 'safeInstance'.
data MultiParams a

-- | Given @(forall ts. Cxt => t)@, return @(Cxt, [t])@.
-- Given @(forall ts. Cxt => 'MultiParams' (t1, t2, t3))@, return
-- @(Cxt, [t1, t2, t3])@.
--
-- This is used in 'safeInstance' to allow types to be specified more
-- easily with TH typequotes.
fromTuple :: Type -> (Cxt, [Type])
fromTuple ty = unTuple <$> case ty of
  ForallT _ cxt ty' -> (cxt, ty')
  _ -> ([], ty)
 where
  unTuple :: Type -> [Type]
  unTuple (AppT (ConT n) ta)
    | n == ''MultiParams = case unrollAppT ta of
      (TupleT{}, ts) -> ts
      _ -> [ty]
  unTuple t = [t]

-- | A helper function to unwind type application. 
-- Given @TyCon t1 t2 t3@, returns @(TyCon, [t1,t2,t3])@
unrollAppT :: Type -> (Type, [Type])
unrollAppT = go []
 where
  go acc (AppT tc ta) = go (ta : acc) tc
  go acc ty = (ty, reverse acc)

-- | Left inverse to 'unrollAppT', equal to @'foldl' 'AppT'@
rollAppT :: Type -> [Type] -> Type
rollAppT = foldl AppT

-- | @'safeInstance'' className cxt types methods@ produces an instance
-- of the given class if and only if one doesn't already exist.
--
-- See 'safeInstance' for a simple way to construct the 'Cxt' and
-- @['Type']@ parameters.
safeInstance' :: Name -> Cxt -> [Type] -> Q [Dec] -> Q [Dec]
safeInstance' cl cxt tys inst = do
  b <- $(scopeLookups ["isInstance", "isClassInstance"]) cl tys
  if b
    then return []
    else do
      ds <- map fixInst <$> inst
      return [InstanceD cxt (rollAppT (ConT cl) tys) ds]
 where
  fixInst (FunD n cls) = FunD (fixName n) cls
  fixInst (ValD (VarP n) rhs wh) = ValD (VarP (fixName n)) rhs wh
  fixInst d = d
  fixName (Name n _) = Name n NameS

-- | 'safeInstance' is a more convenient version of 'safeInstance''
-- that takes the context and type from a @'Q' 'Type'@ with the intention
-- that it be supplied using a type-quote.
--
-- To define an instance @Show a => Show (Wrapper a)@, you'd use:
--
-- > safeInstance ''Show [t| Show a => Wrapper a |]
-- >   [d| show _ = "stuff" |]
--
-- To define an instance of a multi-param type class, use the
-- 'MultiParams' type constructor with a tuple:
--
-- > safeInstance ''MonadState
-- >   [t| MonadState s m => MultiParams (s, MaybeT m) |]
-- >   [d| put = ... |]
safeInstance :: Name -> Q Type -> Q [Dec] -> Q [Dec]
safeInstance n qty inst = do
  (cxt, tys) <- fromTuple <$> qty
  safeInstance' n cxt tys inst