{-|
Module      : $Header$
Description : Interpolation core module
Copyright   : (c) Justus Adam, 2016
License     : BSD3
Maintainer  : dev@justus.science
Stability   : experimental
Portability : POSIX

Please refer to the documentation at https://marvin.readthedocs.io/en/latest/interpolation.html for examples and explanations on how to use this library.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-}
module Marvin.Interpolate
  ( is
  , iq
  -- * Internals/extension points
  , interpolateInto
  ) where


import           Control.Monad
import           Control.Monad.State                 as S
import           Data.Either
import           Data.List                           (intercalate)
import           Data.Monoid
#if __GLASGOW_HASKELL__ < 704
import           Language.Haskell.Meta.Parse.Careful
#else
import           Language.Haskell.Meta.Parse
#endif
import           Language.Haskell.TH
import           Language.Haskell.TH.Quote
import           Text.Parsec
import           Util


type Parsed = [Either String String]


escapeChar :: Char
escapeChar = '~'

type ParseM = Parsec String Int


parser :: ParseM Parsed
parser = manyTill (parseInterpolation <|> parseString) eof

parseString :: ParseM (Either String String)
parseString = do
    chunk <- many $ satisfy (/= '#')
    fmap (Right . (chunk ++)) $ (eof >> return "") <|> (lookAhead (try (char '#' >> anyChar)) >>= endOrEscape) <|> fmap return anyChar
  where
    endOrEscape :: Char -> ParseM String
    endOrEscape '{' = return ""
    endOrEscape '#' = count 2 anyChar >> return "#"
    endOrEscape ']' = count 2 anyChar >> return "]"
    endOrEscape _ = fail ""


parseInterpolation :: ParseM (Either String String)
parseInterpolation = try (string "#{") >> (Left <$> parseExpr)
  where
    parseExpr = do
        chunk <- many $ noneOf ['}', '"', '\'', '{']
        fmap (chunk ++) $ (eof >> error "eof in interpolation") <|> (anyChar >>= continue)

    continue :: Char -> ParseM String
    continue '{' = modifyState succ >> fmap ('{':) parseExpr
    continue '}' = do
        s <- getState
        if s == 0
          then return ""
          else ('}':) <$> (modifyState succ >> parseExpr)
    continue '\"' = ('"':) <$> parseStr
    continue '\'' = parseChar <|> (('\'':) <$> parseExpr)
      
    parseChar = do
        char '\''
        inner <- ((:) <$> char '\\' <*> fmap return anyChar) <|> fmap return anyChar
        char '\''
        return $ '\'':inner ++ "'"

    parseStr = do
        chunk <- many $ noneOf ['"', '\\']
        fmap (chunk ++) $ (eof >>= fail "eof in string literal") 
                          <|> (anyChar >>= continueStr)
      where 
        continueStr '"' = ('"':) <$> parseExpr
        continueStr '\\' = do
            escaped <- anyChar
            (\a -> '\\':escaped:a) <$> parseStr


evalExprs :: Parsed -> [Either Exp String]
evalExprs l = evalState (mapM stitch l) decls
  where
    strDecls = lefts l
    decls = case partitionEithers $ map parseExp strDecls of
                ([], d) -> d
                (errs, _) -> error $ intercalate "\n" errs

    stitch :: Either a b -> S.State [c] (Either c b)
    stitch (Right str) = return $ Right str
    stitch (Left _) = do
        (name:rest) <- get
        put rest
        return $ Left name


-- | Common core of all interpolators.
--
-- @interpolateInto exp str@ parses @str@ as the interpolated string and returns an 'Exp' which looks like 
-- 
-- @
--    "str" \`mappend\` exp1 \`mappend\` "str" \`mappend\` exp2 \`mappend\` "str"
-- @ 
-- 
-- where @exp1@ and @exp2@ are the interpolated expressions with @exp@ prepended.
-- The intended use of @exp@ is to unifomly convert the interpolated expressions into a desired string type.
-- Typically @exp@ will be something like @('VarE' \'convert)@ were @convert@ is some member function of a conversion type class.
interpolateInto :: Exp -> String -> Exp
interpolateInto converter str =
    foldl f (LitE (StringL "")) interleaved

  where
    parsed = either (error . show) id $ runParser parser 0 "inline" str
    interleaved = evalExprs parsed

    f expr bit = AppE (VarE 'mappend) expr `AppE` bitExpr
      where
        bitExpr = case bit of
                      Right str -> LitE (StringL str)
                      Left expr2 -> AppE converter expr2

-- | __i__nterpolate __s__plice 
--
-- Template Haskell splice function, used like @$(is "my str #{expr}")@
-- 
-- Performs no conversion on interpolated expressions like @expr@.
is :: String -> Q Exp
is = return . interpolateInto (VarE 'id)


-- | __i__nterpolate __q__uoter
--
-- QuasiQuoter, used like @[iq|my str #{expr}|]@
--
-- Performs no conversion on interpolated expressions like @expr@.
iq :: QuasiQuoter
iq = mqq { quoteExp = is }