{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Control.Monad.Component.Internal.Core
  ( buildComponent
  , buildComponent_
  , runComponentM
  , runComponentM1
  ) where

import           RIO
import           RIO.Time                               (NominalDiffTime,
                                                         diffUTCTime,
                                                         getCurrentTime)

import qualified RIO.HashMap                            as HashMap
import qualified RIO.Set                                as Set

import           Control.Monad.Component.Internal.Types
import           Control.Teardown                       (emptyTeardown,
                                                         newTeardown,
                                                         runTeardown)

--------------------------------------------------------------------------------

-- | Track duration time of the execution of an IO sub-routine
trackExecutionTime :: IO a -> IO (NominalDiffTime, a)
trackExecutionTime routine = do
  start  <- getCurrentTime
  result <- routine
  end    <- getCurrentTime
  return (diffUTCTime end start, result)

-- | Transforms an `IO` sub-routine into a `ComponentM` sub-routine; the given
-- `IO` sub-routine returns a resource that does not allocate any other
-- resources that would need to be cleaned up on a system shutdown.
--
-- This is similar to using 'liftIO', with the caveat that the library will
-- register the given 'IO' sub-routine as a Component, and it will keep track
-- and report its initialization timespan
--
buildComponent_ :: Text -> IO a -> ComponentM a
buildComponent_ !componentDesc ma = ComponentM $ mask $ \restore -> do
  (buildElapsedTime, result) <- trackExecutionTime (try $ restore ma)
  case result of
    Left err -> do
      let build = Build
            { componentDesc
            , componentTeardown = emptyTeardown componentDesc
            , buildElapsedTime
            , buildFailure      = Just err
            , buildDependencies = Set.empty
            }
          buildTable = HashMap.singleton componentDesc build
      return $ Left ([ComponentAllocationFailed componentDesc err], buildTable)

    Right output -> do
      let build = Build
            { componentDesc
            , componentTeardown = emptyTeardown componentDesc
            , buildElapsedTime
            , buildFailure      = Nothing
            , buildDependencies = Set.empty
            }
          buildTable = HashMap.singleton componentDesc build
      return $ Right (output, buildTable)

-- | Use this function when you want to allocate a new resource (e.g. Database,
-- Socket, etc). It registers the constructed resource in your application
-- component tree and guarantees that its cleanup sub-routine is executed at the
-- end of your program.
--
-- This function is similar to the 'bracket' function with the caveat that it
-- expects a 'Text' argument which identifies the component being allocated.
--
-- NOTE: The name of your component must be unique; otherwise a
-- 'DuplicatedComponentKeyDetected' will be thrown
--
buildComponent
  :: Text         -- ^ Unique name for the component being allocated
  -> IO a         -- ^ Allocation 'IO' sub-routine
  -> (a -> IO ()) -- ^ Cleanup 'IO' sub-routine
  -> ComponentM a
buildComponent !componentDesc construct release =
  ComponentM $ mask $ \restore -> do

    (buildElapsedTime, (result, componentTeardown)) <- trackExecutionTime
      $ startComponent restore

    let buildFailure = either (Just . toException) (const Nothing) result

        build        = Build
          { componentDesc
          , componentTeardown
          , buildElapsedTime
          , buildFailure
          , buildDependencies = Set.empty
          }

        buildTable = HashMap.singleton componentDesc build

    case result of
      Left  err      -> return $ Left ([err], buildTable)
      Right resource -> return $ Right (resource, buildTable)
 where
  startComponent restore = do
    result <- restore (try construct)
    case result of
      Left err -> return
        ( Left $ ComponentAllocationFailed componentDesc err
        , emptyTeardown componentDesc
        )
      Right resource -> do
        resourceTeardown <- newTeardown componentDesc (release resource)
        return (Right resource, resourceTeardown)

-- | Enhances 'runComponentM' with a callback function that emits
-- 'ComponentEvent' records. These events are a great way of tracing the
-- lifecycle and structure of your application.
runComponentM1
  :: (ComponentEvent -> IO ()) -- ^ Callback function to trace 'ComponentEvent' records
  -> Text                      -- ^ Name of your application (used for tracing purposes)
  -> ComponentM a              -- ^ Builder of your application environment
  -> (a -> IO b)               -- ^ Function where your main application will live
  -> IO b
runComponentM1 !logFn !appName (ComponentM buildFn) !appFn =
  mask $ \restore -> do
    result <- restore buildFn
    case result of
      Left (errList, buildTable) -> do
        appTeardown    <- buildTableToTeardown appName buildTable
        teardownResult <- runTeardown appTeardown
        restore $ logFn $ ComponentReleased teardownResult
        throwIO $ ComponentBuildFailed errList teardownResult

      Right (resource, buildTable) -> do
        let buildList = buildTableToOrderedList buildTable
        restore $ logFn $ ComponentBuilt $ BuildResult $ reverse buildList

        appTeardown    <- buildTableToTeardown appName buildTable
        appResult      <- tryAny $ restore $ appFn resource
        teardownResult <- runTeardown appTeardown
        restore $ logFn $ ComponentReleased teardownResult

        case appResult of
          Left appError ->
            throwIO $ ComponentRuntimeFailed appError teardownResult
          Right output -> return output

-- | Constructs the /environment/ of your application by executing the 'IO'
-- sub-routines provided in the 'buildComponent' and 'buildComponent_'
-- functions; it then executes a callback where your main application will run.
--
-- This function:
--
-- * Keeps track of initialization elapsed time for each component of your
--   application
--
-- * Initializes components concurrently as long as they are composed using
--   'Applicative' functions
--
-- * Builds a graph of your dependencies automatically when composing your
--   'ComponentM' values via 'Applicative' or 'Monad' interfaces; it then
--   guarantees the execution of cleanup operations in a topological sorted
--   order
--
-- * Guarantees the proper cleanup of previously allocated resources if the
--   creation of a resource throws an exception on initialization
--
-- * Guarantees best-effort cleanup of resources on application teardown in the
--   scenario where a cleanup sub-routine throws an exception
--
-- * Keeps track of teardown elasped time for each component of your
--   application; and reports what exceptions was thrown in case of failures
--
-- If you want to trace the behavior of your application on initialization and
-- teardown, use 'runComponentM1' instead
runComponentM
  :: Text           -- ^ Name of your application (used for tracing purposes)
  -> ComponentM a   -- ^ Builder of your application environment
  -> (a -> IO b)    -- ^ Function where your main application will live
  -> IO b
runComponentM = runComponentM1 (const $ return ())