module Data.PropertyList.KeyPath 
    ( alterItemAtKeyPathM, alterItemAtKeyPath
    , getItemAtKeyPath, setItemAtKeyPath
    ) where

import Control.Monad
import Control.Monad.Trans.State
import Data.Functor.Identity
import qualified Data.Map as M
import Data.PropertyList.Algebra
import Data.PropertyList.PropertyListItem
import Data.PropertyList.Types (PropertyList)

-- |Alter a @'Maybe' 'PropertyList'@, viewing it as an instance of 'PropertyListItem'
-- and re-synthesizing it from a (possibly different) instance of 'PropertyListItem'.
-- Input and output are lifted by 'Maybe' in order to support adding or deleting of
-- items (as this function is used inside the key-path based versions)
--
-- Actually only needs 'Functor', not 'Monad' but since it's not exported
-- and is only used in 'Monad' contexts, I'm using 'Monad' to avoid needing
-- to add 'Functor' to those other contexts.
{-# INLINE alterPropertyListM #-}
alterPropertyListM ::
     (Monad m, PropertyListItem i, PropertyListItem i') 
     => (Maybe i -> m (Maybe i'))
     -> Maybe PropertyList -> m (Maybe PropertyList)
alterPropertyListM f = liftM (fmap toPropertyList) . f . (>>= fromPropertyList)

-- |Alter the contents of a dictionary (represented as a M.Map String), 
-- viewing its values through the 'PropertyListItem' class.
--
-- If the result of the alteration is 'Nothing', and the resulting dictionary
-- is empty, the result of the whole operation is 'Nothing' (causing cascading
-- deletion of empty key paths in 'alterDictionaryEntryM' et al.)
{-# INLINE alterDictionaryEntryM #-}
alterDictionaryEntryM ::
    (Monad m, PropertyListItem i, PropertyListItem i') 
    => String -> (Maybe i -> m (Maybe i'))
    -> Maybe (M.Map String PropertyList) -> m (Maybe (M.Map String PropertyList))
alterDictionaryEntryM k f Nothing = do
    i' <- f Nothing
    return (fmap (M.singleton k . toPropertyList) i')
alterDictionaryEntryM k f (Just dict) = do
    let (dict', i) = case M.splitLookup k dict of
            (pre, v, post) -> (M.union pre post, fromPropertyList =<< v)
    
    i' <- f i
    return $ case i' of
        Nothing
            | M.null dict'  -> Nothing
            | otherwise     -> Just dict'
        Just i'' -> Just (M.insert k (toPropertyList i'') dict)

-- |Attempt to view a 'PropertyList' as a 'M.Map String' and, if successful,
-- modify an entry of the dictionary (by calling 'alterDictionaryEntryM').
-- If not possible, 'fail's.
--
-- If the result of the alteration is 'Nothing', and the resulting dictionary
-- is empty, that dictionary is deleted in the result.
{-# SPECIALIZE tryAlterDictionaryEntryM :: (PropertyListItem i, PropertyListItem i')
                                        => String 
                                        -> (Maybe i -> Identity (Maybe i'))
                                        -> Maybe PropertyList -> Identity (Maybe PropertyList)
  #-}
{-# SPECIALIZE tryAlterDictionaryEntryM :: (PropertyListItem i)
                                        => String 
                                        -> (Maybe i -> StateT (Maybe i) Maybe (Maybe i))
                                        -> Maybe PropertyList -> StateT (Maybe i) Maybe (Maybe PropertyList)
  #-}
tryAlterDictionaryEntryM ::
    (Monad m, PropertyListItem i, PropertyListItem i') 
    => String -> (Maybe i -> m (Maybe i'))
    -> Maybe PropertyList -> m (Maybe PropertyList)
tryAlterDictionaryEntryM k f mbPl = 
    case fmap fromPlDict mbPl of
        -- outer 'Maybe' is 'Just' if a plist was provided, 
        -- inner is 'Just' if that plist is a dictionary.
        Just (Just d)   -> alterDict (Just d)
        Nothing         -> alterDict Nothing
        Just Nothing    -> fail "Key path tries to pass through non-dictionary thing."
    where alterDict = liftM (fmap plDict) . alterDictionaryEntryM k f

-- |@alterItemAtKeyPathM path f@ applies the function @f@ deep inside the 
-- 'PropertyList' on the property list item at the given key-path @path@
-- (if possible).  This is the same notion of key path as is used in the 
-- Apple plist APIs - each component of the path indicates descending 
-- into a dictionary by selecting the element with that key (if any).  If a 
-- key is not found, it is created.  If a key is found but is not a 
-- dictionary, the operation fails (with 'fail' from the 'Monad' class).
-- 
-- If the result of @f@ is 'Nothing', and the resulting dictionary is empty,
-- that dictionary is deleted in the result (and any empty parent dictionaries).
-- If this is not the behavior you want, you should alter the parent dictionary 
-- itself and return an empty one.
{-# SPECIALIZE alterItemAtKeyPathM :: (PropertyListItem i, PropertyListItem i')
                                   => [String] 
                                   -> (Maybe i -> Identity (Maybe i'))
                                   -> Maybe PropertyList -> Identity (Maybe PropertyList)
  #-}
{-# SPECIALIZE alterItemAtKeyPathM :: (PropertyListItem i)
                                   => [String] 
                                   -> (Maybe i -> StateT (Maybe i) Maybe (Maybe i))
                                   -> Maybe PropertyList -> StateT (Maybe i) Maybe (Maybe PropertyList)
  #-}
alterItemAtKeyPathM ::
    (Monad m, PropertyListItem i, PropertyListItem i')
    => [String] -> (Maybe i -> m (Maybe i'))
    -> Maybe PropertyList -> m (Maybe PropertyList)
alterItemAtKeyPathM [] f = alterPropertyListM f
alterItemAtKeyPathM (k:ks) f = tryAlterDictionaryEntryM k (alterItemAtKeyPathM ks f)

-- |@alterItemAtKeyPath path f@ applies the function @f@ deep inside the 
-- 'PropertyList' on the property list item at the given key-path @path@
-- (if possible).  This is the same notion of key path as is used in the 
-- Apple plist APIs - namely, each component of the path indicates descending 
-- into a dictionary by selecting the element with that key (if any).  If a 
-- key is not found, it is created.  If a key is found but is not a 
-- dictionary, the operation fails (with 'error').
-- 
-- If the result of @f@ is 'Nothing', and the resulting dictionary is empty,
-- that dictionary is deleted in the result (and any empty parent dictionaries).
-- If this is not the behavior you want, you should alter the parent dictionary 
-- itself and return an empty one.
alterItemAtKeyPath :: 
    (PropertyListItem i, PropertyListItem i')
    => [String] -> (Maybe i -> Maybe i')
    -> Maybe PropertyList -> Maybe PropertyList
alterItemAtKeyPath path f = runIdentity . alterItemAtKeyPathM path (Identity . f)

-- |Gets the item, if any (and if convertible to the required type), 
-- at a given key path.  If the key path passes through something that 
-- is not a dictionary, the operation returns 'Nothing'.
getItemAtKeyPath :: PropertyListItem i =>
    [String] -> Maybe PropertyList -> Maybe i
    -- works by running a StateT (Maybe i) Maybe operation at the keypath.
    -- The operation captures the value it is passed and 'put's it into the
    -- state.  The result is the final state of the operation 'join'ed to
    -- the success/failure in the underlying 'Maybe' monad (which is why
    -- we use 'StateT' instead of just 'State' - the latter would cause
    -- failure by 'error' since it's equivalent to @'StateT' s 'Identity'@).
getItemAtKeyPath path plist = join $ execStateT 
    (alterItemAtKeyPathM path (\e -> put e >> return e) plist)
    Nothing

-- |Sets the item at a given key-path.  If the key path does not exist, it is
-- created.  If it exists but passes through something that is not a dictionary,
-- the operation fails (with 'error')
setItemAtKeyPath :: PropertyListItem i =>
    [String] -> Maybe i -> Maybe PropertyList -> Maybe PropertyList
setItemAtKeyPath path value plist = alterItemAtKeyPath path 
    (\e -> value `asTypeOf` e) plist