content: {-# LANGUAGE BangPatterns #-}
  {-# LANGUAGE LambdaCase #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE QuasiQuotes #-}
  {-# LANGUAGE Rank2Types #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  {-# LANGUAGE ViewPatterns #-}
  
  -- | Combinators for working with Shake.
  module Rib.Shake
    ( -- * Basic helpers
      buildStaticFiles,
      forEvery,
  
      -- * Writing only
      writeFileCached,
  
      -- * Misc
      getCliConfig,
      ribInputDir,
      ribOutputDir,
    )
  where
  
  import Control.Monad.Catch
  import Development.Shake
  import Relude
  import Rib.Cli (CliConfig)
  import qualified Rib.Cli as Cli
  import System.Directory
  import System.FilePath
  import System.IO.Error (isDoesNotExistError)
  
  -- | Get rib settings from a shake Action monad.
  getCliConfig :: Action CliConfig
  getCliConfig = getShakeExtra >>= \case
    Just v -> pure v
    Nothing -> fail "CliConfig not initialized"
  
  -- | Input directory containing source files
  --
  -- This is same as the first argument to `Rib.App.run`
  ribInputDir :: Action FilePath
  ribInputDir = Cli.inputDir <$> getCliConfig
  
  -- | Output directory where files are generated
  --
  -- This is same as the second argument to `Rib.App.run`
  ribOutputDir :: Action FilePath
  ribOutputDir = do
    output <- Cli.outputDir <$> getCliConfig
    liftIO $ createDirectoryIfMissing True output
    return output
  
  -- | Shake action to copy static files as is.
  buildStaticFiles :: [FilePath] -> Action ()
  buildStaticFiles staticFilePatterns = do
    input <- ribInputDir
    output <- ribOutputDir
    files <- getDirectoryFiles input staticFilePatterns
    void $ forP files $ \f ->
      copyFileChanged (input </> f) (output </> f)
  
  -- | Run the given action when any file matching the patterns changes
  forEvery ::
    -- | Source file patterns (relative to `ribInputDir`)
    [FilePath] ->
    (FilePath -> Action a) ->
    Action [a]
  forEvery pats f = do
    input <- ribInputDir
    fs <- getDirectoryFiles input pats
    forP fs f
  
  -- | Write the given file but only when it has been modified.
  --
  -- Also, always writes under ribOutputDir
  writeFileCached :: FilePath -> String -> Action ()
  writeFileCached !k !s = do
    f <- fmap (</> k) ribOutputDir
    currentS <- liftIO $ forgivingAbsence $ readFile f
    unless (Just s == currentS) $ do
      writeFile' f $! s
      -- Use a character (like +) that contrasts with what Shake uses (#) for
      -- logging modified files being read.
      putInfo $ "+ " <> f
  
  -- | If argument of the function throws a
  -- 'System.IO.Error.doesNotExistErrorType', 'Nothing' is returned (other
  -- exceptions propagate). Otherwise the result is returned inside a 'Just'.
  forgivingAbsence :: (MonadIO m, MonadCatch m) => m a -> m (Maybe a)
  forgivingAbsence f =
    catchIf
      isDoesNotExistError
      (Just <$> f)
      (const $ return Nothing)
  