{-# LANGUAGE CPP #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE JavaScriptFFI #-}
{-# LANGUAGE GHCForeignImportPrim #-}
{-# LANGUAGE UnliftedFFITypes #-}
{-# LANGUAGE UnboxedTuples #-}

module GHC.JS.Prim ( JSVal(..), JSVal#
                   , JSException(..)
                   , WouldBlockException(..)
#if defined(javascript_HOST_ARCH)
                  , toIO
                  , resolve
                  , resolveIO
                  , mkJSException
                  , fromJSString
                  , toJSString
                  , toJSArray
                  , fromJSArray
                  , fromJSInt
                  , toJSInt
                  , isNull
                  , isUndefined
                  , jsNull
                  , getProp
                  , getProp'
                  , getProp#
                  , unsafeGetProp
                  , unsafeGetProp'
                  , unsafeGetProp#
                  , unpackJSString#
                  , unpackJSStringUtf8#
                  , unsafeUnpackJSString#
                  , unsafeUnpackJSStringUtf8#
                  , unpackJSStringUtf8##
                  , unsafeUnpackJSStringUtf8##

#endif
                  ) where

import           Data.Typeable (Typeable)
import           Unsafe.Coerce (unsafeCoerce)

import           GHC.Prim
import qualified GHC.Exception as Ex
import qualified GHC.Exts as Exts
import qualified GHC.CString as GHC
import           GHC.IO

{-
  JSVal is a boxed type that can be used as FFI
  argument or result.
-}

#if defined(javascript_HOST_ARCH)
data JSVal  = JSVal ByteArray#
type JSVal# = ByteArray#
#else
data JSVal  = JSVal Addr#
type JSVal# = Addr#
#endif

{-
  When a JavaScript exception is raised inside
  a safe or interruptible foreign call, it is converted
  to a JSException
 -}
data JSException = JSException JSVal String
  deriving (Typeable)

instance Ex.Exception JSException

instance Show JSException where
  show (JSException _ xs) = "JavaScript exception: " ++ xs

#if defined(javascript_HOST_ARCH)

{-# NOINLINE toIO #-}
toIO :: Exts.Any -> IO Exts.Any
toIO x = pure x

{-# NOINLINE resolve #-}
resolve :: JSVal# -> JSVal# -> Exts.Any -> IO ()
resolve accept reject x = resolveIO accept reject (pure x)

{-# NOINLINE resolveIO #-}
resolveIO :: JSVal# -> JSVal# -> IO Exts.Any -> IO ()
resolveIO accept reject x =
  (x >>= evaluate >>= js_callback_any accept) `catch`
  (\(e::Ex.SomeException) -> do
    exceptionText <- evaluate (toJSString $ Ex.displayException e) `catch`
                         (\(_::Ex.SomeException) -> evaluate (toJSString "unknown exception"))
    js_callback_jsval reject exceptionText)

mkJSException :: JSVal -> IO JSException
mkJSException ref =
  return (JSException (unsafeCoerce ref) (fromJSString ref))

{- | Low-level conversion utilities for packages that cannot
     depend on ghcjs-base
 -}

{- | returns an empty string if the JSVal does not contain
     a string
 -}
fromJSString :: JSVal -> String
fromJSString = unsafeCoerce . js_fromJSString
{-# INLINE fromJSString #-}

toJSString :: String -> JSVal
toJSString = js_toJSString . unsafeCoerce . seqList
{-# INLINE [0] toJSString #-}
{-# RULES
"GHC.JS.PRIM toJSString/literal" forall a.
  toJSString (GHC.unpackCString# a) = JSVal (unsafeUnpackJSStringUtf8## a)
"GHC.JS.PRIM toJSString/literalUtf8" forall a.
  toJSString (GHC.unpackCStringUtf8# a) = JSVal (unsafeUnpackJSStringUtf8## a)
  #-}

fromJSArray :: JSVal -> IO [JSVal]
fromJSArray = unsafeCoerce . js_fromJSArray
{-# INLINE fromJSArray #-}

toJSArray :: [JSVal] -> IO JSVal
toJSArray = js_toJSArray . unsafeCoerce . seqList
{-# INLINE toJSArray #-}

{- | returns zero if the JSVal does not contain a number
 -}
fromJSInt :: JSVal -> Int
fromJSInt = js_fromJSInt
{-# INLINE fromJSInt #-}

toJSInt :: Int -> JSVal
toJSInt = js_toJSInt
{-# INLINE toJSInt #-}

isNull :: JSVal -> Bool
isNull = js_isNull
{-# INLINE isNull #-}

isUndefined :: JSVal -> Bool
isUndefined = js_isUndefined
{-# INLINE isUndefined #-}

jsNull :: JSVal
jsNull = js_null
{-# INLINE CONLIKE jsNull #-}

getProp :: JSVal -> String -> IO JSVal
getProp o p = js_getProp o (unsafeCoerce $ seqList p)
{-# INLINE [0] getProp #-}
{-# RULES
"GHC.JS.PRIM getProp/literal" forall o a.
  getProp o (GHC.unpackCString# a) = getProp# o a
"GHC.JS.PRIM getProp/literalUtf8" forall o a.
  getProp o (GHC.unpackCStringUtf8# a) = getPropUtf8# o a
  #-}

-- | only safe on immutable object
unsafeGetProp :: JSVal -> String -> JSVal
unsafeGetProp o p = js_unsafeGetProp o (unsafeCoerce $ seqList p)
{-# INLINE [0] unsafeGetProp #-}
{-# RULES
"GHC.JS.PRIM unsafeGetProp/literal" forall o a.
  unsafeGetProp o (GHC.unpackCString# a) = unsafeGetProp# o a
"GHC.JS.PRIM unsafeGetProp/literalUtf8" forall o a.
  unsafeGetProp o (GHC.unpackCStringUtf8# a) = unsafeGetPropUtf8# o a
  #-}

getProp' :: JSVal -> JSVal -> IO JSVal
getProp' o p = js_getProp' o p
{-# INLINE [0] getProp' #-}
{-# RULES
"GHC.JS.PRIM getProp'/literal" forall o a.
  getProp' o (unsafeUnpackJSString# a) = getProp# o a
"GHC.JS.PRIM getProp'/literalUtf8" forall o a.
  getProp' o (unsafeUnpackJSStringUtf8# a) = getPropUtf8# o a
  #-}

-- | only safe on immutable object
unsafeGetProp' :: JSVal -> JSVal -> JSVal
unsafeGetProp' o p = js_unsafeGetProp' o p
{-# INLINE [0] unsafeGetProp' #-}
{-# RULES
"GHC.JS.PRIM unsafeGetProp'/literal" forall o a.
  unsafeGetProp' o (unsafeUnpackJSString# a) = unsafeGetPropUtf8# o a
"GHC.JS.PRIM unsafeGetProp'/literalUtf8" forall o a.
  unsafeGetProp' o (unsafeUnpackJSStringUtf8# a) = unsafeGetPropUtf8# o a
  #-}


-- | only safe on immutable Addr#
getProp# :: JSVal -> Addr# -> IO JSVal
getProp# (JSVal o) p = IO $
  \s -> case getPropUtf8## o p s of (# s', v #) -> (# s', JSVal v #)
{-# INLINE [0] getProp# #-}
-- js_getProp# o p

-- | only safe on immutable Addr#
getPropUtf8# :: JSVal -> Addr# -> IO JSVal
getPropUtf8# (JSVal o) p = IO $
  \s -> case getPropUtf8## o p s of (# s', v #) -> (# s', JSVal v #)
{-# INLINE [0] getPropUtf8# #-}

getPropUtf8## :: JSVal# -> Addr# -> State# s -> (# State# s, JSVal# #)
getPropUtf8## o p = js_getPropUtf8## o p
{-# NOINLINE getPropUtf8## #-}

-- | only safe on immutable Addr# and JSVal
unsafeGetProp# :: JSVal -> Addr# -> JSVal
unsafeGetProp# (JSVal o) p = JSVal (unsafeGetPropUtf8## o p)
{-# INLINE [0] unsafeGetProp# #-}

-- | only safe on immutable Addr# and JSVal
unsafeGetPropUtf8# :: JSVal -> Addr# -> JSVal
unsafeGetPropUtf8# (JSVal o) p = JSVal (unsafeGetPropUtf8## o p)
{-# INLINE [0] unsafeGetPropUtf8# #-}

unsafeGetPropUtf8## :: JSVal# -> Addr# -> JSVal#
unsafeGetPropUtf8## o p = js_unsafeGetPropUtf8## o p
{-# NOINLINE unsafeGetPropUtf8## #-}

unpackJSString# :: Addr# -> IO JSVal
unpackJSString# a = IO $
  \s -> case unpackJSStringUtf8## a s of (# s', v #) -> (# s', JSVal v #)
{-# INLINE [0] unpackJSString# #-}

unpackJSStringUtf8# :: Addr# -> IO JSVal
unpackJSStringUtf8# a = IO $
  \s -> case unpackJSStringUtf8## a s of (# s', v #) -> (# s', JSVal v #)
{-# INLINE [0] unpackJSStringUtf8# #-}

unpackJSStringUtf8## :: Addr# -> State# s -> (# State# s, JSVal# #)
unpackJSStringUtf8## a s = js_unpackJSStringUtf8## a s
{-# NOINLINE unpackJSStringUtf8## #-}

-- | only safe on immutable Addr#
unsafeUnpackJSString# :: Addr# -> JSVal
unsafeUnpackJSString# a = JSVal (unsafeUnpackJSStringUtf8## a)
  -- js_unsafeUnpackJSString# a
{-# INLINE [0] unsafeUnpackJSString# #-}

-- | only safe on immutable Addr#
unsafeUnpackJSStringUtf8# :: Addr# -> JSVal
unsafeUnpackJSStringUtf8# a = JSVal (unsafeUnpackJSStringUtf8## a)
{-# INLINE [0] unsafeUnpackJSStringUtf8# #-}

unsafeUnpackJSStringUtf8## :: Addr# -> JSVal#
unsafeUnpackJSStringUtf8## a = js_unsafeUnpackJSStringUtf8## a
{-# NOINLINE unsafeUnpackJSStringUtf8## #-}


-- reduce the spine and all list elements to whnf
seqList :: [a] -> [a]
seqList xs = go xs `seq` xs
  where go (y:ys) = y `seq` go ys
        go []     = ()

foreign import javascript unsafe "(($1) => { return h$toHsString($1); })"
  js_fromJSString :: JSVal -> Exts.Any

foreign import javascript unsafe "(($1) => { return h$fromHsString($1); })"
  js_toJSString :: Exts.Any -> JSVal

foreign import javascript unsafe "(($1) => { return h$toHsListJSVal($1); })"
  js_fromJSArray :: JSVal -> IO Exts.Any

foreign import javascript unsafe "(($1) => { return h$fromHsListJSVal($1); })"
  js_toJSArray :: Exts.Any -> IO JSVal

foreign import javascript unsafe "(($1) => { return ($1 === null); })"
  js_isNull :: JSVal -> Bool

foreign import javascript unsafe "(($1) => { return ($1 === undefined); })"
  js_isUndefined :: JSVal -> Bool

foreign import javascript unsafe "(($1) => { return (typeof($1) === 'number' ? ($1|0) : 0); })"
  js_fromJSInt :: JSVal -> Int

foreign import javascript unsafe "(($1) => { return $1; })"
  js_toJSInt :: Int -> JSVal

foreign import javascript unsafe "(() => { return null; })"
  js_null :: JSVal

foreign import javascript unsafe "(($1,$2) => { return $1[h$fromHsString($2)]; })"
  js_getProp :: JSVal -> Exts.Any -> IO JSVal

foreign import javascript unsafe "(($1,$2) => { return $1[h$fromHsString($2)]; })"
  js_unsafeGetProp :: JSVal -> Exts.Any -> JSVal

foreign import javascript unsafe "(($1,$2) => { return $1[$2]; })"
  js_getProp' :: JSVal -> JSVal -> IO JSVal

foreign import javascript unsafe "(($1,$2) => { return $1[$2]; })"
  js_unsafeGetProp' :: JSVal -> JSVal -> JSVal

foreign import javascript unsafe "(($1,$2_1,$2_2) => { return $1[h$decodeUtf8z($2_1, $2_2)]; })"
  js_getPropUtf8## :: JSVal# -> Addr# -> State# s -> (# State# s, JSVal# #)

foreign import javascript unsafe "(($1,$2_1,$2_2) => { return $1[h$decodeUtf8z($2_1, $2_2)]; })"
  js_unsafeGetPropUtf8## :: JSVal# -> Addr# -> JSVal#

foreign import javascript unsafe "(($1_1,$1_2) => { return h$decodeUtf8z($1_1, $1_2); })"
  js_unpackJSStringUtf8## :: Addr# -> State# s -> (# State# s, JSVal# #)

foreign import javascript unsafe "(($1_1, $1_2) => { return h$decodeUtf8z($1_1,$1_2); })"
  js_unsafeUnpackJSStringUtf8## :: Addr# -> JSVal#

foreign import javascript unsafe "(($1, $2) => { return $1($2); })"
  js_callback_any :: JSVal# -> Exts.Any -> IO ()

foreign import javascript unsafe "(($1, $2) => { return $1($2); })"
  js_callback_jsval :: JSVal# -> JSVal -> IO ()

#endif

{- | If a synchronous thread tries to do something that can only
     be done asynchronously, and the thread is set up to not
     continue asynchronously, it receives this exception.
 -}
data WouldBlockException = WouldBlockException
  deriving (Typeable)

instance Show WouldBlockException where
  show _ = "thread would block"

instance Ex.Exception WouldBlockException
