{-# LANGUAGE DataKinds       #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators   #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where

import qualified Network.Wai.Handler.Warp as Warp
import Network.Wai.Logger (withStdoutLogger)
import qualified Network.Wai.Handler.CGI as CGI
import Network.Wai.Middleware.Timeout (timeout)
import Data.Aeson
import Data.Aeson.TH
import Data.Maybe (fromMaybe)
import Network.Wai
import Servant
import Text.TeXMath
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
import Text.XML.Light (ppElement)
import System.Environment (getProgName)
import Options.Applicative
import Safe (readMay)

-- This is the data to be supplied by the JSON payload
-- of requests.
data Params = Params
  { text           :: Text
  , from           :: Format
  , to             :: Format
  , display        :: Bool
  } deriving (Show)

data Format =
  TeX | MathML | Eqn | OMML | Typst | StarMath
  deriving (Show, Ord, Eq)

instance FromJSON Format where
   parseJSON (String s) =
               case T.toLower s of
                 "tex" -> pure TeX
                 "mathml" -> pure MathML
                 "eqn" -> pure Eqn
                 "typst" -> pure Typst
                 "omml" -> pure OMML
                 "starmath" -> pure StarMath
                 _ -> fail $ "Unknown format " <> T.unpack s
   parseJSON _ = fail "Expecting string format"

instance ToJSON Format where
   toJSON x = String $ T.toLower $ T.pack $ show x

instance FromHttpApiData Format where
   parseQueryParam t =
               case T.toLower t of
                 "tex" -> pure TeX
                 "mathml" -> pure MathML
                 "eqn" -> pure Eqn
                 "typst" -> pure Typst
                 "omml" -> pure OMML
                 "starmath" -> pure StarMath
                 _ -> Left $ "Unknown format " <> t

-- Automatically derive code to convert to/from JSON.
$(deriveJSON defaultOptions ''Params)
data Opts = Opts
  { port      :: Int }

optsSpec :: Parser Opts
optsSpec = Opts
      <$> option (maybeReader readMay)
          ( long "port"
         <> short 'p'
         <> metavar "NUMBER"
         <> showDefault
         <> value 8080
         <> help "Port on which to run the server" )

main :: IO ()
main = do
  prg <- getProgName
  case prg of
    "texmath-server.cgi" -> CGI.run (timeout 2 app)
    _ -> do
      let options = info (optsSpec <**> helper)
            ( fullDesc
           <> progDesc "Run a server for texmath"
           <> header "texmath-server - an HTTP server for texmath" )
      opts <- execParser options
      putStrLn $ "Starting server on port " <> show (port opts)
      withStdoutLogger $ \logger -> do
        let settings = Warp.setPort (port opts) $ Warp.setLogger logger Warp.defaultSettings
        Warp.runSettings settings app

-- This is the API.  The root endpoint takes a request body
-- consisting of a JSON-encoded Params structure and responds to
-- Get requests with either plain text or JSON, depending on the
-- Accept header. Alternatively, the "/batch" endpoint may be
-- used, accepting a JSON-encoded array of Params and returning
-- an array of results.
type API =
  ReqBody '[JSON] Params :> Post '[PlainText, JSON] Text
  :<|>
  QueryParam "text" Text :> QueryParam "from" Format :> QueryParam "to" Format :> QueryFlag "display" :> Get '[PlainText] Text
  :<|>
  "batch" :> ReqBody '[JSON] [Params] :> Post '[JSON] [Text]

app :: Application
app = serve api server

api :: Proxy API
api = Proxy

server :: Server API
server = convert
    :<|> (\text' from' to' display' ->
             convert Params{ text = fromMaybe "" text',
                             from = fromMaybe TeX from',
                             to = fromMaybe MathML to',
                             display = display' })
    :<|> mapM convert
 where
  convert params
    = let dt = if display params
                  then DisplayBlock
                  else DisplayInline
          txt = text params
          reader = case from params of
                     OMML -> readOMML
                     TeX -> readTeX
                     MathML -> readMathML
                     Eqn -> \_ -> Left "eqn reader not implemented"
                     Typst -> \_ -> Left "typst reader not implemented"
                     StarMath -> \_ -> Left "StarMath reader not implemented"
          writer = case to params of
                     Eqn -> writeEqn dt
                     Typst -> writeTypst dt
                     OMML -> T.pack . ppElement . writeOMML dt
                     TeX -> writeTeX
                     MathML -> T.pack . ppElement . writeMathML dt
                     StarMath -> writeStarMath dt
      in  handleErr $ writer <$> reader txt

  handleErr (Right t) = return t
  handleErr (Left err) = throwError $
    err500 { errBody = TLE.encodeUtf8 $ TL.fromStrict err }
