    {-# LANGUAGE CPP, DeriveDataTypeable, GADTs, StandaloneDeriving #-}
    module Facebook.Types
        ( Credentials(..)
        , appIdBS
        , appSecretBS
        , AccessToken(..)
        , UserAccessToken
        , AppAccessToken
        , AccessTokenData
        , Id(..)
        , UserId
        , accessTokenData
        , accessTokenExpires
        , accessTokenUserId
        , UserKind
        , AppKind
        , Argument
        , (<>)
        , FbUTCTime(..)
        ) where

    import Control.Applicative ((<$>), (<*>), pure)
    import Control.Monad (mzero)
    import Data.ByteString (ByteString)
    import Data.Int (Int64)
    import Data.Monoid (Monoid, mappend)
    import Data.String (IsString)
    import Data.Text (Text)
    import Data.Time (UTCTime, parseTime)
    import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
    import Data.Typeable (Typeable, Typeable1)
    #if MIN_VERSION_time(1,5,0)
    import Data.Time (defaultTimeLocale)
    #else
    import System.Locale (defaultTimeLocale)
    #endif

    import qualified Data.Aeson as A
    import qualified Data.Aeson.Types as A
    import qualified Data.Text as T
    import qualified Data.Text.Encoding as TE
    import qualified Data.Text.Lazy as TL
    import qualified Data.Text.Lazy.Builder as TLB
    import qualified Data.Text.Lazy.Builder.Int as TLBI

    -- | Credentials that you get for your app when you register on
    -- Facebook.
    data Credentials =
        Credentials { appName   :: Text -- ^ Your application name (e.g. for Open Graph calls).
                    , appId     :: Text -- ^ Your application ID.
                    , appSecret :: Text -- ^ Your application secret key.
                    }
        deriving (Eq, Ord, Show, Read, Typeable)

    -- | 'appId' for 'ByteString'.
    appIdBS :: Credentials -> ByteString
    appIdBS = TE.encodeUtf8 . appId

    -- | 'appSecret' for 'ByteString'.
    appSecretBS :: Credentials -> ByteString
    appSecretBS = TE.encodeUtf8 . appSecret

    -- | An access token.  While you can make some API calls without
    -- an access token, many require an access token and some will
    -- give you more information with an appropriate access token.
    --
    -- There are two kinds of access tokens:
    --
    -- [User access token] An access token obtained after an user
    -- accepts your application.  Let's you access more information
    -- about that user and act on their behalf (depending on which
    -- permissions you've asked for).
    --
    -- [App access token] An access token that allows you to take
    -- administrative actions for your application.
    --
    -- These two kinds of access tokens are distinguished by the
    -- phantom type on 'AccessToken', which can be 'UserKind' or
    -- 'AppKind'.
    data AccessToken kind where
        UserAccessToken :: UserId -> AccessTokenData -> UTCTime -> AccessToken UserKind
        AppAccessToken  :: AccessTokenData -> AccessToken AppKind

    -- | Type synonym for @'AccessToken' 'UserKind'@.
    type UserAccessToken = AccessToken UserKind

    -- | Type synonym for @'AccessToken' 'AppKind'@.
    type AppAccessToken = AccessToken AppKind

    deriving instance Eq   (AccessToken kind)
    deriving instance Ord  (AccessToken kind)
    deriving instance Show (AccessToken kind)
    deriving instance Typeable1 AccessToken

    -- | The access token data that is passed to Facebook's API
    -- calls.
    type AccessTokenData = Text

    -- | The identification code of an object.
    newtype Id = Id { idCode :: Text }
        deriving (Eq, Ord, Show, Read, Typeable, IsString)

    instance A.FromJSON Id where
        parseJSON (A.Object v) = v A..: "id"
        parseJSON (A.String s) = pure $ Id s
        parseJSON (A.Number d) = pure $ Id $ from $ floor d
          where from i = TL.toStrict $ TLB.toLazyText $ TLBI.decimal (i :: Int64)
        parseJSON o = fail $ "Can't parse Facebook.Id from " ++ show o

    instance A.ToJSON Id where
        toJSON (Id t) = A.String t

    -- | A Facebook user ID such as @1008905713901@.
    type UserId = Id

    -- | Get the access token data.
    accessTokenData :: AccessToken anyKind -> AccessTokenData
    accessTokenData (UserAccessToken _ d _) = d
    accessTokenData (AppAccessToken d)      = d

    -- | Expire time of an access token.  It may never expire, in
    -- which case it will be @Nothing@.
    accessTokenExpires :: AccessToken anyKind -> Maybe UTCTime
    accessTokenExpires (UserAccessToken _ _ expt) = Just expt
    accessTokenExpires (AppAccessToken _)         = Nothing

    -- | Get the user ID of an user access token.
    accessTokenUserId :: UserAccessToken -> UserId
    accessTokenUserId (UserAccessToken uid _ _) = uid

    -- | Phantom type used mark an 'AccessToken' as an user access
    -- token.
    data UserKind deriving (Typeable)

    -- | Phantom type used mark an 'AccessToken' as an app access
    -- token.
    data AppKind deriving (Typeable)

    -- | An argument given to an API call.
    type Argument = (ByteString, ByteString)

    -- | Synonym for 'mappend'.
    (<>) :: Monoid a => a -> a -> a
    (<>) = mappend

    ----------------------------------------------------------------------

    -- | /Since 0.14.9./ Not a Facebook JSON format, but a custom @fb@
    -- format for convenience if you need to serialize access tokens.
    instance A.ToJSON (AccessToken kind) where
      toJSON (UserAccessToken uid data_ expires) =
        A.object
          [ "kind"    A..= ("user" :: Text)
          , "id"      A..= uid
          , "token"   A..= data_
          , "expires" A..= expires ]
      toJSON (AppAccessToken data_) =
        A.object
          [ "kind"    A..= ("app" :: Text)
          , "token"   A..= data_ ]

    -- | (Internal) Since the user of 'parseJSON' is going to choose
    -- via its @kind@ whether a 'UserAccessToken' or an
    -- 'AppAccessToken' is wanted, we need this type class to
    -- implement 'FromJSON'.
    class ParseAccessToken kind where
      parseTokenJSON :: A.Object -> A.Parser (AccessToken kind)

    instance ParseAccessToken UserKind where
      parseTokenJSON v =
        checkKind v "user" $
          UserAccessToken <$> v A..: "id"
                          <*> v A..: "token"
                          <*> v A..: "expires"

    instance ParseAccessToken AppKind where
      parseTokenJSON v =
        checkKind v "app" $
          AppAccessToken <$> v A..: "token"

    -- | (Internal) Used to implement 'parseTokenJSON'.
    checkKind :: A.Object -> Text -> A.Parser a -> A.Parser a
    checkKind v kind ok = do
      kind' <- v A..: "kind"
      if kind == kind'
        then ok
        else fail $ "Expected access token kind " <> show kind <>
                    " but found " <> show kind' <> "."

    -- | /Since 0.14.9./ Parses the format that 'ToJSON' produces.
    -- Note that you need to statically decide whether you want to
    -- parse a user access token or an app access token.
    instance ParseAccessToken kind => A.FromJSON (AccessToken kind) where
      parseJSON (A.Object v) = parseTokenJSON v
      parseJSON _            = mzero

    ----------------------------------------------------------------------

    -- | @newtype@ for 'UTCTime' that follows Facebook's
    -- conventions of JSON parsing.
    --
    --  * As a string, while @aeson@ expects a format of @%FT%T%Q@,
    --    Facebook gives time values formatted as @%FT%T%z@.
    --
    --  * As a number, 'FbUTCTime' accepts a number of seconds since
    --    the Unix epoch.
    newtype FbUTCTime = FbUTCTime { unFbUTCTime :: UTCTime }
      deriving (Eq, Ord, Show, Read, Typeable)

    instance A.FromJSON FbUTCTime where
      parseJSON (A.String t) =
        case parseTime defaultTimeLocale "%FT%T%z" (T.unpack t) of
          Just d -> return (FbUTCTime d)
          _      -> fail $ "could not parse FbUTCTime string " ++ show t
      parseJSON (A.Number n) =
        return $ FbUTCTime $ posixSecondsToUTCTime $ fromInteger $ floor n
      parseJSON _ = fail "could not parse FbUTCTime from something which is not a string or number"