{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Control.Monad.Component.Development
  (
  -- * Making 'ComponentM' values useful
    ComponentM
  , runComponentDevel

  -- * Error Records
  , ComponentError (..)
  , ComponentBuildError (..)

  -- * 'ComponentM' tracing accessors
  , ComponentEvent (..)
  , Build
  , buildElapsedTime
  , buildFailure
  , BuildResult
  , toBuildList
  )
  where

import           RIO

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

devTeardownStoreNum :: Word32
devTeardownStoreNum = 0

runComponentDevel_
  :: (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 Teardown
runComponentDevel_ !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 $ ComponentErrorDetected
          (ComponentBuildFailed errList teardownResult)
        return $ emptyTeardown "development"

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

        appTeardown      <- buildTableToTeardown appName buildTable
        appAsync         <- asyncWithUnmask $ \unmask -> unmask $ appFn resource

        appAsyncTeardown <- newTeardown "application async"
                                        (cancel appAsync :: IO ())
        newTeardown "development" [appTeardown, appAsyncTeardown]

-- | Similar to 'runComponentM1', when running for the first time, it creates an
-- application in the REPL environment, subsequent invocations will teardown the
-- and build up the application again.
--
-- All 'ComponentM' characteristics are driven by this particular use-case given:
--
-- * It will print out the time spent on initialization and teardown
-- * It guarantees that teardown operations are as robust as possible
-- * It documents your application components to pin-point quickly errors in your
--   reloading logic
--
runComponentDevel
  :: (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, note this function must block the thread
                               -- as how the normal main would
  -> IO ()
runComponentDevel !logFn !appName !builder !appFn = do
  mdevTeardownStore <- lookupStore devTeardownStoreNum
  case mdevTeardownStore of
    Nothing -> do
      devTeardown <- runComponentDevel_ logFn appName builder appFn
      writeStore (Store devTeardownStoreNum) devTeardown

    Just devTeardownStore -> do
      devTeardown0   <- readStore devTeardownStore
      teardownResult <- runTeardown (devTeardown0 :: Teardown)
      logFn $ ComponentReleased teardownResult

      devTeardown <- runComponentDevel_ logFn appName builder appFn
      writeStore (Store devTeardownStoreNum) devTeardown