{-# LANGUAGE RecordWildCards #-}

{-|
  Module      : Starter
  Description : Develop applications without restarts
  Copyright   : (c) Raghu Kaippully, 2018
  License     : MPL-2.0
  Maintainer  : rkaippully@gmail.com
  Stability   : experimental
  Portability : POSIX

  Server side software development usually needs frequent restarts. You would launch a server, make
  code changes, recompile the code, and then restart the server. Starter attempts to automate this
  tedious cycle.

  = How does it work?
  Starter is a library that you link to your program. With a few simple steps, you can set up a
  GHCi session that automatically reloads your program whenever your source code changes.

  1. In some module of your program (e.g. @MyModule@), define a variable @mySettings@ of type 'StarterSettings'.
  2. Define a function @runDevMode = runStarter mySettings@.
  2. Create a @.ghci@ file in your project with the following contents:

       @
       :load MyModule Starter
       :def! starter runDevMode
       @

  3. Now you can start your program with the @:starter@ command. This will run your program under a monitor.
     When the source code changes, the monitor will interrupt the program with an exception, reload
     the modules with a @:reload@ command and restart the program.
  4. You can terminate the session with a Ctrl+C.
-}
module Starter
    ( StarterSettings(..)
    , defaultStarterSettings
    , runStarter
    ) where

import Control.Concurrent (myThreadId, newEmptyMVar, putMVar, throwTo, tryTakeMVar)
import Control.Exception  (AsyncException (..), SomeAsyncException, catch)
import Data.Functor       (void)
import System.FSNotify    (Debounce (..), Event, WatchConfig (..), defaultConfig, eventPath,
                           watchTree, withManagerConf)


data StarterSettings = StarterSettings {
  -- | The program to be run by starter. The command line arguments passed to the GHCi command will
  -- be passed to this function.
  starterProgram             :: String -> IO ()
  -- | The GHCi command name
  , starterCommand           :: String
  -- | The expression that should be bound to the GHCi command. For e.g., if you created a @.ghci@
  -- file with the command @:def! starter runDevMode@, then this should be set to
  -- @runDevMode@.
  , starterCommandExpression :: String
  -- | Predicate to determine if the program should be restarted on change of a file.
  , starterIsRestartable     :: FilePath -> Bool
  }

-- | Default 'StarterSettings' that uses ":starter" as the GHCi command and restarts on all file
-- changes.
defaultStarterSettings :: StarterSettings
defaultStarterSettings = StarterSettings {
  starterProgram = undefined
  , starterCommand = "starter"
  , starterCommandExpression = undefined
  , starterIsRestartable = const True
  }

-- | Run a program under a monitor for source code changes. The 'StarterSettings' argument contains
-- details about what needs to be run and how the monitor behaves. The second argument is the
-- command line passed to the GHCi command from the GHCi session. For e.g, if you start the program
-- with:
--
-- @
-- :starter foo bar
-- @
--
-- then "foo bar" will be passed as the second argument to @runStarter@.
runStarter :: StarterSettings -> String -> IO String
runStarter StarterSettings{..} cmd =
  -- Register a watcher
  withManagerConf (defaultConfig {confDebounce = Debounce 1}) $ \mgr -> do
    t <- myThreadId
    var <- newEmptyMVar
    let
      changeHandler :: Event -> IO ()
      changeHandler event = do
        putMVar var event
        throwTo t UserInterrupt

      handleException :: SomeAsyncException -> IO String
      handleException _ = do
        v <- tryTakeMVar var
        case v of
          Just event -> do
            putStrLn $ "Source changed: " ++ eventPath event
            return $ ":reload\n" ++
                     ":def! " ++ starterCommand ++ " " ++ starterCommandExpression ++ "\n" ++
                     ":" ++ starterCommand ++ " " ++ cmd
          Nothing    -> return ""

    void $ watchTree mgr "." (starterIsRestartable . eventPath) changeHandler
    (starterProgram cmd >> return "") `catch` handleException