-- 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 container type similar to "Data.Dynamic" but which retains
-- information about a list of typeclass (or other constraint) that are known to
-- be available for the type of the object contained inside.
module Data.MultiConstrainedDynamic (
                 -- * Types
                 ClassConstraint(..),MCDynamic,
                 -- * Functions that mirror functions in Data.Dynamic
                 toDyn,fromDynamic,fromDyn,dynTypeRep,
                 -- * Extended API for managing and using class constraints
                 dynConstraintTypes,dynAllConstraintTypes,
                 dynUnmatchedConstraintTypes,applyClassFn
                 )
    where

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

-- | A type that contains a value whose type is unknown at compile time,
-- but for which it is known whether or not it satisfies any of a list of
-- constraints, thus allowing operations to be performed when those
-- constraints are satisfied.
data MCDynamic (cs :: [* -> Constraint]) where
    ConsMCD :: (Typeable a, Typeable cs) =>
               a -> LTDict cs a -> MCDynamic cs

--
-- functions that mirror the functions in Data.Dynamic
--

-- | Create an 'MCDynamic' for a given value.  Note that this
-- function must be used in a context where the required list of constraint
-- types can be determined, for example by explicitly identifying the
-- required type using the form @toDyn value :: MCDynamic [TypeClass,...]@.
--
-- Additionally, there must be appropriate instances of the type class 'HasClass'
-- that describe instances available for type classes that are to be used
-- with the dynamic object and the type to be included in it.  In most
-- circumstances you should at least import @Data.Type.HasClassPreludeInstances@
-- to allow the use of instances of standard Prelude classes and types.
toDyn :: (Typeable a, Typeable cs, LTDictBuilder LTDict cs a) =>
         a -> MCDynamic (cs :: [* -> Constraint])
toDyn obj = ConsMCD obj buildLTDict

-- | Extract a value 'MCDynamic' to a particular type if and only if
-- the value contained with in it has that type, returning @'Just' v@ if the
-- value @v@ has the correct type or @'Nothing'@ otherwise,
fromDynamic :: Typeable a => MCDynamic css -> Maybe a
fromDynamic (ConsMCD obj _) = cast obj

-- | Extract a value 'MCDynamic' to a particular type if and only if
-- the value contained with in it has that type, returning the value if it has
-- the correct type or a default value otherwise.
fromDyn :: Typeable a => MCDynamic cs -> a -> a
fromDyn d def = maybe def id $ fromDynamic d

-- | Return the 'TypeRep' for the type of value contained within a
-- 'MCDynamic'.
dynTypeRep :: MCDynamic cs -> TypeRep
dynTypeRep (ConsMCD obj _) = typeOf obj

-- extended API for handling constraints

-- | Return a list of 'TypeRep's that uniquely identify the constraints
-- from an 'MCDynamic's type arguments that are satisfied by the value
-- contained inside the 'MCDynamic'.  The actual type whose representation
-- is returned for each constraint is is @ClassConstraint c@ where @c@ is
-- the constraint.
dynConstraintTypes :: LTDictConstraintLister a  => MCDynamic a -> [TypeRep]
dynConstraintTypes (ConsMCD _  ltd) = getMatchedConstraints ltd

-- | Return a list of 'TypeRep's that uniquely identify the constraints
-- from an 'MCDynamic's type arguments, whether or not they are satisfied by
-- the value contained inside the 'MCDynamic'.  The actual type whose
-- representation is returned for each constraint is is @ClassConstraint c@
-- where @c@ is the constraint.
dynAllConstraintTypes :: LTDictConstraintLister a => MCDynamic a -> [TypeRep]
dynAllConstraintTypes (ConsMCD _  ltd) = getAllConstraints ltd

-- | Return a list of 'TypeRep's that uniquely identify the constraints
-- from an 'MCDynamic's type arguments, that are not satisfied by
-- the value contained inside the 'MCDynamic'.  The actual type whose
-- representation is returned for each constraint is is @ClassConstraint c@
-- where @c@ is the constraint.
dynUnmatchedConstraintTypes :: LTDictConstraintLister a =>
                               MCDynamic a -> [TypeRep]
dynUnmatchedConstraintTypes (ConsMCD _  ltd) = getUnmatchedConstraints ltd


-- | Apply a polymorphic function that accepts all values matching a
-- constraint to the value stored inside an 'MCDynamic' wherever possible.
-- If the constraint is satisfied, returns @'Just' r@, where r is the result
-- of the function.  If the constraint is not satisfied, returns 'Nothing'.
--
-- Note that the function *must* be a polymorphic function
-- with only a single argument that is constrained by the constraint, so
-- for example the function 'show' from the typeclass 'Show' is allowable,
-- but '==' from the typeclass 'Eq' would not work as it requires a
-- second argument that has the same type as the first, and it is not
-- possible to safely return the partially-applied function as its type is
-- not known in the calling context.
applyClassFn :: LTDictSearch css cs => MCDynamic css -> ClassConstraint cs ->
                (forall a . (cs a,Typeable a) => a -> b) -> Maybe b
applyClassFn (ConsMCD obj ltd) pcs f =
    case ltdSearch pcs ltd of
      JustHasClass -> Just $ f obj
      DoesNotHaveClass -> Nothing

-- To do: cast MCDynamics between different constraint lists

-- | An instance of 'Show' for 'MCDynamic': delegates to the
-- contained value's definition of 'showsPrec' if an instance of Show
-- is available, or displays the type of the item and its list of available
-- instances otherwise.
instance (LTDictSearch css Show, LTDictConstraintLister css) =>
    Show (MCDynamic css) where
        showsPrec i d@(ConsMCD obj ltd) =
            case ltdSearch (ClassConstraint :: ClassConstraint Show) ltd of
              JustHasClass -> showsPrec i obj
              DoesNotHaveClass -> showsPrec i (dynTypeRep d) .
                                  showString " " .
                                  showsPrec i (dynConstraintTypes d)

-- | An instance of 'Eq' for 'MCDynamic': unwrap the right hand side
-- to the same type as the left (if possible) and if it matches, delegate to the
-- left hand item's instance of 'Eq' if one is available.  If either condition
-- fails, return 'False'.
instance LTDictSearch css Eq => Eq (MCDynamic css) where
    (ConsMCD objL ltd) == (ConsMCD objR _) =
        case cast objR of
          Nothing    -> False
          Just objRT ->
              case ltdSearch (ClassConstraint :: ClassConstraint Eq) ltd of
                JustHasClass     -> objL == objRT
                DoesNotHaveClass -> False