module Sos.Template
  ( RawTemplate
  , Template
  , parseTemplate
  , instantiateTemplate
  ) where

import Sos.Exception
import Sos.Job (ShellCommand)
import Sos.Utils

import Control.Applicative
import Control.Monad.Catch (MonadThrow, throwM)
import Data.ByteString (ByteString)
import Text.ParserCombinators.ReadP

import qualified Data.Text.Encoding as Text
import qualified Data.Text.Lazy as LText
import qualified Data.Text.Lazy.Builder as LText


-- | A 'RawTemplate' represents a shell command, possibly containing capture
-- groups, e.g. "ghc \0"
type RawTemplate = ByteString

-- A 'Template' is a parsed 'RawTemplate' that replaces all capture groups with
-- Lefts.
--
-- For example, the raw template
--
--    "gcc -c \1.c -o \1.c"
--
-- will become
--
--    [Right "gcc -c ", Left 1, Right ".c -o ", Left 1, Right ".c"]
--
type Template = [Either Int ByteString]


parseTemplate :: MonadThrow m => RawTemplate -> m Template
parseTemplate raw_template =
  case readP_to_S parser (unpackBS raw_template) of
    [(template, "")] -> pure template
    _ -> throwM (SosCommandParseException raw_template)
 where
  parser :: ReadP Template
  parser = some (capturePart <|||> textPart) <* eof
   where
    capturePart :: ReadP Int
    capturePart = read <$> (char '\\' *> munch1 digit)
     where
      digit :: Char -> Bool
      digit c = c >= '0' && c <= '9'

    textPart :: ReadP ByteString
    textPart = packBS <$> munch1 (/= '\\')

-- Instantiate a template with a list of captured variables, per their indices.
--
-- For example,
--
--    instantiateTemplate ["ONE", "TWO"] [Right "foo", Left 0, Right "bar", Left 1] == "fooONEbarTWO"
--
instantiateTemplate
  :: forall m. MonadThrow m => [ByteString] -> Template -> m ShellCommand
instantiateTemplate vars0 template0 = go 0 vars0 template0
 where
  go :: Int -> [ByteString] -> Template -> m ShellCommand
  go _ [] template =
    case flattenTemplate template of
      Left n ->
        let err = "uninstantiated template variable: \\" ++ show n
        in throwM (SosCommandApplyException template0 vars0 err)
      Right x -> pure x
  go n (t:ts) template = go (n+1) ts (map f template)
   where
    f :: Either Int ByteString -> Either Int ByteString
    f (Left n')
        | n == n'   = Right t
        | otherwise = Left n'
    f x = x

-- Attempt to flatten a list of Rights to a single string.
flattenTemplate :: Template -> Either Int ShellCommand
flattenTemplate = go mempty
 where
  go :: LText.Builder -> Template -> Either Int ShellCommand
  go !acc [] = Right (LText.unpack (LText.toLazyText acc))
  go !acc (x:xs) =
    case x of
      Right s ->
        let acc' = acc <> LText.fromText (Text.decodeUtf8 s)
        in go acc' xs
      Left n -> Left n