{-|
Module      : Game.GoreAndAsh.Sync.Remote.Sync
Description : EDSL for automatic synchronization
Copyright   : (c) Anton Gushcha, 2015-2016
License     : BSD3
Maintainer  : ncrashed@gmail.com
Stability   : experimental
Portability : POSIX
-}
module Game.GoreAndAsh.Sync.Remote.Sync(
  -- * Remote actor API
    Sync(..)
  , FullSync
  , RemoteActor(..)
  , noSync
  , clientSide
  , serverSide
  , condSync
  , syncReject
  -- * Helpers for conditional synchronization
  , fieldChanges
  , fieldChangesWithin
  -- * Dictionary utils
  , Dict(..)
  , encodish
  , decodish
  ) where

import Control.Wire
import Control.Wire.Unsafe.Event
import Data.Serialize
import Data.Word 
import qualified Data.ByteString as BS 
import Prelude hiding (id, (.))

import Game.GoreAndAsh
import Game.GoreAndAsh.Network
import Game.GoreAndAsh.Sync.Message 

-- | Reify typeclass to dictionary
data Dict ctxt where
  Dict :: ctxt => Dict ctxt

-- | Use serialize dictionary to call @encode@
encodish :: Dict (Serialize a) -> a -> BS.ByteString 
encodish Dict = encode 

-- | Use serialize dictionary to call @decode@
decodish :: Dict (Serialize a) -> BS.ByteString -> Either String a
decodish Dict = decode 

-- | Special monad that keeps info about synchronization logic 
-- between client and server for type @a@. Remote collection 
-- uses the description to generate special code to automatic
-- synchronization of shared actor state.
--
-- [@m@] means underlying game monad, that will be used during synchronization
-- 
-- [@i@] means actor unique id type
--
-- [@s@] means actor state that is beeing syncing. As soon as you crafted
--   'Sync i s s' it means you defined full description how to sync actor state.
--
-- [@a@] is actual value type that the 'Sync' value is describing synchronization for.
--   As soon as you crafted 'Sync i s s' it means you defined full description how to sync actor state.
data Sync m i s a where
  SyncPure :: a -> Sync m i s a -- Statically known value
  SyncNone :: (s -> a) -> Sync m i s a -- No synchronization, take local value
  SyncClient :: Dict (Eq a, Serialize a, RemoteActor i s) -> Peer -> !Word64 -> (s -> a) -> Sync m i s a -- The value is controlled by client and synched to server.
  SyncServer :: Dict (Serialize a, RemoteActor i s) -> !Word64 -> (s -> a) -> Sync m i s a -- The value is controlled by server and synched to clients.
  SyncCond :: GameWire m s (Event ()) -> (s -> a) -> Sync m i s a -> Sync m i s a -- Conditional synchronization
  SyncReject :: Dict (Serialize a, RemoteActor i s) -> GameWire m (s, a) (Event a) -> !Word64 -> Sync m i s a -> Sync m i s a -- Validate synchronized value, rollback if failed
  SyncApp :: Sync m i s (a -> b) -> Sync m i s a -> Sync m i s b -- Applicative application of actions

instance Functor (Sync m i s) where
  fmap f s = case s of 
    SyncPure a -> SyncPure (f a)
    _ -> SyncApp (SyncPure f) s

instance Applicative (Sync m i s) where
  pure = SyncPure
  sf <*> s = SyncApp sf s

-- | Type synonim for those Sync DSL programs that defines full synchronization of actor state
type FullSync m i s = Sync m i s s

-- | API to support automatic synchronization of actors between client and server
class NetworkMessage i => RemoteActor i a | i -> a, a -> i where
  -- | State of remote actor (should be equal a)
  type RemoteActorState i :: *
  -- | Id of remote actor (should be equal i)
  type RemoteActorId a :: *

-- | Perphoms no synchronization, the sync primitive returns local value of field
noSync :: (s -> a) -- ^ Getter of the field
  -> Sync m i s a
noSync = SyncNone 

-- | Declares that state field is client side, i.e. it is produced in client actor
-- and then sent to server. For peers that are not equal to specified (owner of the field)
-- the sync behavior acts as @serverSide@.
--
-- If server side changes the value manually, client is forced to new server side value.
clientSide :: (Eq a, Serialize a, RemoteActor i s)
  => Peer -- ^ Which peer controls the field, sync messages from other peers are not processed
  -> Word64 -- ^ Field id, other side actor should define @clientSide@ with matching id
  -> (s -> a) -- ^ Field getter
  -> Sync m i s a
clientSide peer !w getter = SyncClient Dict peer w getter

-- | Declares that state field is server side, i.e. it is produced in server actor
-- and then sent to all clients.
--
-- Clients cannot change the value manually.
serverSide :: (Serialize a, RemoteActor i s)
  => Word64 -- ^ Field id, other side actor should define @serverSide@ with matching id
  -> (s -> a) -- ^ Field getter
  -> Sync m i s a
serverSide !w getter = SyncServer Dict w getter

-- | Makes synchronization appear only when given wire produces an event.
--
-- Note: intended to use with 'serverSide'
condSync :: Monad m => GameWire m s (Event b) -- ^ Wire that produces events when sync should be done
  -> (s -> a) -- ^ Field getter
  -> Sync m i s a -- ^ Sub action that should be done when sync event is produced
  -> Sync m i s a
condSync w getter ms = SyncCond (mapE (const ()) . w) getter ms

-- | Produces event when given field is changed
fieldChanges :: Eq a => (s -> a) -- ^ Field getter
  -> GameWire m s (Event a)
fieldChanges getter = mkSFN $ \s -> let a = getter s in a `seq` (Event a, go a)
  where
    go a = mkSFN $ \s -> if a == getter s 
      then (NoEvent, go a)
      else let a2 = getter s in a2 `seq` (Event a2, go a2)

-- | Produces event when given field is changed
fieldChangesWithin :: (Num a, Ord a) => (s -> a) -- ^ Field getter
  -> a -- ^ Delta, variation greater than the value is treated as change
  -> GameWire m s (Event a)
fieldChangesWithin getter delta = mkSFN $ \s -> let a = getter s in a `seq` (Event a, go a)
  where
    go a = mkSFN $ \s -> let a2 = getter s in if abs (a - a2) < delta
      then (NoEvent, go a)
      else a2 `seq` (Event a2, go a2)

-- | There are sometimes net errors or malicios data change in remote actor,
-- the action provides you ability to reject incorrect values and resync remote
-- actor to fallback value.
--
-- Note: intended to use with 'serverSide'
syncReject :: (Serialize a, RemoteActor i s)
  => GameWire m (s, a) (Event a) -- ^ Fires event when the synced value is invalid, event carries new value that should be placed and sended to remote peer
  -> Word64 -- ^ Id of field to resync at remote host when failed
  -> Sync m i s a -- ^ Sub action that produces synced values for first argument
  -> Sync m i s a
syncReject w wid ms = SyncReject Dict w wid ms