module Data.EasyTpl.Render where

import Data.Aeson (Value(..))
import qualified Data.Aeson.Encode as JE
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as LTB
import qualified Data.Text.Encoding as TE
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as H
import qualified Data.Vector as V

import Data.Attoparsec.ByteString.Char8
import qualified Data.Attoparsec.Text as AT

#ifdef WITH_REGEX
import Text.Regex (Regex)
import qualified Text.Regex as RE
#endif

import Data.Fixed (mod')
import Prelude hiding (takeWhile)

import Data.EasyTpl.Types
import Data.EasyTpl.Parser

-- | Internal render template using hash with variables.
renderTemplate' :: Template -> HashMap Text Value -> ByteString
renderTemplate' tpl env = renderTemplate tpl $ Object env

-- | Internal render template using value as variables.
renderTemplate :: Template -> Value -> ByteString
renderTemplate (Template tokens) env = BS.concat $ map (flip renderToken env) tokens

-- | Internal render token.
renderToken :: TemplateToken -> Value -> ByteString

renderToken (ContentToken text) _ = text

renderToken (LiteralToken expr) env = getString $ toString $ evalExpression expr env
  where
    getString (String text) = TE.encodeUtf8 text
    getString _ = ""

renderToken (ControlToken ctl tpl) env = renderControl ctl tpl env

renderControl :: Control -> Template -> Value -> ByteString

renderControl (Condition expr) tpl env@(Object _) = applyCondition $ toBoolean $ evalExpression expr env
  where
    applyCondition :: Value -> ByteString
    applyCondition (Bool True) = renderTemplate tpl env
    applyCondition _ = ""

renderControl (Iteration (keyVar, valVar) expr) tpl env'@(Object henv) = BS.concat $ applyIteration $ evalExpression expr env'
  where
    applyIteration :: Value -> [ByteString]
    applyIteration (Array list) = V.toList $ V.imap applyIndex list
    applyIteration (Object hash) = map applyField $ H.toList hash
    applyIteration _ = []

    addLocal :: Text -> Value -> HashMap Text Value -> HashMap Text Value
    addLocal var val env = if T.null var then env else H.insert var val env

    applyElement :: Value -> Value -> ByteString
    applyElement key val = renderTemplate' tpl $ addLocal keyVar key $ addLocal valVar val henv
    
    applyIndex :: Int -> Value -> ByteString
    applyIndex key val = applyElement (Number (fromIntegral key)) val
    
    applyField :: (Text, Value) -> ByteString
    applyField (key, val) = applyElement (String key) val

renderControl _ _ _ = ""

-- | Internal evaluate expression.
evalExpression :: Expression -> Value -> Value

evalExpression (Constant val) _ = val

evalExpression (Variable var) (Object henv) = maybe Null id $ H.lookup var henv
evalExpression (Variable _) _ = Null

evalExpression (Range from' to' step') env = genRange (toNumber $ evalExpression from' env)
                                                      (toNumber $ evalExpression to' env)
                                                      (toNumber $ evalExpression step' env)
  where
    genRange :: Value -> Value -> Value -> Value
    genRange (Number from) (Number to) (Number step) =
      Array $ V.fromList $ range from to step
    genRange _ _ _ = Null
    
    range from to step | from > to = []
                       | otherwise = Number from : range (from + step) to step

evalExpression (UnaryOperation op' a') env = unary op' (evalExpression a' env)
  where
    unary :: UnaryOperator -> Value -> Value
    
    unary GetLength = getLength
    unary Stringify = stringify
    unary Evaluate = either (String . T.pack . ("Error: " ++)) (flip evalExpression env) . parseOnly parseExpression . toByteString
    unary LogicNot = booleanOp not
    unary Negate = numericOp (0 -)
    unary ToNumber = numericOp id
    unary NotNull = Bool . notNull

    notNull :: Value -> Bool
    notNull Null = False
    notNull _ = True

    booleanOp :: (Bool -> Bool) -> Value -> Value
    booleanOp op a = case toBoolean a of
                      Bool b -> Bool $ op b
                      _ -> Null
    
    numericOp op a = case toNumber a of
                      Number n -> Number $ op n
                      _ -> Null

    toByteString a = case toString a of
                      String s -> TE.encodeUtf8 s
                      _ -> ""
    
#ifdef WITH_REGEX
evalExpression (BinaryOperation op' a' (Regexp re ml cs)) env = apply op' $ toString $ evalExpression a' env
  where
    apply RegexTest  (String s) = Bool $ maybe False (\_ -> True) $ match s
    apply RegexMatch (String s) = maybe Null (Array . V.fromList . map (String . T.pack)) $ match s
    apply RegexSplit (String s) = Array $ V.fromList $ map String $ split s
    apply _ _ = Null
    
    match :: Text -> Maybe [String]
    match = RE.matchRegex regex . T.unpack

    split :: Text -> [Text]
    split = map T.pack . RE.splitRegex regex . T.unpack
    
    regex :: Regex
    regex = RE.mkRegexWithOpts (BSC.unpack re) ml cs

-- regexps is non-evaluatable
evalExpression (Regexp _ _ _) _ = Null
#endif

evalExpression (BinaryOperation op' a' b') env = binary op' (evalExpression a' env) (evalExpression b' env)
  where
    binary :: BinaryOperator -> Value -> Value -> Value
    
    binary Equal a = Bool . (a ==)
    binary NotEqual a = Bool . (a /=)
    
    binary Append (String a) = applyToString $ String . T.append a
    binary Multiply (String a) = applyToInteger $ String . flip T.replicate a
    binary IntDivide (String a) = applyToString $ Array . V.fromList . map String . flip T.splitOn a
    binary LessThan (String a) = applyToString $ Bool . (a <)
    binary GreatThan (String a) = applyToString $ Bool . (a >)
    binary LessEqual (String a) = applyToString $ Bool . (a <=)
    binary GreatEqual (String a) = applyToString $ Bool . (a >=)
    
    binary GetField (String s) = applyToInteger (\i -> if i >= 0 && i < T.length s then String $ T.singleton $ T.index s i else Null)
    
    binary LessThan (Number a) = applyToNumber $ Bool . (a <)
    binary GreatThan (Number a) = applyToNumber $ Bool . (a >)
    binary LessEqual (Number a) = applyToNumber $ Bool . (a <=)
    binary GreatEqual (Number a) = applyToNumber $ Bool . (a >=)
    
    binary GetField (Array a) = applyToInteger (maybe Null id . (V.!?) a)
    binary GetField (Object a) = applyToString (maybe Null id . flip H.lookup a)
    
    binary op a = binary' op a
    
    binary' LogicOr  = booleanOp (||)
    binary' LogicAnd = booleanOp (&&)
    
    binary' Substract = numericOp (-)
    binary' Append = numericOp (+)
    binary' Multiply = numericOp (*)
    binary' Divide = numericOp (/)
    binary' IntDivide = numericOp' (/)
    binary' Module = numericOp mod'
    --binary' Power = numericOp pow
    binary' _ = \_ _ -> Null
    
    numericOp op a b = case (toNumber a, toNumber b) of
                        (Number na, Number nb) -> Number $ op na nb
                        _ -> Null

    numericOp' op a b = case (toNumber a, toNumber b) of
                         (Number na, Number nb) -> Number $ fromIntegral ((round $ op na nb) :: Integer)
                         _ -> Null
    
    booleanOp op a b = case (toBoolean a, toBoolean b) of
                        (Bool ba, Bool bb) -> Bool $ op ba bb
                        _ -> Null
    
    applyToString fn v = case toString v of
                          String s -> fn s
                          _ -> Null

    applyToNumber fn v = case toNumber v of
                          Number n -> fn $ n
                          _ -> Null
    
    applyToInteger fn = applyToNumber (fn . round)

-- | Generic get length.
getLength :: Value -> Value
getLength (String text) = Number $ fromIntegral $ T.length text
getLength (Array list) = Number $ fromIntegral $ V.length list
getLength (Object hash) = Number $ fromIntegral $ H.size hash
getLength _ = Null

-- | Cast value to boolean.
toBoolean :: Value -> Value
toBoolean bool@(Bool _) = bool
toBoolean (Number val) = Bool $ val /= I 0
toBoolean (String text) = Bool $ not $ T.null text
toBoolean Null = Bool False
toBoolean (Object hash) = Bool $ not $ H.null hash
toBoolean (Array list)  = Bool $ not $ V.null list

-- | Cast any value to number.
--   Returns null if non-castable.
toNumber :: Value -> Value
toNumber val@(Number _) = val
toNumber (Bool bool) = Number $ if bool then 1 else 0
toNumber (String text) = getNumber' $ AT.parse AT.number text
  where
    getNumber' (AT.Done _ val) = Number val
    getNumber' _ = Null
toNumber _ = Null

-- | Cast any value to string.
toString :: Value -> Value
toString text@(String _) = text
toString (Number val) = String $ T.pack $ show val
toString Null = String "[null]"
toString (Bool _) = String "[bool]"
toString (Object _) = String "[object]"
toString (Array _) = String "[array]"

-- | Stringify value to JSON.
--   Stringify any value using JSON format by AESON encode.
stringify :: Value -> Value
stringify = String . LT.toStrict . LTB.toLazyText . JE.fromValue