module Heist.Compiled.Internal where
import           Blaze.ByteString.Builder
import           Blaze.ByteString.Builder.Char.Utf8
import           Control.Arrow
import           Control.Exception
import           Control.Monad
import           Control.Monad.RWS.Strict
import           Control.Monad.State.Strict
import qualified Data.Attoparsec.Text               as AP
import           Data.ByteString                    (ByteString)
import           Data.DList                         (DList)
import qualified Data.DList                         as DL
import qualified Data.HashMap.Strict                as H
import qualified Data.HashSet                       as S
import qualified Data.HeterogeneousEnvironment      as HE
import           Data.Map.Syntax
import           Data.Maybe
import           Data.Text                          (Text)
import qualified Data.Text                          as T
import qualified Data.Text.Encoding                 as T
import qualified Data.Vector                        as V
import           Text.Printf
import qualified Text.XmlHtml                       as X
import qualified Text.XmlHtml.HTML.Meta             as X
#if !MIN_VERSION_base(4,8,0)
import           Data.Foldable                      (Foldable)
#endif
import qualified Data.Foldable                      as Foldable
import           Heist.Common
import           Heist.Internal.Types.HeistState
type Splice n = HeistT n IO (DList (Chunk n))
runChildren :: Monad n => Splice n
runChildren = runNodeList . X.childNodes =<< getParamNode
renderFragment :: Markup -> [X.Node] -> Builder
renderFragment markup ns =
    case markup of
      Html -> X.renderHtmlFragment X.UTF8 ns
      Xml  -> X.renderXmlFragment X.UTF8 ns
pureTextChunk :: Text -> Chunk n
pureTextChunk t = Pure $ T.encodeUtf8 t
yieldPure :: Builder -> DList (Chunk n)
yieldPure = DL.singleton . Pure . toByteString
yieldRuntime :: RuntimeSplice n Builder -> DList (Chunk n)
yieldRuntime = DL.singleton . RuntimeHtml
yieldRuntimeEffect :: Monad n => RuntimeSplice n () -> DList (Chunk n)
yieldRuntimeEffect = DL.singleton . RuntimeAction
yieldPureText :: Text -> DList (Chunk n)
yieldPureText = DL.singleton . pureTextChunk
yieldRuntimeText :: Monad n => RuntimeSplice n Text -> DList (Chunk n)
yieldRuntimeText = yieldRuntime .  liftM fromText
runNodeList :: Monad n => [X.Node] -> Splice n
runNodeList = mapSplices runNode
runDocumentFile :: Monad n
                => TPath
                -> DocumentFile
                -> Splice n
runDocumentFile tpath df = do
    let markup = case dfDoc df of
                   X.XmlDocument _ _ _ -> Xml
                   X.HtmlDocument _ _ _ -> Html
    modifyHS (\hs -> hs { _curMarkup = markup })
    let inDoctype = X.docType $ dfDoc df
    addDoctype $ maybeToList inDoctype
    modifyHS (setCurTemplateFile curPath .  setCurContext tpath)
    res <- runNodeList nodes
    dt <- getsHS (listToMaybe . _doctypes)
    let enc = X.docEncoding $ dfDoc df
    return $! (yieldPure (X.renderDocType enc dt) `mappend` res)
  where
    curPath     = dfFile df
    nodes       = X.docContent $! dfDoc df
compileTemplate
    :: Monad n
    => TPath
    -> DocumentFile
    -> HeistT n IO [Chunk n]
compileTemplate tpath df = do
    !chunks <- runDocumentFile tpath df
    return $! consolidate chunks
compileTemplates
    :: Monad n
    => (TPath -> Bool)
    -> HeistState n
    -> IO (Either [String] (HeistState n))
