{-|
Copyright   : (c) Dave Laing, 2017-2019
License     : BSD3
Maintainer  : dave.laing.80@gmail.com
Stability   : experimental
Portability : non-portable

Low level operations for integrating reflex networks with WAI
'Application's.

If you just want to serve a reflex network then have a look
at "Reflex.Backend.Warp".

-}

{-# LANGUAGE FlexibleContexts #-}
module Reflex.Backend.Wai
  ( WaiSource(..), newWaiSource
  , waiApplicationGuest
  , waiApplicationHost
  , liftWaiApplication, liftWaiApplicationTagged
  )
where

import Network.Wai
import Network.Wai.Internal (ResponseReceived(..))

import Control.Monad (void, forever)

import Control.Monad.Trans (MonadIO, liftIO)

import Data.Map (Map)
import qualified Data.Map as Map

import Control.Concurrent (forkIO)
import Control.Monad.STM
import Control.Concurrent.STM

import Reflex hiding (Request, Response)

-- | The source of the WAI application data.
--
-- Requests generated by the web server are stored here, and read
-- by the reflex network.
--
-- Responses generated by the reflex network are stored here, and
-- read (and subsequently returned) by the web server.
data WaiSource
  = WaiSource
  { wsRequest :: TMVar Request
  , wsResponse :: TMVar Response
  }

-- | Initialise a 'WaiSource'
newWaiSource :: MonadIO m => m WaiSource
newWaiSource =
  liftIO $ WaiSource <$> newEmptyTMVarIO <*> newEmptyTMVarIO

-- | Build an 'Application' that deposits a 'Request' into the
-- 'WaiSource', then reads a 'Response' from the 'WaiSource', then
-- responds with it.
waiApplicationHost :: WaiSource -> Application
waiApplicationHost (WaiSource wReq wRes) req response = do
  atomically $ putTMVar wReq req
  res <- atomically $ takeTMVar wRes
  response res

-- | Build a reflex network that pumps 'Request's and 'Response's to
-- and from the 'WaiSource'
waiApplicationGuest ::
  ( Reflex t
  , MonadIO m
  , PerformEvent t m
  , MonadIO (Performable m)
  , TriggerEvent t m
  ) =>
  WaiSource ->
  (Event t Request -> m (Event t Response)) ->
  m ()
waiApplicationGuest (WaiSource wReq wRes) network = do
  (eReq, onReq) <- newTriggerEvent
  eRes <- network eReq

  performEvent_ $ liftIO . atomically . putTMVar wRes <$> eRes

  void . liftIO . forkIO . forever $ do
    req <- atomically $ takeTMVar wReq
    onReq req

  pure ()

-- | Given a WAI 'Application' and a 'Request' event, create an
-- 'Event' that yield a 'Response' by running the 'Application'.
--
-- The output 'Event' will fire some time after the input 'Event'.
liftWaiApplication ::
  ( Reflex t
  , PerformEvent t m
  , MonadIO (Performable m)
  , TriggerEvent t m
  ) =>
  Application ->
  Event t Request ->
  m (Event t Response)
liftWaiApplication app eReq = do
  (eRes, onRes) <- newTriggerEvent

  let go res = ResponseReceived <$ onRes res

  performEvent_ $ (\req -> void . liftIO $ app req go) <$> eReq

  pure eRes

-- | Similar to 'liftWaiApplication', but the 'Request' event should yield
-- a tag which is then attached to the 'Response'.
liftWaiApplicationTagged ::
  ( Reflex t
  , PerformEvent t m
  , MonadIO (Performable m)
  , TriggerEvent t m
  ) =>
  Application ->
  Event t (tag, Request) ->
  m (Event t (Map tag Response))
liftWaiApplicationTagged app eReq = do
  (eRes, onRes) <- newTriggerEvent

  let go t res = ResponseReceived <$ onRes (Map.singleton t res)

  performEvent_ $ (\(t, req) -> void . liftIO . app req $ go t) <$> eReq

  pure eRes