{- | Parse Servant API into documentation
-}
{-# LANGUAGE UndecidableInstances #-}

module Servant.Docs.Simple.Parse (HasParsable (..)) where


import Data.Foldable (fold)
import Data.Proxy
import Data.Text (Text, pack)
import Data.Typeable (Typeable, typeRep)
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)

import Servant.API ((:>), AuthProtect, BasicAuth, Capture', CaptureAll, Description, EmptyAPI,
                    Header', HttpVersion, IsSecure, QueryFlag, QueryParam', QueryParams, RemoteHost,
                    ReqBody', StreamBody', Summary, Vault, Verb)
import qualified Servant.API.TypeLevel as S (Endpoints)

import Servant.Docs.Simple.Render (Details (..), Endpoints (..), Node (..))

-- | Flattens API into type level list of 'Endpoints'
class HasParsable api where
    parse :: Endpoints

instance HasCollatable (S.Endpoints a) => HasParsable a where
    parse = collate @(S.Endpoints a)

instance {-# OVERLAPPING #-} HasParsable EmptyAPI where
    parse = collate @'[]

-- | Folds api endpoints into documentation
class HasCollatable api where
    -- | Folds list of endpoints to documentation
    collate :: Endpoints

instance (HasDocumentApi api, HasCollatable b) => HasCollatable (api ': b) where
    collate = Endpoints $ documentEndpoint @api : previous
      where Endpoints previous = collate @b

instance HasCollatable '[] where
    collate = Endpoints []

-- | Folds an api endpoint into documentation
documentEndpoint :: forall a. HasDocumentApi a => Node
documentEndpoint = document @a "" []

-- | Folds an api endpoint into documentation
class HasDocumentApi api where

    -- | We use this to destructure the API type and convert it into documentation
    document :: Text -- ^ Route documentation
             -> [Node] -- ^ Everything else documentation
             -> Node -- ^ Generated documentation for the route

-- | Static route documentation
instance (HasDocumentApi b, KnownSymbol route) => HasDocumentApi ((route :: Symbol) :> b) where
    document r = document @b formatted
        where formatted = fold [r, "/", fragment]
              fragment = symbolVal' @route

-- | Capture documentation
instance (HasDocumentApi b, KnownSymbol dRoute, Typeable t) => HasDocumentApi (Capture' m (dRoute :: Symbol) t :> b) where
    document r = document @b formatted
        where formatted = fold [r, "/", "{", var, "::", format, "}"]
              var = symbolVal' @dRoute
              format = typeText @t

-- | CaptureAll documentation
instance (HasDocumentApi b, KnownSymbol dRoute, Typeable t) => HasDocumentApi (CaptureAll (dRoute :: Symbol) t :> b) where
    document r = document @b formatted
        where formatted = fold [r, "/", "{", var, "::", format, "}"]
              var = symbolVal' @dRoute
              format = typeText @t

-- | Request HttpVersion documentation
instance HasDocumentApi b => HasDocumentApi (HttpVersion :> b) where
    document r a = document @b r (a <> [desc])
        where desc = Node "Captures Http Version" (Detail "True")

-- | IsSecure documentation
instance HasDocumentApi b => HasDocumentApi (IsSecure :> b) where
    document r a = document @b r (a <> [desc])
        where desc = Node "SSL Only" (Detail "True")

-- | Request Remote host documentation
instance HasDocumentApi b => HasDocumentApi (RemoteHost :> b) where
    document r a = document @b r (a <> [desc])
        where desc = Node "Captures RemoteHost/IP" (Detail "True")

-- | Description documentation
instance (HasDocumentApi b, KnownSymbol desc) => HasDocumentApi (Description (desc :: Symbol) :> b) where
    document r a = document @b r (a <> [desc])
        where desc = Node "Description" (Detail $ symbolVal' @desc)

-- | Summary documentation
instance (HasDocumentApi b, KnownSymbol s) => HasDocumentApi (Summary (s :: Symbol) :> b) where
    document r a = document @b r (a <> [desc])
        where desc = Node "Summary" (Detail $ symbolVal' @s)

-- | Vault documentation
instance HasDocumentApi b => HasDocumentApi (Vault :> b) where
    document r a = document @b r (a <> [desc])
        where desc = Node "Vault" (Detail "True")

-- | Basic authentication documentation
instance (HasDocumentApi b, KnownSymbol realm, Typeable a) => HasDocumentApi (BasicAuth (realm :: Symbol) a :> b) where
    document r a = document @b r (a <> [formatted])
        where formatted = Node "Basic Authentication" $
                               Details [ Node "Realm" (Detail realm)
                                       , Node "UserData" (Detail userData)
                                       ]
              realm = symbolVal' @realm
              userData = typeText @a

-- | Authentication documentation
instance (HasDocumentApi b, KnownSymbol token) => HasDocumentApi (AuthProtect (token :: Symbol) :> b) where
    document r a = document @b r (a <> [formatted])
        where formatted = Node "Authentication" (Detail authDoc)
              authDoc = symbolVal' @token

-- | Request header documentation
instance (HasDocumentApi b, KnownSymbol ct, Typeable typ) => HasDocumentApi (Header' m (ct :: Symbol) typ :> b) where
    document r a = document @b r (a <> [formatted])
        where formatted = Node "RequestHeaders" $
                               Details [ Node "Name" (Detail $ symbolVal' @ct)
                                       , Node "ContentType" (Detail $ typeText @typ)
                                       ]

-- | Query flag documentation
instance (HasDocumentApi b, KnownSymbol param) => HasDocumentApi (QueryFlag (param :: Symbol) :> b) where
    document r a = document @b r (a <> [formatted])
        where formatted = Node "QueryFlag" $
                                Details [ Node "Param" (Detail $ symbolVal' @param) ]

-- | Query param documentation
instance (HasDocumentApi b, KnownSymbol param, Typeable typ) => HasDocumentApi (QueryParam' m (param :: Symbol) typ :> b) where
    document r a = document @b r (a <> [formatted])
        where formatted = Node "QueryParam" $
                                Details [ Node "Param" (Detail $ symbolVal' @param)
                                        , Node "ContentType" (Detail $ typeText @typ)
                                        ]

-- | Query params documentation
instance (HasDocumentApi b, KnownSymbol param, Typeable typ) => HasDocumentApi (QueryParams (param :: Symbol) typ :> b) where
    document r a = document @b r (a <> [formatted])
        where formatted = Node "QueryParams" $
                                Details [ Node "Param" (Detail $ symbolVal' @param)
                                        , Node "ContentType" (Detail $ typeText @typ)
                                        ]

-- | Request body documentation
instance (HasDocumentApi b, Typeable ct, Typeable typ) => HasDocumentApi (ReqBody' m ct typ :> b) where
    document r a = document @b r (a <> [formatted])
        where formatted = Node "RequestBody" $
                                Details [ Node "Format" (Detail $ typeText @ct)
                                        , Node "ContentType" (Detail $ typeText @typ)
                                        ]

-- | Stream body documentation
instance (HasDocumentApi b, Typeable ct, Typeable typ) => HasDocumentApi (StreamBody' m ct typ :> b) where
    document r a = document @b r (a <> [formatted])
        where formatted = Node "StreamBody" $
                                Details [ Node "Format" (Detail $ typeText @ct)
                                        , Node "ContentType" (Detail $ typeText @typ)
                                        ]

-- | Response documentation
--   Terminates here as responses are last parts of api endpoints
--   Note that request type information (GET, POST etc...) is contained here
instance (Typeable m, Typeable ct, Typeable typ) => HasDocumentApi (Verb m s ct typ) where
    document r a = Node r $
                        Details (a <> [ requestType
                                      , response
                                      ])
        where requestType = Node "RequestType" (Detail $ typeText @m)
              response = Node "Response" $
                              Details [ Node "Format" (Detail $ typeText @ct)
                                      , Node "ContentType" (Detail $ typeText @typ)
                                      ]

-- | Internal Helper utilities
typeText :: forall a. (Typeable a) => Text
typeText = pack . show . typeRep $ Proxy @a

symbolVal' :: forall n. KnownSymbol n => Text
symbolVal' = pack . symbolVal $ Proxy @n