    {-# LANGUAGE TemplateHaskell, QuasiQuotes, ScopedTypeVariables #-}
    -- | A generator is executed at compile time to load a list of entries
    -- to embed into the subsite.  This module contains several basic generators,
    -- but the design of generators and entries is such that it is straightforward
    -- to make custom generators for your own specific purposes, see <#g:4 this section>.
    module Yesod.EmbeddedStatic.Generators (
      -- * Generators
        Location
      , embedFile
      , embedFileAt
      , embedDir
      , embedDirAt
      , concatFiles
      , concatFilesWith

      -- * Compression options for 'concatFilesWith'
      , jasmine
      , uglifyJs
      , yuiJavascript
      , yuiCSS
      , closureJs
      , compressTool
      , tryCompressTools

      -- * Util
      , pathToName

      -- * Custom Generators

      -- $example
    ) where

    import Control.Applicative ((<$>))
    import Control.Exception (try, SomeException)
    import Control.Monad (forM, when)
    import Control.Monad.Trans.Resource (runResourceT)
    import Data.Char (isDigit, isLower)
    import Data.Conduit (($$), (=$))
    import Data.Conduit.Process (proc, conduitProcess)
    import Data.Default (def)
    import Data.Maybe (isNothing)
    import Language.Haskell.TH
    import Network.Mime (defaultMimeLookup)
    import System.Directory (doesDirectoryExist, getDirectoryContents, findExecutable)
    import System.FilePath ((</>))
    import Text.Jasmine (minifym)
    import qualified Data.ByteString.Lazy as BL
    import qualified Data.Conduit.List as C
    import qualified Data.Text as T

    import Yesod.EmbeddedStatic.Types

    -- | Embed a single file.  Equivalent to passing the same string twice to 'embedFileAt'.
    embedFile :: FilePath -> Generator
    embedFile f = embedFileAt f f

    -- | Embed a single file at a given location within the static subsite and generate a
    --   route variable based on the location via 'pathToName'.  The @FilePath@ must be a relative
    --   path to the directory in which you run @cabal build@.  During development, the file located
    --   at this filepath will be reloaded on every request.  When compiling for production, the contents
    --   of the file will be embedded into the executable and so the file does not need to be
    --   distributed along with the executable.
    embedFileAt :: Location -> FilePath -> Generator
    embedFileAt loc f = do
        let mime = defaultMimeLookup $ T.pack f
        let entry = def {
                        ebHaskellName = Just $ pathToName loc
                      , ebLocation = loc
                      , ebMimeType = mime
                      , ebProductionContent = BL.readFile f
                      , ebDevelReload = [| BL.readFile $(litE $ stringL f) |]
                      }
        return [entry]

    -- | List all files recursively in a directory
    getRecursiveContents :: Location -- ^ The directory to search
                         -> FilePath   -- ^ The prefix to add to the filenames
                         -> IO [(Location,FilePath)]
    getRecursiveContents prefix topdir = do
      names <- getDirectoryContents topdir
      let properNames = filter (`notElem` [".", ".."]) names
      paths <- forM properNames $ \name -> do
        let path = topdir </> name
        let loc = if null prefix then name else prefix ++ "/" ++ name
        isDirectory <- doesDirectoryExist path
        if isDirectory
          then getRecursiveContents loc path
          else return [(loc, path)]
      return (concat paths)

    -- | Embed all files in a directory into the static subsite.
    --
    -- Equivalent to passing the empty string as the location to 'embedDirAt',
    -- so the directory path itself is not part of the resource locations (and so
    -- also not part of the generated route variable names).
    embedDir :: FilePath -> Generator
    embedDir = embedDirAt ""

    -- | Embed all files in a directory to a given location within the static subsite.
    --
    -- The directory tree rooted at the 'FilePath' (which must be relative to the directory in
    -- which you run @cabal build@) is embedded into the static subsite at the given
    -- location.  Also, route variables will be created based on the final location
    -- of each file.  For example, if a directory \"static\" contains the files
    --
    -- * css/bootstrap.css
    --
    -- * js/jquery.js
    --
    -- * js/bootstrap.js
    --
    -- then @embedDirAt \"somefolder\" \"static\"@ will
    --
    -- * Make the file @static\/css\/bootstrap.css@ available at the location
    --   @somefolder\/css\/bootstrap.css@ within the static subsite and similarly
    --   for the other two files.
    --
    -- * Create variables @somefolder_css_bootstrap_css@, @somefolder_js_jquery_js@,
    --   @somefolder_js_bootstrap_js@ all of type @Route EmbeddedStatic@.
    --
    -- * During development, the files will be reloaded on every request.  During
    --   production, the contents of all files will be embedded into the executable.
    --
    -- * During development, files that are added to the directory while the server
    --   is running will not be detected.  You need to recompile the module which
    --   contains the call to @mkEmbeddedStatic@.  This will also generate new route
    --   variables for the new files.
    embedDirAt :: Location -> FilePath -> Generator
    embedDirAt loc dir = do
        files <- runIO $ getRecursiveContents loc dir
        concat <$> mapM (uncurry embedFileAt) files

    -- | Concatinate a list of files and embed it at the location.  Equivalent to passing @return@ to
    --   'concatFilesWith'.
    concatFiles :: Location -> [FilePath] -> Generator
    concatFiles loc files = concatFilesWith loc return files

    -- | Concatinate a list of files into a single 'BL.ByteString', run the resulting content through the given
    --   function, embed it at the given location, and create a haskell variable name for the route based on
    --   the location.
    --
    --   The processing function is only run when compiling for production, and the processing function is
    --   executed at compile time.  During development, on every request the files listed are reloaded,
    --   concatenated, and served as a single resource at the given location without being processed.
    concatFilesWith :: Location -> (BL.ByteString -> IO BL.ByteString) -> [FilePath] -> Generator
    concatFilesWith loc process files = do
        let load = do putStrLn $ "Creating " ++ loc
                      BL.concat <$> mapM BL.readFile files >>= process
            expFiles = listE $ map (litE . stringL) files
            expCt = [| BL.concat <$> mapM BL.readFile $expFiles |]
            mime = defaultMimeLookup $ T.pack loc
        return [def { ebHaskellName = Just $ pathToName loc
                    , ebLocation = loc
                    , ebMimeType = mime
                    , ebProductionContent = load
                    , ebDevelReload = expCt
                    }]

    -- | Convienient rexport of 'minifym' with a type signature to work with 'concatFilesWith'.
    jasmine :: BL.ByteString -> IO BL.ByteString
    jasmine ct = return $ either (const ct) id $ minifym ct

    -- | Use <https://github.com/mishoo/UglifyJS2 UglifyJS2> to compress javascript.
    -- Assumes @uglifyjs@ is located in the path and uses options @[\"-m\", \"-c\"]@
    -- to both mangle and compress and the option \"-\" to cause uglifyjs to read from
    -- standard input.
    uglifyJs :: BL.ByteString -> IO BL.ByteString
    uglifyJs = compressTool "uglifyjs" ["-m", "-c", "-"]

    -- | Use <http://yui.github.io/yuicompressor/ YUI Compressor> to compress javascript.
    -- Assumes a script @yuicompressor@ is located in the path.  If not, you can still
    -- use something like
    --
    -- > compressTool "java" ["-jar", "/path/to/yuicompressor.jar", "--type", "js"]
    yuiJavascript :: BL.ByteString -> IO BL.ByteString
    yuiJavascript = compressTool "yuicompressor" ["--type", "js"]

    -- | Use <http://yui.github.io/yuicompressor/ YUI Compressor> to compress CSS.
    -- Assumes a script @yuicompressor@ is located in the path.
    yuiCSS :: BL.ByteString -> IO BL.ByteString
    yuiCSS = compressTool "yuicompressor" ["--type", "css"]

    -- | Use <https://developers.google.com/closure/compiler/ Closure> to compress
    -- javascript using the default options.  Assumes a script @closure@ is located in
    -- the path. If not, you can still run using
    --
    -- > compressTool "java" ["-jar", "/path/to/compiler.jar"]
    closureJs :: BL.ByteString -> IO BL.ByteString
    closureJs = compressTool "closure" []

    -- | Helper to convert a process into a compression function.  The process
    -- should be set up to take input from standard input and write to standard output.
    compressTool :: FilePath -- ^ program
                 -> [String] -- ^ options
                 -> BL.ByteString -> IO BL.ByteString
    compressTool f opts ct = do
        mpath <- findExecutable f
        when (isNothing mpath) $
            fail $ "Unable to find " ++ f
        let src = C.sourceList $ BL.toChunks ct
            p = proc f opts
            sink = C.consume
        compressed <- runResourceT (src $$ conduitProcess p =$ sink)
        putStrLn $ "Compressed successfully with " ++ f
        return $ BL.fromChunks compressed

    -- | Try a list of processing functions (like the compressions above) one by one until
    -- one succeeds (does not raise an exception).  Once a processing function succeeds,
    -- none of the remaining functions are used.  If none succeeds, the input is just
    -- returned unprocessed.  This is helpful if you are distributing
    -- code on hackage and do not know what compressors the user will have installed.  You
    -- can list several and they will be tried in order until one succeeds.
    tryCompressTools :: [BL.ByteString -> IO BL.ByteString] -> BL.ByteString -> IO BL.ByteString
    tryCompressTools [] x = return x
    tryCompressTools (p:ps) x = do
        mres <- try $ p x
        case mres of
            Left (err :: SomeException) -> do
                putStrLn $ show err
                tryCompressTools ps x
            Right res -> return res

    -- | Clean up a path to make it a valid haskell name by replacing all non-letters
    --   and non-numbers by underscores.  In addition, if the path starts with a capital
    --   letter or number add an initial underscore.
    pathToName :: FilePath -> Name
    pathToName f = routeName
        where
          replace c
            | 'A' <= c && c <= 'Z' = c
            | 'a' <= c && c <= 'z' = c
            | '0' <= c && c <= '9' = c
            | otherwise = '_'
          name = map replace f
          routeName = mkName $
                case () of
                    ()
                        | null name -> error "null-named file"
                        | isDigit (head name) -> '_' : name
                        | isLower (head name) -> name
                        | otherwise -> '_' : name

    -- $example
    -- Here is an example of creating your own custom generator.
    -- Because of template haskell stage restrictions, you must define generators in a
    -- different module from where you use them.  The following generator will embed a
    -- JSON document that contains the compile time.
    --
    -- >{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-}
    -- >module CompileTime where
    -- >
    -- >import Data.Aeson
    -- >import Data.Default
    -- >import Data.Time
    -- >import Yesod.EmbeddedStatic.Generators
    -- >import Yesod.EmbeddedStatic.Types
    -- >import qualified Data.ByteString.Lazy as BL
    -- >
    -- >getTime :: IO BL.ByteString
    -- >getTime = do
    -- >    t <- getCurrentTime
    -- >    return $ encode $
    -- >        object [ "compile_time" .= show t ]
    -- >
    -- >timeGenerator :: Location -> Generator
    -- >timeGenerator loc =
    -- >    return $ [def
    -- >        { ebHaskellName = Just $ pathToName loc
    -- >        , ebLocation    = loc
    -- >        , ebMimeType    = "application/json"
    -- >        , ebProductionContent = getTime
    -- >        , ebDevelReload = [| getTime |]
    -- >        }]
    --
    -- Notice how the @getTime@ action is given as both 'ebProductionContent' and
    -- 'ebDevelReload'.  The result is that during development, the @getTime@ action
    -- will be re-executed on every request so the time returned will be different
    -- for each reload.  When compiling for production, the @getTime@ action will
    -- be executed once at compile time to produce the content to embed and never
    -- called at runtime.
    --
    -- Here is a small example yesod program using this generator.  Try toggling
    -- the development argument to @mkEmbeddedStatic@.
    --
    -- >{-# LANGUAGE TemplateHaskell, QuasiQuotes, TypeFamilies #-}
    -- >module Main where
    -- >
    -- >import Yesod
    -- >import Yesod.EmbeddedStatic
    -- >import CompileTime (timeGenerator)
    -- >
    -- >mkEmbeddedStatic True "eStatic" [timeGenerator "compile-time.json"]
    -- >
    -- >-- The above will generate variables
    -- >-- eStatic :: EmbeddedStatic
    -- >-- compile_time_json :: Route EmbeddedStatic
    -- >
    -- >data MyApp = MyApp { getStatic :: EmbeddedStatic }
    -- >
    -- >mkYesod "MyApp" [parseRoutes|
    -- >/ HomeR GET
    -- >/static StaticR EmbeddedStatic getStatic
    -- >|]
    -- >
    -- >instance Yesod MyApp
    -- >
    -- >getHomeR :: Handler Html
    -- >getHomeR = defaultLayout $ [whamlet|
    -- ><h1>Hello
    -- ><p>Check the
    -- >    <a href=@{StaticR compile_time_json}>compile time
    -- >|]
    -- >
    -- >main :: IO ()
    -- >main = warp 3000 $ MyApp eStatic