-- Copyright 2016 Julian Hall.  See LICENSE file at top level for details.

-- TODO - are we still using all of these extensions?
{-# LANGUAGE GADTs,ConstraintKinds,RankNTypes,FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables,KindSignatures,PolyKinds #-}
{-# LANGUAGE TypeFamilies,MultiParamTypeClasses,UndecidableInstances #-}
{-# LANGUAGE DataKinds,TypeOperators,FlexibleContexts #-}

-- | Provides a type for keeping a list of typeclass dictionaries that may or
-- may not exist for a given type, and utility functions for working with the
-- list.
module Data.Type.LTDict where

import GHC.Exts (Constraint)
import Data.ConstrainedDynamic(ClassConstraint(..))
import Data.Typeable
import Data.Type.HasClass

-- | Optionally provide a dictionary for a constraint and a type.
data MaybeHasClass :: (* -> Constraint) -> * -> * where
    JustHasClass     :: cs t => MaybeHasClass cs t
    DoesNotHaveClass :: MaybeHasClass cs t

-- | Maintain a list of dictionaries encapsulating many constraints
-- over a single type (where they are available).
data LTDict :: [* -> Constraint] -> * -> * where
    LTDCons :: MaybeHasClass cs t -> LTDict css t -> LTDict (cs ': css) t
    LTDNil  :: LTDict '[] t

class TypeConstraintBuilder mhctype (cs :: * -> Constraint) t (flag :: k) where
    buildTypeConstraint :: proxy flag -> mhctype cs t

instance (HasClass cs t True) =>
    TypeConstraintBuilder MaybeHasClass cs t True where
        buildTypeConstraint _ =
            case classDict (Proxy :: Proxy cs) (Proxy :: Proxy t)
                           (Proxy :: Proxy true) of
              TDict -> JustHasClass

instance {-# OVERLAPPABLE #-} f ~ False =>
    TypeConstraintBuilder MaybeHasClass cs t f where
        buildTypeConstraint _ = DoesNotHaveClass

-- | Produce an appropriate 'MaybeHasClass' value for the context.
-- Callers should ensure that appropriate 'HasClass' instances are in scope
-- at the point of calling, or declare them as part of their context.
checkClass :: forall cs t f .
              (HasClass cs t f, TypeConstraintBuilder MaybeHasClass cs t f) =>
              MaybeHasClass cs t
checkClass = buildTypeConstraint (Proxy :: Proxy f)

-- | A class for building objects that recurse over a list of constraints.
-- Instances are provided for building 'LTDict' values, but could be used
-- for other values too.
class LTDictBuilder dtype (css :: [(* -> Constraint)]) t where
    buildLTDict :: dtype css t

instance (HasClass cs t f,
          TypeConstraintBuilder MaybeHasClass cs t f,
          LTDictBuilder LTDict css t)
              => LTDictBuilder LTDict (cs ': css) t where
    buildLTDict = LTDCons checkClass (buildLTDict :: LTDict css t)

instance LTDictBuilder LTDict '[] t where
    buildLTDict = LTDNil

-- | Functions for extracting the list of constraints in an LTDict
-- as 'TypeRep's of 'ClassConstraint' types.
class LTDictConstraintLister (css :: [(* -> Constraint)]) where
    -- Return all constraints in the list
    getAllConstraints :: LTDict css a -> [TypeRep]
    -- Return only the matched constraints in the list
    getMatchedConstraints :: LTDict css a -> [TypeRep]
    -- Return only the unmatched constraints in the list
    getUnmatchedConstraints :: LTDict css a -> [TypeRep]

instance LTDictConstraintLister '[] where
    getAllConstraints _ = []
    getMatchedConstraints _ = []
    getUnmatchedConstraints _ = []

instance (Typeable cs, LTDictConstraintLister css) => LTDictConstraintLister (cs ': css) where
    getAllConstraints (LTDCons _ t) =
        typeOf (ClassConstraint :: ClassConstraint cs) :
               getAllConstraints t
    getMatchedConstraints (LTDCons JustHasClass t) =
        typeOf (ClassConstraint :: ClassConstraint cs) :
               getMatchedConstraints t
    getMatchedConstraints (LTDCons DoesNotHaveClass t) =
        getMatchedConstraints t
    getUnmatchedConstraints (LTDCons DoesNotHaveClass t) =
        typeOf (ClassConstraint :: ClassConstraint cs) :
               getUnmatchedConstraints t
    getUnmatchedConstraints (LTDCons JustHasClass t) =
        getUnmatchedConstraints t

-- | A class for handling searches for a specific single constraint in the
-- list.
class LTDictSearch (css :: [(* -> Constraint)]) (cs :: * -> Constraint) where
    ltdSearch :: proxy cs -> LTDict css a -> MaybeHasClass cs a

instance LTDictSearch '[] cs where
    ltdSearch _ _ = DoesNotHaveClass

instance LTDictSearch (cs ': css) cs where
    ltdSearch _ (LTDCons m _) = m

instance {-# OVERLAPPABLE #-} LTDictSearch css cs =>
    LTDictSearch (unmatched ': css) cs where
        ltdSearch p (LTDCons _ t) = ltdSearch p t