{-# LANGUAGE BangPatterns               #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE TemplateHaskell            #-}

module Data.API.Types
    ( API
    , Thing(..)
    , APINode(..)
    , TypeName(..)
    , FieldName(..)
    , MDComment
    , Prefix
    , Spec(..)
    , SpecNewtype(..)
    , SpecRecord(..)
    , FieldType(..)
    , SpecUnion(..)
    , SpecEnum(..)
    , Conversion
    , APIType(..)
    , DefaultValue(..)
    , BasicType(..)
    , Filter(..)
    , IntRange(..)
    , UTCRange(..)
    , RegEx(..)
    , Binary(..)
    , defaultValueAsJsValue
    , mkRegEx
    , inIntRange
    , inUTCRange
    , base64ToBinary
    ) where

import           Data.API.Utils

import           Control.DeepSeq
import qualified Data.CaseInsensitive           as CI
import           Data.String
import           Data.Time
import           Data.Aeson
import           Data.Aeson.Types
import           Data.Aeson.TH
import qualified Codec.Serialise     as CBOR
import           Data.Maybe
import           Data.SafeCopy
import qualified Data.Text                      as T
import qualified Data.Text.Encoding             as T
import qualified Data.ByteString.Char8          as B
import           Test.QuickCheck                as QC
import           Control.Applicative
import qualified Data.ByteString.Base64         as B64
import           Language.Haskell.TH
import           Language.Haskell.TH.Syntax
import           Text.Regex
import           Prelude


-- | an API spec is made up of a list of type/element specs, each
--   specifying a Haskell type and JSON wrappers

type API = [Thing]

data Thing
    = ThComment MDComment
    | ThNode    APINode
    deriving (Eq,Show)

instance NFData Thing where
  rnf (ThComment x) = rnf x
  rnf (ThNode    x) = rnf x

-- | Specifies an individual element/type of the API

data APINode
    = APINode
        { anName    :: TypeName         -- ^ name of Haskell type
        , anComment :: MDComment        -- ^ comment describing type in Markdown
        , anPrefix  :: Prefix           -- ^ distinct short prefix (see below)
        , anSpec    :: Spec             -- ^ the type specification
        , anConvert :: Conversion       -- ^ optional conversion functions
        }
    deriving (Eq,Show)

instance NFData APINode where
  rnf (APINode a b c d e) = rnf a `seq` rnf b `seq` rnf c `seq` rnf d `seq` rnf e

-- | TypeName must contain a valid Haskell type constructor
newtype TypeName = TypeName { _TypeName :: T.Text }
    deriving (Eq, Ord, Show, NFData, IsString)

-- | FieldName identifies recod fields and union alternatives
--   must contain a valid identifier valid in Haskell and
--   any API client wrappers (e.g., if Ruby wrappers are to be
--   generated the names should easily map into Ruby)
newtype FieldName = FieldName { _FieldName :: T.Text }
    deriving (Eq, Ord, Show, NFData, IsString)

-- | Markdown comments are represented by strings

type MDComment = String

-- | a distinct case-insensitive short prefix used to form unique record field
--   names and data constructors:
--
--      * must be a valid Haskell identifier
--
--      * must be unique within the API

type Prefix = CI.CI String

-- | type/element specs are either simple type isomorphisms of basic JSON
--   types, records, unions or enumerated types

data Spec
    = SpNewtype SpecNewtype
    | SpRecord  SpecRecord
    | SpUnion   SpecUnion
    | SpEnum    SpecEnum
    | SpSynonym APIType
    deriving (Eq,Show)

instance NFData Spec where
  rnf (SpNewtype x) = rnf x
  rnf (SpRecord  x) = rnf x
  rnf (SpUnion   x) = rnf x
  rnf (SpEnum    x) = rnf x
  rnf (SpSynonym x) = rnf x

-- | SpecNewtype elements are isomorphisms of string, inetgers or booleans

data SpecNewtype =
    SpecNewtype
        { snType   :: BasicType
        , snFilter :: Maybe Filter
        }
    deriving (Eq,Show)

instance NFData SpecNewtype where
  rnf (SpecNewtype x y) = rnf x `seq` rnf y

data Filter
    = FtrStrg RegEx
    | FtrIntg IntRange
    | FtrUTC  UTCRange
    deriving (Eq,Show)

instance NFData Filter where
  rnf (FtrStrg x) = rnf x
  rnf (FtrIntg x) = rnf x
  rnf (FtrUTC  x) = rnf x

data IntRange
    = IntRange
        { ir_lo :: Maybe Int
        , ir_hi :: Maybe Int
        }
    deriving (Eq, Show)

instance NFData IntRange where
  rnf (IntRange x y) = rnf x `seq` rnf y

inIntRange :: Int -> IntRange -> Bool
_ `inIntRange` IntRange Nothing   Nothing   = True
i `inIntRange` IntRange (Just lo) Nothing   = lo <= i
i `inIntRange` IntRange Nothing   (Just hi) = i <= hi
i `inIntRange` IntRange (Just lo) (Just hi) = lo <= i && i <= hi

data UTCRange
    = UTCRange
        { ur_lo :: Maybe UTCTime
        , ur_hi :: Maybe UTCTime
        }
    deriving (Eq, Show)

instance NFData UTCRange where
  rnf (UTCRange x y) = rnf x `seq` rnf y

inUTCRange :: UTCTime -> UTCRange -> Bool
_ `inUTCRange` UTCRange Nothing   Nothing   = True
u `inUTCRange` UTCRange (Just lo) Nothing   = lo <= u
u `inUTCRange` UTCRange Nothing   (Just hi) = u <= hi
u `inUTCRange` UTCRange (Just lo) (Just hi) = lo <= u && u <= hi


data RegEx =
    RegEx
        { re_text  :: T.Text
        , re_regex :: Regex
        }

mkRegEx :: T.Text -> RegEx
mkRegEx txt = RegEx txt $ mkRegexWithOpts (T.unpack txt) False True

instance NFData RegEx where
  rnf (RegEx x !_) = rnf x

instance ToJSON RegEx where
    toJSON RegEx{..} = String re_text

instance FromJSON RegEx where
    parseJSON = withText "RegEx" (return . mkRegEx)

instance Eq RegEx where
    r == s = re_text r == re_text s

instance Show RegEx where
    show = T.unpack . re_text

-- | SpecRecord is your classsic product type.

data SpecRecord = SpecRecord
    { srFields :: [(FieldName, FieldType)]
    }
    deriving (Eq,Show)

instance NFData SpecRecord where
  rnf (SpecRecord x) = rnf x

-- | In addition to the type and comment, record fields may carry a
-- flag indicating that they are read-only, and may have a default
-- value, which must be of a compatible type.

data FieldType = FieldType
    { ftType     :: APIType
    , ftReadOnly :: Bool
    , ftDefault  :: Maybe DefaultValue
    , ftComment  :: MDComment
    }
    deriving (Eq,Show)

instance NFData FieldType where
  rnf (FieldType a b c d) = rnf a `seq` rnf b `seq` rnf c `seq` rnf d

-- | SpecUnion is your classsic union type

data SpecUnion = SpecUnion
    { suFields :: [(FieldName,(APIType,MDComment))]
    }
    deriving (Eq,Show)

instance NFData SpecUnion where
  rnf (SpecUnion x) = rnf x

-- | SpecEnum is your classic enumerated type

data SpecEnum = SpecEnum
    { seAlts :: [(FieldName,MDComment)]
    }
    deriving (Eq,Show)

instance NFData SpecEnum where
  rnf (SpecEnum x) = rnf x

-- | Conversion possibly converts to an internal representation.  If
-- specified, a conversion is a pair of an injection function name and
-- a projection function name.
type Conversion = Maybe (FieldName,FieldName)

-- | Type is either a list, Maybe, a named element of the API or a basic type
data APIType
    = TyList  APIType       -- ^ list elements are types
    | TyMaybe APIType       -- ^ Maybe elements are types
    | TyName  TypeName      -- ^ the referenced type must be defined by the API
    | TyBasic BasicType     -- ^ a JSON string, int, bool etc.
    | TyJSON                -- ^ a generic JSON value
    deriving (Eq, Show)

-- | It is sometimes helpful to write a type name directly as a string
instance IsString APIType where
  fromString = TyName . fromString

instance NFData APIType where
  rnf (TyList  ty) = rnf ty
  rnf (TyMaybe ty) = rnf ty
  rnf (TyName  tn) = rnf tn
  rnf (TyBasic bt) = rnf bt
  rnf TyJSON       = ()

-- | the basic JSON types (N.B., no floating point numbers, yet)
data BasicType
    = BTstring -- ^ a JSON UTF-8 string
    | BTbinary -- ^ a base-64-encoded byte string
    | BTbool   -- ^ a JSON bool
    | BTint    -- ^ a JSON integral number
    | BTutc    -- ^ a JSON UTC string
    deriving (Eq, Show)

instance NFData BasicType where
  rnf !_ = ()

-- | A default value for a field
data DefaultValue
    = DefValList
    | DefValMaybe
    | DefValString T.Text  -- used for binary fields (base64 encoded)
    | DefValBool   Bool
    | DefValInt    Int
    | DefValUtc    UTCTime
    deriving (Eq, Show)

instance NFData DefaultValue where
  rnf DefValList       = ()
  rnf DefValMaybe      = ()
  rnf (DefValString t) = rnf t
  rnf (DefValBool   b) = rnf b
  rnf (DefValInt    i) = rnf i
  rnf (DefValUtc    u) = rnf u

-- | Convert a default value to an Aeson 'Value'.  This differs from
-- 'toJSON' as it will not round-trip with 'fromJSON': UTC default
-- values are turned into strings.
defaultValueAsJsValue :: DefaultValue -> Value
defaultValueAsJsValue  DefValList                = toJSON ([] :: [()])
defaultValueAsJsValue  DefValMaybe               = Null
defaultValueAsJsValue (DefValString s)           = String s
defaultValueAsJsValue (DefValBool   b)           = Bool b
defaultValueAsJsValue (DefValInt    n)           = Number (fromIntegral n)
defaultValueAsJsValue (DefValUtc    t)           = mkUTC t


-- | Binary data is represented in JSON format as a base64-encoded
-- string
newtype Binary = Binary { _Binary :: B.ByteString }
    deriving (Show,Eq,Ord,NFData,CBOR.Serialise)

instance ToJSON Binary where
    toJSON = String . T.decodeLatin1 . B64.encode . _Binary

instance FromJSON Binary where
    parseJSON = withBinary "Binary" return

instance QC.Arbitrary T.Text where
    arbitrary = T.pack <$> QC.arbitrary

instance QC.Arbitrary Binary where
    arbitrary = Binary <$> B.pack <$> QC.arbitrary

withBinary :: String -> (Binary->Parser a) -> Value -> Parser a
withBinary lab f = withText lab g
  where
    g t =
        case base64ToBinary t of
          Left  _  -> typeMismatch lab (String t)
          Right bs -> f bs

base64ToBinary :: T.Text -> Either String Binary
base64ToBinary t = Binary <$> B64.decode (T.encodeUtf8 t)

$(deriveSafeCopy 0 'base ''Binary)

deriveJSON defaultOptions ''Thing
deriveJSON defaultOptions ''APINode
deriveJSON defaultOptions ''TypeName
deriveJSON defaultOptions ''FieldName
deriveJSON defaultOptions ''Spec
deriveJSON defaultOptions ''APIType
deriveJSON defaultOptions ''DefaultValue
deriveJSON defaultOptions ''SpecEnum
deriveJSON defaultOptions ''SpecUnion
deriveJSON defaultOptions ''SpecRecord
deriveJSON defaultOptions ''FieldType
deriveJSON defaultOptions ''SpecNewtype
deriveJSON defaultOptions ''Filter
deriveJSON defaultOptions ''IntRange
deriveJSON defaultOptions ''UTCRange
deriveJSON defaultOptions ''BasicType
deriveJSON defaultOptions ''CI.CI


instance Lift Thing where
  lift (ThComment c) = [e| ThComment c |]
  lift (ThNode    n) = [e| ThNode    n |]

instance Lift APINode where
  lift (APINode a b c d e) = [e| APINode a b $(liftPrefix c) d e |]

liftPrefix :: Prefix -> ExpQ
liftPrefix ci = let s = CI.original ci in [e| CI.mk s |]

instance Lift TypeName where
  lift (TypeName s) = [e| TypeName $(litE (stringL (T.unpack s))) |]

instance Lift FieldName where
  lift (FieldName s) = [e| FieldName $(litE (stringL (T.unpack s))) |]

instance Lift Spec where
  lift (SpNewtype s) = [e| SpNewtype s |]
  lift (SpRecord  s) = [e| SpRecord  s |]
  lift (SpUnion   s) = [e| SpUnion   s |]
  lift (SpEnum    s) = [e| SpEnum    s |]
  lift (SpSynonym s) = [e| SpSynonym s |]

instance Lift SpecNewtype where
  lift (SpecNewtype a b) = [e| SpecNewtype a b |]

instance Lift Filter where
  lift (FtrStrg re) = [e| FtrStrg re |]
  lift (FtrIntg ir) = [e| FtrIntg ir |]
  lift (FtrUTC  ur) = [e| FtrUTC  ur |]

instance Lift IntRange where
    lift (IntRange lo hi) = [e| IntRange lo hi |]

instance Lift UTCRange where
    lift (UTCRange lo hi) = [e| UTCRange $(liftMaybeUTCTime lo) $(liftMaybeUTCTime hi) |]

liftUTC :: UTCTime -> ExpQ
liftUTC u = [e| fromMaybe (error "liftUTC") (parseUTC_ $(stringE (mkUTC_ u))) |]

liftMaybeUTCTime :: Maybe UTCTime -> ExpQ
liftMaybeUTCTime Nothing  = [e| Nothing |]
liftMaybeUTCTime (Just u) = [e| Just $(liftUTC u) |]

instance Lift RegEx where
    lift re = [e| mkRegEx $(stringE (T.unpack (re_text re))) |]

instance Lift SpecRecord where
  lift (SpecRecord s) = [e| SpecRecord s |]

instance Lift FieldType where
  lift (FieldType a b c d) = [e| FieldType a b c d |]

instance Lift SpecUnion where
  lift (SpecUnion s) = [e| SpecUnion s |]

instance Lift SpecEnum where
  lift (SpecEnum s) = [e| SpecEnum s |]

instance Lift APIType where
  lift (TyList  t) = [e| TyList  t |]
  lift (TyMaybe t) = [e| TyMaybe t |]
  lift (TyName  t) = [e| TyName  t |]
  lift (TyBasic t) = [e| TyBasic t |]
  lift TyJSON      = [e| TyJSON    |]

instance Lift BasicType where
  lift BTstring = [e| BTstring |]
  lift BTbinary = [e| BTbinary |]
  lift BTbool   = [e| BTbool   |]
  lift BTint    = [e| BTint    |]
  lift BTutc    = [e| BTutc    |]

instance Lift DefaultValue where
  lift DefValList       = [e| DefValList     |]
  lift DefValMaybe      = [e| DefValMaybe    |]
  lift (DefValString s) = [e| DefValString (T.pack $(lift (T.unpack s))) |]
  lift (DefValBool   b) = [e| DefValBool b   |]
  lift (DefValInt    i) = [e| DefValInt i    |]
  lift (DefValUtc    u) = [e| DefValUtc $(liftUTC u) |]