------------------------------------------------------------------------------
-- | The Heist snaplet makes it easy to add Heist to your application and use
-- it in other snaplets.
--

module Snap.Snaplet.Heist
  (
  -- * Heist and its type class
    Heist
  , HasHeist(..)

  -- * Initializer Functions
  -- $initializerSection
  , heistInit
  , heistInit'
  , Unclassed.heistReloader
  , Unclassed.setInterpreted
  , Unclassed.getCurHeistConfig
  , addTemplates
  , addTemplatesAt
  , Unclassed.addConfig
  , getHeistState
  , modifyHeistState
  , withHeistState

  -- * Handler Functions
  -- $handlerSection
  , gRender
  , gRenderAs
  , gHeistServe
  , gHeistServeSingle
  , chooseMode

  , cRender
  , cRenderAs
  , cHeistServe
  , cHeistServeSingle

  , render
  , renderAs
  , heistServe
  , heistServeSingle
  , heistLocal
  , withSplices
  , renderWithSplices

  -- * Writing Splices
  -- $spliceSection
  , Unclassed.SnapletHeist
  , Unclassed.SnapletCSplice
  , Unclassed.SnapletISplice

  , clearHeistCache
  ) where

------------------------------------------------------------------------------
import           Prelude hiding (id, (.))
import           Control.Monad.State
import           Data.ByteString (ByteString)
import           Heist
------------------------------------------------------------------------------
import           Snap.Snaplet
import           Snap.Snaplet.Heist.Internal
import qualified Snap.Snaplet.HeistNoClass as Unclassed
import           Snap.Snaplet.HeistNoClass ( heistInit
                                           , heistInit'
                                           , clearHeistCache
                                           )


------------------------------------------------------------------------------
-- | A single snaplet should never need more than one instance of Heist as a
-- subsnaplet.  This type class allows you to make it easy for other snaplets
-- to get the lens that identifies the heist snaplet.  Here's an example of
-- how the heist snaplet might be declared:
--
-- > data App = App { _heist :: Snaplet (Heist App) }
-- > makeLenses ''App
-- >
-- > instance HasHeist App where heistLens = subSnaplet heist
-- >
-- > appInit = makeSnaplet "app" "" Nothing $ do
-- >     h <- nestSnaplet "heist" heist $ heistInit "templates"
-- >     addConfig h heistConfigWithMyAppSplices
-- >     return $ App h
class HasHeist b where
    -- | A lens to the Heist snaplet.  The b parameter to Heist will
    -- typically be the base state of your application.
    heistLens :: SnapletLens (Snaplet b) (Heist b)


-- $initializerSection
-- This section contains functions for use in setting up your Heist state
-- during initialization.


------------------------------------------------------------------------------
-- | Adds templates to the Heist HeistState.  Other snaplets should use
-- this function to add their own templates.  The templates are automatically
-- read from the templates directory in the current snaplet's filesystem root.
addTemplates :: HasHeist b
             => Snaplet (Heist b)
             -> ByteString
                 -- ^ The url prefix for the template routes
             -> Initializer b v ()
addTemplates h pfx = withTop' heistLens (Unclassed.addTemplates h pfx)


------------------------------------------------------------------------------
-- | Adds templates to the Heist HeistState, and lets you specify where
-- they are found in the filesystem.  Note that the path to the template
-- directory is an absolute path.  This allows you more flexibility in where
-- your templates are located, but means that you have to explicitly call
-- getSnapletFilePath if you want your snaplet to use templates within its
-- normal directory structure.
addTemplatesAt :: HasHeist b
               => Snaplet (Heist b)
               -> ByteString
                   -- ^ URL prefix for template routes
               -> FilePath
                   -- ^ Path to templates
               -> Initializer b v ()
addTemplatesAt h pfx p =
    withTop' heistLens (Unclassed.addTemplatesAt h pfx p)


