{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

module Facebook.Graph
  ( getObject
  , postObject
  , deleteObject
  , searchObjects
  , ( #= )
  , SimpleType(..)
  , Place(..)
  , Location(..)
  , GeoCoordinates(..)
  , Tag(..)
  ) where
#if __GLASGOW_HASKELL__ <= 784
import Control.Applicative
#endif
import Control.Monad (mzero)
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int8, Int16, Int32, Int64)
import Data.List (intersperse)
import Data.Text (Text)
import Data.Typeable (Typeable)
import Data.Word (Word, Word8, Word16, Word32, Word64)
#if MIN_VERSION_time(1,5,0)
import Data.Time (defaultTimeLocale)
#else
import System.Locale (defaultTimeLocale)
#endif
import qualified Control.Monad.Trans.Resource as R
import qualified Data.Aeson as A
#if MIN_VERSION_aeson(1,0,0)
import Data.Aeson.Text (encodeToTextBuilder)
#else
import Data.Aeson.Encode (encodeToTextBuilder)
#endif
import qualified Data.ByteString.Char8 as B
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.Time as TI
import qualified Network.HTTP.Conduit as H
import qualified Network.HTTP.Types as HT

import Facebook.Auth
import Facebook.Base
import Facebook.Monad
import Facebook.Types
import Facebook.Pager

-- | Make a raw @GET@ request to Facebook's Graph API.
getObject
  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, A.FromJSON a)
  => Text -- ^ Path (should begin with a slash @\/@)
  -> [Argument] -- ^ Arguments to be passed to Facebook
  -> Maybe (AccessToken anyKind) -- ^ Optional access token
  -> FacebookT anyAuth m a
getObject path query mtoken =
  runResourceInFb $ asJson =<< fbhttp =<< fbreq path mtoken query

-- | Make a raw @POST@ request to Facebook's Graph API.
postObject
  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, A.FromJSON a)
  => Text -- ^ Path (should begin with a slash @\/@)
  -> [Argument] -- ^ Arguments to be passed to Facebook
  -> AccessToken anyKind -- ^ Access token
  -> FacebookT Auth m a
postObject = methodObject HT.methodPost

-- | Make a raw @DELETE@ request to Facebook's Graph API.
deleteObject
  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, A.FromJSON a)
  => Text -- ^ Path (should begin with a slash @\/@)
  -> [Argument] -- ^ Arguments to be passed to Facebook
  -> AccessToken anyKind -- ^ Access token
  -> FacebookT Auth m a
deleteObject = methodObject HT.methodDelete

-- | Helper function used by 'postObject' and 'deleteObject'.
methodObject
  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, A.FromJSON a)
  => HT.Method
  -> Text -- ^ Path (should begin with a slash @\/@)
  -> [Argument] -- ^ Arguments to be passed to Facebook
  -> AccessToken anyKind -- ^ Access token
  -> FacebookT Auth m a
methodObject method path query token =
  runResourceInFb $
  do req <- fbreq path (Just token) query
     asJson =<<
       fbhttp
         req
         { H.method = method
         }

-- | Make a raw @GET@ request to the /search endpoint of Facebook’s
-- Graph API.  Returns a raw JSON 'A.Value'.
searchObjects
  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, A.FromJSON a)
  => Text -- ^ A Facebook object type to search for
  -> Text -- ^ The keyword to search for
  -> [Argument] -- ^ Additional arguments to pass
  -> Maybe UserAccessToken -- ^ Optional access token
  -> FacebookT anyAuth m (Pager a)
searchObjects objectType keyword query = getObject "/search" query'
  where
    query' = ("q" #= keyword) : ("type" #= objectType) : query

