{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | CLI interface for Rib.
--
-- Mostly you would only need `Rib.App.run`, passing it your Shake build action.
module Rib.App
  ( run,
    runWith,
  )
where

import Control.Concurrent.Async (race_)
import Control.Exception.Safe (catch)
import Development.Shake hiding (command)
import Development.Shake.Forward (shakeForward)
import Options.Applicative
import Relude
import Rib.Cli (CliConfig (CliConfig), cliParser)
import qualified Rib.Cli as Cli
import Rib.Log
import qualified Rib.Server as Server
import Rib.Watch (onTreeChange)
import System.Directory
import System.FSNotify (Event (..), eventPath)
import System.FilePath
import System.IO (BufferMode (LineBuffering), hSetBuffering)

-- | Run Rib using arguments passed in the command line.
run ::
  -- | Default value for `Cli.inputDir`
  FilePath ->
  -- | Deault value for `Cli.outputDir`
  FilePath ->
  -- | Shake build rules for building the static site
  Action () ->
  IO ()
run src dst buildAction = runWith buildAction =<< execParser opts
  where
    opts =
      info
        (cliParser src dst <**> helper)
        ( fullDesc
            <> progDesc "Generate a static site at OUTPUTDIR using input from INPUTDIR"
        )

-- | Like `run` but with an explicitly passed `CliConfig`
runWith :: Action () -> CliConfig -> IO ()
runWith buildAction cfg@CliConfig {..} = do
  -- For saner output
  flip hSetBuffering LineBuffering `mapM_` [stdout, stderr]
  case (watch, serve) of
    (True, Just (host, port)) -> do
      race_
        (Server.serve cfg host port $ outputDir)
        (runShakeAndObserve cfg buildAction)
    (True, Nothing) ->
      runShakeAndObserve cfg buildAction
    (False, Just (host, port)) ->
      Server.serve cfg host port $ outputDir
    (False, Nothing) ->
      runShakeBuild cfg buildAction

shakeOptionsFrom :: CliConfig -> ShakeOptions
shakeOptionsFrom cfg'@CliConfig {..} =
  shakeOptions
    { shakeVerbosity = verbosity,
      shakeFiles = shakeDbDir,
      shakeRebuild = bool [] [(RebuildNow, "**")] rebuildAll,
      shakeLintInside = [""],
      shakeExtra = addShakeExtra cfg' (shakeExtra shakeOptions)
    }

runShakeBuild :: CliConfig -> Action () -> IO ()
runShakeBuild cfg@CliConfig {..} buildAction = do
  runShake cfg $ do
    logStrLn cfg $ "[Rib] Generating " <> inputDir <> " (rebuildAll=" <> show rebuildAll <> ")"
    buildAction

runShake :: CliConfig -> Action () -> IO ()
runShake cfg shakeAction = do
  shakeForward (shakeOptionsFrom cfg) shakeAction
    `catch` handleShakeException
  where
    handleShakeException (e :: ShakeException) =
      -- Gracefully handle any exceptions when running Shake actions. We want
      -- Rib to keep running instead of crashing abruptly.
      logErr $
        "[Rib] Unhandled exception when building " <> shakeExceptionTarget e <> ": " <> show e

runShakeAndObserve :: CliConfig -> Action () -> IO ()
runShakeAndObserve cfg@CliConfig {..} buildAction = do
  -- Begin with a *full* generation as the HTML layout may have been changed.
  -- TODO: This assumption is not true when running the program from compiled
  -- binary (as opposed to say via ghcid) as the HTML layout has become fixed
  -- by being part of the binary. In this scenario, we should not do full
  -- generation (i.e., toggle the bool here to False). Perhaps provide a CLI
  -- flag to disable this.
  runShakeBuild (cfg {Cli.rebuildAll = True}) buildAction
  -- And then every time a file changes under the current directory
  logStrLn cfg $ "[Rib] Watching " <> inputDir <> " for changes"
  onSrcChange $ runShakeBuild cfg buildAction
  where
    onSrcChange :: IO () -> IO ()
    onSrcChange f = do
      -- Canonicalizing path is important as we are comparing path ancestor using isPrefixOf
      dir <- canonicalizePath inputDir
      -- Top-level directories to ignore from notifications
      let isBlacklisted :: FilePath -> Bool
          isBlacklisted p = or $ flip fmap watchIgnore $ \b -> (dir </> b) `isPrefixOf` p
      onTreeChange dir $ \allEvents -> do
        let events = filter (not . isBlacklisted . eventPath) allEvents
        unless (null events) $ do
          -- Log the changed events for diagnosis.
          logEvent `mapM_` events
          f
    logEvent :: Event -> IO ()
    logEvent e = do
      logStrLn cfg $ eventLogPrefix e <> " " <> eventPath e
    eventLogPrefix = \case
      -- Single character log prefix to indicate file actions is a convention in Rib.
      Added _ _ _ -> "A"
      Modified _ _ _ -> "M"
      Removed _ _ _ -> "D"
      Unknown _ _ _ -> "?"