{-# LANGUAGE CPP                      #-}
{-# LANGUAGE FlexibleInstances        #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE OverloadedStrings        #-}
{-# LANGUAGE PatternGuards            #-}
-- | Haste-specific JSON library. JSON is common enough that it's a good idea
--   to create as fast and small an implementation as possible. To that end,
--   the parser is implemented entirely in Javascript, and works with any
--   browser that supports JSON.parse; IE does this from version 8 and up, and
--   everyone else has done it since just about forever.
module Haste.JSON (JSON (..), encodeJSON, decodeJSON, toObject, (!), (~>)) where
import Prelude hiding (null)
import Haste
import Haste.Prim
import Data.String as S
#ifndef __HASTE__
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif
import Haste.Parsing
#endif
import Haste.Foreign hiding (toObject)

-- | Create a JavaScript object from a JSON object. Only makes sense in a
--   browser context, obviously.
toObject :: JSON -> JSAny
toObject = jsJSONParse . encodeJSON
jsJSONParse :: JSString -> JSAny
jsJSONParse = veryUnsafePerformIO . _jsJSONParse

_jsJSONParse :: JSString -> IO JSAny
_jsJSONParse = ffi "JSON.parse"

-- Remember to update jsParseJSON if this data type changes!
data JSON
  = Num  {-# UNPACK #-} !Double
  | Str  !JSString
  | Bool !Bool
  | Arr  ![JSON]
  | Dict ![(JSString, JSON)]
  | Null

instance IsString JSON where
  fromString = Str . S.fromString

instance JSType JSON where
  toJSString = encodeJSON
  fromJSString x =
    case decodeJSON x of
      Right x' -> Just x'
      _        -> Nothing

numFail :: a
numFail = error "Num JSON: not a numeric JSON node!"

-- | This instance may be a bad idea, but it's nice to be able to create JSON
--   objects using plain numeric literals.
instance Num JSON where
  (Num a) + (Num b) = Num (a+b)
  _ + _             = numFail
  (Num a) * (Num b) = Num (a*b)
  _ * _             = numFail
  (Num a) - (Num b) = Num (a-b)
  _ - _             = numFail
  negate (Num a)    = Num (negate a)
  negate _          = numFail
  abs (Num a)       = Num (abs a)
  abs _             = numFail
  signum (Num a)    = signum (Num a)
  signum _          = numFail
  fromInteger n     = Num (fromInteger n)

#ifdef __HASTE__
foreign import ccall "jsShow" jsShowD :: Double -> JSString
foreign import ccall "jsParseJSON" jsParseJSON :: JSString -> Ptr (Maybe JSON)
jsStringify :: JSString -> IO JSString
jsStringify = ffi "JSON.stringify"
#else
jsShowD :: Double -> JSString
jsShowD = toJSStr . show

jsStringify :: JSString -> IO JSString
jsStringify = return . toJSStr . ('"' :) . unq . fromJSStr
  where
    unq ('"' : cs) = "\\\"" ++ unq cs
    unq (c : cs)
      | c == '\\'          = "\\\\" ++ unq cs
      | otherwise          = c : unq cs
    unq _          = ['"']
#endif

-- | Look up a JSON object from a JSON dictionary. Panics if the dictionary
--   isn't a dictionary, or if it doesn't contain the given key.
(!) :: JSON -> JSString -> JSON
dict ! k =
  case dict ~> k of
    Just x -> x
    _      -> error $ "Haste.JSON.!: unable to look up key " ++ fromJSStr k
infixl 5 !

class JSONLookup a where
  -- | Look up a key in a JSON dictionary. Return Nothing if the key can't be
  --   found for some reason.
  (~>) :: a -> JSString -> Maybe JSON
infixl 5 ~>

instance JSONLookup JSON where
  (Dict m) ~> key = lookup key m
  _        ~> _   = Nothing

instance JSONLookup (Maybe JSON) where
  (Just (Dict m)) ~> key = lookup key m
  _               ~> _   = Nothing

encodeJSON :: JSON -> JSString
encodeJSON = catJSStr "" . enc []
  where
    comma   = ","
    openbr  = "["
    closebr = "]"
    opencu  = "{"
    closecu = "}"
    colon   = ":"
    quote   = "\""
    true    = "true"
    false   = "false"
    null    = "null"
    enc acc Null         = null : acc
    enc acc (Str s)      = veryUnsafePerformIO (jsStringify s) : acc
    enc acc (Num d)      = jsShowD d : acc
    enc acc (Bool True)  = true : acc
    enc acc (Bool False) = false : acc
    enc acc (Arr elems)
      | (x:xs) <- elems =
        openbr : enc (foldr (\s a -> comma:enc a s) (closebr:acc) xs) x
      | otherwise =
        openbr : closebr : acc
    enc acc (Dict elems)
      | ((key,val):xs) <- elems =
        let encElem (k, v) a = comma : quote : k : quote : colon : enc a v
            encAll =
              opencu : veryUnsafePerformIO (jsStringify key) : colon : encRest
            encRest = enc (foldr encElem (closecu:acc) xs) val
        in encAll
      | otherwise =
        opencu : closecu : acc

decodeJSON :: JSString -> Either String JSON
#ifdef __HASTE__
decodeJSON = liftMaybe . fromPtr . jsParseJSON
  where
    liftMaybe (Just x) = Right x
    liftMaybe _        = Left "Invalid JSON!"
#else
decodeJSON = liftMaybe . runParser json . fromJSStr
  where
    liftMaybe (Just x) = Right x
    liftMaybe _        = Left "Invalid JSON!"
    json = oneOf [Num  <$> double,
                  Bool <$> boolean,
                  Str  <$> jsstring,
                  Arr  <$> array,
                  Dict <$> object,
                  null]
    jsstring = toJSStr <$> oneOf [quotedString '\'', quotedString '"']
    boolean = oneOf [string "true" >> pure True, string "false" >> pure False]
    null = string "null" >> pure Null
    array = do
      _ <- char '[' >> possibly whitespace
      elements <- commaSeparated json
      _ <- possibly whitespace >> char ']'
      return elements
    commaSeparated p =
      oneOf [do x <- p
                _ <- possibly whitespace >> char ',' >> possibly whitespace
                xs <- commaSeparated p
                return (x:xs),
             do x <- p
                return [x],
             do return []]
    object = do
      _ <- char '{' >> possibly whitespace
      pairs <- commaSeparated kvPair
      _ <- possibly whitespace >> char '}'
      return pairs
    kvPair = do
      k <- jsstring
      _ <- possibly whitespace >> char ':' >> possibly whitespace
      v <- json
      return (k, v)
#endif

instance Show JSON where
  show = fromJSStr . encodeJSON