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

import TcType (tcSplitNestedSigmaTys, checkValidClsArgs)
import Type hiding (Type(..))
import PrelNames (eqTyConName, eqTyConKey)
import Unique (hasKey)
import qualified Name as GHC
import qualified GHC

import Data.List
import Data.Generics.Uniplate.Data()
import Data.Generics.Uniplate.Operations

import Control.Reference ((^.))

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

gblChkQNamesForFC :: CheckNode Module
gblChkQNamesForFC = conditional gblChkQNamesForFC' FlexibleContexts

chkFlexibleContexts :: CheckNode Context
chkFlexibleContexts = conditional chkFlexibleContexts' FlexibleContexts

chkFlexibleContexts' :: CheckNode Context
chkFlexibleContexts' ctx@(Context a) = chkAssertion a >> return ctx

chkAssertion :: CheckNode Assertion
chkAssertion a@(ClassAssert con annTys) = do
  -- If a lookup fails, add MissingInformation FlexibleContexts
  let errDefault = addMI FlexibleContexts a >> return False
  isClass <- fromMaybeTM errDefault (isClassTyConNameM con)
  if not isClass || all hasTyVarHead (annTys ^. annListElems)
    then return a
    else addEvidence FlexibleContexts a
chkAssertion a@(InfixAssert lhs op rhs)
  -- If the constraint is of form a ~ b, then we dont need to check
  | Just name <- semanticsName op
  , name == eqTyConName
  = return a
  -- If the constraint has only type variable heads, it doesn't need FlexibleContexts
  | hasTyVarHead lhs && hasTyVarHead rhs = return a
  -- In any other case, we keep FlexibleContexts
  | otherwise = addEvidence FlexibleContexts a
chkAssertion a@(TupleAssert xs) = do
  mapM_ chkAssertion xs
  return a
chkAssertion a@ImplicitAssert{} = return a

chkFlexibleContextsDecl :: CheckNode Decl
chkFlexibleContextsDecl = conditional chkFlexibleContextsDecl' FlexibleContexts

chkFlexibleContextsDecl' :: CheckNode Decl
chkFlexibleContextsDecl' d@(TypeDecl dh rhs) = do
  let ty = typeOrKindFromId . declHeadQName $ dh
  when (hasConstraintKind ty || tcReturnsConstraintKind ty)
       (chkClassesInside rhs)
  return d
chkFlexibleContextsDecl' d = return d

-- | Checks whether all type class applications in a type synonym rhs
-- (that has kind Constraint) have only type variable heads.
-- Returns False if a lookup inside is not succesful.
-- If it isn't applied to a type that has kind Constraint, it may give
-- false positive results.
chkClassesInside :: Type -> ExtMonad ()
chkClassesInside (TupleType annTys) =
  mapM_ chkClassesInside (annTys ^. annListElems)
chkClassesInside t = maybe (addFC t) f (splitTypeAppMaybe t)
  where
    addFC :: Type -> ExtMonad ()
    addFC = addEvidence_ FlexibleContexts

    f :: (GHC.Name, [Type]) -> ExtMonad ()
    f (n,args) = do
       isClass <- isJustT . lookupClass $ n
       when (n /= eqTyConName && isClass && any (not . hasTyVarHead) args)
            (addFC t)

    -- Separates the type into its head and its arguments.
    -- This functions should only be applied to types that have kind Constraint.
    splitTypeAppMaybe :: Type -> Maybe (GHC.Name, [Type])
    splitTypeAppMaybe (TypeApp f arg) = do
      (f', args) <- splitTypeAppMaybe f
      return (f', args ++ [arg])
    splitTypeAppMaybe (InfixTypeApp lhs op rhs) = do
      opName <- semanticsName op
      return (opName, [lhs,rhs])
    splitTypeAppMaybe (VarType n) = do
      sname <- semanticsName n
      return (sname, [])
    splitTypeAppMaybe (ParenType t) = splitTypeAppMaybe t
    splitTypeAppMaybe (KindedType t _) = splitTypeAppMaybe t
    splitTypeAppMaybe _ = Nothing


chkQNameForFC :: QualifiedName -> ExtMonad Bool
chkQNameForFC name = do
  mty <- runMaybeT . lookupTypeFromId $ name
  case mty of
    Just ty -> do
      -- First we get all subtypes of the given type, while expanding all type synonyms,
      -- then we remove all implicit predicates,
      -- and partition the types into predicate types and theta types.
      let expanded = expandTypeSynonyms ty
          subTys = universeBi expanded :: [GHC.Type]
          (preds, tys) = partition isPredTy . filter (not . isIPPred) $ subTys

          -- We don't need type equalities, so we get rid of them and their children.
          -- Then we extract the constraints from the types.
          nomEqs = universeBi (filter isNominalEqPred preds) :: [GHC.Type]
          preds' = preds \\ nomEqs
          thetas = concatMap (snd' . tcSplitNestedSigmaTys) tys

          -- We split each theta type into its class tycon and its arguments.
          -- We also filter out tuple constraints
          -- (we already have the constraints in it).
          xs = mapMaybe getClassPredTys_maybe (thetas ++ preds')
          xs' = filter (not . isCTupleClass . fst) xs

      if all (uncurry hasValidClsArgs) xs' then return False
                                           else return True
    Nothing -> return False
  where
    hasValidClsArgs :: GHC.Class -> [GHC.Type] -> Bool
    hasValidClsArgs cls args = cls `hasKey` eqTyConKey
                            || isIPClass cls
                            || checkValidClsArgs False cls args

    snd' :: (a,b,c) -> b
    snd' (_,x,_) = x

    isNominalEqPred :: GHC.Type -> Bool
    isNominalEqPred ty
      | Just (tc,_) <- splitTyConApp_maybe ty = tc `hasKey` eqTyConKey
      | otherwise = False

gblChkQNamesForFC' :: CheckNode Module
gblChkQNamesForFC' m = do
  -- Names appearing on the rhs of bindings are just hints,
  -- they don't necessarily require FlexibleContexts.
  let allNames   = universeBi (m ^. modDecl) :: [QualifiedName]
      rhs        = universeBi (m ^. modDecl) :: [Rhs]
      hints      = universeBi rhs            :: [QualifiedName]

      -- Separating hints from evidence,
      -- and grouping them together based on their GHC.Name
      evidence  = filter (isJust . semanticsName) (allNames \\ hints)
      hints'    = filter (isJust . semanticsName) hints
      groupedEs = equivalenceGroupsBy semanticsName evidence
      groupedHs = equivalenceGroupsBy semanticsName hints'

  -- Checking the representative elements, whether they need FC,
  -- if they do, add occurences for every node in their group.
  es <- filterM (chkQNameForFC . fst) groupedEs
  hs <- filterM (chkQNameForFC . fst) groupedHs
  mapM_ (addEvidence FlexibleContexts) (concatMap snd es)
  mapM_ (addHint     FlexibleContexts) (concatMap snd hs)

  return m
