-- | Exposes a high-level interface for starting a node of a distributed
-- program, taking into account a local configuration file, command
-- line arguments, and commonly-used system processes.
module Remote.Init (remoteInit) where

import qualified Prelude as Prelude
import Prelude hiding (lookup)

import Remote.Peer (startDiscoveryService)
import Remote.Task (__remoteCallMetaData)
import Remote.Process (startProcessRegistryService,suppressTransmitException,localRegistryRegisterNode,localRegistryHello,localRegistryUnregisterNode,
                       startProcessMonitorService,startNodeMonitorService,startLoggingService,startSpawnerService,ProcessM,readConfig,initNode,startLocalRegistry,
                       forkAndListenAndDeliver,waitForThreads,roleDispatch,Node,runLocalProcess,performFinalization,startFinalizerService)
import Remote.Reg (registerCalls,RemoteCallMetaData)

import System.Environment (getEnvironment)
import Control.Concurrent (threadDelay)
import Control.Monad.Trans (liftIO)
import Control.Exception (finally)
import Control.Concurrent.MVar (MVar,takeMVar,putMVar,newEmptyMVar)

startServices :: ProcessM ()
startServices = 
           do
              startProcessRegistryService
              startNodeMonitorService
              startProcessMonitorService
              startLoggingService
              startDiscoveryService
              startSpawnerService
              startFinalizerService (suppressTransmitException localRegistryUnregisterNode >> return ())

dispatchServices :: MVar Node -> IO ()
dispatchServices node = do mv <- newEmptyMVar
                           _ <- runLocalProcess node (startServices >> liftIO (putMVar mv ()))
                           takeMVar mv

-- | This is the usual way create a single node of distributed program.
-- The intent is that 'remoteInit' be called in your program's 'Main.main'
-- function. A typical call takes this form:
--
-- > main = remoteInit (Just "config") [Main.__remoteCallMetaData] initialProcess
--
-- This will:
--
-- 1. Read the configuration file @config@ in the current directory or, if specified, from the file whose path is given by the environment variable @RH_CONFIG@. If the given file does not exist or is invalid, an exception will be thrown.
--
-- 2. Use the configuration given in the file as well as on the command-line to create a new node. The usual system processes will be started, including logging, discovery, and spawning.
--
-- 3. Compile-time metadata, generated by 'Remote.Call.remotable', will used for invoking closures. Metadata from each module must be explicitly mentioned.
--
-- 4. The function initialProcess will be called, given as a parameter a string indicating the value of the cfgRole setting of this node. initialProcess is provided by the user and provides an entrypoint for controlling node behavior on startup.
remoteInit :: Maybe FilePath -> [RemoteCallMetaData] -> (String -> ProcessM ()) -> IO ()
remoteInit defaultConfig metadata f = 
       let
          defaultMetaData = [Remote.Task.__remoteCallMetaData]
          lookup = registerCalls (defaultMetaData ++ metadata)
       in
       do
          configFileName <- getConfigFileName
          cfg <- readConfig True configFileName
              -- TODO sanity-check cfg
          node <- initNode cfg lookup
          _ <- startLocalRegistry cfg False -- potentially fails silently
          forkAndListenAndDeliver node cfg
          dispatchServices node
          (roleDispatch node userFunction >> waitForThreads node) `finally` (performFinalization node)
          threadDelay 500000 -- TODO make configurable, or something
   where  getConfigFileName = do env <- getEnvironment
                                 return $ maybe defaultConfig Just (Prelude.lookup "RH_CONFIG" env)
          userFunction s = localRegistryHello >> localRegistryRegisterNode >> f s