compileTemplates f hs = do
    (tmap, hs') <- runHeistT (compileTemplates' f) (X.TextNode "") hs
    let pre = _splicePrefix hs'
    let canError = _errorNotBound hs'
    let errs = _spliceErrors hs'
    let nsErr = if not (T.null pre) && (_numNamespacedTags hs' == 0)
                  then Left [noNamespaceSplicesMsg $ T.unpack pre]
                  else Right ()
    return $ if canError
               then case errs of
                     [] -> nsErr >>
                           (Right $! hs { _compiledTemplateMap = tmap })
                     es -> Left $ either (++) (const id) nsErr $
                           map (T.unpack . spliceErrorText) es
               else nsErr >> (Right $! hs { _compiledTemplateMap = tmap
                                          , _spliceErrors = errs
                                          })
noNamespaceSplicesMsg :: String -> String
noNamespaceSplicesMsg pre = unwords
    [ printf "You are using a namespace of '%s', but you don't have any" ns
    , printf "tags starting with '%s'.  If you have not defined any" pre
    , "splices, then change your namespace to the empty string to get rid"
    , "of this message."
    ]
  where
    ns = reverse $ drop 1 $ reverse pre
compileTemplates'
    :: Monad n
    => (TPath -> Bool)
    -> HeistT n IO (H.HashMap TPath ([Chunk n], MIMEType))
compileTemplates' f = do
    hs <- getHS
    let tpathDocfiles :: [(TPath, DocumentFile)]
        tpathDocfiles = filter (f . fst)
                            (H.toList $ _templateMap hs)
    foldM runOne H.empty tpathDocfiles
  where
    runOne tmap (tpath, df) = do
        modifyHS (\hs -> hs { _doctypes = []})
        !mHtml <- compileTemplate tpath df
        return $! H.insert tpath (mHtml, mimeType $! dfDoc df) tmap
consolidate :: (Monad n) => DList (Chunk n) -> [Chunk n]
consolidate = consolidateL . DL.toList
  where
    consolidateL []     = []
    consolidateL (y:ys) = boilDown [] $! go [] y ys
      where
        
        go soFar x [] = x : soFar
        go soFar (Pure a) ((Pure b) : xs) =
            go soFar (Pure $! a `mappend` b) xs
        go soFar (RuntimeHtml a) ((RuntimeHtml b) : xs) =
            go soFar (RuntimeHtml $! a `mappend` b) xs
        go soFar (RuntimeHtml a) ((RuntimeAction b) : xs) =
            go soFar (RuntimeHtml $! a >>= \x -> b >> return x) xs
        go soFar (RuntimeAction a) ((RuntimeHtml b) : xs) =
            go soFar (RuntimeHtml $! a >> b) xs
        go soFar (RuntimeAction a) ((RuntimeAction b) : xs) =
            go soFar (RuntimeAction $! a >> b) xs
        go soFar a (b : xs) = go (a : soFar) b xs
        
        boilDown soFar []              = soFar
        boilDown soFar ((Pure h) : xs) = boilDown ((Pure $! h) : soFar) xs
        boilDown soFar (x : xs) = boilDown (x : soFar) xs
codeGen :: Monad n => DList (Chunk n) -> RuntimeSplice n Builder
codeGen l = V.foldr mappend mempty $!
            V.map toAct $! V.fromList $! consolidate l
  where
    toAct !(RuntimeHtml !m)   = m
    toAct !(Pure !h)          = return $! fromByteString h
    toAct !(RuntimeAction !m) = m >> return mempty
lookupSplice :: Text -> HeistT n IO (Maybe (Splice n))
lookupSplice nm = do
    pre <- getsHS _splicePrefix
    res <- getsHS (H.lookup nm . _compiledSpliceMap)
    if isNothing res && T.isPrefixOf pre nm && not (T.null pre)
      then do
          tellSpliceError $ "No splice bound for " `mappend` nm
          return Nothing
      else return res
runNode :: Monad n => X.Node -> Splice n
runNode node = localParamNode (const node) $ do
    hs <- getHS
    let pre = _splicePrefix hs
    let hasPrefix = (T.isPrefixOf pre `fmap` X.tagName node) == Just True
    when (not (T.null pre) && hasPrefix) incNamespacedTags
    hs' <- getHS
    
    (res, hs'') <- liftIO $ catches (compileIO hs')
                     [ Handler (\(ex :: CompileException) -> throwIO ex)
                     , Handler (\(ex :: SomeException) -> handleError ex hs')]
    putHS hs''
    return res
  where
    localSplicePath =
        localHS (\hs -> hs {_splicePath = (_curContext hs,
                                           _curTemplateFile hs,
                                           X.elementTag node):
                                          (_splicePath hs)})
    compileIO hs = runHeistT compile node hs
    compile = do
        isStatic <- subtreeIsStatic node
        dl <- compile' isStatic
        liftIO $ evaluate $ DL.fromList $! consolidate dl
    compile' True = do
        markup <- getsHS _curMarkup
        return $! yieldPure $! renderFragment markup [parseAttrs node]
    compile' False = localSplicePath $ compileNode node
    handleError ex hs = do
        errs <- evalHeistT (do localSplicePath $ tellSpliceError $ T.pack $
                                 "Exception in splice compile: " ++ show ex
                               getsHS _spliceErrors) node hs
        throwIO $ CompileException ex errs
parseAttrs :: X.Node -> X.Node
parseAttrs (X.Element nm attrs ch) = newAttrs `seq` X.Element nm newAttrs ch
  where
    newAttrs = map parseAttr attrs
parseAttrs !n = n
parseAttr :: (Text, Text) -> (Text, Text)
parseAttr (k,v) = (k, T.concat $! map cvt ast)
  where
    !ast = case AP.feed (AP.parse attParser v) "" of
            (AP.Done _ res) -> res
            (AP.Fail _ _ _) -> []
            (AP.Partial _ ) -> []
    cvt (Literal x) = x
    cvt (Ident i) = T.concat ["${", i, "}"]
subtreeIsStatic :: X.Node -> HeistT n IO Bool
subtreeIsStatic (X.Element nm attrs ch) = do
    isNodeDynamic <- liftM isJust $ lookupSplice nm
    attrSplices <- getsHS _attrSpliceMap
    let hasSubstitutions (k,v) = hasAttributeSubstitutions v ||
                                 H.member k attrSplices
    if isNodeDynamic
      then return False
      else do
          let hasDynamicAttrs = any hasSubstitutions attrs
          if hasDynamicAttrs
            then return False
            else do
                staticSubtrees <- mapM subtreeIsStatic ch
                return $ and staticSubtrees
subtreeIsStatic _ = return True
hasAttributeSubstitutions :: Text -> Bool
hasAttributeSubstitutions txt = any isIdent ast
  where
    ast = case AP.feed (AP.parse attParser txt) "" of
            (AP.Done _ res) -> res
            (AP.Fail _ _ _) -> []
            (AP.Partial _ ) -> []
compileNode :: Monad n => X.Node -> Splice n
compileNode (X.Element nm attrs ch) = do
    msplice <- lookupSplice nm
    fromMaybe compileStaticElement msplice
  where
    tag0 = T.append "<" nm
    end = T.concat [ "</" , nm , ">"]
    
    compileStaticElement = do
        
        compiledAttrs <- runAttributes attrs
        childHtml <- runNodeList ch
        return $! if null (DL.toList childHtml) && nm `S.member` X.voidTags
          then DL.concat [ DL.singleton $! pureTextChunk $! tag0
                         , DL.concat compiledAttrs
                         , DL.singleton $! pureTextChunk " />"
                         ]
          else DL.concat [ DL.singleton $! pureTextChunk $! tag0
                         , DL.concat compiledAttrs
                         , DL.singleton $! pureTextChunk ">"
                         , childHtml
                         , DL.singleton $! pureTextChunk $! end
                         ]
compileNode _ = error "impossible"
parseAtt :: Monad n => (Text, Text) -> HeistT n IO (DList (Chunk n))
parseAtt (k,v) = do
    mas <- getsHS (H.lookup k . _attrSpliceMap)
    maybe doInline (return . doAttrSplice) mas
  where
    cvt (Literal x) = return $ yieldPureText x
    cvt (Ident x) =
        localParamNode (const $ X.Element x [] []) $ getAttributeSplice x
    
    doInline = do
        let ast = case AP.feed (AP.parse attParser v) "" of
                    (AP.Done _ res) -> res
                    (AP.Fail _ _ _) -> []
                    (AP.Partial _ ) -> []
        chunks <- mapM cvt ast
        let value = DL.concat chunks
        return $ attrToChunk k value
    
    doAttrSplice splice = DL.singleton $ RuntimeHtml $ do
        res <- splice v
        return $ mconcat $ map attrToBuilder res
parseAtt2 :: Monad n
          => (Text, Text)
          -> HeistT n IO (RuntimeSplice n [(Text, Text)])
parseAtt2 (k,v) = do
    mas <- getsHS (H.lookup k . _attrSpliceMap)
    maybe doInline (return . doAttrSplice) mas
  where
    cvt (Literal x) = return $ return x
    cvt (Ident x) =
        localParamNode (const $ X.Element x [] []) $ getAttributeSplice2 x
    
    doInline = do
        let ast = case AP.feed (AP.parse attParser v) "" of
                    (AP.Done _ res) -> res
                    (AP.Fail _ _ _) -> []
                    (AP.Partial _ ) -> []
        chunks <- mapM cvt ast
        return $ do
            list <- sequence chunks
            return [(k, T.concat list)]
    
    doAttrSplice splice = splice v
runAttributes :: Monad n
              => [(Text, Text)] 
              -> HeistT n IO [DList (Chunk n)]
runAttributes = mapM parseAtt
runAttributesRaw :: Monad n
                 
                 => [(Text, Text)] 
                 -> HeistT n IO (RuntimeSplice n [(Text, Text)])
runAttributesRaw attrs = do
    arrs <- mapM parseAtt2 attrs
    return $ liftM concat $ sequence arrs
attrToChunk :: Text -> DList (Chunk n) -> DList (Chunk n)
attrToChunk !k !v = do
    DL.concat
        [ DL.singleton $! pureTextChunk $! T.concat [" ", k, "=\""]
        , v, DL.singleton $! pureTextChunk "\"" ]
attrToBuilder :: (Text, Text) -> Builder
attrToBuilder (k,v)
  | T.null v  = mconcat
    [ fromText " "
    , fromText k
    ]
  | otherwise = mconcat
    [ fromText " "
    , fromText k
    , fromText "=\""
    , fromText v
    , fromText "\""
    ]
getAttributeSplice :: Text -> HeistT n IO (DList (Chunk n))
getAttributeSplice name =
    lookupSplice name >>= fromMaybe
      (return $ DL.singleton $ Pure $ T.encodeUtf8 $
       T.concat ["${", name, "}"])
getAttributeSplice2 :: Monad n => Text -> HeistT n IO (RuntimeSplice n Text)
getAttributeSplice2 name = do
    mSplice <- lookupSplice name
    case mSplice of
      Nothing -> return $ return $ T.concat ["${", name, "}"]
      Just splice -> do
        res <- splice
        return $ liftM (T.decodeUtf8 . toByteString) $ codeGen res
newtype Promise a = Promise (HE.Key a)
getPromise :: (Monad n) => Promise a -> RuntimeSplice n a
getPromise (Promise k) = do
    mb <- gets (HE.lookup k)
    return $ fromMaybe e mb
  where
    e = error $ "getPromise: dereferenced empty key (id "
                ++ show (HE.getKeyId k) ++ ")"
putPromise :: (Monad n) => Promise a -> a -> RuntimeSplice n ()
putPromise (Promise k) x = modify (HE.insert k x)
adjustPromise :: Monad n => Promise a -> (a -> a) -> RuntimeSplice n ()
adjustPromise (Promise k) f = modify (HE.adjust f k)
newEmptyPromise :: HeistT n IO (Promise a)
newEmptyPromise = do
    keygen <- getsHS _keygen
    key    <- liftIO $ HE.makeKey keygen
    return $! Promise key
bindSplice :: Text             
           -> Splice n         
           -> HeistState n     
           -> HeistState n
bindSplice n v ts =
    ts { _compiledSpliceMap = H.insert n' v (_compiledSpliceMap ts) }
  where
    n' = _splicePrefix ts `mappend` n
bindSplices :: Splices (Splice n)  
            -> HeistState n        
            -> HeistState n
bindSplices ss hs =
    hs { _compiledSpliceMap = applySpliceMap hs _compiledSpliceMap ss }
withLocalSplices :: Splices (Splice n)
                 -> Splices (AttrSplice n)
                 -> HeistT n IO a
                 -> HeistT n IO a
withLocalSplices ss as = localHS (bindSplices ss . bindAttributeSplices as)
renderTemplate :: Monad n
               => HeistState n
               -> ByteString
               -> Maybe (n Builder, MIMEType)
renderTemplate hs nm =
    fmap (first (interpret . DL.fromList) . fst) $!
      lookupTemplate nm hs _compiledTemplateMap
callTemplate :: Monad n
             => ByteString
             -> Splice n
callTemplate nm = do
    hs <- getHS
    maybe (error err) call $ lookupTemplate nm hs _templateMap
  where
    err = "callTemplate: "++(T.unpack $ T.decodeUtf8 nm)++(" does not exist")
    call (df,_) = localHS (\hs' -> hs' {_curTemplateFile = dfFile df}) $
                    runNodeList $ X.docContent $ dfDoc df
interpret :: Monad n => DList (Chunk n) -> n Builder
interpret = flip evalStateT HE.empty . unRT . codeGen
textSplice :: (a -> Text) -> a -> Builder
textSplice f = fromText . f
                               
                               
                               
nodeSplice :: (a -> [X.Node]) -> a -> Builder
nodeSplice f = X.renderHtmlFragment X.UTF8 . f
xmlNodeSplice :: (a -> [X.Node]) -> a -> Builder
xmlNodeSplice f = X.renderXmlFragment X.UTF8 . f
htmlNodeSplice :: (a -> [X.Node]) -> a -> Builder
htmlNodeSplice f = X.renderHtmlFragment X.UTF8 . f
pureSplice :: Monad n => (a -> Builder) -> RuntimeSplice n a -> Splice n
pureSplice f n = return $ yieldRuntime (return . f =<< n)
withSplices :: Monad n
            => Splice n
            
            -> Splices (RuntimeSplice n a -> Splice n)
            
            -> RuntimeSplice n a
            
            -> Splice n
withSplices splice splices runtimeAction =
    withLocalSplices splices' mempty splice
  where
    splices' = mapV ($runtimeAction) splices
foldMapM :: (Monad f, Monoid m, Foldable list)
         => (a -> f m)
         -> list a
         -> f m
foldMapM f =
  Foldable.foldlM (\xs x -> xs `seq` liftM (xs <>) (f x)) mempty
manyWithSplices :: (Foldable f, Monad n)
                => Splice n
                -> Splices (RuntimeSplice n a -> Splice n)
                -> RuntimeSplice n (f a)
                -> Splice n
manyWithSplices splice splices runtimeAction =
    manyWith splice splices mempty runtimeAction
manyWith :: (Foldable f, Monad n)
         => Splice n
         -> Splices (RuntimeSplice n a -> Splice n)
         -> Splices (RuntimeSplice n a -> AttrSplice n)
         -> RuntimeSplice n (f a)
         -> Splice n
manyWith splice splices attrSplices runtimeAction = do
    p <- newEmptyPromise
    let splices' = mapV ($ getPromise p) splices
    let attrSplices' = mapV ($ getPromise p) attrSplices
    chunks <- withLocalSplices splices' attrSplices' splice
    return $ yieldRuntime $ do
        items <- runtimeAction
        foldMapM (\item -> putPromise p item >> codeGen chunks) items
deferMany :: (Foldable f, Monad n)
          => (RuntimeSplice n a -> Splice n)
          -> RuntimeSplice n (f a)
          -> Splice n
deferMany f getItems = do
    promise <- newEmptyPromise
    chunks <- f $ getPromise promise
    return $ yieldRuntime $ do
        items <- getItems
        foldMapM (\item -> putPromise promise item >> codeGen chunks) items
defer :: Monad n
      => (RuntimeSplice n a -> Splice n)
      -> RuntimeSplice n a -> Splice n
defer pf n = do
    p2 <- newEmptyPromise
    let action = yieldRuntimeEffect $ putPromise p2 =<< n
    res <- pf $ getPromise p2
    return $ action `mappend` res
deferMap :: Monad n
         => (a -> RuntimeSplice n b)
         -> (RuntimeSplice n b -> Splice n)
         -> RuntimeSplice n a -> Splice n
deferMap f pf n = defer pf $ f =<< n
mayDeferMap :: Monad n
            => (a -> RuntimeSplice n (Maybe b))
            -> (RuntimeSplice n b -> Splice n)
            -> RuntimeSplice n a -> Splice n
mayDeferMap f pf n = deferMany pf $ f =<< n
bindLater :: (Monad n)
          => (a -> RuntimeSplice n Builder)
          -> RuntimeSplice n a
          -> Splice n
bindLater f p = return $ yieldRuntime $ f =<< p