module Heist.Common where
import           Control.Applicative      (Alternative (..))
import           Control.Exception        (SomeException)
import qualified Control.Exception.Lifted as C
import           Control.Monad            (liftM, mplus)
import qualified Data.Attoparsec.Text     as AP
import           Data.ByteString          (ByteString)
import qualified Data.ByteString          as B
import qualified Data.ByteString.Char8    as BC
import           Data.Hashable            (Hashable)
import           Data.HashMap.Strict      (HashMap)
import qualified Data.HashMap.Strict      as Map
import           Data.List                (isSuffixOf)
import           Data.Map.Syntax
import           Data.Maybe               (isJust)
import           Data.Monoid              ((<>))
import           Data.Text                (Text)
import qualified Data.Text                as T
import           Heist.Internal.Types.HeistState
import           System.FilePath          (pathSeparator)
import qualified Text.XmlHtml             as X
#if !MIN_VERSION_base(4,8,0)
import           Control.Applicative      (Applicative (..), (<$>))
import           Data.Monoid              (Monoid (..))
#endif
runHashMap
    :: Splices s
    -> Either [String] (HashMap T.Text s)
runHashMap ms =
    case runMapSyntax Map.lookup Map.insert ms of
      Left keys -> Left $ map (T.unpack . mkMsg) keys
      Right hm -> Right hm
  where
    mkMsg k = "You tried to bind "<>k<>" more than once!"
runMapNoErrors :: (Eq k, Hashable k) => MapSyntaxM k v a -> HashMap k v
runMapNoErrors = either (const mempty) id .
    runMapSyntax' (\_ new _ -> Just new) Map.lookup Map.insert
applySpliceMap :: HeistState n
                -> (HeistState n -> HashMap Text v)
                -> MapSyntaxM Text v a
                -> HashMap Text v
applySpliceMap hs f =  (flip Map.union (f hs)) .
    runMapNoErrors .
    mapK (mappend pre)
  where
    pre = _splicePrefix hs
orError :: Monad m => HeistT n m b -> String -> HeistT n m b
orError silent msg = do
    hs <- getHS
    if _preprocessingMode hs
      then do fullMsg <- heistErrMsg (T.pack msg)
              error $ T.unpack fullMsg
      else silent
heistErrMsg :: Monad m => Text -> HeistT n m Text
heistErrMsg msg = do
    tf <- getsHS _curTemplateFile
    return $ (maybe "" ((`mappend` ": ") . T.pack) tf) `mappend` msg
tellSpliceError :: Monad m => Text -> HeistT n m ()
tellSpliceError msg = do
    hs <- getHS
    node <- getParamNode
    let spliceError = SpliceError
                      { spliceHistory = _splicePath hs
                      , spliceTemplateFile = _curTemplateFile hs
                      , visibleSplices = Map.keys $ _compiledSpliceMap hs
                      , contextNode = node
                      , spliceMsg = msg
                      }
    modifyHS (\hs' -> hs { _spliceErrors = spliceError : _spliceErrors hs' })
