    {-# LANGUAGE GADTs, DeriveDataTypeable, GeneralizedNewtypeDeriving #-}

    module Propellor.Types.Info (
    	Info(..),
    	InfoEntry(..),
    	IsInfo(..),
    	PropagateInfo(..),
    	addInfo,
    	toInfo,
    	fromInfo,
    	mapInfo,
    	InfoVal(..),
    	fromInfoVal,
    	Typeable,
    ) where

    import Data.Dynamic
    import Data.Maybe
    import Data.Monoid
    import qualified Data.Typeable as T
    import Prelude

    -- | Information about a Host, which can be provided by its properties.
    --
    -- Many different types of data can be contained in the same Info value
    -- at the same time. See `toInfo` and `fromInfo`.
    newtype Info = Info [InfoEntry]
    	deriving (Monoid, Show)

    data InfoEntry where
    	InfoEntry :: (IsInfo v, Typeable v) => v -> InfoEntry

    instance Show InfoEntry where
    	show (InfoEntry v) = show v

    -- Extracts the value from an InfoEntry but only when
    -- it's of the requested type.
    extractInfoEntry :: Typeable v => InfoEntry -> Maybe v
    extractInfoEntry (InfoEntry v) = T.cast v

    -- | Values stored in Info must be members of this class.
    --
    -- This is used to avoid accidentially using other data types
    -- as info, especially type aliases which coud easily lead to bugs.
    -- We want a little bit of dynamic types here, but not too far..
    class (Typeable v, Monoid v, Show v) => IsInfo v where
    	-- | Should this info be propagated out of a container to its Host?
    	propagateInfo :: v -> PropagateInfo

    data PropagateInfo
    	= PropagateInfo Bool
    	| PropagatePrivData
    	-- ^ Info about PrivData generally will be propigated even in cases
    	-- where other Info is not, so it treated specially.

    -- | Any value in the `IsInfo` type class can be added to an Info.
    addInfo :: IsInfo v => Info -> v -> Info
    addInfo (Info l) v = Info (l++[InfoEntry v])

    -- | Converts any value in the `IsInfo` type class into an Info,
    -- which is otherwise empty.
    toInfo :: IsInfo v => v -> Info
    toInfo = addInfo mempty

    fromInfo :: IsInfo v => Info -> v
    fromInfo (Info l) = mconcat (mapMaybe extractInfoEntry l)

    -- | Maps a function over all values stored in the Info that are of the
    -- appropriate type.
    mapInfo :: IsInfo v => (v -> v) -> Info -> Info
    mapInfo f (Info l) = Info (map go l)
      where
    	go i = case extractInfoEntry i of
    		Nothing -> i
    		Just v -> InfoEntry (f v)

    -- | Use this to put a value in Info that is not a monoid.
    -- The last value set will be used. This info does not propagate
    -- out of a container.
    data InfoVal v = NoInfoVal | InfoVal v
    	deriving (Typeable, Show)

    instance Monoid (InfoVal v) where
    	mempty = NoInfoVal
    	mappend _ v@(InfoVal _) = v
    	mappend v NoInfoVal = v

    instance (Typeable v, Show v) => IsInfo (InfoVal v) where
    	propagateInfo _ = PropagateInfo False

    fromInfoVal :: InfoVal v -> Maybe v
    fromInfoVal NoInfoVal = Nothing
    fromInfoVal (InfoVal v) = Just v