{-# LANGUAGE RecordWildCards, CPP #-}
{-# LANGUAGE RecursiveDo #-}
module Foreign.JavaScript.EventLoop (
    eventLoop,
    runEval, callEval, debug, onDisconnect,
    newHandler, fromJSStablePtr,
    ) where

import           Control.Applicative
import           Control.Concurrent
import           Control.Concurrent.Async
import           Control.Concurrent.STM   as STM
import           Control.DeepSeq                  (deepseq)
import           Control.Exception        as E
import           Control.Monad
import qualified Data.Aeson               as JSON
import qualified Data.ByteString.Char8    as BS
import           Data.IORef
import qualified Data.Map                 as Map
import qualified Data.Text                as T
import qualified System.Mem

import Foreign.RemotePtr             as Foreign
import Foreign.JavaScript.CallBuffer
import Foreign.JavaScript.Types

rebug :: IO ()
#ifdef REBUG
rebug = System.Mem.performGC
#else
rebug = return ()
#endif

{-----------------------------------------------------------------------------
    Event Loop
------------------------------------------------------------------------------}
-- | Handle a single event
handleEvent w@(Window{..}) (name, args) = do
    mhandler <- Foreign.lookup name wEventHandlers
    case mhandler of
        Nothing -> return ()
        Just f  -> withRemotePtr f (\_ f -> f args)


type Result = Either String JSON.Value

-- | Event loop for a browser window.
-- Supports concurrent invocations of `runEval` and `callEval`.
eventLoop :: (Window -> IO void) -> EventLoop
eventLoop init server info comm = void $ do
    -- To support concurrent FFI calls, we need three threads.
    -- A fourth thread supports 
    --
    -- The thread `multiplexer` reads from the client and
    --   sorts the messages into the appropriate queue.
    events      <- newTQueueIO
    results     <- newTQueueIO :: IO (TQueue Result)
    -- The thread `handleCalls` executes FFI calls
    --    from the Haskell side in order.
    -- The corresponding queue records `TMVar`s in which to put the results.
    calls       <- newTQueueIO :: IO (TQueue (Maybe (TMVar Result), ServerMsg))
    -- The thread `handleEvents` handles client Events in order.

    -- We only want to make an FFI call when the connection browser<->server is open
    -- Otherwise, throw an exception.
    let atomicallyIfOpen stm = do
            r <- atomically $ do
                b <- readTVar $ commOpen comm
                if b then fmap Right stm else return (Left ())
            case r of
                Right a -> return a
                Left  _ -> throwIO $ ErrorCall "Foreign.JavaScript: Browser <-> Server communication broken."

    -- FFI calls are made by writing to the `calls` queue.
    let run  msg = msg `deepseq` do     -- see [ServerMsg strictness]
            atomicallyIfOpen $ writeTQueue calls (Nothing , msg)
        call msg = msg `deepseq` do     -- see [ServerMsg strictness]
            ref <- newEmptyTMVarIO
            atomicallyIfOpen $ writeTQueue calls (Just ref, msg)
            er  <- atomicallyIfOpen $ takeTMVar ref
            case er of
                Left  e -> E.throwIO $ JavaScriptException e
                Right x -> return x
        debug s  = s `deepseq` do       -- see [ServerMsg strictness]
            atomicallyIfOpen $ writeServer comm $ Debug s

    -- We also send a separate event when the client disconnects.
    disconnect <- newTVarIO $ return ()
    -- FIXME: Make it possible to store *multiple* event handlers
    let onDisconnect m = atomically $ writeTVar disconnect m

    w0 <- newPartialWindow
    let w = w0 { getServer    = server
               , getCookies   = info
               , runEval      = run  . RunEval
               , callEval     = call . CallEval
               , debug        = debug
               , timestamp    = run Timestamp
               , onDisconnect = onDisconnect
               }

    -- The individual threads are as follows:
    --
    -- Read client messages and send them to the
    -- thread that handles events or the thread that handles FFI calls.
    let multiplexer = forever $ atomically $ do
            msg <- readClient comm
            case msg of
                Event x y   -> writeTQueue events (x,y)
                Result x    -> writeTQueue results (Right x)
                Exception e -> writeTQueue results (Left  e)

    -- Send FFI calls to client and collect results
    let handleCalls = forever $ do
            ref <- atomically $ do
                (ref, msg) <- readTQueue calls
                writeServer comm msg
                return ref
            atomically $
                case ref of
                    Just ref -> do
                        result <- readTQueue results
                        putTMVar ref result
                    Nothing  -> return ()

    -- Receive events from client and handle them in order.
    let handleEvents = do
            me <- atomically $ do
                open <- readTVar $ commOpen comm
                if open
                    then Just <$> readTQueue events
                    else return Nothing -- channel is closed
            case me of
                Nothing -> return ()    -- channel is closed, we're done
                Just e  -> do
                    handleEvent w e
                        `E.onException` commClose comm -- close channel in case of exception
                    rebug
                    handleEvents

    -- Execute an IO action, but also print any exceptions that it may throw.
    -- (The exception is rethrown.)
    let
        printException :: IO a -> IO a
        printException = E.handle $ \e -> do
            sLog server . BS.pack $ show (e :: E.SomeException)
            E.throwIO e

    -- NOTE: Due to an issue with `snap-server` library,
    -- we print the exception ourselves.
    printException $
        -- Wrap the main loop into `withRemotePtr` in order to keep the root alive.
        Foreign.withRemotePtr (wRoot w) $ \_ _ ->
        -- run `multiplexer` and `handleCalls` concurrently
        withAsync multiplexer $ \_ ->
        withAsync handleCalls $ \_ ->
        withAsync (flushCallBufferPeriodically w) $ \_ ->
        E.finally (init w >> handleEvents) $ do
            putStrLn "Foreign.JavaScript: Browser window disconnected."
            -- close communication channel if still necessary
            commClose comm
            -- trigger the `disconnect` event
            -- FIXME: Asynchronous exceptions should not be masked during the disconnect handler
            m <- atomically $ readTVar disconnect
            m

-- | Thread that periodically flushes the call buffer
flushCallBufferPeriodically :: Window -> IO ()
flushCallBufferPeriodically w =
    forever $ threadDelay (flushPeriod*1000) >> flushCallBuffer w


{-----------------------------------------------------------------------------
    Exports, Imports and garbage collection
------------------------------------------------------------------------------}
-- | Turn a Haskell function into an event handler.
newHandler :: Window -> ([JSON.Value] -> IO ()) -> IO HsEvent
newHandler w@(Window{..}) handler = do
    coupon <- newCoupon wEventHandlers
    newRemotePtr coupon (handler . parseArgs) wEventHandlers
    where
    fromSuccess (JSON.Success x) = x
    -- parse a genuine JavaScript array
    parseArgs x = fromSuccess (JSON.fromJSON x) :: [JSON.Value]
    -- parse a JavaScript arguments object
    -- parseArgs x = Map.elems (fromSuccess (JSON.fromJSON x) :: Map.Map String JSON.Value)


-- | Convert a stable pointer from JavaScript into a 'JSObject'.
fromJSStablePtr :: JSON.Value -> Window -> IO JSObject
fromJSStablePtr js w@(Window{..}) = do
    let JSON.Success coupon = JSON.fromJSON js
    mhs <- Foreign.lookup coupon wJSObjects
    case mhs of
        Just hs -> return hs
        Nothing -> do
            ptr <- newRemotePtr coupon (JSPtr coupon) wJSObjects
            addFinalizer ptr $
                runEval ("Haskell.freeStablePtr('" ++ T.unpack coupon ++ "')")
            return ptr