-- GSoC 2013 - Communicating with mobile devices.

{-# LANGUAGE FlexibleContexts #-}

-- | This Module define the main data types for sending Push Notifications through Apple Push Notification Service.

module Network.PushNotify.Apns.Types
    ( -- * APNS Settings
      APNSConfig(..)
    , APNSManager(..)
    , DeviceToken
    , Env(..)
      -- * APNS Messages
    , APNSmessage(..)
    , AlertDictionary(..)
      -- * APNS Results
    , APNSresult(..)
    , APNSFeedBackresult(..)
    ) where

import Network.PushNotify.Apns.Constants
import Network.TLS                          (PrivateKey)
import Control.Concurrent
import Control.Concurrent.STM.TChan
import Control.Monad.Writer
import Control.Retry
import Data.Aeson.Types
import Data.Certificate.X509                (X509)
import Data.Default
import qualified Data.HashMap.Strict        as HM
import qualified Data.HashSet               as HS
import Data.IORef
import Data.Text
import Data.Time.Clock

-- | 'Env' represents the three possible working environments. This determines the url and port to connect to.
data Env = Development -- ^ Development environment (by Apple).
         | Production  -- ^ Production environment (by Apple).
         | Local       -- ^ Local environment, just to test the service in the \"localhost\".
         deriving Show

-- | 'APNSConfig' represents the main necessary information for sending notifications through APNS.
--
-- For loading the certificate and privateKey you can use: 'Network.TLS.Extra.fileReadCertificate' and 'Network.TLS.Extra.fileReadPrivateKey' .
data APNSConfig = APNSConfig
    {   apnsCertificate   :: X509          -- ^ Certificate provided by Apple.
    ,   apnsPrivateKey    :: PrivateKey    -- ^ Private key provided by Apple.
    ,   environment       :: Env           -- ^ One of the possible environments.
    ,   timeoutLimit      :: Int           -- ^ The time to wait for a server response. (microseconds)
    ,   apnsRetrySettings :: RetrySettings -- ^ How to retry to connect to APNS servers.
    }

instance Default APNSConfig where
    def = APNSConfig {
        apnsCertificate   = undefined
    ,   apnsPrivateKey    = undefined
    ,   environment       = Development
    ,   timeoutLimit      = 200000
    ,   apnsRetrySettings = RetrySettings {
                                backoff     = True
                            ,   baseDelay   = 200
                            ,   numRetries  = limitedRetries 5
                            }
    }

data APNSManager = APNSManager
    {   mState        :: IORef (Maybe ())
    ,   mApnsChannel  :: TChan ( MVar (Maybe (Chan Int,Int)) , APNSmessage)
    ,   mWorkerID     :: ThreadId
    ,   mTimeoutLimit :: Int
    }

-- | Binary token stored in hexadecimal representation as text.
type DeviceToken = Text


-- | 'APNSmessage' represents a message to be sent through APNS.
data APNSmessage = APNSmessage
    {   deviceTokens :: HS.HashSet DeviceToken -- ^ Destination.
    ,   expiry       :: Maybe UTCTime -- ^ Identifies when the notification is no longer valid and can be discarded. 
    ,   alert        :: Either Text AlertDictionary -- ^ For the system to displays a standard alert.
    ,   badge        :: Maybe Int     -- ^ Number to display as the badge of the application icon.
    ,   sound        :: Text          -- ^ The name of a sound file in the application bundle.
    ,   rest         :: Maybe Object  -- ^ Extra information.
    } deriving Show

instance Default APNSmessage where
    def = APNSmessage {
        deviceTokens = HS.empty
    ,   expiry       = Nothing
    ,   alert        = Left empty
    ,   badge        = Nothing
    ,   sound        = empty
    ,   rest         = Nothing
    }

-- | 'AlertDictionary' represents the possible dictionary in the 'alert' label.
data AlertDictionary = AlertDictionary
    {   body           :: Text
    ,   action_loc_key :: Text
    ,   loc_key        :: Text
    ,   loc_args       :: [Text]
    ,   launch_image   :: Text
    } deriving Show

instance Default AlertDictionary where
    def = AlertDictionary{
        body           = empty
    ,   action_loc_key = empty
    ,   loc_key        = empty
    ,   loc_args       = []
    ,   launch_image   = empty
    }

-- | 'APNSresult' represents information about messages after a communication with APNS Servers.
data APNSresult = APNSresult
    {   successfulTokens :: HS.HashSet DeviceToken
    ,   toReSendTokens   :: HS.HashSet DeviceToken -- ^ Failed tokens that you need to resend the message to,
                                               -- because there was a problem.
    } deriving Show

instance Default APNSresult where
    def = APNSresult HS.empty HS.empty

-- | 'APNSFeedBackresult' represents information after connecting with the Feedback service.
data APNSFeedBackresult = APNSFeedBackresult
    {   unRegisteredTokens :: HM.HashMap DeviceToken UTCTime -- ^ Devices tokens and time indicating when APNS determined
                                                             -- that the application no longer exists on the device.
    } deriving Show

instance Default APNSFeedBackresult where
    def = APNSFeedBackresult HM.empty


ifNotDef :: (ToJSON a,MonadWriter [Pair] m,Eq a,Default b)
            => Text
            -> (b -> a)
            -> b
            -> m ()
ifNotDef label f msg = if f def /= f msg
                        then tell [(label .= (f msg))]
                        else tell []

instance ToJSON APNSmessage where
    toJSON msg = case rest msg of
                     Nothing    -> object [(cAPPS .= toJSONapps msg)]
                     Just (map) -> Object $ HM.insert cAPPS (toJSONapps msg) map

toJSONapps msg = object $ execWriter $ do
                                        case alert msg of
                                            Left xs  -> if xs == empty
                                                            then tell []
                                                            else tell [(cALERT .= xs)]
                                            Right m  -> tell [(cALERT .= (toJSON m))]
                                        ifNotDef cBADGE badge msg
                                        ifNotDef cSOUND sound msg

instance ToJSON AlertDictionary where
    toJSON msg = object $ execWriter $ do
                                        ifNotDef cBODY body msg
                                        ifNotDef cACTION_LOC_KEY action_loc_key msg
                                        ifNotDef cLOC_KEY loc_key msg
                                        if loc_key def /= loc_key msg
                                            then ifNotDef cLOC_ARGS loc_args msg
                                            else tell []
                                        ifNotDef cLAUNCH_IMAGE launch_image msg