{-# LANGUAGE CPP #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleInstances #-}

module JavaScript.Extras.Cast
    ( ToJS(..)
    , FromJS(..)
    ) where

import qualified Data.JSString as JS
import qualified Data.Text as T
import GHC.Int
import GHC.Word
import qualified GHCJS.Foreign.Callback as J
import qualified GHCJS.Foreign.Export as J
import qualified GHCJS.Foreign.Internal as JFI
import qualified GHCJS.Marshal.Pure as J
import qualified GHCJS.Nullable as J
import qualified GHCJS.Types as J
import qualified JavaScript.Array.Internal as JAI
import qualified JavaScript.Object.Internal as JOI

-- | This provides a consistent way to convert to JSVal, with different semantics for Char.
-- In the Char's instance of ToJS, it converts to a string instead of integer - IMHO this is less surprising.
--
-- The other reason for this class is while GHCJS base provide both IsJSVal and PToJSVal to convert to jsval,
-- some types are instances of one or the other class.
-- This means you can't use the "Maybe a" instance of PToJSVal if it contains IsISJVal but not pToJSVal.
class ToJS a where
    -- | This is a pure conversion, so instances must be able to convert
    -- the same or equivalent JSVal each time.
    toJS :: a -> J.JSVal
    default toJS :: J.IsJSVal a => a -> J.JSVal
    toJS = J.jsval

instance ToJS J.JSVal where
    toJS = id
instance ToJS (J.Callback a)
instance ToJS (J.Export a)
instance ToJS (J.Nullable a) where
    toJS (J.Nullable a) = a
instance ToJS (JAI.SomeJSArray m)
instance ToJS JOI.Object
instance ToJS Bool where
    toJS = J.pToJSVal
-- | Char instance converts to string
instance ToJS Char where
    toJS a = J.pToJSVal [a]
instance ToJS Double where
    toJS = J.pToJSVal
instance ToJS Float where
    toJS = J.pToJSVal
instance ToJS Int where
    toJS = J.pToJSVal
instance ToJS Int8 where
    toJS = J.pToJSVal
instance ToJS Int16 where
    toJS = J.pToJSVal
instance ToJS Int32 where
    toJS = J.pToJSVal
instance ToJS Word where
    toJS = J.pToJSVal
instance ToJS Word8 where
    toJS = J.pToJSVal
instance ToJS Word16 where
    toJS = J.pToJSVal
instance ToJS Word32 where
    toJS = J.pToJSVal
instance ToJS T.Text where
    toJS = J.pToJSVal
instance ToJS String where
    toJS = J.pToJSVal
instance ToJS J.JSString
instance ToJS a => ToJS (Maybe a) where
    toJS Nothing  = J.nullRef
    toJS (Just a) = toJS a

-- | This provides a consistent way to safely convert from JSVal.
-- The semantics is that if the return value is a Just, then the JSVal is not a null value.
-- Also, Nothing is also returned for values out of range. They are not silently truncated.
-- (Except for Float where there may be loss of precision) during conversion.
--
-- The reason for this class is because GHCJS.Marshal.fromJSVal and GHCJS.Marshal.pFromJSVal
-- are not safe to use as it assumes that the JSVal are of the correct type and not null.
-- (https://github.com/ghcjs/ghcjs-base/issues/87).
-- The safe way to convert from JSVal is to use JavaScript.Cast or to use the 'Maybe a' instance of FromJSVal,
-- ie @fromJSVal :: JSVal -> IO (Maybe (Maybe a))@, which is a bit more awkward to use, and requires IO.
-- Also, Javascript.Cast doesn't have much instances, and it hardcodes the instance detection method
-- to javascript `isinstance` which is not sufficient for complex types (https://github.com/ghcjs/ghcjs-base/issues/86).
--
-- It is actually safe to convert from JSVal without IO because every JSVal is a copy of a value or reference.
-- The copy never change, so the conversion will always convert to the same result/object every time.
class FromJS a where
    fromJS :: J.JSVal -> Maybe a

instance FromJS J.JSVal where
    fromJS a | J.isUndefined a || J.isNull a = Nothing
    fromJS a = Just a

instance FromJS (JAI.SomeJSArray m) where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONArray = Just $ JAI.SomeJSArray a
    fromJS _ = Nothing

instance FromJS JOI.Object where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONObject = Just $ JOI.Object a
    fromJS _ = Nothing

instance FromJS Bool where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONBool = J.pFromJSVal a
    fromJS _ = Nothing

-- | This will only succeed on a single character string
instance FromJS Char where
    fromJS a =
        case JFI.jsonTypeOf a of
            JFI.JSONString ->
                let a' = J.pFromJSVal a -- convert to JSString
                    mb = JS.uncons a'
                in case mb of
                       Nothing -> Nothing
                       Just (h, t) ->
                           if JS.null t
                               then Just h
                               else Nothing
            _ -> Nothing

instance FromJS Double where
    fromJS a = let t = JFI.jsonTypeOf a
               in if t == JFI.JSONInteger || t == JFI.JSONFloat
                      then J.pFromJSVal a
                      else Nothing

instance FromJS Float where
    fromJS a = let t = JFI.jsonTypeOf a
               in if t == JFI.JSONInteger || t == JFI.JSONFloat
                      then J.pFromJSVal a
                      else Nothing

instance FromJS Int where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONInteger && js_withinIntBounds a minBound maxBound = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS Int8 where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONInteger && js_withinInt8Bounds a minBound maxBound = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS Int16 where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONInteger && js_withinInt16Bounds a minBound maxBound = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS Int32 where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONInteger && js_withinInt32Bounds a minBound maxBound = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS Word where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONInteger && js_withinWordBounds a minBound maxBound = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS Word8 where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONInteger && js_withinWord8Bounds a minBound maxBound = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS Word16 where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONInteger && js_withinWord16Bounds a minBound maxBound = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS Word32 where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONInteger && js_withinWord32Bounds a minBound maxBound = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS T.Text where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONString = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS String where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONString = J.pFromJSVal a
    fromJS _ = Nothing

instance FromJS J.JSString where
    fromJS a | JFI.jsonTypeOf a == JFI.JSONString = J.pFromJSVal a
    fromJS _ = Nothing

#ifdef __GHCJS__

foreign import javascript unsafe
  "($1 >= $2) || ($1 <= $3)"
  js_withinIntBounds :: J.JSVal -> Int -> Int -> Bool

foreign import javascript unsafe
  "($1 >= $2) || ($1 <= $3)"
  js_withinInt8Bounds :: J.JSVal -> Int8 -> Int8 -> Bool

foreign import javascript unsafe
  "($1 >= $2) || ($1 <= $3)"
  js_withinInt16Bounds :: J.JSVal -> Int16 -> Int16 -> Bool

foreign import javascript unsafe
  "($1 >= $2) || ($1 <= $3)"
  js_withinInt32Bounds :: J.JSVal -> Int32 -> Int32 -> Bool

foreign import javascript unsafe
  "($1 >= $2) || ($1 <= $3)"
  js_withinWordBounds :: J.JSVal -> Word -> Word -> Bool

foreign import javascript unsafe
  "($1 >= $2) || ($1 <= $3)"
  js_withinWord8Bounds :: J.JSVal -> Word8 -> Word8 -> Bool

foreign import javascript unsafe
  "($1 >= $2) || ($1 <= $3)"
  js_withinWord16Bounds :: J.JSVal -> Word16 -> Word16 -> Bool

foreign import javascript unsafe
  "($1 >= $2) || ($1 <= $3)"
  js_withinWord32Bounds :: J.JSVal -> Word32 -> Word32 -> Bool

#else

js_withinIntBounds :: J.JSVal -> Int -> Int -> Bool
js_withinIntBounds _ _ _ = False

js_withinInt8Bounds :: J.JSVal -> Int8 -> Int8 -> Bool
js_withinInt8Bounds _ _ _ = False

js_withinInt16Bounds :: J.JSVal -> Int8 -> Int8 -> Bool
js_withinInt16Bounds _ _ _ = False

js_withinInt32Bounds :: J.JSVal -> Int8 -> Int8 -> Bool
js_withinInt32Bounds _ _ _ = False

js_withinWordBounds :: J.JSVal -> Word -> Word -> Bool
js_withinWordBounds _ _ _ = False

js_withinWord8Bounds :: J.JSVal -> Word8 -> Word8 -> Bool
js_withinWord8Bounds _ _ _ = False

js_withinWord16Bounds :: J.JSVal -> Word16 -> Word16 -> Bool
js_withinWord16Bounds _ _ _ = False

js_withinWord32Bounds :: J.JSVal -> Word32 -> Word32 -> Bool
js_withinWord32Bounds _ _ _ = False

#endif