{-# LANGUAGE BangPatterns, QuasiQuotes, TemplateHaskell, OverloadedStrings, ScopedTypeVariables, FlexibleContexts #-}

{-|
Module      : Quickjs
Description : Haskell bindings to the [QuickJS](https://bellard.org/quickjs/) library
Copyright   : (c) Samuel Balco, 2020
License     : MIT
Maintainer  : goodlyrottenapple@gmail.com

This is a very basic wrapper for the [QuickJS](https://bellard.org/quickjs/) .

The current functionality includes evaluating JS code, calling a JS function in the global scope
and marshalling 'Value's to and from 'JSValue's.
-}
module Quickjs (JSValue, JSContextPtr, quickjs, quickjsMultithreaded, call, eval, eval_, withJSValue, fromJSValue_) where

import           Foreign
import           Foreign.C                   (CString, CInt, CDouble, CSize)
import           Data.ByteString             (ByteString, useAsCString, useAsCStringLen, packCString)
import           Data.Text.Encoding          (encodeUtf8)
import qualified Language.C.Inline           as C
import           Control.Monad.Catch         (MonadThrow(..), MonadCatch(..), MonadMask(..), finally)
import           Control.Monad               (when, forM_)
import           Control.Monad.Reader        (MonadReader, runReaderT, ask)
import           Control.Monad.Trans.Reader  (ReaderT)
import           Control.Monad.IO.Class      (MonadIO, liftIO)
import           Control.Monad.IO.Unlift     (MonadUnliftIO(..), UnliftIO(..), askUnliftIO)
import           Data.Aeson                  (Value(..), encode, toJSON)
import qualified Data.Aeson                  as Aeson
import           Data.Scientific             (fromFloatDigits, toRealFloat, toBoundedInteger, isInteger)
import           Data.Text                   (Text)
import           Data.Vector                 (fromList, imapM_)
import           Data.HashMap.Strict         (HashMap, empty, insert, toList)
import           Data.String.Conv            (toS)
import           Data.Time.Clock.POSIX       (posixSecondsToUTCTime)
import           Control.Concurrent          (rtsSupportsBoundThreads, runInBoundThread)

import           Quickjs.Types
import           Quickjs.Error


C.context quickjsCtx
C.include "quickjs.h"
C.include "quickjs-libc.h"


foreign import ccall "JS_NewRuntime"
  jsNewRuntime :: IO (Ptr JSRuntime)

foreign import ccall "JS_FreeRuntime"
  jsFreeRuntime :: Ptr JSRuntime -> IO ()



foreign import ccall "JS_NewContext"
  jsNewContext :: Ptr JSRuntime -> IO (Ptr JSContext)

foreign import ccall "JS_FreeContext"
  jsFreeContext :: Ptr JSContext -> IO ()



jsFreeValue :: JSContextPtr -> JSValue -> IO ()
jsFreeValue ctx val = with val $ \v -> [C.block| void {
    if (JS_VALUE_HAS_REF_COUNT(*$(JSValue *v))) {
      JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(*$(JSValue *v));
      if (--p->ref_count <= 0) {
        __JS_FreeValue($(JSContext *ctx), *$(JSValue *v));
      }
    }
  } |]



type JSContextPtr = Ptr JSContext
type JSValueConstPtr = Ptr JSValueConst

jsIs_ :: (MonadIO m, Storable p, Eq n, Num n) => p -> (Ptr p -> IO n) -> m Bool
jsIs_ val fun = do
  b <- liftIO $ with val fun
  return $ b == 1

-- jsIsNumber :: MonadIO m => JSValue -> m Bool
-- jsIsNumber val = jsIs_ val $ \valPtr -> [C.block| int { return JS_IsNumber(*$(JSValueConst *valPtr)); } |]

jsIsArray :: MonadIO m => JSContextPtr -> JSValue -> m Bool
jsIsArray ctxPtr val = jsIs_ val $ \valPtr -> [C.block| int { return JS_IsArray($(JSContext *ctxPtr), *$(JSValueConst *valPtr)); } |]

jsIsDate :: MonadIO m => JSContextPtr -> JSValue -> m Bool
jsIsDate ctxPtr val = do
  globalObject <- liftIO $ C.withPtr_ $ \globalObjectPtr ->
      [C.block| void { *$(JSValue *globalObjectPtr) = JS_GetGlobalObject($(JSContext *ctxPtr)); } |]
  dateConstructor <- jsGetPropertyStr ctxPtr globalObject "Date"
  liftIO $ do
    jsFreeValue ctxPtr globalObject
    res <- with val $ \valPtr -> with dateConstructor $ \dateCPtr ->
      [C.block| int { return JS_IsInstanceOf($(JSContext *ctxPtr), *$(JSValueConst *valPtr), *$(JSValueConst *dateCPtr)); } |]
    jsFreeValue ctxPtr dateConstructor
    return $ res > 0


jsIsTryAll :: MonadThrow m =>
  JSValue -> [JSValue -> m Bool] -> [JSTypeEnum] -> JSTypeEnum -> m JSTypeEnum
jsIsTryAll _ [] _ def = return def
jsIsTryAll jsval (f:funs)(l:lbls) def = do
  b <- f jsval
  if b then return l else jsIsTryAll jsval funs lbls def
jsIsTryAll _ _ _ _ = throwM $ InternalError $ "jsIsTryAll_ unreachable case"


jsIs :: (MonadIO m, MonadThrow m) => JSContextPtr -> JSValue -> m JSTypeEnum
jsIs ctx jsval = case fromCType $ tag jsval of
  Just JSTagObject ->
    jsIsTryAll jsval [jsIsArray ctx, jsIsDate ctx] [JSIsArray, JSIsDate] (JSTypeFromTag JSTagObject)
  Just t | t == JSTagBigDecimal ||
           t == JSTagBigInt ||
           t == JSTagBigFloat ||
           t == JSTagInt ||
           t == JSTagFloat64 -> return JSIsNumber
         | otherwise -> return $ JSTypeFromTag t
  Nothing -> throwM $ UnknownJSTag (tag jsval)



jsNullValue :: JSValue
jsNullValue = JSValue { u = 0, tag = toCType JSTagNull }

jsNewBool :: JSContextPtr -> Bool -> IO JSValue
jsNewBool ctxPtr bool = do
  let b = if bool then 1 else 0
  C.withPtr_ $ \ptr -> [C.block| void { *$(JSValue *ptr) = JS_NewBool($(JSContext *ctxPtr), $(int b)); } |]

jsNewFloat64 :: JSContextPtr -> CDouble -> IO JSValue
jsNewFloat64 ctxPtr num =
  C.withPtr_ $ \ptr -> [C.block| void { *$(JSValue *ptr) = JS_NewFloat64($(JSContext *ctxPtr), $(double num)); } |]

jsNewInt64 :: JSContextPtr -> Int64 -> IO JSValue
jsNewInt64 ctxPtr num = do
  C.withPtr_ $ \ptr -> [C.block| void { *$(JSValue *ptr) = JS_NewInt64($(JSContext *ctxPtr), $(int64_t num)); } |]

jsNewString :: JSContextPtr -> ByteString -> IO JSValue
jsNewString ctxPtr s = C.withPtr_ $ \ptr -> useAsCStringLen s $ \(cstringPtr, cstringLen) -> do
  let len = fromIntegral cstringLen
  [C.block| void { *$(JSValue *ptr) = JS_NewStringLen($(JSContext *ctxPtr), $(const char *cstringPtr), $(size_t len)); } |]



checkIsException :: (MonadThrow m, MonadIO m) => Text -> JSContextPtr -> JSValue -> m ()
checkIsException loc ctxPtr val =
  case fromCType $ tag val of
    Just JSTagException -> do
      err <- getErrorMessage ctxPtr
      liftIO $ jsFreeValue ctxPtr val
      throwM $ JSException loc err
    _ -> pure ()



jsonToJSValue :: (MonadThrow m, MonadIO m) => JSContextPtr -> Value -> m JSValue
jsonToJSValue _ Null = pure jsNullValue
jsonToJSValue ctx (Bool b) = liftIO $ jsNewBool ctx b
jsonToJSValue ctx (Number n) =
  if not (isInteger n) then liftIO $ jsNewFloat64 ctx (toRealFloat n)
  else case toBoundedInteger n of
    Just i -> liftIO $ jsNewInt64 ctx i
    Nothing -> throwM $ InternalError "Value does not fit in Int64"
jsonToJSValue ctx (String s) = liftIO $ jsNewString ctx $ toS s
jsonToJSValue ctxPtr (Array xs) = do
  arrVal <- liftIO (C.withPtr_ $ \arrValPtr -> [C.block| void { *$(JSValueConst *arrValPtr) = JS_NewArray($(JSContext *ctxPtr)); } |])

  checkIsException "jsonToJSValue/Array/1" ctxPtr arrVal

  flip imapM_ xs $ \index value -> do
    val <- jsonToJSValue ctxPtr value
    checkIsException "jsonToJSValue/Array/2" ctxPtr val

    let idx = fromIntegral index
    code <- liftIO (with arrVal $ \arrValPtr -> with val $ \valPtr ->
      [C.block| int { return JS_DefinePropertyValueUint32(
        $(JSContext *ctxPtr), 
        *$(JSValueConst *arrValPtr),
        $(uint32_t idx),
        *$(JSValueConst *valPtr),
        JS_PROP_C_W_E
      ); } |])
    return ()

    if (code < 0) then do
      liftIO $ jsFreeValue ctxPtr arrVal
      throwM $ InternalError "Could not append element to array"
    else return ()

  return arrVal
jsonToJSValue ctxPtr (Object o) = do
  objVal <- liftIO (C.withPtr_ $ \objValPtr ->
    [C.block| void { *$(JSValueConst *objValPtr) = JS_NewObject($(JSContext *ctxPtr)); } |])

  checkIsException "jsonToJSValue/Object/1" ctxPtr objVal

  forM_ (toList o) $ \(key,value) -> do
    val <- jsonToJSValue ctxPtr value
    checkIsException "jsonToJSValue/Object/2" ctxPtr val

    code <- liftIO (with objVal $ \objValPtr -> with val $ \valPtr ->
      useAsCString (encodeUtf8 key) $ \cstringPtr -> do
        [C.block| int { 
          return JS_DefinePropertyValueStr(
            $(JSContext *ctxPtr), 
            *$(JSValueConst *objValPtr),
            $(const char *cstringPtr),
            *$(JSValueConst *valPtr),
            JS_PROP_C_W_E
          ); 
        } |])

    when (code < 0) $ do
      liftIO $ jsFreeValue ctxPtr objVal
      throwM $ InternalError "Could not add add property to object"

  return objVal


jsToBool :: (MonadThrow m, MonadIO m) => JSContextPtr -> JSValue -> m Bool
jsToBool ctxPtr val = do
    code <- liftIO $ with val $ \valPtr -> [C.block| int { return JS_ToBool($(JSContext *ctxPtr), *$(JSValueConst *valPtr)); } |]
    case code of
        -1 -> getErrorMessage ctxPtr >>= throwM . JSException "jsToBool"
        0 -> return False
        _ -> return True

jsToInt64 :: (MonadThrow m, MonadIO m) => JSContextPtr -> JSValue -> m Int64
jsToInt64 ctxPtr val = do
  (res, code) <- liftIO $ C.withPtr $ \intPtr -> with val $ \valPtr -> [C.block| int { return JS_ToInt64($(JSContext *ctxPtr), $(int64_t *intPtr), *$(JSValueConst *valPtr)); } |]
  if code == 0 then return res
  else getErrorMessage ctxPtr >>= throwM . JSException "jsToInt64"


jsToFloat64 :: (MonadThrow m, MonadIO m) => JSContextPtr -> JSValue -> m CDouble
jsToFloat64 ctxPtr val = do
  (res, code) <- liftIO $ C.withPtr $ \doublePtr -> with val $ \valPtr -> [C.block| int { return JS_ToFloat64($(JSContext *ctxPtr), $(double *doublePtr), *$(JSValueConst *valPtr)); } |]
  if code == 0 then return res
  else getErrorMessage ctxPtr >>= throwM . JSException "jsToFloat64"



jsToString :: MonadIO m => JSContextPtr -> JSValue -> m ByteString
jsToString ctxPtr val = liftIO $ do
    cstring <- with val $ \valPtr -> [C.block| const char * { return JS_ToCString($(JSContext *ctxPtr), *$(JSValueConst *valPtr)); } |]
    if cstring == nullPtr then return ""
    else do
      string <- packCString cstring
      jsFreeCString ctxPtr cstring
      return string


foreign import ccall "JS_FreeCString"
  jsFreeCString :: JSContextPtr -> CString -> IO ()


jsToJSON :: (MonadCatch m, MonadIO m) => JSContextPtr -> JSValue -> m Value
jsToJSON ctx jsval = do
  ty <- jsIs ctx jsval
  case ty of
    JSTypeFromTag JSTagException -> do
      err <- getErrorMessage ctx
      liftIO $ jsFreeValue ctx jsval
      throwM $ JSException "jsToJSON/JSTagException" err
    JSTypeFromTag JSTagNull -> return Null
    JSTypeFromTag JSTagUndefined -> return Null
    JSTypeFromTag JSTagBool -> do
      b <- jsToBool ctx jsval
      return $ Bool b
    JSIsNumber -> do
      n <- jsToFloat64 ctx jsval
      return $ Number $ fromFloatDigits n
    JSTypeFromTag JSTagString -> do
      s <- jsToString ctx jsval
      return $ String $ toS s
    JSIsArray -> do
      len <- do
        lenVal <- jsGetPropertyStr ctx jsval "length"
        len' <- jsToInt64 ctx lenVal
        liftIO $ jsFreeValue ctx lenVal
        return len'
      vs <- jsArrayToJSON ctx jsval 0 (fromIntegral len)
      return $ Array $ fromList vs
    JSIsDate -> do
      getter <- jsGetPropertyStr ctx jsval "getTime"

      timestampRaw <- liftIO $ C.withPtr_ $ \res -> with getter $ \getterPtr -> with jsval $ \jsvalPtr ->
        [C.block| void { *$(JSValue *res) = JS_Call($(JSContext *ctx), *$(JSValueConst *getterPtr), *$(JSValueConst *jsvalPtr), 0, NULL); } |]

      timestamp <- jsToFloat64 ctx timestampRaw
      liftIO $ do
        jsFreeValue ctx getter
        jsFreeValue ctx timestampRaw
      return $ toJSON $ posixSecondsToUTCTime $ realToFrac $ timestamp / 1000
    JSTypeFromTag JSTagObject -> do
      o <- jsObjectToJSON ctx jsval
      return $ Object o
    JSTypeFromTag f -> throwM $ UnsupportedTypeTag f
    JSIsError -> throwM $ InternalError "JSIsError unreachable"


jsArrayToJSON :: (MonadCatch m, MonadIO m) => JSContextPtr -> JSValue -> Int -> Int -> m [Value]
jsArrayToJSON ctxPtr jsval index len =
  if index < len then do
    v <- do
      let idx = fromIntegral index
      val <- liftIO $ C.withPtr_ $ \ptr -> with jsval $ \jsvalPtr ->
        [C.block| void { *$(JSValue *ptr) = JS_GetPropertyUint32($(JSContext *ctxPtr), *$(JSValueConst *jsvalPtr), $(uint32_t idx)); } |]

      checkIsException "jsArrayToJSON" ctxPtr val
      res <- jsToJSON ctxPtr val
      liftIO $ jsFreeValue ctxPtr val
      return res

    vs <- jsArrayToJSON ctxPtr jsval (index+1) len
    return $ v:vs
  else return []






forLoop :: (Num a, Ord a, Monad m) => a -> (a -> m ()) -> m ()
forLoop end f = go 0
  where
    go !x | x < end   = f x >> go (x+1)
          | otherwise = return ()




jsObjectToJSON :: (MonadCatch m, MonadIO m) => JSContextPtr -> JSValue -> m (HashMap Text Value)
jsObjectToJSON ctxPtr obj = do
    let flags = unJSGPNMask $ jsGPNStringMask .|. jsGPNSymbolMask .|. jsGPNEnumOnly
    properties <- liftIO $ malloc
    plen <- jsGetOwnPropertyNames ctxPtr obj properties flags
      `catch` (\(e::SomeJSRuntimeException) -> do
        liftIO $ free properties
        throwM e
      )
    objPtr <- liftIO $ malloc
    liftIO $ poke objPtr obj

    res <- collectVals properties objPtr 0 plen `catch` (\(e::SomeJSRuntimeException) -> do
        liftIO $ free objPtr
        throwM e
      )
    cleanup properties plen
    return res
  where
    collectVals :: (MonadCatch m, MonadIO m) => Ptr (Ptr JSPropertyEnum) -> JSValueConstPtr -> Int -> Int -> m (HashMap Text Value)
    collectVals properties objPtr !index end
      | index < end = do
        let i = fromIntegral index

        key <- do
          key' <- liftIO $ C.withPtr_ $ \ptr -> [C.block| void { *$(JSValue *ptr) = JS_AtomToString($(JSContext *ctxPtr), (*$(JSPropertyEnum **properties))[$(uint32_t i)].atom); } |]
          checkIsException "jsObjectToJSON/collectVals/1" ctxPtr key'
          res <- jsToJSON ctxPtr key'
          liftIO $ jsFreeValue ctxPtr key'
          return res

        case key of
          String k -> do
            val <-  do
              val' <- liftIO $ C.withPtr_ $ \ptr ->
                [C.block| void { *$(JSValue *ptr) = JS_GetProperty($(JSContext *ctxPtr), *$(JSValueConst *objPtr), (*$(JSPropertyEnum **properties))[$(uint32_t i)].atom); } |]
              checkIsException "jsObjectToJSON/collectVals/2" ctxPtr val'
              res <- jsToJSON ctxPtr val'
              liftIO $ jsFreeValue ctxPtr val'
              return res

            xs <- collectVals properties objPtr (index+1) end
            return $ insert k val xs
          x -> throwM $ InternalError $ "Could not get property name" <> toS (encode x)

      | otherwise = return empty

    cleanup :: MonadIO m => Ptr (Ptr JSPropertyEnum) -> Int -> m ()
    cleanup properties plen = liftIO $ do
      forLoop plen $ \index -> do
        let i = fromIntegral index
        [C.block| void { JS_FreeAtom($(JSContext *ctxPtr), (*$(JSPropertyEnum **properties))[$(uint32_t i)].atom); }|]

      let void_ptr = castPtr properties
      [C.block| void { js_free($(JSContext *ctxPtr), *$(void **void_ptr)); }|]

      free properties



getErrorMessage :: MonadIO m => JSContextPtr -> m Text
getErrorMessage ctxPtr = liftIO $ do
  ex <- C.withPtr_ $ \ptr -> [C.block| void { *$(JSValue *ptr) = JS_GetException($(JSContext *ctxPtr)); } |]
  res <- jsToString ctxPtr ex
  jsFreeValue ctxPtr ex
  return $ toS res



jsGetPropertyStr :: MonadIO m => JSContextPtr -> JSValue -> ByteString -> m JSValue
jsGetPropertyStr ctxPtr val str = liftIO $
  C.withPtr_ $ \ptr -> useAsCString str $ \prop -> with val $ \valPtr ->
    [C.block| void { *$(JSValue *ptr) = JS_GetPropertyStr($(JSContext *ctxPtr), *$(JSValueConst *valPtr), $(const char *prop)); } |]


jsGetOwnPropertyNames :: (MonadThrow m, MonadIO m) => JSContextPtr -> JSValue -> Ptr (Ptr JSPropertyEnum) -> CInt -> m Int
jsGetOwnPropertyNames ctxPtr val properties flags = do
  (len,code) <- liftIO $ C.withPtr $ \plen -> with val $ \valPtr ->
    [C.block| int { return JS_GetOwnPropertyNames($(JSContext *ctxPtr), $(JSPropertyEnum **properties), $(uint32_t *plen), *$(JSValueConst *valPtr), $(int flags)); } |]
  if code == 0 then return (fromIntegral len)
  else throwM $ InternalError "Could not get object properties"


jsCall :: JSContextPtr -> JSValue -> CInt -> (Ptr JSValue) -> IO JSValue
jsCall ctxt fun_obj argc argv = C.withPtr_ $ \res -> with fun_obj $ \funPtr ->
  [C.block| void { *$(JSValue *res) = JS_Call($(JSContext *ctxt), *$(JSValueConst *funPtr), JS_NULL, $(int argc), $(JSValueConst *argv)); } |]


jsEval :: JSContextPtr -> CString -> CSize -> CString -> CInt -> IO JSValue
jsEval ctxPtr input input_len filename eval_flags = C.withPtr_ $ \ptr ->
  [C.block| void { *$(JSValue *ptr) = JS_Eval($(JSContext *ctxPtr), $(const char *input), $(size_t input_len), $(const char *filename), $(int eval_flags)); } |]


evalRaw :: JSContextPtr -> JSEvalType -> ByteString -> IO JSValue
evalRaw ctx eTyp code =
    useAsCString "script.js" $ \cfilename ->
        useAsCStringLen code $ \(ccode, ccode_len) ->
            jsEval ctx ccode (fromIntegral ccode_len) cfilename (toCType eTyp)




evalAs :: (MonadMask m, MonadReader JSContextPtr m, MonadIO m) => JSEvalType -> ByteString -> m Value
evalAs eTyp code = do
  ctx <- ask
  val <- liftIO $ evalRaw ctx eTyp code
  -- checkIsException "evalAs" ctx val
  jsToJSON ctx val `finally` freeJSValue val



{-|
Evaluates the given string and returns a 'Value' (if the result can be converted).
-}
eval :: (MonadMask m, MonadReader JSContextPtr m, MonadIO m) => ByteString -> m Value
eval = evalAs Global

evalAs_ :: (MonadThrow m, MonadReader JSContextPtr m, MonadIO m) => JSEvalType -> ByteString -> m ()
evalAs_ eTyp code = do
  ctx <- ask
  val <- liftIO $ evalRaw ctx eTyp code
  checkIsException "evalAs_" ctx val
  freeJSValue val



{-|
More efficient than 'eval' if we don't care about the value of the expression, 
e.g. if we are evaluating a function definition or performing other side-effects such as
printing to console/modifying state.
-}
eval_ :: (MonadThrow m, MonadReader JSContextPtr m, MonadIO m) => ByteString -> m ()
eval_ = evalAs_ Global


fromJSValue_ :: (MonadCatch m, MonadReader JSContextPtr m, MonadIO m) => JSValue -> m Value
fromJSValue_ val = do
  ctx <- ask
  jsToJSON ctx val



-- fromJSValue :: (Aeson.FromJSON a, MonadCatch m, MonadReader JSContextPtr m, MonadIO m) => JSValue -> m a
-- fromJSValue val = do
--   jsonval <- fromJSValue_ val

--   case Aeson.fromJSON jsonval of
--     Aeson.Success a -> return a
--     Aeson.Error err -> throwM $ InternalError err



{-|
Takes a value with a defined 'ToJSON' instance. This value is marshalled to a 'JSValue'
and passed as an argument to the callback function, provided as the second argument to 'withJSValue'
-}
withJSValue :: (MonadMask m, MonadReader JSContextPtr m, MonadIO m, Aeson.ToJSON a) => a -> (JSValue -> m b) -> m b
withJSValue v f = do

  ctx <- ask
  val <- jsonToJSValue ctx (Aeson.toJSON v)
  f val `finally` freeJSValue val




callRaw :: (MonadThrow m, MonadIO m) => JSContextPtr -> ByteString -> [JSValue] -> m JSValue
callRaw ctxPtr funName args = do
    globalObject <- liftIO $ C.withPtr_ $ \globalObjectPtr ->
      [C.block| void { *$(JSValue *globalObjectPtr) = JS_GetGlobalObject($(JSContext *ctxPtr)); } |]

    fun <- jsGetPropertyStr ctxPtr globalObject funName

    liftIO $ jsFreeValue ctxPtr globalObject

    ty <- jsIs ctxPtr fun
    case ty of
      JSTypeFromTag JSTagException -> do
        err <- getErrorMessage ctxPtr
        liftIO $ jsFreeValue ctxPtr fun
        throwM $ JSException "callRaw" err
      JSTypeFromTag JSTagUndefined -> throwM $ JSValueUndefined $ toS funName
      JSTypeFromTag JSTagObject -> do
        res <- liftIO $ withArrayLen args $ \len argv -> jsCall ctxPtr fun (fromIntegral $ len) argv
        liftIO $ jsFreeValue ctxPtr fun
        return res
      _ -> throwM $ JSValueIncorrectType {name = toS funName, expected = JSTypeFromTag JSTagObject, found = ty }


-- call :: (MonadThrow m, MonadReader JSContextPtr m, MonadIO m) => String -> [JSValue] -> m JSValue
-- call funName args = do
--   ctx <- ask
--   val <- callRaw ctx funName args
--   checkIsException ctx val
--   return val



call :: (MonadMask m, MonadReader JSContextPtr m, MonadIO m) => ByteString -> [JSValue] -> m Value
call funName args = do
  ctx <- ask
  val <- callRaw ctx funName args
  jsToJSON ctx val `finally` freeJSValue val


freeJSValue :: (MonadThrow m, MonadReader JSContextPtr m, MonadIO m) => JSValue -> m ()
freeJSValue val = do
  ctx <- ask
  liftIO $ jsFreeValue ctx val

{-|
This function initialises a new JS runtime and performs the given computation within this context.

For example, we can evaluate an expression:

>quickjs $ do
>  res <- eval "1+2"
>  liftIO $ print res

Declare a function and call it on an argument:

>quickjs $ do
>  _ <- eval_ "f = (x) => x+1"
>  res <- eval "f(2)"
>  liftIO $ print res

Pass a Haskell value to the JS runtime:

>quickjs $ do
>  _ <- eval_ "f = (x) => x+1"
>  res <- withJSValue (3::Int) $ \x -> call "f" [x]
>  liftIO $ print res

-}
quickjs :: MonadIO m => ReaderT (Ptr JSContext) m b -> m b
quickjs f = do
  (rt, ctx) <- liftIO $ do
    _rt <- jsNewRuntime
    _ctx <- jsNewContext _rt

    [C.block| void { 
      js_std_add_helpers($(JSContext *_ctx), -1, NULL);
    } |]
    return (_rt, _ctx)

  res <- runReaderT f ctx
  cleanup ctx rt
  return res
  where
    cleanup ctx rt = liftIO $ do
      jsFreeContext ctx
      jsFreeRuntime rt

{-|
This env differs from regular 'quickjs', in that it wraps the computation in the 'runInBoundThread' function.
This is needed when running the Haskell program mutithreaded (e.g. when using the testing framework Tasty),
since  quickjs does not like being called from an OS thread other than the one it was started in.
Because Haskell uses lightweight threads, this might happen if threaded mode is enabled, as is the case in Tasty.
This problem does not occur when running via Main.hs, if compiled as single threaded...
For more info see the paper [Extending the Haskell Foreign Function Interface with Concurrency](https://simonmar.github.io/bib/papers/conc-ffi.pdf)
-}
quickjsMultithreaded :: MonadUnliftIO m => ReaderT (Ptr JSContext) m b -> m b
quickjsMultithreaded f
  | rtsSupportsBoundThreads = do
    (u :: UnliftIO m) <- askUnliftIO

    liftIO $ runInBoundThread $ do
      rt <- jsNewRuntime
      ctx <- jsNewContext rt

      [C.block| void { 
        js_std_add_helpers($(JSContext *ctx), -1, NULL);
      } |]

      res <-  unliftIO u $ runReaderT f ctx
      cleanup ctx rt
      return res
  | otherwise = quickjs f
  where
    cleanup ctx rt = do
      jsFreeContext ctx
      jsFreeRuntime rt