{-# OPTIONS_GHC -fglasgow-exts #-}
-- this could be {-# LANGUAGE FlexibleInstances #-}
module Network.XMPP.XMPPMonad
                 ( XMPP
                 , runXMPP
                 , sendStanza
                 , addHandler
                 , waitForStanza
                 , quit
                 , StanzaPredicate
                 , StanzaHandler
                 , Control.Monad.State.liftIO
                 )
    where

import Control.Monad.State

import Network.XMPP.XMLParse
import Network.XMPP.XMPPConnection hiding ( sendStanza )
import qualified Network.XMPP.XMPPConnection as XMPPConnection
import Network.XMPP.MyDebug

-- |A stanza predicate.
type StanzaPredicate = (XMLElem -> Bool)
type StanzaHandlerPart a = (XMLElem -> XMPP a)
-- |A handler function for a stanza.
type StanzaHandler = (XMLElem -> XMPP ())

-- |A function in the XMPP monad behaves a bit like a thread in a
-- cooperative threading system: when it decides to wait for more
-- input, it \"sleeps\", letting other \"threads\" run, until input
-- matching a certain predicate arrives.
data XMPP a = 
    XMPP { xmppFn :: XMPPConnection c => 
                     (c -> XMPPState -> IO (XMPPState, XMPPRes a)) }
type XMPPState =
    [(StanzaPredicate,          -- predicate
      StanzaHandler,            -- handler
      Bool)]                    -- more than once?

data XMPPRes a = XMPPJust a
               | WaitingFor StanzaPredicate (StanzaHandlerPart a) Bool

instance Monad XMPP where
    f >>= g = XMPP $
              \c state ->
                  do
                    (state', result) <- xmppFn f c state
                    case result of
                      XMPPJust a ->
                          xmppFn (g a) c state'
                      WaitingFor pred mangler keep ->
                          return $ (state', 
                                    WaitingFor pred
                                    (\stanza -> (mangler stanza) >>= g) keep)
    return a = XMPP $ \_ state -> return (state, XMPPJust a)

-- This used to use MonadState, but there is no point.
getState :: XMPP XMPPState
getState = XMPP $ \_ state -> return (state, XMPPJust state)
putState :: XMPPState -> XMPP ()
putState state = XMPP $ \_ _ -> return (state, XMPPJust ())
modifyState :: (XMPPState -> XMPPState) -> XMPP ()
modifyState fn = XMPP $ \_ state -> return (fn state, XMPPJust ())

instance MonadIO XMPP where
    liftIO iofn = XMPP $ \c state -> do 
                        iores <- iofn
                        return (state, XMPPJust iores)

initialState :: XMPPState
initialState = []

-- |Run a function in the XMPP monad using the given XMPP connection.
-- After that, keep looping as long as there are handlers waiting for
-- incoming stanzas.
runXMPP :: XMPPConnection c => c -> XMPP () -> IO ()
runXMPP c x = runXMPP' initialState c [x] []
    where runXMPP' :: XMPPConnection c => XMPPState -> c -> [XMPP ()] -> [XMLElem] -> IO ()
          runXMPP' [] c [] _ =
              -- if there are no functions and no handlers, there is
              -- nothing left to do.
              return ()
          runXMPP' s c (x:xs) stanzas =
              -- if we have functions waiting to be run, run the first of them
              -- (actually, the list will always contain 0 or 1 element)
              do
                (s', result) <- xmppFn x c s
                let s'' = (case result of
                            XMPPJust () ->
                                s'
                            WaitingFor pred mangler keep ->
                                (pred,mangler,keep):s'
                          )
                runXMPP' s'' c xs stanzas
          runXMPP' s c [] (stanza:stanzas) =
              -- if there are unprocessed stanzas, process the first of them
              runXMPP' s c [actOnStanza stanza] stanzas
          runXMPP' s c [] [] =
              -- if there are no more functions to run, but there are
              -- handlers left, wait for incoming stanzas
              do
                myDebug $ show (length s) ++ " handlers left"
                newStanzas <- getStanzas c
                myDebug $ show (length newStanzas) ++ " new stanzas"
                runXMPP' s c [] newStanzas

-- |Send an XMPP stanza.
sendStanza :: XMLElem -> XMPP ()
sendStanza stanza =
    XMPP $ \c state -> do
      XMPPConnection.sendStanza c stanza
      return (state, XMPPJust ())

-- |When a stanza matching the predicate arrives, call the given
-- handler.  This is analogous to spawning a new thread, except that
-- the \"thread\" is only run if and when a matching stanza arrives.
--
-- Stanza handlers can be one-shot or permanent, as indicated by the
-- third argument.
addHandler :: StanzaPredicate   -- ^Stanza predicate.
           -> StanzaHandler     -- ^Stanza handler.
           -> Bool              -- ^Catch more than one stanza?
           -> XMPP ()
addHandler pred handler keep =
    modifyState ((pred, handler, keep):)

-- |Suspend execution of current function while waiting for a stanza
-- matching the predicate.
waitForStanza :: StanzaPredicate -> XMPP XMLElem
waitForStanza pred =
    XMPP $ \c state -> return (state, WaitingFor pred return False)

-- |Terminate the loop as soon as the current function exits.  This
-- works by removing all stanza handlers, which makes 'runXMPP' exit.
quit :: XMPP ()
quit =
    putState []

actOnStanza :: XMLElem -> XMPP ()
actOnStanza stanza =
    do
      table <- getState
      liftIO $ myDebug $ "checking " ++ show (length table) ++ " active handlers"
      case findHandler table stanza of
        Just (table', handler) ->
            do
              putState table'
              liftIO $ myDebug $ show (length table') ++ " handlers left"
              handler stanza
        Nothing ->
            return ()

-- Find handler whose predicate matches the stanza, possibly removing
-- the entry from the table.
findHandler :: [(StanzaPredicate, StanzaHandler, Bool)] -> XMLElem ->
               Maybe ([(StanzaPredicate, StanzaHandler, Bool)], StanzaHandler)
findHandler ((pred, handler, keep):table) stanza =
    case pred stanza of
      True ->
          let table' = if keep 
                       then
                           ((pred, handler, keep):table)
                       else
                           table
          in return (table', handler)
      False ->
          do
            (table', handler') <- findHandler table stanza
            return ((pred, handler, keep):table', handler')
findHandler [] _ = Nothing