{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeOperators         #-}

module Servant.PureScript (
  HasBridge
, languageBridge
, defaultBridge
, defaultBridgeProxy
, DefaultBridge
, writeAPIModule
, writeAPIModuleWithSettings
, Settings (..)
, apiModuleName
, readerParams
, standardImports
, defaultSettings
, addReaderParam
, jsonParseUrlPiece
, jsonToUrlPiece
, jsonParseHeader
, jsonToHeader
) where


import           Control.Lens
import           Control.Monad                 (when)
import           Data.Aeson
import           Data.Bifunctor
import           Data.ByteString               (ByteString)
import qualified Data.ByteString.Lazy          as BS
import           Data.Monoid
import           Data.Proxy
import           Data.Text                     (Text)
import qualified Data.Text                     as T
import qualified Data.Text.Encoding            as T
import qualified Data.Text.IO                          as T
import           Language.PureScript.Bridge
import           Network.HTTP.Types            (urlDecode, urlEncode)
import           Servant.Foreign
import           Servant.PureScript.CodeGen
import           Servant.PureScript.Internal
import qualified Servant.PureScript.Subscriber as SubGen
import qualified Servant.PureScript.MakeRequests as MakeRequests
import           System.Directory
import           System.FilePath
import           System.IO                     (IOMode (..), withFile)
import           Text.PrettyPrint.Mainland     (hPutDocLn, Doc)

-- | Standard entry point - just create a purescript module with default settings
--   for accessing the servant API.
writeAPIModule :: forall bridgeSelector api.
  ( HasForeign (PureScript bridgeSelector) PSType api
  , GenerateList PSType (Foreign PSType api)
  , HasBridge bridgeSelector
  ) => FilePath -> Proxy bridgeSelector -> Proxy api -> IO ()
writeAPIModule = writeAPIModuleWithSettings defaultSettings

writeAPIModuleWithSettings :: forall bridgeSelector api.
  ( HasForeign (PureScript bridgeSelector) PSType api
  , GenerateList PSType (Foreign PSType api)
  , HasBridge bridgeSelector
  ) => Settings -> FilePath -> Proxy bridgeSelector -> Proxy api -> IO ()
writeAPIModuleWithSettings opts root pBr pAPI = do
    writeModule (opts ^. apiModuleName) genModule
    when (opts ^. generateSubscriberAPI) $ do
      writeModule (opts ^. apiModuleName <> ".Subscriber") SubGen.genModule
      writeModule (opts ^. apiModuleName <> ".MakeRequests") MakeRequests.genModule
    T.putStrLn "\nSuccessfully created your servant API purescript functions!"
    T.putStrLn "Please make sure you have purescript-servant-support version 5.0.0 or above installed:\n"
    T.putStrLn "  bower i --save purescript-servant-support\n"
  where
    apiList  = apiToList pAPI pBr

    writeModule :: Text -> (Settings -> [Req PSType] -> Doc) -> IO ()
    writeModule mName genModule' = let
        fileName = (joinPath . map T.unpack . T.splitOn "." $ mName) <> ".purs"
        mPath = root </> fileName
        mDir = takeDirectory mPath
        contents = genModule' opts apiList
      in do
        unlessM (doesDirectoryExist mDir) $ createDirectoryIfMissing True mDir
        withFile mPath WriteMode $ flip hPutDocLn contents


-- | Use this function for implementing 'parseUrlPiece' in your FromHttpApiData instances
--   in order to be compatible with the generated PS code.
--
-- >  
-- > instance ToHttpApiData MyDataType where
-- >   toUrlPiece = jsonToUrlPiece
-- >   toHeader   = jsonToHeader
-- >
-- > instance FromHttpApiData MyDataType where
-- >   parseUrlPiece = jsonParseUrlPiece
-- >   parseHeader   = jsonParseHeader
-- >
--
jsonParseUrlPiece :: FromJSON a => Text -> Either Text a
jsonParseUrlPiece = jsonParseHeader . T.encodeUtf8

-- | Use this function for implementing 'toUrlPiece' in your ToHttpApiData instances
--   in order to be compatible with the generated PS code.
jsonToUrlPiece :: ToJSON a => a -> Text
jsonToUrlPiece = T.decodeUtf8 . jsonToHeader

-- | Use this function for implementing 'parseHeader' in your FromHttpApiData instances
--   in order to be compatible with the generated PS code.
jsonParseHeader :: FromJSON a => ByteString -> Either Text a
jsonParseHeader = first T.pack . eitherDecodeStrict

-- | Use this function for implementing 'toHeader' in your ToHttpApiData instances
--   in order to be compatible with the generated PS code.
jsonToHeader :: ToJSON a => a -> ByteString
jsonToHeader = BS.toStrict . encode