{-# LANGUAGE JavaScriptFFI
           , OverloadedStrings
           #-}

{-|

Module      : Data.JSVal.Promise 
Copyright   : (c) Alejandro Durán Pallarés, 2016
License     : BSD3
Maintainer  : vwwv@correo.ugr.es
Stability   : experimental


Data.JSVal.Promise defines `Promise`, a direct bind to javascript promise objects.

- You can import/export them from javascript code using its `FromJSVal` and `ToJSVal` instances.

- You can extract its value, blocking till computation has finished, using `await`. (you can safely call 
  it several time from different threads, the  associated computation will run once, and then memorized)

- You can create new promise (to possible use js side) containing arbitrary haskell code using `promise`.

For some usage example, checkout this [blog entry](http://the.spaghetticodeball.xyz/haskell/javascript/2016/10/10/new-library-ghcjs-promise.html).

-}


module Data.JSVal.Promise( Promise()
                         , await
                         , promise
                         ) where

import GHCJS.Marshal
import GHCJS.Types
import GHCJS.Foreign
import Control.Exception
import Control.Concurrent


newtype Promise = Promise {fromPromise :: JSVal}

instance FromJSVal Promise where
      fromJSVal x = do is_promise <- js_check_if_promise x
                       if is_promise
                        then return . Just $ Promise x
                        else return Nothing

instance ToJSVal Promise where
      toJSVal   =  return . fromPromise

-- | If the promise is return through "then", it will return `Right`;
--   if it return through "catch", then it will return `Left`
await   :: Promise -> IO (Either JSVal JSVal)
await (Promise jsval) = do result <- js_await     jsval
                           x      <- js_attribute "result" result
                           ok     <- isTruthy <$> js_attribute "ok" result
                           if ok
                            then return (Right x)
                            else return (Left  x)

-- | A `Right` value will be sent as a normal value through "then", a left
--   value will be sent through "catch" (by javascript convention, representing 
--   an exception).
--
--   The block will start executing immediately, no mater if there's something waiting
--   for it or not.
--
--   If the execution block launches an exception, then the promise will be receive
--   as "reject", the javascript value "new Error('Haskell side error')"
promise :: IO (Either JSVal JSVal) -> IO Promise
promise action = do ref     <- js_book_promise
                    promise <- js_set_promise ref
                    myid    <- myThreadId
                    forkIO $ do val_ <- try action
                                case val_ of
                                 
                                 Right (Right x) -> js_do_resolve ref x
                                 
                                 Right (Left  x) -> js_do_reject  ref x
                                 
                                 Left  exc       -> do throwTo myid (exc::SomeException)
                                                       js_do_reject  ref =<< create_error 
                    return $ Promise promise
-----------------------------------------------------------------------
-----------------------------------------------------------------------


-- This works because the [algorithm](http://www.ecma-international.org/ecma-262/6.0/#sec-promise.resolve) 
-- explicitly demands that Promise.resolve must return the exact object passed in if and only if 
-- it is a promise by the definition of the spec.
-- (from stackoverflow http://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise)
foreign import javascript safe 
    "Promise.resolve($1) == $1"  
    js_check_if_promise :: JSVal -> IO Bool

foreign import javascript safe 
    "$2[$1]"
    js_attribute        :: JSString -> JSVal -> IO JSVal

foreign import javascript safe 
    "new Error('Haskell side error')"
    create_error        :: IO JSVal


foreign import javascript safe 
    "__js_book_promise()" 
    js_book_promise     :: IO JSVal 

foreign import javascript safe 
    "__js_set_promise($1)"
    js_set_promise      :: JSVal -> IO JSVal

foreign import javascript safe 
    "__js_do_reject($1,$2);"
    js_do_reject        :: JSVal -> JSVal -> IO ()

foreign import javascript safe 
    "__js_do_resolve($1, $2);"
    js_do_resolve       :: JSVal -> JSVal -> IO ()

foreign import javascript interruptible
    "__js_await($1,$c);" 
    js_await            :: JSVal -> IO JSVal