module Language.Haskell.Tools.Refactor.Builtin.ExtensionOrganizer.Checkers.UndecidableInstancesChecker where

import Name
import CoAxiom
import FamInstEnv
import InstEnv
import TcType
import GHC hiding (Module, ClassDecl, ClosedTypeFamily)

import Data.Maybe (isJust)

import Control.Reference ((^.))
import Control.Monad.Trans.Maybe (MaybeT(..))

import Language.Haskell.Tools.AST
import Language.Haskell.Tools.Refactor
import Language.Haskell.Tools.Refactor.Builtin.ExtensionOrganizer.ExtMonad
import Language.Haskell.Tools.Refactor.Builtin.ExtensionOrganizer.Utils.GHCHelpers


-- | If UndecidableInstances is turned on,
-- it checks whether any type class or type family instances needs
-- UndecidableInstances in the module.
gblChkUndecidableInstances :: CheckNode Module
gblChkUndecidableInstances = conditional gblChkUndecidableInstances' UndecidableInstances

-- | Checks whether any type class or type family instances needs
-- UndecidableInstances in the module.
gblChkUndecidableInstances' :: CheckNode Module
gblChkUndecidableInstances' m = do
  (clsInsts, famInsts) <- getInstances [semanticsModule m]
  mapM_ chkClsInst clsInsts
  mapM_ chkFamInst famInsts
  return m

-- | If the type class instance requires UndecidableInstances,
-- it adds the occurence with location of the instance.
chkClsInst :: ClsInst -> ExtMonad ()
chkClsInst inst = when (clsInstNeedsUD inst)
                       (addEvidenceLoc UndecidableInstances (getSrcSpan inst))

-- | Decides whether a type class instance requires UndecidableInstances.
clsInstNeedsUD :: ClsInst -> Bool
clsInstNeedsUD inst = checkInstTermination args theta
  where (_, theta, _, args) = instanceSig inst

-- | If the type class instance requires UndecidableInstances,
-- it adds the occurence with location of the instance.
chkFamInst :: FamInst -> ExtMonad ()
chkFamInst inst = when (famInstNeedsUD inst)
                       (addEvidenceLoc UndecidableInstances (getSrcSpan inst))

-- | Decides whether a family instance requires UndecidableInstances.
-- If it is a data family instance, it does not need the extension.
famInstNeedsUD :: FamInst -> Bool
famInstNeedsUD inst
  | isTyFamInst inst
  , lhs <- fi_tys inst
  , rhs <- tcTyFamInsts . fi_rhs $ inst
  = checkFamEq lhs rhs
  | otherwise = False

-- | Checks a declaration whether it needs UndecidableInstances.
-- (If the extension is turned on)
chkUndecidableInstancesDecl :: CheckNode Decl
chkUndecidableInstancesDecl = conditional chkUndecidableInstancesDecl' UndecidableInstances

-- | Checks a declaration whether it needs UndecidableInstances.
-- If a lookup is not successful, it keeps the extension.
chkUndecidableInstancesDecl' :: CheckNode Decl
chkUndecidableInstancesDecl' d = do
  mDecl <- runMaybeT (chkUndecidableInstancesDeclMaybe d)
  maybe (addEvidence UndecidableInstances d) return mDecl

-- | Checks a class declaration whether it has a type function in its context,
-- or a closed type family declaration needs UndecidableInstances.
-- For more information on the family check, see checkFamInstRhs.
-- May fail on lookup.
chkUndecidableInstancesDeclMaybe :: Decl -> MaybeT ExtMonad Decl
chkUndecidableInstancesDeclMaybe d@(ClassDecl mCtx _ _ _)
  | isJust (mCtx ^. annMaybe) = do
    ctx <- liftMaybe $ mCtx ^. annMaybe
    let assert = ctx ^. contextAssertion
        names  = assertionQNames assert
    types <- mapM lookupTypeFromId names
    if any hasTyFunHead types then addEvidence UndecidableInstances d
                              else return d
  | otherwise = return d
chkUndecidableInstancesDeclMaybe d@(ClosedTypeFamily dh _ _) = do
  tyFam <- lookupClosedTyFam dh
  let brs    = fromBranches . coAxiomBranches $ tyFam
      famEqs = map ((,) <$> coAxBranchLHS <*> tcTyFamInsts . coAxBranchRHS) brs
  mapM_ (lift . uncurry chkFamEqM) famEqs >> return d
  where
    chkFamEqM :: [GHC.Type] -> [(GHC.TyCon, [GHC.Type])] -> ExtMonad ()
    chkFamEqM lhs rhs = when (checkFamEq lhs rhs)
                             (addEvidence_ UndecidableInstances d)
chkUndecidableInstancesDeclMaybe d = return d