showTPath :: TPath -> String
showTPath = BC.unpack . (`BC.append` ".tpl") . tpathName
tpathName :: TPath -> ByteString
tpathName = BC.intercalate "/" . reverse
setCurTemplateFile :: Maybe FilePath -> HeistState n -> HeistState n
setCurTemplateFile Nothing ts = ts
setCurTemplateFile fp ts = ts { _curTemplateFile = fp }
setCurContext :: TPath -> HeistState n -> HeistState n
setCurContext tp ts = ts { _curContext = tp }
attParser :: AP.Parser [AttAST]
attParser = liftM ($! []) (loop id)
  where
    append !dl !x = dl . (x:)
    loop !dl = go id
      where
        finish subDL = let !txt = T.concat $! subDL []
                           lit  = Literal $! T.concat $! subDL []
                       in return $! if T.null txt
                                      then dl
                                      else append dl lit
        go !subDL = (gobbleText >>= go . append subDL)
                    <|> (AP.endOfInput *> finish subDL)
                    <|> (do
                            idp <- identParser
                            dl' <- finish subDL
                            loop $! append dl' idp)
    gobbleText = AP.takeWhile1 (AP.notInClass "$")
    identParser = AP.char '$' *> (ident <|> return (Literal "$"))
    ident = (AP.char '{' *> (Ident <$> AP.takeWhile (/='}')) <* AP.string "}")
splitPathWith :: Char -> ByteString -> TPath
splitPathWith s p = if BC.null p then [] else (reverse $ BC.split s path)
  where
    path = if BC.head p == s then BC.tail p else p
splitLocalPath :: ByteString -> TPath
splitLocalPath = splitPathWith pathSeparator
splitTemplatePath :: ByteString -> TPath
splitTemplatePath = splitPathWith '/'
lookupTemplate :: ByteString
               -> HeistState n
               -> (HeistState n -> HashMap TPath t)
               -> Maybe (t, TPath)
lookupTemplate nameStr ts tm = f (tm ts) path name
  where
    (name:p) = case splitTemplatePath nameStr of
                   [] -> [""]
                   ps -> ps
    ctx = if B.isPrefixOf "/" nameStr then [] else _curContext ts
    path = p ++ ctx
    f = if '/' `BC.elem` nameStr
            then singleLookup
            else traversePath
hasTemplate :: ByteString -> HeistState n -> Bool
hasTemplate nameStr ts =
    isJust $ lookupTemplate nameStr ts _templateMap
singleLookup :: (Eq a, Hashable a)
             => HashMap [a] t -> [a] -> a -> Maybe (t, [a])
singleLookup tm path name = fmap (\a -> (a,path)) $ Map.lookup (name:path) tm
traversePath :: (Eq a, Hashable a)
             => HashMap [a] t -> [a] -> a -> Maybe (t, [a])
traversePath tm [] name = fmap (\a -> (a,[])) (Map.lookup [name] tm)
traversePath tm path name =
    singleLookup tm path name `mplus`
    traversePath tm (tail path) name
mapSplices :: (Monad m, Monoid b)
           => (a -> m b)
           
           -> [a]
           
           -> m b
           
mapSplices f vs = liftM mconcat $ mapM f vs
getContext :: Monad m => HeistT n m TPath
getContext = getsHS _curContext
getTemplateFilePath :: Monad m => HeistT n m (Maybe FilePath)
getTemplateFilePath = getsHS _curTemplateFile
loadTemplate :: String 
             -> String 
             -> IO [Either String (TPath, DocumentFile)] --TemplateMap
loadTemplate templateRoot fname = do
    c <- loadTemplate' fname
    return $ map (fmap (\t -> (splitLocalPath $ BC.pack tName, t))) c
  where 
    isHTMLTemplate = ".tpl"  `isSuffixOf` fname
    correction = if last templateRoot == '/' then 0 else 1
    extLen     = if isHTMLTemplate then 4 else 5
    tName = drop ((length templateRoot)+correction) $
            
            take ((length fname)  extLen) fname
loadTemplate' :: String -> IO [Either String DocumentFile]
loadTemplate' fullDiskPath
    | isHTMLTemplate = liftM (:[]) $ getDoc fullDiskPath
    | isXMLTemplate = liftM (:[]) $ getXMLDoc fullDiskPath
    | otherwise = return []
  where
    isHTMLTemplate = ".tpl"  `isSuffixOf` fullDiskPath
    isXMLTemplate  = ".xtpl" `isSuffixOf` fullDiskPath
type ParserFun = String -> ByteString -> Either String X.Document
getDocWith :: ParserFun -> String -> IO (Either String DocumentFile)
getDocWith parser f = do
    bs <- C.catch (liftM Right $ B.readFile f)
                (\(e::SomeException) -> return $ Left $ show e)
    let eitherDoc = either Left (parser f) bs
    return $ either (\s -> Left $ f ++ " " ++ s)
                    (\d -> Right $ DocumentFile d (Just f)) eitherDoc
getDoc :: String -> IO (Either String DocumentFile)
getDoc = getDocWith X.parseHTML
getXMLDoc :: String -> IO (Either String DocumentFile)
getXMLDoc = getDocWith X.parseXML
setTemplates :: HashMap TPath DocumentFile -> HeistState n -> HeistState n
setTemplates m ts = ts { _templateMap = m }
insertTemplate :: TPath
               -> DocumentFile
               -> HeistState n
               -> HeistState n
insertTemplate p t st =
    setTemplates (Map.insert p t (_templateMap st)) st
mimeType :: X.Document -> MIMEType
mimeType d = case d of
    (X.HtmlDocument e _ _) -> "text/html;charset=" `BC.append` enc e
    (X.XmlDocument  e _ _) -> "text/xml;charset="  `BC.append` enc e
  where
    enc X.UTF8    = "utf-8"
    
    
    enc X.UTF16BE = "utf-16"
    enc X.UTF16LE = "utf-16"
    enc X.ISO_8859_1 = "iso-8859-1"
bindAttributeSplices :: Splices (AttrSplice n) 
                     -> HeistState n           
                     -> HeistState n
bindAttributeSplices ss hs =
    hs { _attrSpliceMap = applySpliceMap hs _attrSpliceMap ss }
addDoctype :: Monad m => [X.DocType] -> HeistT n m ()
addDoctype dt = do
    modifyHS (\s -> s { _doctypes = _doctypes s `mappend` dt })