{-# LANGUAGE ExistentialQuantification, MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings, FlexibleInstances #-}

-- | Utility and base types and functions for the Discord Rest API
module Network.Discord.Rest.Prelude where
  import Control.Concurrent (threadDelay)
  import Data.Version     (showVersion)

  import Control.Lens
  import Data.Aeson
  import Data.ByteString.Char8 (pack)
  import Data.Default
  import Data.Hashable
  import Data.Time.Clock.POSIX
  import Network.Wreq
  import System.Log.Logger
  import qualified Control.Monad.State as St

  import Network.Discord.Types
  import Paths_discord_hs (version)

  -- | Read function specialized for Integers
  readInteger :: String -> Integer
  readInteger = read

  -- | The base url for API requests
  baseURL :: String
  baseURL = "https://discordapp.com/api/v6"

  -- | Construct base request with auth from Discord state
  baseRequest :: DiscordM Options
  baseRequest = do
    DiscordState {getClient=client} <- St.get
    return $ defaults
      & header "Authorization" .~ [pack . show $ getAuth client]
      & header "User-Agent"    .~
        [pack  $ "DiscordBot (https://github.com/jano017/Discord.hs,"
          ++ showVersion version
          ++ ")"]
      & header "Content-Type" .~ ["application/json"]

  -- | Class for rate-limitable actions
  class RateLimit a where
    -- | Return seconds to expiration if we're waiting
    --   for a rate limit to reset
    getRateLimit  :: a -> DiscordM (Maybe Int)
    -- | Set seconds to the next rate limit reset when
    --   we hit a rate limit
    setRateLimit  :: a -> Int -> DiscordM ()
    -- | If we hit a rate limit, wait for it to reset
    waitRateLimit :: a -> DiscordM ()
    waitRateLimit endpoint = do
      rl <- getRateLimit endpoint
      case rl of
        Nothing -> return ()
        Just a  -> do
          now <- St.liftIO (fmap round getPOSIXTime :: IO Int)
          St.liftIO $ do
            infoM "Discord-hs.Rest" "Waiting for rate limit to reset..."
            threadDelay $ 1000000 * (a - now)
            putStrLn "Done"
          return ()
  
  -- | Class over which performing a data retrieval action is defined
  class DoFetch a where
    doFetch :: a -> DiscordM Fetched

  -- | Polymorphic type for all DoFetch types
  data Fetchable = forall a. (DoFetch a, Hashable a) => Fetch a

  instance DoFetch Fetchable where
    doFetch (Fetch a) = doFetch a

  instance Hashable Fetchable where
    hashWithSalt s (Fetch a) = hashWithSalt s a

  instance Eq Fetchable where
    (Fetch a) == (Fetch b) = hash a == hash b

  -- | Result of a data retrieval action
  data Fetched = forall a. (FromJSON a) => SyncFetched a
  
  -- | Represents a range of 'Snowflake's
  data Range = Range { after :: Snowflake, before :: Snowflake, limit :: Int}

  instance Default Range where
    def = Range 0 18446744073709551615 100
  
  -- | Convert a Range to a query string
  toQueryString :: Range -> String
  toQueryString (Range a b l) = 
    "after=" ++ show a ++ "&before=" ++ show b ++ "&limit=" ++ show l