{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveDataTypeable #-}
--------------------------------------------------------------------
-- |
-- Module    : Text.JSON.Canonical.Types
-- Copyright : (c) Duncan Coutts 2015, 2017
--
module Text.JSON.Canonical.Types
  ( JSValue(..)
  , Int54(..)
  , JSString
  , toJSString
  , fromJSString
  ) where

import Control.Arrow (first)
import Control.DeepSeq (NFData(..))
import Data.Bits (Bits)
#if MIN_VERSION_base(4,7,0)
import Data.Bits (FiniteBits)
#endif
import Data.Data (Data)
import Data.Int (Int64)
import Data.Ix (Ix)
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid (Monoid)
#endif
#if MIN_VERSION_base(4,9,0)
import Data.Semigroup (Semigroup)
#endif
import Data.Typeable (Typeable)
import Foreign.Storable (Storable)
#if MIN_VERSION_base(4,7,0)
import Text.Printf (PrintfArg(..))
import qualified Text.Printf as Printf
#else
import Text.Printf (PrintfArg)
#endif
import Data.String (IsString)
import Data.ByteString.Short (ShortByteString)
import qualified Data.ByteString.Short as BS


data JSValue
    = JSNull
    | JSBool     !Bool
    | JSNum      !Int54
    | JSString   !JSString
    | JSArray    [JSValue]
    | JSObject   [(JSString, JSValue)]
    deriving (Show, Read, Eq, Ord)

instance NFData JSValue where
  rnf JSNull        = ()
  rnf JSBool{}      = ()
  rnf JSNum{}       = ()
  rnf JSString{}    = ()
  rnf (JSArray  vs) = rnf vs
  rnf (JSObject vs) = rnf vs

-- | Canonical JSON strings are in fact just bytes.
--
newtype JSString = JStr ShortByteString
    deriving (Eq, Ord, IsString,
#if MIN_VERSION_base(4,9,0)
              Semigroup,
#endif
              Monoid)

instance NFData JSString where
  rnf JStr{} = ()

instance Show JSString where
  showsPrec n (JStr bs) = showsPrec n bs

instance Read JSString where
  readsPrec p = map (first JStr) . readsPrec p

#if MIN_VERSION_base(4,7,0)
instance PrintfArg JSString where
    formatArg = Printf.formatString . fromJSString
#endif

toJSString :: String -> JSString
toJSString str
  | all (<= '\255') str = JStr . BS.pack . map (fromIntegral . fromEnum) $ str
  | otherwise           = error "toJSString: cannot use non-ASCII chars"

fromJSString :: JSString -> String
fromJSString = map (toEnum . fromIntegral) . BS.unpack . (\(JStr bs) -> bs)

-- | 54-bit integer values
--
-- JavaScript can only safely represent numbers between @-(2^53 - 1)@ and
-- @2^53 - 1@.
--
-- TODO: Although we introduce the type here, we don't actually do any bounds
-- checking and just inherit all type class instance from Int64. We should
-- probably define `fromInteger` to do bounds checking, give different instances
-- for type classes such as `Bounded` and `FiniteBits`, etc.
newtype Int54 = Int54 { int54ToInt64 :: Int64 }
  deriving ( Enum
           , Eq
           , Integral
           , Data
           , Num
           , Ord
           , Real
           , Ix
#if MIN_VERSION_base(4,7,0)
           , FiniteBits
#endif
           , Bits
           , Storable
           , PrintfArg
           , Typeable
           )

instance Bounded Int54 where
  maxBound = Int54 (  2^(53 :: Int) - 1)
  minBound = Int54 (-(2^(53 :: Int) - 1))

instance Show Int54 where
  show = show . int54ToInt64

instance Read Int54 where
  readsPrec p = map (first Int54) . readsPrec p