{-|
Module      : Network.Clarifai
Description : API Client for the Clarifai API.
Copyright   : (c) Joseph Canero, 2015
License     : MIT
Maintainer  : caneroj1@tcnj.edu
Stability   : experimental
Portability : portable

Provides functionality for interacting with Clarifai's
Image Tagging API. Allows users to submit images/videos to be recognized and
tagged by Clarifai. Users will need a Clarifai account to make full use of this
client.
-}

{-# LANGUAGE OverloadedStrings #-}

module Network.Clarifai
  (
    VerificationStatus(..),
    Client(..),
    TagSet(..),
    Info(..),
    Tag(..),
    verifyImageBatchSize,
    verifyVideoBatchSize,
    verifyFiles,
    authorize,
    info,
    tag
  ) where

import qualified Control.Exception          as E
import           Control.Lens
import           Data.Aeson
import           Data.Aeson.Lens
import qualified Data.ByteString            as BL
import qualified Data.ByteString.Char8      as BStrict
import qualified Data.ByteString.Lazy.Char8 as BS
import           Data.Either
import           Data.List
import qualified Data.Map.Lazy              as Map
import qualified Data.Text                  as T
import qualified Data.Vector                as V
import           Network.HTTP
import qualified Network.HTTP.Client        as Net
import           Network.Utilities
import           Network.Wreq
import           System.EasyFile

-- | The Client data type has two constructors. The first should be used
-- when constructing a client with an access token. The second constructor
-- should be used when passing in an application's client id and client secret.
-- The flow usually proceeds by creating an App and then authorizing it.
data Client = Client String | App String String deriving (Show)

-- Turn our authorized Client into an Authorization header
authHeader :: Client -> Options
authHeader (Client token) = defaults' & header "Authorization" .~ [packed]
  where auth = "Bearer " ++ token
        packed = BStrict.pack auth
authHeader _ = defaults

-- | The Info data type is used to encapsulate a response from the
-- /info endpoint. This type contains information about the
-- various usage limits for the API.
data Info = Info {
  maxBatchSize      :: Integer,
  maxImageSize      :: Integer,
  minImageSize      :: Integer,
  maxImageBytes     :: Integer,
  maxVideoBatchSize :: Integer,
  maxVideoSize      :: Integer,
  minVideoSize      :: Integer,
  maxVideoBytes     :: Integer,
  maxVideoDuration  :: Integer
} deriving (Show)

-- Convert a Map of Strings and Values into an Info type
toInfo :: Obj -> Info
toInfo origObj = Info mbs maxis minis maxib mvbs maxvs minvs mvb mvd
  where obj = getMap "results" origObj
        mbs   = getInt' "max_batch_size"       obj
        maxis = getInt' "max_image_size"       obj
        minis = getInt' "min_image_size"       obj
        maxib = getInt' "max_image_bytes"      obj
        mvbs  = getInt' "max_video_batch_size" obj
        maxvs = getInt' "max_video_size"       obj
        minvs = getInt' "min_video_size"       obj
        mvb   = getInt' "max_video_bytes"      obj
        mvd   = getInt' "max_video_duration"   obj

-- | A Tag is a pair of String and Double and represents a word class
-- from the Clarifai model and the associated probability.
data Tag = Tag String Double deriving (Show)

-- | A TagSet represents a single result from the Tag endpoint.
-- A TagSet is identified uniquely by its docID, and uniquely within a request
-- by its localID. The most important feature of the TagSet is the vector of
-- Tags.
data TagSet = TagSet {
  docID   :: Integer,
  localID :: String,
  tags    :: V.Vector Tag
} deriving (Show)

getTags :: HObj -> V.Vector Tag
getTags o = V.map (uncurry Tag) (V.zip classes probs)
  where classes = V.map value2String (getVec' "classes" o)
        probs = V.map value2Double (getVec' "probs" o)

objToTagSet :: HObj -> TagSet
objToTagSet o = TagSet docID localID tags
  where docID = getInt' "docid" o
        localID = getString' "local_id" o
        tags = getTags (getMap' "tag" $ getMap' "result" o)

--------------------
---- API Routes ----
--------------------
-- For authentication
tokenUrl  = "https://api.clarifai.com/v1/token/"
-- For API info
infoUrl   = "https://api.clarifai.com/v1/info/"
-- Tag images/videos
tagUrl    = "https://api.clarifai.com/v1/tag/"

-- | Authorize an application
-- Sends a POST request to Clarifai's authentication endpoint.
-- If we have a Client, we just return the client because I'm assuming
-- the client was constructed with a valid access token. If we have an App,
-- we would POST the client ID and client secret to Clarifai to get an
-- access token.
authorize :: Client -> IO (Either Errors Client)
authorize (Client token) = return (Right (Client token))
authorize (App clientID clientSecret) = resp
  where params = ["client_id" := clientID,
                  "client_secret" := clientSecret,
                  "grant_type" := BS.pack "client_credentials"]
        key = "access_token"
        resp = do (status, body) <- processRequest $ postWith' tokenUrl params
                  let code = status ^. statusCode in
                    if code /= 200 then
                      return (Left (code, apiErr code body))
                    else
                      return (Right (Client $ getString key body))

-- | Gets the Clarifai API limits and information.
-- Sends a get request to the /info endpoint and encapsulates
-- the results into an Info type.
info :: Client -> IO (Either Errors Info)
info (App _ _) = return (Left (0, "You have not authorized your app yet."))
info client = resp
  where opts = authHeader client
        resp = do (status, body) <- processRequest $ getWith opts infoUrl
                  let code = status ^. statusCode in
                    if code /= 200 then
                      return (Left (code, apiErr code body))
                    else
                      return (Right (toInfo body))

-- TODO: support localIDs
-- TODO: use partFileSource?
-- | Utilizes the tag endpoint of the clarifai API to tag
-- multiple files from the local file system.
tag :: Client -> [FilePath] -> IO (Either Errors (V.Vector TagSet))
tag (App _ _) _ = return (Left (0, "You have not authorized your app yet."))
tag c fs = resp
  where opts = authHeader c
        toParts = partFile "encoded_data"
        files = map toParts fs
        resp = do (status, body) <- processRequest $ postWith opts tagUrl files
                  let extractedVec = vecOfObjects $ getVec "results" body
                  let code = status ^. statusCode in
                    if code /= 200 then
                      return (Left (code, apiErr code body))
                    else
                      return (Right (V.map objToTagSet extractedVec))
                    where

vecOfObjects :: V.Vector Value -> V.Vector HObj
vecOfObjects = V.map value2Map

-- | Given an API Info type and a list of FilePaths, we verify each of the files.
-- If the file has an extension, we decide which Info attribute to use
-- to verify the file. If it has no extension, we choose not to verify. This
-- function maps each FilePath to a tuple of (FilePath, VerificationStatus),
-- where VerificationStatus = Good | Bad | Unknown
verifyFiles :: Info -> [FilePath] -> IO [(FilePath, IO VerificationStatus)]
verifyFiles info fs = do
  let zipped = zip fs (map getFileSize fs)
  return (map (verify info) zipped)
  where verify (Info _ _ _ ib _ _ _ _ vb) (path, ioSize)
          | ext `elem` imageExtensions = (path, fmap imgC ioSize)
          | ext `elem` videoExtensions = (path, fmap vidC ioSize)
          | otherwise = (path, return Unknown)
          where ext = takeExtension path
                vidC = fileCheck vb
                imgC = fileCheck ib

-- | Given an API Info type and a list of FilePaths, we verify
-- the length of the list with what the Info type specifies is
-- acceptable batch size for images. This function assumes that
-- the FilePaths all point to images.
verifyImageBatchSize :: Info -> [FilePath] -> Bool
verifyImageBatchSize (Info size _ _ _ _ _ _ _ _) xs = size >= conv
  where conv = fromIntegral (length xs) :: Integer

-- | Given an API Info type and a list of FilePaths, we verify
-- the length of the list with what the Info type specifies is
-- acceptable batch size for videos. This function assumes that
-- the FilePaths all point to videos.
verifyVideoBatchSize :: Info -> [FilePath] -> Bool
verifyVideoBatchSize (Info _ _ _ _ size _ _ _ _) xs = size >= conv
  where conv = fromIntegral (length xs) :: Integer