----------------------------------------------------------------------
-- | Create an 'Argument' with a 'SimpleType'.  See the docs on
-- 'createAction' for an example.
( #= )
  :: SimpleType a
  => ByteString -> a -> Argument
p #= v = (p, encodeFbParam v)

-- | Class for data types that may be represented as a Facebook
-- simple type. (see
-- <https://developers.facebook.com/docs/opengraph/simpletypes/>).
class SimpleType a  where
  encodeFbParam :: a -> B.ByteString

-- | Facebook's simple type @Boolean@.
instance SimpleType Bool where
  encodeFbParam b =
    if b
      then "1"
      else "0"

-- | Facebook's simple type @DateTime@ with only the date.
instance SimpleType TI.Day where
  encodeFbParam = B.pack . TI.formatTime defaultTimeLocale "%Y-%m-%d"

-- | Facebook's simple type @DateTime@.
instance SimpleType TI.UTCTime where
  encodeFbParam = B.pack . TI.formatTime defaultTimeLocale "%Y%m%dT%H%MZ"

-- | Facebook's simple type @DateTime@.
instance SimpleType TI.ZonedTime where
  encodeFbParam = encodeFbParam . TI.zonedTimeToUTC

-- @Enum@ doesn't make sense to support as a Haskell data type.
-- | Facebook's simple type @Float@ with less precision than supported.
instance SimpleType Float where
  encodeFbParam = showBS

-- | Facebook's simple type @Float@.
instance SimpleType Double where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Int where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Word where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Int8 where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Word8 where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Int16 where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Word16 where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Int32 where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Word32 where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Int64 where
  encodeFbParam = showBS

-- | Facebook's simple type @Integer@.
instance SimpleType Word64 where
  encodeFbParam = showBS

-- | Facebook's simple type @String@.
instance SimpleType Text where
  encodeFbParam = TE.encodeUtf8

-- | Facebook's simple type @String@.
instance SimpleType ByteString where
  encodeFbParam = id

-- | An object's 'Id' code.
instance SimpleType Id where
  encodeFbParam = TE.encodeUtf8 . idCode

-- | 'Permission' is a @newtype@ of 'Text'
instance SimpleType Permission where
  encodeFbParam = encodeFbParam . unPermission

-- | A comma-separated list of simple types.  This definition
-- doesn't work everywhere, just for a few combinations that
-- Facebook uses (e.g. @[Int]@).  Also, encoding a list of lists
-- is the same as encoding the concatenation of all lists.  In
-- other words, this instance is here more for your convenience
-- than to make sure your code is correct.
instance SimpleType a =>
         SimpleType [a] where
  encodeFbParam = B.concat . intersperse "," . map encodeFbParam

showBS
  :: Show a
  => a -> B.ByteString
showBS = B.pack . show

----------------------------------------------------------------------
-- | Information about a place.  This is not a Graph Object,
-- instead it's just a field of a Object.  (Not to be confused
-- with the @Page@ object.)
data Place = Place
  { placeId :: Id -- ^ @Page@ ID.
  , placeName :: Maybe Text -- ^ @Page@ name.
  , placeLocation :: Maybe Location
  } deriving (Eq, Ord, Show, Read, Typeable)

instance A.FromJSON Place where
  parseJSON (A.Object v) =
    Place <$> v A..: "id" <*> v A..:? "name" <*> v A..:? "location"
  parseJSON _ = mzero

-- | A geographical location.
data Location = Location
  { locationStreet :: Maybe Text
  , locationCity :: Maybe Text
  , locationState :: Maybe Text
  , locationCountry :: Maybe Text
  , locationZip :: Maybe Text
  , locationCoords :: Maybe GeoCoordinates
  } deriving (Eq, Ord, Show, Read, Typeable)

instance A.FromJSON Location where
  parseJSON obj@(A.Object v) =
    Location <$> v A..:? "street" <*> v A..:? "city" <*> v A..:? "state" <*>
    v A..:? "country" <*>
    v A..:? "zip" <*>
    A.parseJSON obj
  parseJSON _ = mzero

-- | Geographical coordinates.
data GeoCoordinates = GeoCoordinates
  { latitude :: !Double
  , longitude :: !Double
  } deriving (Eq, Ord, Show, Read, Typeable)

instance A.FromJSON GeoCoordinates where
  parseJSON (A.Object v) =
    GeoCoordinates <$> v A..: "latitude" <*> v A..: "longitude"
  parseJSON _ = mzero

instance SimpleType GeoCoordinates where
  encodeFbParam c =
    let obj =
          A.object ["latitude" A..= latitude c, "longitude" A..= longitude c]
        toBS = TE.encodeUtf8 . TL.toStrict . TLB.toLazyText . encodeToTextBuilder
    in toBS obj

-- | A tag (i.e. \"I'll /tag/ you on my post\").
data Tag = Tag
  { tagId :: Id -- ^ Who is tagged.
  , tagName :: Text -- ^ Name of the tagged person.
  } deriving (Eq, Ord, Show, Read, Typeable)

instance A.FromJSON Tag where
  parseJSON (A.Object v) = Tag <$> v A..: "id" <*> v A..: "name"
  parseJSON _ = mzero