{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

{- |
Module      :  Servant.Checked.Exceptions.Internal.Servant.Client

Copyright   :  Dennis Gosnell 2017
License     :  BSD3

Maintainer  :  Dennis Gosnell (cdep.illabout@gmail.com)
Stability   :  experimental
Portability :  unknown

This module only exports 'HasClient' instances for 'Throws' and 'Throwing'.
-}

module Servant.Checked.Exceptions.Internal.Servant.Client where

import Data.Proxy (Proxy(Proxy))
import Servant.API (Verb, (:>), (:<|>))
import Servant.Client (HasClient(clientWithRoute, Client))
import Servant.Client.Core

import Servant.Checked.Exceptions.Internal.Envelope (Envelope)
import Servant.Checked.Exceptions.Internal.Servant.API
       (NoThrow, Throws, Throwing, ThrowingNonterminal)

-- TODO: Make sure to also account for when headers are being used.

-- | Change a 'Throws' into 'Throwing'.
instance (RunClient m, HasClient m (Throwing '[e] :> api)) => HasClient m (Throws e :> api) where
  type Client m (Throws e :> api) = Client m (Throwing '[e] :> api)

  clientWithRoute
    :: Proxy m
    -> Proxy (Throws e :> api)
    -> Request
    -> Client m (Throwing '[e] :> api)
  clientWithRoute p Proxy = clientWithRoute p (Proxy :: Proxy (Throwing '[e] :> api))

-- | When @'Throwing' es@ comes before a 'Verb', change it into the same 'Verb'
-- but returning an @'Envelope' es@.
instance (HasClient m (Verb method status ctypes (Envelope es a))) =>
    HasClient m (Throwing es :> Verb method status ctypes a) where

  type Client m (Throwing es :> Verb method status ctypes a) =
    Client m (Verb method status ctypes (Envelope es a))

  clientWithRoute
    :: Proxy m
    -> Proxy (Throwing es :> Verb method status ctypes a)
    -> Request
    -> Client m (Verb method status ctypes (Envelope es a))
  clientWithRoute p Proxy =
    clientWithRoute p (Proxy :: Proxy (Verb method status ctypes (Envelope es a)))

-- | When 'NoThrow' comes before a 'Verb', change it into the same 'Verb'
-- but returning an @'Envelope' \'[]@.
instance (RunClient m, HasClient m (Verb method status ctypes (Envelope '[] a))) =>
    HasClient m (NoThrow :> Verb method status ctypes a) where

  type Client m (NoThrow :> Verb method status ctypes a) =
    Client m (Verb method status ctypes (Envelope '[] a))

  clientWithRoute
    :: Proxy m
    -> Proxy (NoThrow :> Verb method status ctypes a)
    -> Request
    -> Client m (Verb method status ctypes (Envelope '[] a))
  clientWithRoute p Proxy =
    clientWithRoute p (Proxy :: Proxy (Verb method status ctypes (Envelope '[] a)))

-- | When @'Throwing' es@ comes before ':<|>', push @'Throwing' es@ into each
-- branch of the API.
instance (RunClient m, HasClient m ((Throwing es :> api1) :<|> (Throwing es :> api2))) =>
    HasClient m (Throwing es :> (api1 :<|> api2)) where

  type Client m (Throwing es :> (api1 :<|> api2)) =
    Client m ((Throwing es :> api1) :<|> (Throwing es :> api2))

  clientWithRoute
    :: Proxy m
    -> Proxy (Throwing es :> (api1 :<|> api2))
    -> Request
    -> Client m ((Throwing es :> api1) :<|> (Throwing es :> api2))
  clientWithRoute p _ =
    clientWithRoute p (Proxy :: Proxy ((Throwing es :> api1) :<|> (Throwing es :> api2)))

-- | When 'NoThrow' comes before ':<|>', push 'NoThrow' into each branch of the
-- API.
instance (RunClient m, HasClient m ((NoThrow :> api1) :<|> (NoThrow :> api2))) =>
    HasClient m (NoThrow :> (api1 :<|> api2)) where

  type Client m (NoThrow :> (api1 :<|> api2)) =
    Client m ((NoThrow :> api1) :<|> (NoThrow :> api2))

  clientWithRoute
    :: Proxy m
    -> Proxy (NoThrow :> (api1 :<|> api2))
    -> Request
    -> Client m ((NoThrow :> api1) :<|> (NoThrow :> api2))
  clientWithRoute p _ =
    clientWithRoute p (Proxy :: Proxy ((NoThrow :> api1) :<|> (NoThrow :> api2)))

-- | When a @'Throws' e@ comes immediately after a @'Throwing' es@, 'Snoc' the
-- @e@ onto the @es@. Otherwise, if @'Throws' e@ comes before any other
-- combinator, push it down so it is closer to the 'Verb'.
instance (RunClient m, HasClient m (ThrowingNonterminal (Throwing es :> api :> apis))) =>
    HasClient m (Throwing es :> api :> apis) where

  type Client m (Throwing es :> api :> apis) =
    Client m (ThrowingNonterminal (Throwing es :> api :> apis))

  clientWithRoute
    :: Proxy m
    -> Proxy (Throwing es :> api :> apis)
    -> Request
    -> Client m (ThrowingNonterminal (Throwing es :> api :> apis))
  clientWithRoute p _ =
    clientWithRoute p (Proxy :: Proxy (ThrowingNonterminal (Throwing es :> api :> apis)))

-- | When 'NoThrow' comes before any other combinator, push it down so it is
-- closer to the 'Verb'.
instance (RunClient m, HasClient m (api :> NoThrow :> apis)) =>
    HasClient m (NoThrow :> api :> apis) where

  type Client m (NoThrow :> api :> apis) =
    Client m (api :> NoThrow :> apis)

  clientWithRoute
    :: Proxy m
    -> Proxy (NoThrow :> api :> apis)
    -> Request
    -> Client m (api :> NoThrow :> apis)
  clientWithRoute p _ =
    clientWithRoute p (Proxy :: Proxy (api :> NoThrow :> apis))