module Composite.Aeson.Default where

import BasicPrelude
import Composite.Aeson.Base (JsonFormat(JsonFormat), JsonProfunctor(JsonProfunctor))
import Data.Aeson (FromJSON, ToJSON(toJSON))
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.BetterErrors as ABE
import Data.Int (Int8, Int16)
import Data.Scientific (Scientific)
import qualified Data.Scientific as Scientific
import Data.Word (Word8, Word16)

-- |Class for associating a default JSON format with a type.
--
-- DO NOT use this as the primary interface. It should only be used for defaulting in contexts where an explicit choice can also be used.
class DefaultJsonFormat a where
  -- |Produce the default 'JsonFormat' for type @a@, which must not produce any custom errors.
  defaultJsonFormat :: JsonFormat e a

instance DefaultJsonFormat Bool         where defaultJsonFormat = boolJsonFormat
instance DefaultJsonFormat Char         where defaultJsonFormat = charJsonFormat
instance DefaultJsonFormat Scientific   where defaultJsonFormat = scientificJsonFormat
instance DefaultJsonFormat Float        where defaultJsonFormat = realFloatJsonFormat
instance DefaultJsonFormat Double       where defaultJsonFormat = realFloatJsonFormat
instance DefaultJsonFormat Int          where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat Int8         where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat Int16        where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat Int32        where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat Int64        where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat Word8        where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat Word16       where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat Word32       where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat Word64       where defaultJsonFormat = integralJsonFormat
instance DefaultJsonFormat String       where defaultJsonFormat = stringJsonFormat
instance DefaultJsonFormat Text         where defaultJsonFormat = textJsonFormat
instance DefaultJsonFormat Aeson.Value  where defaultJsonFormat = aesonValueJsonFormat
instance DefaultJsonFormat Aeson.Object where defaultJsonFormat = aesonObjectJsonFormat
instance DefaultJsonFormat Aeson.Array  where defaultJsonFormat = aesonArrayJsonFormat

-- |Produce an explicit 'JsonFormat' by using the implicit Aeson 'FromJSON' and 'ToJSON' instances.
--
-- If an @aeson-better-errors@ parser is available for @a@, it's probably better to use 'abeJsonFormat' to get the enhanced error reporting.
aesonJsonFormat :: (ToJSON a, FromJSON a) => JsonFormat e a
aesonJsonFormat = JsonFormat $ JsonProfunctor toJSON ABE.fromAesonParser

-- |Produce an explicit 'JsonFormat' by using the implicit Aeson 'ToJSON' instance and an explicit @aeson-better-errors@ 'ABE.Parse'.
abeJsonFormat :: ToJSON a => ABE.Parse e a -> JsonFormat e a
abeJsonFormat p = JsonFormat $ JsonProfunctor toJSON p

-- |'JsonFormat' for 'Bool', mapping to a JSON boolean.
boolJsonFormat :: JsonFormat e Bool
boolJsonFormat = abeJsonFormat ABE.asBool

-- |'JsonFormat' for 'Char', mapping to a JSON string.
charJsonFormat :: JsonFormat e Char
charJsonFormat = aesonJsonFormat

-- |'JsonFormat' for 'Scientific', mapping to a JSON number.
--
-- __Warning:__ some JSON parsing libraries do not accept the scientific number notation even though it's part of the JSON standard, and this format
-- uses 'Data.ByteString.Builder.Scientific.scientificBuilder' transitively which encodes very small (< 0.1) and large (> 9,999,999.0) fractional numbers
-- using scientific notation.
scientificJsonFormat :: JsonFormat e Scientific
scientificJsonFormat = abeJsonFormat ABE.asScientific

-- |Convert some 'RealFloat' value to 'Aeson.Value'. Copied from Aeson internals which do not export it.
realFloatToJson :: RealFloat a => a -> Aeson.Value
realFloatToJson d
  | isNaN d || isInfinite d = Aeson.Null
  | otherwise = Aeson.Number $ Scientific.fromFloatDigits d
{-# INLINE realFloatToJson #-}

-- |Polymorphic JSON format for any type which instances 'RealFloat'. See warning in documentation for 'scientificJsonFormat' about scientific notation.
realFloatJsonFormat :: RealFloat a => JsonFormat e a
realFloatJsonFormat = JsonFormat $ JsonProfunctor realFloatToJson ABE.asRealFloat

-- |Polymorphic JSON format for any type which instances 'Integral'.
integralJsonFormat :: Integral a => JsonFormat e a
integralJsonFormat = JsonFormat $ JsonProfunctor (Aeson.Number . fromIntegral) ABE.asIntegral

-- |JSON format for 'String'.
stringJsonFormat :: JsonFormat e String
stringJsonFormat = abeJsonFormat ABE.asString

-- |JSON format for 'Text'.
textJsonFormat :: JsonFormat e Text
textJsonFormat = abeJsonFormat ABE.asText

-- |JSON format for '()' which maps to JSON as @null@.
nullJsonFormat :: JsonFormat e ()
nullJsonFormat = abeJsonFormat ABE.asNull

-- |JSON format which does no parsing or encoding.
aesonValueJsonFormat :: JsonFormat e Aeson.Value
aesonValueJsonFormat = abeJsonFormat ABE.asValue

-- |JSON format for 'Aeson.Object' which maps to any object in JSON.
aesonObjectJsonFormat :: JsonFormat e Aeson.Object
aesonObjectJsonFormat = abeJsonFormat ABE.asObject

-- |JSON format for 'Aeson.Array' which maps to any array in JSON.
aesonArrayJsonFormat :: JsonFormat e Aeson.Array
aesonArrayJsonFormat = abeJsonFormat ABE.asArray

-- FIXME all the rest!