------------------------------------------------------------------------------
-- | More general function allowing arbitrary HeistState modification.
getHeistState :: (HasHeist b)
              => Handler b v (HeistState (Handler b b))
getHeistState = Unclassed.getHeistState heistLens


------------------------------------------------------------------------------
-- | More general function allowing arbitrary HeistState modification.
modifyHeistState :: (HasHeist b)
                 => (HeistState (Handler b b) -> HeistState (Handler b b))
                     -- ^ HeistState modifying function
                 -> Initializer b v ()
modifyHeistState = Unclassed.modifyHeistState' heistLens


------------------------------------------------------------------------------
-- | Runs a function on with the Heist snaplet's 'HeistState'.
withHeistState :: (HasHeist b)
               => (HeistState (Handler b b) -> a)
                   -- ^ HeistState function to run
               -> Handler b v a
withHeistState = Unclassed.withHeistState' heistLens


-- $handlerSection
-- This section contains functions in the 'Handler' monad that you'll use in
-- processing requests.  Functions beginning with a 'g' prefix use generic
-- rendering that checks the preferred rendering mode and chooses
-- appropriately.  Functions beginning with a 'c' prefix use compiled template
-- rendering.  The other functions use the older interpreted rendering.
-- Interpreted splices added with addConfig will only work if you use
-- interpreted rendering.
--
-- The generic functions are useful if you are writing general snaplets that
-- use heist, but need to work for applications that use either interpreted
-- or compiled mode.


------------------------------------------------------------------------------
-- | Generic version of 'render'/'cRender'.
gRender :: HasHeist b
        => ByteString
            -- ^ Template name
        -> Handler b v ()
gRender t = withTop' heistLens (Unclassed.gRender t)


------------------------------------------------------------------------------
-- | Generic version of 'renderAs'/'cRenderAs'.
gRenderAs :: HasHeist b
          => ByteString
              -- ^ Content type to render with
          -> ByteString
              -- ^ Template name
          -> Handler b v ()
gRenderAs ct t = withTop' heistLens (Unclassed.gRenderAs ct t)


------------------------------------------------------------------------------
-- | Generic version of 'heistServe'/'cHeistServe'.
gHeistServe :: HasHeist b => Handler b v ()
gHeistServe = withTop' heistLens Unclassed.gHeistServe


------------------------------------------------------------------------------
-- | Generic version of 'heistServeSingle'/'cHeistServeSingle'.
gHeistServeSingle :: HasHeist b
                  => ByteString
                      -- ^ Template name
                  -> Handler b v ()
gHeistServeSingle t = withTop' heistLens (Unclassed.gHeistServeSingle t)


------------------------------------------------------------------------------
-- | Chooses between a compiled action and an interpreted action based on the
-- configured default.
chooseMode :: HasHeist b
           => Handler b v a
               -- ^ A compiled action
           -> Handler b v a
               -- ^ An interpreted action
           -> Handler b v a
chooseMode cAction iAction = do
    mode <- withTop' heistLens $ gets _defMode
    case mode of
      Unclassed.Compiled -> cAction
      Unclassed.Interpreted -> iAction


------------------------------------------------------------------------------
-- | Renders a compiled template as text\/html. If the given template is not
-- found, this returns 'empty'.
cRender :: HasHeist b
        => ByteString
            -- ^ Template name
        -> Handler b v ()
cRender t = withTop' heistLens (Unclassed.cRender t)


------------------------------------------------------------------------------
-- | Renders a compiled template as the given content type.  If the given
-- template is not found, this returns 'empty'.
cRenderAs :: HasHeist b
          => ByteString
              -- ^ Content type to render with
          -> ByteString
              -- ^ Template name
          -> Handler b v ()
cRenderAs ct t = withTop' heistLens (Unclassed.cRenderAs ct t)


------------------------------------------------------------------------------
-- | A compiled version of 'heistServe'.
cHeistServe :: HasHeist b => Handler b v ()
cHeistServe = withTop' heistLens Unclassed.cHeistServe


