-- | This module contains functions to build assets (that is, run preprocessing
-- if necessary, and copy to destination directory).
module Web.Herringbone.Internal.BuildAsset where

import Control.Monad.Reader
import Data.Maybe
import Data.Time
import Filesystem.Path.CurrentOS (FilePath, (</>))
import qualified Filesystem.Path.CurrentOS as F
import qualified Filesystem as F
import Prelude hiding (FilePath)

import Web.Herringbone.Internal.Types

-- | Build an asset based on a BuildSpec to produce a 'Asset'. This action
-- checks whether the compilation is necessary based on the modified times of
-- the source and destination files.
buildAsset :: Herringbone
            -> BuildSpec
            -> IO (Either AssetError Asset)
buildAsset hb spec = fmap (mapLeft AssetCompileError) (buildAsset' hb spec)
    where
    mapLeft :: (a -> b) -> Either a r -> Either b r
    mapLeft f (Left x)  = Left $ f x
    mapLeft _ (Right x) = Right x

buildAsset' :: Herringbone
           -> BuildSpec
           -> IO (Either CompileError Asset)
buildAsset' hb (BuildSpec sourcePath' destPath' pp) = do
    verbosePut hb $
        "asset requested: " ++ show sourcePath' ++
        "\n\tto: " ++ show destPath' ++
        maybe "" ("\n\twith preprocessor: " ++) (fmap show pp)

    let sourcePath = hbSourceDir hb </> sourcePath'
    let destPath = hbDestDir hb </> destPath'

    sourceModifiedTime <- F.getModified sourcePath
    compileNeeded <- shouldCompile hb sourceModifiedTime destPath

    result <- if compileNeeded
                then do
                    verbosePut hb "compiling asset..."
                    compileAsset hb sourcePath destPath (maybeToList pp)
                else do
                    verbosePut hb "asset compilation not needed"
                    return $ Right ()

    either (return . Left)
           (\_ -> do size <- F.getSize destPath
                     return . Right $ Asset
                                        size
                                        sourcePath
                                        destPath
                                        sourceModifiedTime)
           result

-- | Should we compile an asset? True if either the asset doesn't exist, or if
-- its modified time is older than the supplied source modification time.
shouldCompile :: Herringbone -> UTCTime -> FilePath -> IO Bool
shouldCompile hb sourceModifiedTime destPath = do
    exists <- F.isFile destPath
    if not exists
        then return True
        else do
            destModifiedTime <- F.getModified destPath
            let changedTime = max sourceModifiedTime (herringboneStartTime hb)
            return $ changedTime > destModifiedTime

-- | Compile the given asset by invoking any preprocessors on the source path,
-- and copying the result to the destination path.
compileAsset :: Herringbone
             -> FilePath -- ^ Source path
             -> FilePath -- ^ Dest path
             -> [PP]     -- ^ List of preprocessors to apply
             -> IO (Either CompileError ())
compileAsset hb sourcePath destPath pps = do
    -- Ensure the destination directory exists
    let destDir = F.directory destPath
    verbosePut hb $ "creating dest dir: " ++ show destDir
    F.createTree destDir

    sourceData <- F.readFile sourcePath
    let computation = chainEither (map ppAction pps) sourceData
    let readerData = PPReader
            { ppReaderHb          = hb
            , ppReaderSourcePath  = sourcePath
            , ppReaderPPs         = pps
            }
    result <- runPPM computation readerData

    either (return . Left)
           (\resultData -> do F.writeFile destPath resultData
                              return (Right ()))
           result

chainEither :: Monad m => [a -> m (Either b a)] -> a -> m (Either b a)
chainEither fs m = foldl go z fs
    where
        go acc f = acc >>= either (return . Left) f
        z  = return (Right m)