{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, ScopedTypeVariables #-}
-- | Types definitions
module Instagram.Types (
  Credentials(..)
  ,clientIDBS
  ,clientSecretBS
  ,OAuthToken(..)
  ,AccessToken(..)
  ,UserID
  ,User(..)
  ,UserCounts(..)
  ,Scope(..)
  ,IGException(..)
  ,Envelope(..)
  ,ErrEnvelope(..)
  ,IGError(..)
  ,Pagination(..)
  ,MediaID
  ,Media(..)
  ,Position(..)
  ,UserPosition(..)
  ,LocationID
  ,Location(..)
  ,ImageData(..)
  ,Images(..)
  ,CommentID
  ,Comment(..)
  ,Count(..)
  ,Aspect(..)
  ,media
  ,CallbackUrl
  ,Subscription(..)
  ,Update(..)
  ,TagName
  ,Tag(..)
  ,OutgoingStatus(..)
  ,IncomingStatus(..)
  ,Relationship(..)
  ,NoResult
  ,GeographyID
)where

import Control.Applicative
import Data.Text
import Data.Typeable (Typeable)
import Data.Data (Data)
import Data.ByteString (ByteString)

import Data.Aeson


import qualified Data.Text.Encoding as TE
import Control.Exception.Base (Exception)
import Data.Time.Clock.POSIX (POSIXTime)
import qualified Data.Text as T (pack)
import Data.Aeson.Types (Parser)
import qualified Data.HashMap.Strict as HM (lookup)

-- | the app credentials
data Credentials = Credentials {
  cClientID :: Text -- ^ client id
  ,cClientSecret :: Text -- ^ client secret
  }
  deriving (Show,Read,Eq,Ord,Typeable, Data)

-- | get client id in ByteString form
clientIDBS :: Credentials -> ByteString
clientIDBS=TE.encodeUtf8 . cClientID

-- | get client secret in ByteString form
clientSecretBS :: Credentials -> ByteString
clientSecretBS=TE.encodeUtf8 . cClientSecret

-- | the oauth token returned after authentication
data OAuthToken = OAuthToken {
  oaAccessToken :: AccessToken -- ^ the access token
  ,oaUser :: User -- ^ the user structure returned
  }
  deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON OAuthToken  where
    toJSON oa=object ["access_token" .= oaAccessToken oa, "user" .= oaUser oa]

-- | from json as per Instagram format
instance FromJSON OAuthToken where
    parseJSON (Object v) =OAuthToken <$>
                         v .: "access_token" <*>
                         v .: "user"
    parseJSON _= fail "OAuthToken"

-- | the access token is simply a Text
newtype AccessToken=AccessToken Text
    deriving (Eq, Ord, Read, Show, Typeable)

-- | simple string
instance ToJSON AccessToken  where
        toJSON (AccessToken at)=String at

-- | simple string
instance FromJSON  AccessToken where
        parseJSON (String s)=pure $ AccessToken s
        parseJSON _= fail "AccessToken"

-- | User ID
type UserID = Text

-- | the User partial profile returned by the authentication
data User = User {
        uID :: UserID,
        uUsername :: Text,
        uFullName :: Text,
        uProfilePicture :: Maybe Text,
        uWebsite :: Maybe Text,
        uBio :: Maybe Text,
        uCounts :: Maybe UserCounts
        }
        deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON User  where
    toJSON u = object
        [ "id" .= uID u
        , "username" .= uUsername u
        , "full_name" .= uFullName u
        , "profile_picture" .= uProfilePicture u
        , "website" .= uWebsite u
        , "bio" .= uBio u
        , "counts" .= uCounts u 
        ]

-- | from json as per Instagram format
instance FromJSON User where
    parseJSON (Object v) =User <$>
                         v .: "id" <*>
                         v .: "username" <*>
                         v .: "full_name" <*>
                         v .:? "profile_picture" <*>
                         v .:? "website" <*>
                         v .:? "bio" <*>
                         v .:? "counts"
    parseJSON _= fail "User"

-- | the User counts info returned by some endpoints
data UserCounts = UserCounts
    { ucMedia :: Int
    , ucFollows :: Int
    , ucFollowedBy :: Int
    }
    deriving (Show,Read,Eq,Ord,Typeable)

-- | from json as per Instagram format
instance FromJSON UserCounts where
    parseJSON (Object v) = UserCounts <$>
                         v .: "media" <*>
                         v .: "follows" <*>
                         v .: "followed_by"
    parseJSON _= fail "UserCounts"

-- | to json as per Instagram format
instance ToJSON UserCounts where
    toJSON uc = object
        [ "media" .= ucMedia uc
        , "follows" .= ucFollows uc
        , "followed_by" .= ucFollowedBy uc
        ]

-- | the scopes of the authentication
data Scope = Basic | PublicContent | FollowerList | Comments | Relationships | Likes
  deriving (Read,Eq,Ord,Enum,Bounded,Typeable)

instance Show Scope where
  show Basic         = "basic"
  show PublicContent = "public_content"
  show FollowerList  = "follower_list"
  show Comments      = "comments"
  show Relationships = "relationships"
  show Likes         = "likes"

-- | an error returned to us by Instagram
data IGError = IGError {
  igeCode :: Int
  ,igeType :: Maybe Text
  ,igeMessage :: Maybe Text
  }
  deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON IGError  where
    toJSON e=object ["code" .= igeCode e, "error_type" .= igeType e , "error_message" .= igeMessage e]

-- | from json as per Instagram format
instance FromJSON IGError where
    parseJSON (Object v) =IGError <$>
                         v .: "code" <*>
                         v .:? "error_type" <*>
                         v .:? "error_message"
    parseJSON _= fail "IGError"

-- | an exception that a call to instagram may throw
data IGException = JSONException String -- ^ JSON parsingError
  | IGAppException IGError -- ^ application exception
  deriving (Show,Typeable)

-- | make our exception type a normal exception
instance Exception IGException

-- | envelope for Instagram OK response
data Envelope d=Envelope{
  eMeta :: IGError -- ^ this should only say 200, no error, but put here for completeness
  ,eData :: d -- ^ data, garanteed to be present (otherwise we get an ErrEnvelope)
  ,ePagination :: Maybe Pagination
  }
  deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance (ToJSON d)=>ToJSON (Envelope d)  where
    toJSON e=object ["meta" .= eMeta e, "data" .= eData e, "pagination" .= ePagination e]

-- | from json as per Instagram format
instance (FromJSON d)=>FromJSON (Envelope d) where
    parseJSON (Object v) =Envelope <$>
                         v .: "meta" <*>
                         v .: "data" <*>
                         v .:? "pagination"
    parseJSON _= fail "Envelope"

-- | error envelope for Instagram error response
data ErrEnvelope=ErrEnvelope{
  eeMeta :: IGError
  }
  deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON ErrEnvelope  where
    toJSON e=object ["meta" .= eeMeta e]

-- | from json as per Instagram format
instance FromJSON ErrEnvelope where
    parseJSON (Object v) =ErrEnvelope <$>
                         v .: "meta"
    parseJSON _= fail "ErrEnvelope"

-- | pagination info for responses that can return a lot of data
data Pagination = Pagination {
   pNextUrl :: Maybe Text
   ,pNextMaxID :: Maybe Text
   ,pNextMinID :: Maybe Text
   ,pNextMaxTagID :: Maybe Text
   ,pMinTagID :: Maybe Text
   }
  deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON Pagination  where
    toJSON p=object ["next_url" .= pNextUrl p, "next_max_id" .= pNextMaxID p, "next_min_id" .= pNextMinID p, "next_max_tag_id" .= pNextMaxTagID p,"min_tag_id" .= pMinTagID p]

-- | from json as per Instagram format
instance FromJSON Pagination where
    parseJSON (Object v) =Pagination <$>
                         v .:? "next_url" <*>
                         v .:? "next_max_id" <*>
                         v .:? "next_min_id" <*>
                         v .:? "next_max_tag_id" <*>
                         v .:? "min_tag_id"
    parseJSON _= fail "Pagination"

-- | Media ID
type MediaID=Text

-- | instagram media object
data Media = Media {
  mID :: MediaID
  ,mCaption :: Maybe Comment
  ,mLink :: Text
  ,mUser :: User
  ,mCreated :: POSIXTime
  ,mImages :: Images
  ,mType :: Text
  ,mUsersInPhoto :: [UserPosition]
  ,mFilter :: Maybe Text
  ,mTags :: [Text]
  ,mLocation :: Maybe Location
  ,mComments :: Count
  ,mLikes :: Count
  ,mUserHasLiked :: Bool
  ,mAttribution :: Maybe Object -- ^ seems to be open format https://groups.google.com/forum/?fromgroups#!topic/instagram-api-developers/KvGH1cnjljQ
  }
  deriving (Show,Eq,Typeable)

-- | to json as per Instagram format
instance ToJSON Media  where
    toJSON m=object ["id" .= mID m,"caption" .= mCaption m,"user".= mUser m,"link" .= mLink m, "created_time" .= toJSON (show ((round $ mCreated m) :: Integer))
      ,"images" .= mImages m,"type" .= mType m,"users_in_photo" .= mUsersInPhoto m, "filter" .= mFilter m,"tags" .= mTags m
      ,"location" .= mLocation m,"comments" .= mComments m,"likes" .= mLikes m,"user_has_liked" .= mUserHasLiked m,"attribution" .= mAttribution m]

-- | from json as per Instagram format
instance FromJSON Media where
    parseJSON (Object v) =do
      ct::String<-v .: "created_time"
      Media <$>
                         v .: "id" <*>
                         v .:? "caption" <*>
                         v .: "link" <*>
                         v .: "user" <*>
                         pure (fromIntegral (read ct::Integer)) <*>
                         v .: "images" <*>
                         v .: "type" <*>
                         v .: "users_in_photo" <*>
                         v .:? "filter" <*>
                         v .: "tags" <*>
                         v .:? "location" <*>
                         v .:? "comments" .!= Count 0 <*>
                         v .:? "likes" .!= Count 0 <*>
                         v .:? "user_has_liked" .!= False <*>
                         v .:? "attribution"
    parseJSON _= fail "Media"

-- | position in picture
data Position = Position {
  pX ::Double
  ,pY :: Double
} deriving (Show,Eq,Typeable)


-- | to json as per Instagram format
instance ToJSON Position where
  toJSON p=object ["x" .= pX p,"y" .= pY p]

-- | from json as per Instagram format
instance FromJSON Position where
  parseJSON (Object v) = Position <$>
    v .: "x" <*>
    v .: "y"
  parseJSON _=fail "Position"

-- | position of a user
data UserPosition = UserPosition {
  upPosition :: Position
  ,upUser :: User
  } deriving (Show,Eq,Typeable)


-- | to json as per Instagram format
instance ToJSON UserPosition where
  toJSON p=object ["position" .= upPosition p,"user" .= upUser p]

-- | from json as per Instagram format
instance FromJSON UserPosition where
  parseJSON (Object v) = UserPosition <$>
    v .: "position" <*>
    v .: "user"
  parseJSON _=fail "UserPosition"

-- | location ID
type LocationID = Text

-- | geographical location info
data Location = Location {
  lID :: Maybe LocationID
  ,lLatitude :: Maybe Double
  ,lLongitude :: Maybe Double
  ,lStreetAddress :: Maybe Text
  ,lName :: Maybe Text
  }
  deriving (Show,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON Location where
  toJSON l=object ["id" .= lID l,"latitude" .= lLatitude l,"longitude" .= lLongitude l, "street_address" .= lStreetAddress l,"name" .= lName l]

-- | from json as per Instagram format
instance FromJSON Location where
  parseJSON (Object v) =
    Location <$>
      parseID v <*>
      v .:? "latitude" <*>
      v .:? "longitude" <*>
      v .:? "street_address" <*>
      v .:? "name"
    where
      -- | the Instagram API hasn't made its mind up, sometimes location id is an int, sometimes a string
      parseID :: Object -> Parser (Maybe LocationID)
      parseID obj=case HM.lookup "id" obj of
        Just (String s)->pure $ Just s
        Just (Number n)->pure $ Just $ T.pack $ show $ (round n :: Int)
        Nothing->pure Nothing
        _->fail "LocationID"
  parseJSON _= fail "Location"

-- | data for a single image
data ImageData = ImageData {
  idURL :: Text,
  idWidth :: Integer,
  idHeight :: Integer
  }
  deriving (Show,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON ImageData where
  toJSON i=object ["url" .= idURL i,"width" .= idWidth i,"height" .= idHeight i]

-- | from json as per Instagram format
instance FromJSON ImageData where
  parseJSON (Object v) = ImageData <$>
    v .: "url" <*>
    v .: "width" <*>
    v .: "height"
  parseJSON _= fail "ImageData"

-- | different images for the same media
data Images = Images {
  iLowRes :: ImageData
  ,iThumbnail :: ImageData
  ,iStandardRes :: ImageData
  }
  deriving (Show,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON Images where
  toJSON i=object ["low_resolution" .= iLowRes i,"thumbnail" .= iThumbnail i,"standard_resolution" .= iStandardRes i]

-- | from json as per Instagram format
instance FromJSON Images where
  parseJSON (Object v) = Images <$>
    v .: "low_resolution" <*>
    v .: "thumbnail" <*>
    v .: "standard_resolution"
  parseJSON _= fail "Images"

-- | comment id
type CommentID = Text

-- | Commenton on a medium
data Comment = Comment {
  cID :: CommentID
  ,cCreated :: POSIXTime
  ,cText :: Text
  ,cFrom :: User
  }
  deriving (Show,Eq,Ord,Typeable)

-- | to json asCommentstagram format
instance ToJSON Comment  where
    toJSON c=object ["id" .= cID c,"created_time" .= toJSON (show ((round $ cCreated c) :: Integer))
      ,"text" .= cText c,"from" .= cFrom c]

-- | from json asCommentstagram format
instance FromJSON Comment where
    parseJSON (Object v) =do
      ct::String<-v .: "created_time"
      Comment <$>
                         v .: "id" <*>
                         pure (fromIntegral (read ct::Integer)) <*>
                         v .: "text" <*>
                         v .: "from"
    parseJSON _= fail "Caption"

data Count = Count {
  cCount :: Integer
  }
  deriving (Show,Eq,Ord,Typeable)


-- | to json as per Instagram format
instance ToJSON Count where
    toJSON igc=object ["count" .= cCount igc]

-- | from json as per Instagram format
instance FromJSON Count where
    parseJSON (Object v) = Count <$>
                         v .: "count"
    parseJSON _= fail "Count"


-- | the URL to receive notifications to
type CallbackUrl = Text

-- | notification aspect
data Aspect = Aspect Text
  deriving (Show, Read, Eq, Ord, Typeable)

-- | to json as per Instagram format
instance ToJSON Aspect  where
    toJSON (Aspect t)=String t

-- | from json as per Instagram format
instance FromJSON Aspect where
    parseJSON (String t) = pure $ Aspect t
    parseJSON _= fail "Aspect"

-- | the media Aspect, the only one supported for now
media :: Aspect
media = Aspect "media"

-- | a subscription to a real time notification
data Subscription= Subscription {
  sID :: Text
  ,sType :: Text
  ,sObject :: Text
  ,sObjectID :: Maybe Text
  ,sAspect :: Aspect
  ,sCallbackUrl :: CallbackUrl
  ,sLatitude :: Maybe Double
  ,sLongitude :: Maybe Double
  ,sRadius :: Maybe Integer
  }
  deriving (Show,Eq,Typeable)

-- | to json as per Instagram format
instance ToJSON Subscription  where
    toJSON s=object ["id" .= sID s,"type" .= sType s,"object" .= sObject s,"object_id" .= sObjectID s,"aspect" .= sAspect s
      ,"callback_url".=sCallbackUrl s,"lat".= sLatitude s,"lng".=sLongitude s,"radius".=sRadius s]

-- | from json as per Instagram format
instance FromJSON Subscription where
    parseJSON (Object v) = Subscription <$>
                         v .: "id" <*>
                         v .: "type" <*>
                         v .: "object" <*>
                         v .:? "object_id" <*>
                         v .: "aspect" <*>
                         v .: "callback_url" <*>
                         v .:? "lat" <*>
                         v .:? "lng" <*>
                         v .:? "radius"
    parseJSON _= fail "Subscription"

-- | an update from a subscription
data Update = Update {
  uSubscriptionID :: Integer
  ,uObject :: Text
  ,uObjectID :: Text
  ,uChangedAspect :: Aspect
  ,uTime :: POSIXTime
  }
  deriving (Show,Eq,Typeable)

-- | to json as per Instagram format
instance ToJSON Update  where
    toJSON u=object ["subscription_id" .= uSubscriptionID u      ,"object" .= uObject u,"object_id" .= uObjectID u
      ,"changed_aspect" .= uChangedAspect u,"time" .= toJSON ((round $ uTime u) :: Integer)]

-- | from json as per Instagram format
instance FromJSON Update where
    parseJSON (Object v) =do
      ct::Integer<-v .: "time"
      Update <$>
                         v .: "subscription_id" <*>
                         v .: "object" <*>
                         v .: "object_id" <*>
                         v .: "changed_aspect" <*>
                         pure (fromIntegral ct)
    parseJSON _= fail "Update"

-- | Tag Name
type TagName = Text

-- | a Tag
data Tag = Tag {
  tName :: TagName,
  tMediaCount :: Integer
  }
  deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON Tag  where
    toJSON t=object ["name" .= tName t,"media_count" .= tMediaCount t]

-- | from json as per Instagram format
instance FromJSON Tag where
    parseJSON (Object v) = Tag <$>
                         v .: "name" <*>
                         v .:? "media_count" .!= 0
    parseJSON _= fail "Tag"

-- | outgoing relationship status
data OutgoingStatus = Follows | Requested | OutNone
  deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)

-- | to json as per Instagram format
instance ToJSON OutgoingStatus  where
    toJSON Follows = String "follows"
    toJSON Requested = String "requested"
    toJSON OutNone = String "none"

-- | from json as per Instagram format
instance FromJSON OutgoingStatus where
  parseJSON (String "follows")=pure Follows
  parseJSON (String "requested")=pure Requested
  parseJSON (String "none")=pure OutNone
  parseJSON _= fail "OutgoingStatus"

-- | incoming relationship status
data IncomingStatus = FollowedBy | RequestedBy | BlockedByYou | InNone
  deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)

-- | to json as per Instagram format
instance ToJSON IncomingStatus  where
    toJSON FollowedBy = String "followed_by"
    toJSON RequestedBy = String "requested_by"
    toJSON BlockedByYou = String "blocked_by_you"
    toJSON InNone = String "none"

-- | from json as per Instagram format
instance FromJSON IncomingStatus where
  parseJSON (String "followed_by")=pure FollowedBy
  parseJSON (String "requested_by")=pure RequestedBy
  parseJSON (String "blocked_by_you")=pure BlockedByYou
  parseJSON (String "none")=pure InNone
  parseJSON _= fail "IncomingStatus"

-- | a relationship between two users
data Relationship = Relationship {
  rOutgoing :: OutgoingStatus
  ,rIncoming :: IncomingStatus
  ,rTargetUserPrivate :: Bool -- ^ not present in doc
  }
  deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON Relationship  where
    toJSON r=object ["outgoing_status" .= rOutgoing r,"incoming_status" .= rIncoming r,"target_user_is_private" .= rTargetUserPrivate r]

-- | from json as per Instagram format
instance FromJSON Relationship where
    parseJSON (Object v) = Relationship <$>
                         v .:? "outgoing_status" .!= OutNone <*>
                         v .:? "incoming_status" .!= InNone <*>
                         v .:? "target_user_is_private" .!= False
    parseJSON _= fail "Relationship"

-- | Instagram returns data:null for nothing, but Aeson considers that () maps to an empty array...
-- so we model the fact that we expect null via NoResult
data NoResult = NoResult
  deriving (Show,Read,Eq,Ord,Typeable)

-- | to json as per Instagram format
instance ToJSON NoResult  where
  toJSON _=Null

-- | from json as per Instagram format
instance FromJSON NoResult where
    parseJSON Null = pure NoResult
    parseJSON _= fail "NoResult"

-- | geography ID
type GeographyID = Text