------------------------------------------------------------------------------
-- | Analogous to 'fileServeSingle'. If the given template is not found,
-- this throws an error.
cHeistServeSingle :: HasHeist b
                 => ByteString
                     -- ^ Template name
                 -> Handler b v ()
cHeistServeSingle t = withTop' heistLens (Unclassed.cHeistServeSingle t)


------------------------------------------------------------------------------
-- | Renders a template as text\/html. If the given template is not found,
-- this returns 'empty'.
render :: HasHeist b
       => ByteString
           -- ^ Template name
       -> Handler b v ()
render t = withTop' heistLens (Unclassed.render t)


------------------------------------------------------------------------------
-- | Renders a template as the given content type.  If the given template
-- is not found, this returns 'empty'.
renderAs :: HasHeist b
         => ByteString
             -- ^ Content type to render with
         -> ByteString
             -- ^ Template name
         -> Handler b v ()
renderAs ct t = withTop' heistLens (Unclassed.renderAs ct t)


------------------------------------------------------------------------------
-- | A handler that serves all the templates (similar to 'serveDirectory').
-- If the template specified in the request path is not found, it returns
-- 'empty'.  Also, this function does not serve any templates beginning with
-- an underscore.  This gives you a way to prevent some templates from being
-- served.  For example, you might have a template that contains only the
-- navbar of your pages, and you probably wouldn't want that template to be
-- visible to the user as a standalone template.  So if you put it in a file
-- called \"_nav.tpl\", this function won't serve it.
heistServe :: HasHeist b => Handler b v ()
heistServe = withTop' heistLens Unclassed.heistServe


------------------------------------------------------------------------------
-- | Handler for serving a single template (similar to 'fileServeSingle'). If
-- the given template is not found, this throws an error.
heistServeSingle :: HasHeist b
                 => ByteString
                     -- ^ Template name
                 -> Handler b v ()
heistServeSingle t = withTop' heistLens (Unclassed.heistServeSingle t)


------------------------------------------------------------------------------
-- | Renders a template with a given set of splices.  This is syntax sugar for
-- a common combination of heistLocal, bindSplices, and render.
renderWithSplices :: HasHeist b
                  => ByteString
                      -- ^ Template name
                  -> Splices (Unclassed.SnapletISplice b)
                      -- ^ Splices to bind
                  -> Handler b v ()
renderWithSplices = Unclassed.renderWithSplices' heistLens


------------------------------------------------------------------------------
-- | Runs an action with additional splices bound into the Heist
-- 'HeistState'.
withSplices :: HasHeist b
            => Splices (Unclassed.SnapletISplice b)
                -- ^ Splices to bind
            -> Handler b v a
                -- ^ Handler to run
            -> Handler b v a
withSplices = Unclassed.withSplices' heistLens


------------------------------------------------------------------------------
-- | Runs a handler with a modified 'HeistState'.  You might want to use
-- this if you had a set of splices which were customised for a specific
-- action.  To do that you would do:
--
-- > heistLocal (bindSplices mySplices) handlerThatNeedsSplices
heistLocal :: HasHeist b
           => (HeistState (Handler b b) -> HeistState (Handler b b))
               -- ^ HeistState modifying function
           -> Handler b v a
               -- ^ Handler to run
           -> Handler b v a
heistLocal = Unclassed.heistLocal' heistLens


-- $spliceSection
-- The type signature for SnapletHeist uses @(Handler b b)@ as the Heist
-- snaplet's runtime monad.  This means that your splices must use the
-- top-level snaplet's @Handler b b@ monad.  The reasons for this are beyond
-- the scope of this discussion, but the result is that 'lift' inside a splice
-- only works with @Handler b b@ actions.  When you're writing your own
-- snaplets using some snaplet-specific monad @Handler b v@ you still have to
-- use @Handler b b@ for your splices.  If the splices need any of the context
-- provided by the @v@, you must pass it in as a parameter to the splice
-- function.