{-|
    Code for setting up the RIO environment.
-}
module Urbit.King.App
  ( KingEnv
  , runKingEnvStderr
  , runKingEnvStderrRaw
  , runKingEnvLogFile
  , runKingEnvNoLog
  , kingEnvKillSignal
  , killKingActionL
  , onKillKingSigL
  , HostEnv
  , runHostEnv
  , PierEnv
  , runPierEnv
  , killPierActionL
  , onKillPierSigL
  , HasStderrLogFunc(..)
  , HasKingId(..)
  , HasProcId(..)
  , HasKingEnv(..)
  , HasMultiEyreApi(..)
  , HasHostEnv(..)
  , HasPierEnv(..)
  , module Urbit.King.Config
  )
where

import Urbit.King.Config
import Urbit.Prelude

import RIO                    (logGeneric)
import System.Directory       ( createDirectoryIfMissing
                              , getXdgDirectory
                              , XdgDirectory(XdgCache)
                              )
import System.Posix.Internals (c_getpid)
import System.Posix.Types     (CPid(..))
import System.Random          (randomIO)
import Urbit.King.App.Class   (HasStderrLogFunc(..))
import Urbit.Vere.Eyre.Multi  (MultiEyreApi)
import Urbit.Vere.Ports       (PortControlApi, HasPortControlApi(..))


-- KingEnv ---------------------------------------------------------------------

class HasKingId a where
  kingIdL :: Lens' a Word16

class HasProcId a where
  procIdL :: Lens' a Int32

class (HasLogFunc a, HasStderrLogFunc a, HasKingId a, HasProcId a)
   => HasKingEnv a
 where
  kingEnvL :: Lens' a KingEnv

data KingEnv = KingEnv
  { _kingEnvLogFunc       :: !LogFunc
  , _kingEnvStderrLogFunc :: !LogFunc
  , _kingEnvKingId        :: !Word16
  , _kingEnvProcId        :: !Int32
  , _kingEnvKillSignal    :: !(TMVar ())
  }

makeLenses ''KingEnv

instance HasKingEnv KingEnv where
  kingEnvL = id

instance HasLogFunc KingEnv where
  logFuncL = kingEnvLogFunc

instance HasStderrLogFunc KingEnv where
  stderrLogFuncL = kingEnvStderrLogFunc

instance HasProcId KingEnv where
  procIdL = kingEnvProcId

instance HasKingId KingEnv where
  kingIdL = kingEnvKingId

-- Running KingEnvs ------------------------------------------------------------

runKingEnvStderr :: Bool -> LogLevel -> RIO KingEnv a -> IO a
runKingEnvStderr verb lvl inner = do
  logOptions <-
    logOptionsHandle stderr verb
      <&> setLogUseTime True
      <&> setLogUseLoc False
      <&> setLogMinLevel lvl
  withLogFunc logOptions $ \logFunc -> runKingEnv logFunc logFunc inner

runKingEnvStderrRaw :: Bool -> LogLevel -> RIO KingEnv a -> IO a
runKingEnvStderrRaw verb lvl inner = do
  logOptions <-
    logOptionsHandle stderr verb
      <&> setLogUseTime True
      <&> setLogUseLoc False
      <&> setLogMinLevel lvl
  withLogFunc logOptions $ \logFunc ->
    let lf = wrapCarriage logFunc
    in runKingEnv lf lf inner

-- XX loses callstack
wrapCarriage :: LogFunc -> LogFunc
wrapCarriage lf = mkLogFunc $ \_ ls ll bldr ->
  runRIO lf $ logGeneric ls ll (bldr <> "\r")

runKingEnvLogFile :: Bool -> LogLevel -> Maybe FilePath -> RIO KingEnv a -> IO a
runKingEnvLogFile verb lvl fileM inner = do
  logFile <- case fileM of
    Just f  -> pure f
    Nothing -> defaultLogFile
  withLogFileHandle logFile $ \h -> do
    logOptions <-
      logOptionsHandle h verb
        <&> setLogUseTime True
        <&> setLogUseLoc False
        <&> setLogMinLevel lvl
    stderrLogOptions <-
      logOptionsHandle stderr verb
        <&> setLogUseTime False
        <&> setLogUseLoc False
        <&> setLogMinLevel lvl
    withLogFunc stderrLogOptions $ \stderrLogFunc -> withLogFunc logOptions
      $ \logFunc -> runKingEnv logFunc stderrLogFunc inner

withLogFileHandle :: FilePath -> (Handle -> IO a) -> IO a
withLogFileHandle f act =
  withFile f AppendMode $ \handle -> do
    hSetBuffering handle LineBuffering
    act handle

defaultLogFile :: IO FilePath
defaultLogFile = do
  logDir <- getXdgDirectory XdgCache "urbit"
  createDirectoryIfMissing True logDir
  logId :: Word32 <- randomIO
  pure (logDir </> "king-" <> show logId <> ".log")

runKingEnvNoLog :: RIO KingEnv a -> IO a
runKingEnvNoLog act = runKingEnv mempty mempty act

