{-# LANGUAGE PatternGuards, DeriveDataTypeable, ScopedTypeVariables #-}

module General.Template(
    Template, templateFile, templateMarkup, templateApply, templateRender
    ) where

import Data.Data
import Data.Monoid
import Text.Blaze
import Text.Blaze.Renderer.Utf8
import General.Str
import Data.List.Extra
import Control.Exception
import Data.Generics.Uniplate.Data
import Control.Applicative
import System.IO.Unsafe
import System.Directory
import Control.Monad
import Data.IORef
import Prelude

---------------------------------------------------------------------
-- TREE DATA TYPE

data Tree = Lam FilePath -- #{foo} defines a lambda
          | Var BStr -- a real variable
          | App Tree [(BStr, Tree)] -- applies a foo string to the lambda
          | Lit BStr
          | List [Tree]
            deriving (Typeable,Data,Show)


-- | Turn all Lam into Var/Lit
treeRemoveLam :: Tree -> IO Tree
treeRemoveLam = transformM f
    where
        f (Lam file) = List . parse <$> bstrReadFile file
        f x = pure x

        parse x | Just (a,b) <- bstrSplitInfix (bstrPack "#{") x
                , Just (b,c) <- bstrSplitInfix (bstrPack "}") b
                = Lit a : Var b : parse c
        parse x = [Lit x]

treeRemoveApp :: Tree -> Tree
treeRemoveApp = f []
    where
        f seen (App t xs) = f (xs ++ seen) t
        f seen (Var x) | Just t <- lookup x seen = f seen t
        f seen x = descend (f seen) x

treeOptimise :: Tree -> Tree
treeOptimise = transform f . treeRemoveApp
    where
        fromList (List xs) = xs; fromList x = [x]
        toList [x] = x; toList xs = List xs
        isLit (Lit x) = True; isLit _ = False
        fromLit (Lit x) = x

        f = toList . g . concatMap fromList . fromList

        g [] = []
        g (x:xs) | not $ isLit x = x : g xs
        g xs = [Lit x | let x = mconcatMap fromLit a, x /= mempty] ++ g b
            where (a,b) = span isLit xs

treeEval :: Tree -> [BStr]
treeEval = f . treeRemoveApp
    where f (Lit x) = [x]
          f (List xs) = concatMap f xs
          f _ = []


---------------------------------------------------------------------
-- TEMPLATE DATA TYPE

-- a tree, and a pre-optimised tree you can create
data Template = Template Tree (IO Tree)

{-# NOINLINE treeCache #-}
treeCache :: Tree -> IO Tree
treeCache t0 = unsafePerformIO $ do
    let files = [x | Lam x <- universe t0]
    ref <- newIORef ([], treeOptimise t0)
    pure $ do
        (old,t) <- readIORef ref
        new <- forM files $ \file ->
            -- the standard getModificationTime message on Windows doesn't say the file
            getModificationTime file `catch` \(e :: IOException) ->
                fail $ "Failed: getModificationTime on " ++ file ++ ", " ++ show e
        if old == new then pure t else do
            t <- treeOptimise <$> treeRemoveLam t0
            writeIORef ref (new,t)
            pure t

templateTree :: Tree -> Template
templateTree t = Template t $ treeCache t

templateFile :: FilePath -> Template
templateFile = templateTree . Lam

templateMarkup :: Markup -> Template
templateMarkup = templateStr . renderMarkup

templateStr :: LBStr -> Template
templateStr = templateTree . List . map Lit . lbstrToChunks

templateApply :: Template -> [(String, Template)] -> Template
templateApply (Template t _) args = templateTree $ App t [(bstrPack a, b) | (a,Template b _) <- args]

templateRender :: Template -> [(String, Template)] -> IO LBStr
templateRender (Template _ t) args = do
    t <- t
    let Template t2 _ = templateApply (Template t $ pure t) args
    lbstrFromChunks . treeEval <$> treeRemoveLam t2
