{-# LANGUAGE ScopedTypeVariables, MagicHash, ExistentialQuantification, GeneralizedNewtypeDeriving, DeriveDataTypeable #-}
-- Copyright (c) Jean-Philippe Bernardy 2005-2007.

module Yi.Dynamic 
  -- * Nonserializable (\"config\") dynamics
  ConfigVariables, configVariableA,
  -- * Serializable dynamics
  DynamicValues, dynamicValueA,

import Prelude ()
import Yi.Prelude

import Data.Maybe(fromJust)
import Data.HashMap.Strict as M
import Data.Monoid
import Data.ConcreteTypeRep
import Data.Binary
import Data.Typeable(cast)
import Data.ByteString.Lazy(ByteString)
import Data.IORef
import System.IO.Unsafe(unsafePerformIO)
import qualified Data.Dynamic as D

--------------------------------- Nonserializable dynamics
-- | Class of values that can go in a 'ConfigDynamic' or a 'ConfigDynamicValues'.
-- These will typically go in a 'Config'. As the 'Config' has no mutable state,
-- there is no need to serialize these values: if needed, they will be set in the
-- user's configuration file. The 'Initializable' constraint ensures that, even if
-- the user hasn't customised this config variable, a value is stil available.
class (Initializable a, Typeable a) => YiConfigVariable a

-- | An \"extensible record\" of 'YiConfigVariable's. Can be constructed and accessed with 'initial' and 'configVariableA'.
-- This type can be thought of as a record containing /all/ 'YiConfigVariable's in existence.
newtype ConfigVariables = CV (M.HashMap ConcreteTypeRep D.Dynamic)

instance Initializable ConfigVariables where initial = mempty

-- | Accessor for any 'YiConfigVariable'. Neither reader nor writer can fail:
-- if the user's config file hasn't set a value for a 'YiConfigVariable',
-- then the default value is used.
configVariableA :: forall a. YiConfigVariable a => Accessor ConfigVariables a
configVariableA = accessor getCV setCV
      setCV v (CV m) = CV (M.insert (cTypeOf (undefined :: a)) (D.toDyn v) m)
      getCV (CV m) = 
         case M.lookup (cTypeOf (undefined :: a)) m of
             Nothing -> initial
             Just x -> fromJust $ D.fromDynamic x

-- | Class of values that can go in a 'Dynamic' or a 'DynamicValues'. These are
-- typically for storing custom state in a 'FBuffer' or an 'Editor'.
class (Initializable a, Binary a, Typeable a) => YiVariable a

--------------------------- Serializable dynamics
[Serialization and the use of unsafePerformIO]
To implement deserialization, we store the value as a ByteString (i.e. in its
serialized form) until someone tries to read from the Dynamic (at which time we
have access to the deserializer). To avoid having to repeatedly deserialize when
reading, we cheat (unsafePerformIO) and cache the deserialized value.

A pure implementation would be possible if we omitted this caching, which gives
at least some justification for the impurity.

Currently, we don't export 'Dynamic' as there are no users of it in Yi. It is 
hard to see where a 'Dynamic' would be preferable to a 'DynamicValues'.

-- | Serializable, initializable dynamically-typed values.
newtype Dynamic = D (IORef DynamicHelper)

data DynamicHelper
  = forall a. YiVariable a => Dynamic !a
  | Serial !ConcreteTypeRep !ByteString

-- | Build a 'Dynamic'
toDyn :: YiVariable a => a -> Dynamic
toDyn a = D (unsafePerformIO (newIORef $! Dynamic a))

-- | Try to extract a value from the 'Dynamic'.
fromDynamic :: forall a. YiVariable a => Dynamic -> Maybe a
fromDynamic (D r) = unsafePerformIO (readIORef r >>= f) where
   f (Dynamic b) = return $ cast b
   f (Serial tr bs) = 
      if cTypeOf (undefined :: a) == tr 
      then do
          let b = decode bs
          writeIORef r (Dynamic b)
          return (Just b)
      else return Nothing

-- | Converts a dynamic to a serializable value
toSerialRep :: Dynamic -> (ConcreteTypeRep, ByteString)
toSerialRep (D r) = 
  case unsafePerformIO (readIORef r) of
      Dynamic a -> (cTypeOf a, encode a)
      Serial ctr bs -> (ctr, bs)

-- | Converts a serializable value to a dynamic.
fromSerialRep :: (ConcreteTypeRep, ByteString) -> Dynamic
fromSerialRep (ctr, bs) = D (unsafePerformIO (newIORef (Serial ctr bs)))

instance Binary Dynamic where
    put = put . toSerialRep
    get = fromSerialRep <$> get

---------------------- Dynamic records
-- | An extensible record, indexed by type.
newtype DynamicValues = DV (M.HashMap ConcreteTypeRep Dynamic)
  deriving(Typeable, Monoid)

-- | Accessor for a dynamic component. If the component is not found, the value 'initial' is used.
dynamicValueA :: forall a. YiVariable a => Accessor DynamicValues a
dynamicValueA = accessor getDynamicValue setDynamicValue
      setDynamicValue :: a -> DynamicValues -> DynamicValues
      setDynamicValue v (DV dv) = DV (M.insert (cTypeOf (undefined :: a)) (toDyn v) dv)

      getDynamicValue :: DynamicValues -> a
      getDynamicValue (DV dv) = case M.lookup (cTypeOf (undefined::a)) dv of
                                   Nothing -> initial
                                   Just x -> fromJust $ fromDynamic x

instance Binary DynamicValues where
    put (DV dv) = put dv
    get = DV <$> get

instance Initializable DynamicValues where  initial = mempty

TODO: since a 'DynamicValues' is now serialisable, it could potentially
exist for a long time (days/months?). No operations are provided to remove
entries from a 'DynamicValues'. If these start accumulating a lot of junk,
it may be necessary to prune them (perhaps keep track of access date for
'YiVariable's and remove the ones more than a month old?).