{-# LANGUAGE OverloadedStrings,KindSignatures, GADTs, ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
module Network.JavaScript.Services
  ( -- * Web Services
    Engine(..)
  , start
  , addListener
  , listen
  , readEventChan
  , Application
  ) where

import Control.Applicative((<|>))
import qualified Data.Text.Lazy as LT
import Data.Time.Clock
import qualified Network.Wai.Handler.WebSockets as WS
import Network.Wai (Application)
import qualified Network.WebSockets as WS

import Control.Concurrent (forkIO, ThreadId)
import Control.Exception (try, SomeException)
import Control.Monad (forever)
import Control.Concurrent.STM
import Data.Aeson (Value(..), decode', FromJSON(..),withObject,(.:))
import qualified Data.IntMap.Strict as IM

import Network.JavaScript.Internal(JavaScript(..))

-- | This accepts WebSocket requests, calls the callback with
--   an 'Engine' that can be used to access JavaScript.

start :: (Engine -> IO ())
      -> Application -> Application
start kE = WS.websocketsOr WS.defaultConnectionOptions $ \ pc -> do
  conn <- WS.acceptRequest pc
  -- Use ping to keep connection alive
  WS.forkPingThread conn 10
  -- Bootstrap the remote handler
  WS.sendTextData conn bootstrap
  -- Handling packets
  nonceRef <- newTVarIO 0
  replyMap <- newTVarIO IM.empty
  eventQueue <- newTChanIO

  let catchMe m = try m >>= \ (_ :: Either SomeException ()) -> return ()
  _ <- forkIO $ catchMe $ forever $ do
    d <- WS.receiveData conn
    case decode' d of
      Just (Result _ []) -> return ()
      Just (Result n replies) -> atomically
                      $ modifyTVar replyMap
                      $ IM.insert n
                      $ Right
                      $ replies
      Just (Error n obj) -> atomically
                      $ modifyTVar replyMap
                      $ IM.insert n
                      $ Left
                      $ obj
      Just (Event event) -> do
        utc <- getCurrentTime
        atomically $ writeTChan eventQueue (event,utc)

      Nothing -> print ("bad (non JSON) reply from JavaScript"::String,d)

  kE $ Engine
     { sendJavaScript = \ (JavaScript js) -> WS.sendTextData conn js
     , genNonce = atomically $ do
         n <- readTVar nonceRef
         writeTVar nonceRef $ succ n
         return n
     , replyBox = \ n -> atomically $ do
         t <- readTVar replyMap
         case IM.lookup n t of
           Nothing -> retry
           Just v -> return v
     , eventChan = readTChan eventQueue
     }

-- | An 'Engine' is a handle to a specific JavaScript engine
data Engine = Engine
  { sendJavaScript :: JavaScript -> IO ()      -- send text to the JS engine
  , genNonce       ::               IO Int     -- nonce generator
  , replyBox       :: Int        -> IO (Either Value [Value]) -- reply mailbox
  , eventChan      ::               STM (Value, UTCTime)
  }

bootstrap :: LT.Text
bootstrap = LT.unlines
   [     "jsb.event =  function(ev) {"
   ,     "         if (jsb.debug) { console.log('event',{event: ev}); }"
   ,     "         jsb.ws.send(JSON.stringify({event: ev}));"
   ,     "   };"
   ,     "jsb.error = function(n,err) {"
   ,     "         if (jsb.debug) { console.log('send',{id: n, error: err}); }"
   ,     "         jsb.ws.send(JSON.stringify({id: n, error: err}));"
   ,     "         throw(err);"
   ,     "   };"
   ,     "jsb.reply = function(n,obj) {"
   ,     "       Promise.all(obj).then(function(obj){"
   ,     "         if (jsb.debug) { console.log('reply',{id:n, result:obj}); }"
   ,     "         jsb.ws.send(JSON.stringify({id: n, result: obj}));"
   ,     "       }).catch(function(err){"
   ,     "         jsb.error(n,err);"
   ,     "       });"
   ,     "   };"
   ,     "jsb.ws.onmessage = function(evt){ "
   ,     "   if (jsb.debug) { console.log('eval',evt.data); }"
   ,     "   eval('(function(){' + evt.data + '})()');"
   ,     "};"
   ,     "jsb.rs = [];"
   ]

--
-- | Add a listener for events. There can be many. non-blocking.
--
--   From JavaScript, you can call event(..) to send
--   values to this listener. Any valid JSON value can be sent.
addListener :: Engine -> (Value -> IO ()) -> IO ThreadId
addListener e k = forkIO $ forever $ listen e >>= k

-- | 'listen' for the next event. blocking.
--
--   From JavaScript, you can call event(..) to send
--   values to this listener. Any valid JSON value can be sent.
--
--
listen :: Engine -> IO Value
listen e = atomically $ fst <$> readEventChan e

-- | 'readEventChan' uses STM to read the next event.
--
--   From JavaScript, you can call event(..) to send
--   values to this channel. Any valid JSON value can be sent.
--
--
readEventChan :: Engine -> STM (Value, UTCTime)
readEventChan = eventChan

------------------------------------------------------------------------------

-- This is what we send back from JavaScript.
data Reply = Result Int [Value]
           | Error Int Value
           | Event Value
  deriving Show

instance FromJSON Reply where
  parseJSON o =  parseEvent o
             <|> parseResult o
             <|> parseError o
    where
      parseEvent = withObject "Event" $ \v -> Event
        <$> v .: "event"
      parseResult = withObject "Result" $ \v -> Result
        <$> v .: "id"
        <*> v .: "result"
      parseError = withObject "Error" $ \v -> Error
        <$> v .: "id"
        <*> v .: "error"