content: {-# LANGUAGE Arrows #-}
  {-# LANGUAGE FlexibleContexts #-}
  {-# LANGUAGE MultiParamTypeClasses #-}
  {-# LANGUAGE RecordWildCards #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  
  module LiveCoding.Handle where
  
  -- base
  import Control.Arrow (arr, (>>>))
  import Data.Data
  
  -- transformers
  import Control.Monad.Trans.Class (MonadTrans (lift))
  
  -- mmorph
  import Control.Monad.Morph
  
  -- essence-of-live-coding
  import LiveCoding.Cell
  import LiveCoding.HandlingState
  import LiveCoding.Migrate.NoMigration
  
  {- | Container for unserialisable values,
  such as 'IORef's, threads, 'MVar's, pointers, and device handles.
  
  In a 'Handle', you can store a mechanism to create and destroy a value
  that survives reloads occuring during live coding
  even if does not have a 'Data' instance.
  Using the function 'handling', you can create a cell that will
  automatically initialise your value,
  and register it in the 'HandlingStateT' monad transformer,
  which takes care of automatically destroying it (if necessary) when it does not occur anymore in a later revision of your live program.
  
  Have a look at 'LiveCoding.Handle.Examples' for some ready-to-use implementations.
  
  In short, 'Handle' is an opaque, automatically constructing and garbage collecting container for arbitrary values in the live coding environment.
  -}
  data Handle m h = Handle
    { create :: m h
    , destroy :: h -> m ()
    }
  
  instance MFunctor Handle where
    hoist morphism Handle {..} =
      Handle
        { create = morphism create
        , destroy = morphism . destroy
        }
  
  {- | Combine two handles to one.
  
  'Handle's are not quite 'Monoid's because of the extra type parameter,
  but it is possible to combine them.
  In the combined handle, the first handle is created first and destroyed last.
  
  Note: 'Handle' is not an 'Applicative' because it is not a 'Functor'
  (because the destructor is contravariant in @h@).
  -}
  combineHandles :: (Applicative m) => Handle m h1 -> Handle m h2 -> Handle m (h1, h2)
  combineHandles handle1 handle2 =
    Handle
      { create = (,) <$> create handle1 <*> create handle2
      , destroy = \(h1, h2) -> destroy handle2 h2 *> destroy handle1 h1
      }
  
  {- | Hide a handle in a cell,
  taking care of initialisation and destruction.
  
  Upon the first tick (or directly after migration),
  the 'create' method of the 'Handle' is called,
  and the result stored.
  This result is then not changed anymore until the cell is removed again.
  Once it is removed, the destructor will be called on the next tick.
  
  Migrations will by default not inspect the interior of a 'handling' cell.
  This means that handles are only migrated if they have exactly the same type.
  -}
  handling ::
    ( Typeable h
    , Monad m
    ) =>
    Handle m h ->
    Cell (HandlingStateT m) arbitrary h
  handling handle = arr (const ()) >>> handlingParametrised (toParametrised handle)
  
  {- | Generalisation of 'Handle' carrying an additional parameter which may change at runtime.
  
  Like in a 'Handle', the @h@ value of a 'ParametrisedHandle' is preserved through live coding reloads.
  Additionally, the parameter @p@ value can be adjusted,
  and triggers a destruction and reinitialisation whenever it changes.
  -}
  data ParametrisedHandle p m h = ParametrisedHandle
    { createParametrised :: p -> m h
    , changeParametrised :: p -> p -> h -> m h
    , destroyParametrised :: p -> h -> m ()
    }
  
  instance MFunctor (ParametrisedHandle p) where
    hoist morphism ParametrisedHandle {..} =
      ParametrisedHandle
        { createParametrised = morphism . createParametrised
        , changeParametrised = ((morphism .) .) . changeParametrised
        , destroyParametrised = (morphism .) . destroyParametrised
        }
  
  {- | Given the methods 'createParametrised' and 'destroyParametrised',
     build a fitting method for 'changeParametrised' which
  -}
  defaultChange :: (Eq p, Monad m) => (p -> m h) -> (p -> h -> m ()) -> p -> p -> h -> m h
  defaultChange creator destructor pOld pNew h
    | pOld == pNew = return h
    | otherwise = do
        destructor pOld h
        creator pNew
  
  -- | Like 'combineHandles', but for 'ParametrisedHandle's.
  combineParametrisedHandles ::
    (Applicative m) =>
    ParametrisedHandle p1 m h1 ->
    ParametrisedHandle p2 m h2 ->
    ParametrisedHandle (p1, p2) m (h1, h2)
  combineParametrisedHandles handle1 handle2 =
    ParametrisedHandle
      { createParametrised = \(p1, p2) -> (,) <$> createParametrised handle1 p1 <*> createParametrised handle2 p2
      , changeParametrised = \(pOld1, pOld2) (pNew1, pNew2) (h1, h2) -> (,) <$> changeParametrised handle1 pOld1 pNew1 h1 <*> changeParametrised handle2 pOld2 pNew2 h2
      , destroyParametrised = \(p1, p2) (h1, h2) -> destroyParametrised handle1 p1 h1 *> destroyParametrised handle2 p2 h2
      }
  
  {- | Hide a 'ParametrisedHandle' in a cell,
  taking care of initialisation and destruction.
  
  Upon the first tick, directly after migration, and after each parameter change,
  the 'create' method of the 'Handle' is called,
  and the result stored.
  This result is then not changed anymore until the cell is removed again, or the parameter changes.
  A parameter change triggers the destructor immediately,
  but if the cell is removed, the destructor will be called on the next tick.
  
  Migrations will by default not inspect the interior of a 'handling' cell.
  This means that parametrised handles are only migrated if they have exactly the same type.
  -}
  handlingParametrised ::
    ( Typeable h
    , Typeable p
    , Monad m
    , Eq p
    ) =>
    ParametrisedHandle p m h ->
    Cell (HandlingStateT m) p h
  handlingParametrised handleImpl@ParametrisedHandle {..} = Cell {..}
    where
      cellState = Uninitialized
      cellStep Uninitialized parameter = do
        mereHandle <- lift $ createParametrised parameter
        let handle = (mereHandle, parameter)
        key <- register $ destroyParametrised parameter mereHandle
        return (mereHandle, Initialized Handling {handle = handle, ..})
      cellStep handling@(Initialized Handling {handle = (mereHandle, lastParameter), ..}) parameter
        | parameter == lastParameter = do
            reregister (destroyParametrised parameter mereHandle) key
            return (mereHandle, handling)
        | otherwise = do
            mereHandle <- lift $ changeParametrised lastParameter parameter mereHandle
            reregister (destroyParametrised parameter mereHandle) key
            return (mereHandle, Initialized Handling {handle = (mereHandle, parameter), ..})
  
  {- | Every 'Handle' is trivially a 'ParametrisedHandle'
     when the parameter is the trivial type.
  -}
  toParametrised :: (Monad m) => Handle m h -> ParametrisedHandle () m h
  toParametrised Handle {..} =
    ParametrisedHandle
      { createParametrised = const create
      , changeParametrised = const $ const return
      , destroyParametrised = const destroy
      }
  