{-# LANGUAGE DeriveDataTypeable #-}

------------------------------------------------------------------------------
-- | Nest this Snaplet within another to have it retrieve and minify the CSS
-- in its directory.
--
-- First, embed this Snaplet in your application:
--
-- > import Snap.Snaplet.CSS.Minify
-- >
-- > data App = App { cssMin :: Snaplet CssMin, ... }
--
-- Then nest this Snaplet in your initializer at the route you want your
-- stylesheets to be available at:
--
-- > nestSnaplet "style" cssMin cssMinInit
--
-- The stylesheets in @snaplets/css-min@ will now be available in minified
-- form at the @/style@ route.
--
-- To have the files reloaded in development mode add @\"snaplets/css-min\"@
-- to the list of watched directories in the Main module generated by Snap.
module Snap.Snaplet.CSS.Minify
    ( CssMin
    , cssMinInit
    , ParseException
    ) where

------------------------------------------------------------------------------
import qualified Data.Text              as T
import qualified Data.Text.IO           as T
import qualified Data.Text.Lazy         as LT
import qualified Data.Text.Lazy.Builder as LT
import qualified Data.ByteString.UTF8   as BS

------------------------------------------------------------------------------
import Control.Applicative    ((<$), (<$>), (<*>))
import Control.Exception      (Exception (..), SomeException (..), throw)
import Control.Lens           (Lens', (<&>), over, view)
import Control.Monad          (unless)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.State    (get, modify)
import Data.List              (isSuffixOf)
import Data.Text              (Text)
import Data.Typeable          (Typeable, cast)
import Snap.Core
import Snap.Snaplet
import System.FilePath        ((</>))
import System.Directory       (doesFileExist)
import Text.CSS.Parse         (NestedBlock, parseNestedBlocks)
import Text.CSS.Render        (renderNestedBlocks)


------------------------------------------------------------------------------
-- | The Snaplet's state, storing the cache of minified files.
data CssMin = CssMin { _cache :: [(FilePath, Text)] }

cache :: Lens' CssMin [(FilePath, Text)]
cache f m = f (_cache m) <&> \ c -> m { _cache = c }


------------------------------------------------------------------------------
-- | Initializes the CSS minifier by adding a route for reading, minifying and
-- serving the CSS files in the snaplet/css-min directory.
cssMinInit :: SnapletInit b CssMin
cssMinInit = makeSnaplet "css-min" "CSS minifier" Nothing $
    CssMin [] <$ addRoutes [("", serveCss)]

serveCss :: Handler b CssMin ()
serveCss = do
    fp <- (</>) <$> getSnapletFilePath <*>
        (BS.toString . rqPathInfo <$> getRequest)
    liftIO (doesFileExist fp) >>=
        flip unless pass . (".css" `isSuffixOf` fp &&)
    view cache <$> get >>= maybe (minify fp) writeCss . lookup fp

minify :: FilePath -> Handler b CssMin ()
minify fp = parseNestedBlocks <$>
    (getSnapletFilePath >>= liftIO . T.readFile . (</> fp)) >>=
        either (throw . ParseException) (cacheAndWrite fp)

cacheAndWrite :: FilePath -> [NestedBlock] -> Handler b CssMin ()
cacheAndWrite fp css = do
    let text = LT.toStrict $ LT.toLazyText $ renderNestedBlocks css
    modify $ over cache ((fp, text) :)
    writeCss text

writeCss :: Text -> Handler b v ()
writeCss css = do
    modifyResponse
        $ setContentLength (fromIntegral $ T.length css)
        . setContentType "text/css"
        . setResponseCode 200
    writeText css


------------------------------------------------------------------------------
data ParseException = ParseException String
    deriving (Typeable)

instance Show ParseException where
    show (ParseException msg) = "CSS parse exception: " ++ msg

instance Exception ParseException where
    toException = SomeException
    fromException = cast