{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE ViewPatterns          #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE UndecidableInstances  #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE DataKinds             #-}

-- | Provides instances to be able to use combinators from
-- "Servant.API.NamedArgs" with "Servant.Client", returning functions
-- using named parameters from "Named"
module Servant.Client.NamedArgs where

import Named ((:!), (:?), arg, argF, argDef, Name(..))
import Data.Text (pack, Text)
import Data.Functor.Identity (Identity)
import Data.List (foldl')
import Servant.API ((:>), SBoolI, ToHttpApiData, toQueryParam, toUrlPiece, MimeRender(mimeRender), contentType)
import Servant.API.Modifiers (FoldRequired)
import Servant.API.NamedArgs ( foldRequiredNamedArgument, NamedCapture', NamedFlag
                             , NamedParam, NamedParams, RequiredNamedArgument
                             , NamedCaptureAll, NamedHeader', NamedBody')
import Servant.Client.Core (HasClient(..), appendToPath, appendToQueryString, Request, addHeader, setRequestBodyLBS)
import Data.String (fromString)
import GHC.TypeLits (KnownSymbol, symbolVal)
import Data.Proxy (Proxy(..))

-- | 'NamedCapture''s become required named arguments
instance (KnownSymbol name, ToHttpApiData a, HasClient m api)
    => HasClient m (NamedCapture' mods name a :> api) where

  type Client m (NamedCapture' mods name a :> api) =
    (name :! a) -> Client m api

  clientWithRoute pm _ req (arg (Name @name) -> capture) =
      clientWithRoute pm (Proxy @api) (appendToPath (toUrlPiece capture) req)

  hoistClientMonad pm _ f cl = \a ->
      hoistClientMonad pm (Proxy @api) f (cl a)

-- | 'NamedCaptureAll's become optional named arguments taking a list and
-- defaulting to an empty list
instance (KnownSymbol name, ToHttpApiData a, HasClient m api)
    => HasClient m (NamedCaptureAll name a :> api) where

  type Client m (NamedCaptureAll name a :> api) =
    (name :? [a]) -> Client m api

  clientWithRoute pm _ req (argDef (Name @name) [] -> captures) =
      clientWithRoute pm (Proxy @api) (foldl' (flip appendToPath) req ps)
    where
      ps = map toUrlPiece captures

  hoistClientMonad pm _ f cl = \as ->
      hoistClientMonad pm (Proxy @api) f (cl as)

-- | 'NamedFlag's become optional named arguments, defaulting to 'False'
instance (KnownSymbol name, HasClient m api)
    => HasClient m (NamedFlag name :> api) where

  type Client m (NamedFlag name :> api) =
    (name :? Bool) -> Client m api

  clientWithRoute pm _ req (argDef (Name @name) False -> mflag) =
      clientWithRoute pm (Proxy @api) $
      if mflag
        then appendToQueryString pname Nothing req
        else req
    where
      pname :: Text
      pname = pack $ symbolVal (Proxy @name)

  hoistClientMonad pm _ f cl = \b ->
      hoistClientMonad pm (Proxy @api) f (cl b)

-- | 'NamedHeader''s become either required or optional named arguments
-- depending on if 'Servant.API.Modifiers.Required' or
-- 'Servant.API.Modifiers.Optional' are in the modifiers
instance (KnownSymbol name, ToHttpApiData a, HasClient m api, SBoolI (FoldRequired mods))
    => HasClient m (NamedHeader' mods name a :> api) where

  type Client m (NamedHeader' mods name a :> api) =
     RequiredNamedArgument mods name a -> Client m api

  clientWithRoute pm _ req mval =
      clientWithRoute pm (Proxy @api) $
        foldRequiredNamedArgument @mods @name
          add
          (maybe req add)
          mval

    where
      add :: a -> Request
      add value = addHeader hname value req

      hname  = fromString $ symbolVal (Proxy @name)

  hoistClientMonad pm _ f cl = \a ->
      hoistClientMonad pm (Proxy @api) f (cl a)

-- | 'NamedParam's become either required or optional named arguments
-- depending on if 'Servant.API.Modifiers.Required' or
-- 'Servant.API.Modifiers.Optional' are in the modifiers
instance (KnownSymbol name, ToHttpApiData a, HasClient m api, SBoolI (FoldRequired mods))
    => HasClient m (NamedParam mods name a :> api) where

  type Client m (NamedParam mods name a :> api) =
     RequiredNamedArgument mods name a -> Client m api

  clientWithRoute pm _ req mparam =
      clientWithRoute pm (Proxy @api) $
        foldRequiredNamedArgument @mods @name
          add
          (maybe req add)
          mparam

    where
      add :: a -> Request
      add param = appendToQueryString pname (Just $ toQueryParam param) req

      pname :: Text
      pname  = pack $ symbolVal (Proxy @name)

  hoistClientMonad pm _ f cl = \a ->
      hoistClientMonad pm (Proxy @api) f (cl a)

-- | 'NamedParams's become optional named arguments taking a list and
-- defaulting to an empty list
instance (KnownSymbol name, ToHttpApiData a, HasClient m api)
    => HasClient m (NamedParams name a :> api) where

  type Client m (NamedParams name a :> api) =
    (name :? [a]) -> Client m api

  clientWithRoute pm _ req (argDef (Name @name) [] -> mparams) =
      clientWithRoute pm (Proxy @api) $
        case mparams of
          [] -> req
          ls -> foldl'
                  (\req' param -> appendToQueryString pname (Just $ toQueryParam param) req')
                  req
                  ls
    where
      pname :: Text
      pname = pack $ symbolVal (Proxy :: Proxy name)

  hoistClientMonad pm _ f cl = \as ->
      hoistClientMonad pm (Proxy @api) f (cl as)

-- | 'NamedBody''s become required named arguments 
instance (KnownSymbol name, MimeRender ct a, HasClient m api)
    => HasClient m (NamedBody' mods name (ct ': cts) a :> api) where

  type Client m (NamedBody' mods name (ct ': cts) a :> api) =
    (name :! a) -> Client m api

  clientWithRoute pm _ req (arg (Name @name) -> body) =
      clientWithRoute pm (Proxy @api) $
        setRequestBodyLBS (mimeRender ctProxy body)
                          (contentType ctProxy)
                          req
     where
       ctProxy = Proxy @ct

  hoistClientMonad pm _ f cl = \a ->
    hoistClientMonad pm (Proxy @api) f (cl a)