{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE QuasiQuotes               #-}
{-# LANGUAGE RecordWildCards           #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE TupleSections             #-}

{-|

This module provides infrastructure for polling the status of processes that
run longer than a single request-response lifecycle.  To do this, we issue
AJAX calls at regular intervals to a route that updates the status on the
page.  There are two main components necessary to use this library: splices
and a handler.

The handler is a simple 'jobStatusHandler' function.  Typically you'll add it
to your site's list of routes like this:

> route [ ...
>       , ("myJobStatus", jobStatusHandler "statusTemplate" ".statusDiv")
>       , ...
>       ]

The first argument to jobsHandler is the template that will be rendered for
status update requests.  It will typically be not much more than a
@\<myJobStatus\>@ tag enclosing the custom markup for displaying your job status.
Here's an example using a bootstrap progress bar:

> <myJobStatus interval="300">
>   <ifRunning>
>     <elapsedSeconds/> seconds elapsed
>
>     <div class="progress">
>       <div class="progress-bar progress-bar-striped active"  role="progressbar"
>            aria-valuenow="${amountCompleted}" aria-valuemin="0"
>            aria-valuemax="${amountTotal}" style="width: ${percentCompleted}%">
>         <percentCompleted/>%
>       </div>
>     </div>
>
>   </ifRunning>
>   <ifFinished>
>     <div class="progress">
>       <div class="progress-bar progress-bar-striped"  role="progressbar"
>            aria-valuenow="${amountCompleted}" aria-valuemin="0"
>            aria-valuemax="${amountTotal}" style="width: ${percentCompleted}%">
>         <percentCompleted/>%
>       </div>
>     </div>
>   </ifFinished>
> </myJobStatus>

This will poll for updates every 300 milliseconds.  See the documentation for
'statusSplice' for more details.

To get the above code working, you would have the myJobStatusPage handler
return markup that contains something like this:

> <h1>Status</h1>
> <div class="statusdiv col-md-4">
>   <apply template="statusTemplate"/>
> </div>

You will also need to bind the main splice provided by this module.

> splices = do
>     ...
>     "myJobStatus" MS.## statusSplice splices getUrl getMyJobStatus isFinished

You need to bind this splice once for each type of action that you are
polling, each with its own splice name and function for getting the job
status.

-}

module Snap.Extras.PollStatus
  ( jobStatusHandler
  , statusSplice

  -- * Generic Status Type

  , JobState(..)
  , jobFinished
  , statusFinished
  , Status(..)
  , statusPercentCompleted
  , statusElapsed
  , statusSplices
  ) where

------------------------------------------------------------------------------
import qualified Blaze.ByteString.Builder.Char8 as BB
import           Control.Applicative            as A
import           Control.Monad
import           Control.Monad.Trans
import           Control.Monad.Trans.Maybe
import           Data.ByteString                (ByteString)
import qualified Data.Map.Syntax                as MS
import           Data.Maybe
import           Data.Monoid                    (mempty)
import           Data.Readable
import           Data.Text
import qualified Data.Text                      as T
import           Data.Time
import           Heist
import           Heist.Compiled
import           Heist.Splices
import           Language.Javascript.JMacro
import           Snap.Snaplet
import           Snap.Snaplet.Heist.Compiled    (HasHeist)
import           Text.PrettyPrint.Leijen.Text   (renderOneLine)
import qualified Text.XmlHtml                   as X
------------------------------------------------------------------------------
import           Snap.Extras.Ajax
------------------------------------------------------------------------------


------------------------------------------------------------------------------
-- | Top-level handler that handles the job polling infrastructure.
jobStatusHandler
    :: HasHeist b
    => ByteString
    -- ^ Name of the template to use for the updateStatus endpoint.
    -> T.Text
    -- ^ A CSS selector string used to substitute the contents of the AJAX
    -- template into the status page.
    -> Handler b v ()
jobStatusHandler updateTemplate selector =
    replaceWithTemplate updateTemplate selector


------------------------------------------------------------------------------
-- | Function to generate a status splice.  This splice makes the following
-- splices available to its children:
--
-- Conditional splices:
-- ifPending, ifRunning, ifNotFinished, ifFinished, ifFinishedSuccess,
-- ifFinishedFailure
--
-- Data splices:
-- elapsedSeconds, startTime, endTime, jobState, messages, percentCompleted,
-- amountCompleted, amountTotal
statusSplice
    :: Monad n
    => Splices (RuntimeSplice n status -> Splice n)
    -- ^ Splices defined for your job status type status.
    -> n (Maybe Text)
    -- ^ Handler to get the URL that should be used for requesting AJAX status
    -- updates.  This must be a handler because it will usually contain some
    -- form of job ID that is only obtainable at runtime.
    -> n (Maybe status)
    -- ^ Handler that knows how to get the status of the job in question.
    -- This typically will require it to know how to get some sort of job ID
    -- from the request.
    -> (status -> Bool)
    -- ^ A function to tell whether the status is finished
    -> Splice n
statusSplice splices getUrl getStatus isFinished = do
    n <- getParamNode
    runAttrs <- runAttributesRaw $ X.elementAttrs n
    let nodes = X.elementChildren n ++ [X.Element "updateJs" [] []]
        run = runNodeList nodes
    mayDeferMap return (withSplices run allSplices) $ do
        attrs <- runAttrs
        let delay = fromMaybe 1000 $ fromText =<< lookup "interval" attrs
        runMaybeT $ do
            url <- MaybeT $ lift getUrl
            let js = T.concat ["<script>", updateJS url delay, "</script>"]
            s <- MaybeT $ lift getStatus
            return (js, s)
  where
    allSplices = do
        "updateJs" MS.## pureSplice internalUpdate
        MS.mapV (. liftM snd) splices
    internalUpdate (js,s) =
        if not $ isFinished s
          then BB.fromText js
          else mempty


------------------------------------------------------------------------------
-- | JS code to do an AJAX request to updateStatus after a delay.
updateJS
    :: Text
    -- ^ The url for incremental status updates
    -> Int
    -- ^ The amount of time to wait before requesting the page in milliseconds
    -> Text
updateJS url delay = T.pack $ show $ renderOneLine $ renderJs $
    [jmacro|
      setTimeout(function() {
        $.get(`(T.unpack url)`);
      }, `(show delay)`);
    |]


------------------------------------------------------------------------------
--                          Example Implementation
------------------------------------------------------------------------------


-- The above functions are everything you need to use the polling
-- infrastructure using yo

------------------------------------------------------------------------------
-- | Enumeration of the states a job can be in.
data JobState = Pending | Running | FinishedSuccess | FinishedFailure
  deriving (Eq,Show,Read,Enum)


------------------------------------------------------------------------------
-- | Returns a bool indicating whether the job is finished.
jobFinished :: JobState -> Bool
jobFinished FinishedSuccess = True
jobFinished FinishedFailure = True
jobFinished _               = False


------------------------------------------------------------------------------
-- | Returns a bool indicating whether the job is finished.
statusFinished :: Status -> Bool
statusFinished = jobFinished . statusJobState


------------------------------------------------------------------------------
-- | The complete status of a job.
data Status = Status
    { statusStartTime       :: Maybe UTCTime
    , statusTimestamp       :: UTCTime
    , statusJobState        :: JobState
    , statusMessages        :: [Text]
    , statusAmountCompleted :: Double
    , statusAmountTotal     :: Double
    } deriving (Eq,Show)


------------------------------------------------------------------------------
-- | Calculates the percent completed as an Int.
statusPercentCompleted :: Status -> Int
statusPercentCompleted Status{..} =
    round $ 100.0 * statusAmountCompleted / statusAmountTotal


------------------------------------------------------------------------------
tshow :: Show a => a -> Text
tshow = T.pack . show


------------------------------------------------------------------------------
-- | The status splices
statusSplices
    :: Monad n
    => Splices (RuntimeSplice n Status -> Splice n)
statusSplices = do
    "ifPending" MS.## ifCSplice ((==Pending) . statusJobState)
    "ifRunning" MS.## ifCSplice ((==Running) . statusJobState)
    "ifNotFinished" MS.## ifCSplice (not . statusFinished)
    "ifFinished" MS.## ifCSplice (statusFinished)
    "ifFinishedSuccess" MS.## ifCSplice ((==FinishedSuccess) . statusJobState)
    "ifFinishedFailure" MS.## ifCSplice ((==FinishedFailure) . statusJobState)
    "messages" MS.## manyWithSplices runChildren
                    ("msgText" MS.## pureSplice (textSplice id)) .
                    (liftM $ statusMessages)
    MS.mapV pureSplice $ do
      "startTime" MS.## textSplice (tshow . statusStartTime)
      "endTime" MS.## textSplice (tshow . statusTimestamp)
      "jobState" MS.## textSplice (tshow . statusJobState )
      "percentCompleted" MS.## textSplice (tshow . statusPercentCompleted)
      "amountCompleted" MS.## textSplice (tshow . statusAmountCompleted)
      "amountTotal" MS.## textSplice (tshow . statusAmountTotal)


------------------------------------------------------------------------------
statusElapsed :: Status -> Maybe Int
statusElapsed Status{..} =
    round . diffUTCTime statusTimestamp A.<$> statusStartTime