{-# LANGUAGE DataKinds, GADTs, RankNTypes, TypeApplications #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RecordWildCards #-}

module Language.Coformat.Formatter where

import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Text as T
import Control.Monad.Except.CoHas
import System.Command hiding(cmd)
import System.Exit

import Language.Coformat.Descr
import Language.Coformat.Formatter.Failure
import Language.Coformat.Util

data OptsDescription stage = OptsDescription
  { knownOpts :: [ConfigItemT stage]
  , baseStyles :: [T.Text]
  }

data OptsSource opts
  = StaticOpts opts
  | OptsFromFile FilePath (LBS.ByteString -> Either String opts)
  | OptsFromCmd CmdArgs (BS.ByteString -> Either String opts)

parseOpts :: (MonadIO m, MonadError String m) => String -> OptsSource opts -> m opts
parseOpts _     (StaticOpts d) = pure d
parseOpts _     (OptsFromFile path parser) = parser <$> liftIO (LBS.readFile path) >>= liftEither
parseOpts exec  (OptsFromCmd args parser) = convert (show @Failure) (runCommand exec args) >>= liftEither . parser

data FormatterInfo = FormatterInfo
  { execName :: String

  , formatterOpts :: OptsSource (OptsDescription 'Supported)
  , hardcodedOpts :: [ConfigItemT 'Value]
  , defaultStyleOpts :: T.Text -> [ConfigItemT 'Supported] -> [ConfigItemT 'Value] -> OptsSource [ConfigItemT 'Value]

  , formatFile :: T.Text -> [ConfigItemT 'Value] -> FilePath -> CmdArgs

  , serializeOptions :: T.Text -> [ConfigItemT 'Value] -> BS.ByteString
  }

newtype CmdArgs = CmdArgs
  { args :: [BS.ByteString]
  } deriving (Show)

runCommand :: (MonadError err m, CoHas UnexpectedFailure err, CoHas ExpectedFailure err, MonadIO m)
           => String -> CmdArgs -> m BS.ByteString
runCommand exec (CmdArgs args) = do
  (ec, stdout, stderr) <- liftIO $ command [] exec $ BS.unpack <$> args
  case ec of ExitSuccess -> pure $ BS.pack $ fromStdout stdout
             ExitFailure n | n == cfCrashRetCode -> throwError $ FormatterSegfaulted $ T.pack $ fromStderr stderr
                           | otherwise -> throwError $ FormatterFailure n $ T.pack $ fromStderr stderr
  where
    cfCrashRetCode = -8

data Formatter where
  Formatter :: forall resumeObj.
               { formatterInfo :: FormatterInfo
               , parseResumeObject :: BS.ByteString -> Either String resumeObj
               , parseResumeOptions :: [ConfigItemT 'Supported] -> resumeObj -> Either String (T.Text, [ConfigItemT 'Value])
               } -> Formatter