runKingEnv :: LogFunc -> LogFunc -> RIO KingEnv a -> IO a
runKingEnv logFunc stderr action = do
  kid      <- randomIO
  CPid pid <- c_getpid
  kil      <- newEmptyTMVarIO
  runRIO (KingEnv logFunc stderr kid pid kil) action


-- KingEnv Utils ---------------------------------------------------------------

onKillKingSigL :: HasKingEnv e => Getter e (STM ())
onKillKingSigL = kingEnvL . kingEnvKillSignal . to readTMVar

killKingActionL :: HasKingEnv e => Getter e (STM ())
killKingActionL =
  kingEnvL . kingEnvKillSignal . to (\kil -> void (tryPutTMVar kil ()))

-- HostEnv ------------------------------------------------------------------

-- The host environment is everything in King, eyre configuration shared
-- across ships, and nat punching data.

class HasMultiEyreApi a where
  multiEyreApiL :: Lens' a MultiEyreApi

class (HasKingEnv a, HasMultiEyreApi a, HasPortControlApi a) =>
      HasHostEnv a where
  hostEnvL :: Lens' a HostEnv

data HostEnv = HostEnv
  { _hostEnvKingEnv        :: !KingEnv
  , _hostEnvMultiEyreApi   :: !MultiEyreApi
  , _hostEnvPortControlApi :: !PortControlApi
  }

makeLenses ''HostEnv

instance HasKingEnv HostEnv where
  kingEnvL = hostEnvKingEnv

instance HasLogFunc HostEnv where
  logFuncL = kingEnvL . logFuncL

instance HasStderrLogFunc HostEnv where
  stderrLogFuncL = kingEnvL . stderrLogFuncL

instance HasProcId HostEnv where
  procIdL = kingEnvL . procIdL

instance HasKingId HostEnv where
  kingIdL = kingEnvL . kingEnvKingId

instance HasMultiEyreApi HostEnv where
  multiEyreApiL = hostEnvMultiEyreApi

instance HasPortControlApi HostEnv where
  portControlApiL = hostEnvPortControlApi

-- Running Running Envs --------------------------------------------------------

runHostEnv :: MultiEyreApi -> PortControlApi -> RIO HostEnv a
           -> RIO KingEnv a
runHostEnv multi ports action = do
    king <- ask

    let hostEnv = HostEnv { _hostEnvKingEnv        = king
                          , _hostEnvMultiEyreApi   = multi
                          , _hostEnvPortControlApi = ports
                          }

    io (runRIO hostEnv action)

-- PierEnv ---------------------------------------------------------------------

class (HasKingEnv a, HasHostEnv a, HasPierConfig a, HasNetworkConfig a) =>
      HasPierEnv a where
  pierEnvL :: Lens' a PierEnv

data PierEnv = PierEnv
  { _pierEnvHostEnv       :: !HostEnv
  , _pierEnvPierConfig    :: !PierConfig
  , _pierEnvNetworkConfig :: !NetworkConfig
  , _pierEnvKillSignal    :: !(TMVar ())
  }

makeLenses ''PierEnv

instance HasKingEnv PierEnv where
  kingEnvL = pierEnvHostEnv . kingEnvL

instance HasHostEnv PierEnv where
  hostEnvL = pierEnvHostEnv

instance HasMultiEyreApi PierEnv where
  multiEyreApiL = pierEnvHostEnv . multiEyreApiL

instance HasPortControlApi PierEnv where
  portControlApiL = pierEnvHostEnv . portControlApiL

instance HasPierEnv PierEnv where
  pierEnvL = id

instance HasKingId PierEnv where
  kingIdL = kingEnvL . kingEnvKingId

instance HasStderrLogFunc PierEnv where
  stderrLogFuncL = kingEnvL . stderrLogFuncL

instance HasLogFunc PierEnv where
  logFuncL = kingEnvL . logFuncL

instance HasPierPath PierEnv where
  pierPathL = pierEnvPierConfig . pierPathL

instance HasDryRun PierEnv where
  dryRunL = pierEnvPierConfig . dryRunL

instance HasPierConfig PierEnv where
  pierConfigL = pierEnvPierConfig

instance HasNetworkConfig PierEnv where
  networkConfigL = pierEnvNetworkConfig

instance HasProcId PierEnv where
  procIdL = kingEnvL . kingEnvProcId


-- PierEnv Utils ---------------------------------------------------------------

onKillPierSigL :: HasPierEnv e => Getter e (STM ())
onKillPierSigL = pierEnvL . pierEnvKillSignal . to readTMVar

killPierActionL :: HasPierEnv e => Getter e (STM ())
killPierActionL =
  pierEnvL . pierEnvKillSignal . to (\kil -> void (tryPutTMVar kil ()))


-- Running Pier Envs -----------------------------------------------------------

runPierEnv
  :: PierConfig -> NetworkConfig -> TMVar () -> RIO PierEnv a -> RIO HostEnv a
runPierEnv pierConfig networkConfig vKill action = do
  host <- ask

  let pierEnv = PierEnv { _pierEnvHostEnv       = host
                        , _pierEnvPierConfig    = pierConfig
                        , _pierEnvNetworkConfig = networkConfig
                        , _pierEnvKillSignal    = vKill
                        }

  io (runRIO pierEnv action)
