{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Module that provides the relevant functions to render a given AST into
--     a PDF-bytestring. Core function is the generatePDF function that takes
--     part of an ast and returns either a bytestring or an errortext.
module Language.Ltml.ToLaTeX.PDFGenerator
    ( generatePDFFromSection -- deprecated, use 'generatePDF' instead
    , generatePDF
    , generateLaTeX
    --   generatePDFFromDocument
    ) where

import Control.Exception (Exception (displayException), SomeException, try)
import Control.Lens (view)
import Control.Monad.State (runState)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BSL
import Data.Text (Text)
import qualified Data.Text.Encoding as TE
import Language.Ltml.ToLaTeX.GlobalState
    ( initialGlobalState
    , labelToRef
    , preDocument
    )
import Language.Ltml.ToLaTeX.PreLaTeXType (document)
import Language.Ltml.ToLaTeX.Renderer (renderLaTeX)
import Language.Ltml.ToLaTeX.ToLaTeX (toLaTeX)
import Language.Ltml.ToLaTeX.ToPreLaTeXM (ToPreLaTeXM (toPreLaTeXM))
import System.Exit (ExitCode (..))
import System.FilePath ((</>))
import System.IO (hClose)
import System.IO.Temp (withSystemTempDirectory)
import System.Process
    ( CreateProcess (cwd, std_err, std_in, std_out)
    , StdStream (CreatePipe)
    , createProcess
    , proc
    , waitForProcess
    )

-------------------------------- Public -----------------------------------

-- | top level function to generate a pdf bytestring from part of an ast.
--   its main purpose is to catch all exceptions that might be thrown in
--   compilePDF
generatePDF
    :: (ToPreLaTeXM a) => a -> IO (Either String BSL.ByteString)
generatePDF :: forall a. ToPreLaTeXM a => a -> IO (Either String ByteString)
generatePDF a
input = do
    Either SomeException ByteString
res <- IO ByteString -> IO (Either SomeException ByteString)
forall e a. Exception e => IO a -> IO (Either e a)
try (IO ByteString -> IO (Either SomeException ByteString))
-> IO ByteString -> IO (Either SomeException ByteString)
forall a b. (a -> b) -> a -> b
$ a -> IO ByteString
forall p. ToPreLaTeXM p => p -> IO ByteString
compilePDF a
input
    case Either SomeException ByteString
res of
        Left (SomeException
e :: SomeException) -> Either String ByteString -> IO (Either String ByteString)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String ByteString -> IO (Either String ByteString))
-> Either String ByteString -> IO (Either String ByteString)
forall a b. (a -> b) -> a -> b
$ String -> Either String ByteString
forall a b. a -> Either a b
Left (SomeException -> String
forall e. Exception e => e -> String
displayException SomeException
e)
        Right ByteString
pdf -> Either String ByteString -> IO (Either String ByteString)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Either String ByteString -> IO (Either String ByteString))
-> Either String ByteString -> IO (Either String ByteString)
forall a b. (a -> b) -> a -> b
$ ByteString -> Either String ByteString
forall a b. b -> Either a b
Right ByteString
pdf

-------------------------------- Pipeline -----------------------------------

-- | creates a temporary directory, turns the ast into latex code and writes
--   that code into a .tex file in the temp-dir. at last it runs latexmk on
--   the file and reads the resulting pdf as a bytestring. (or throws an
--   exception, if latexmk fails.)
compilePDF
    :: (ToPreLaTeXM p) => p -> IO BSL.ByteString
compilePDF :: forall p. ToPreLaTeXM p => p -> IO ByteString
compilePDF p
input =
    String -> (String -> IO ByteString) -> IO ByteString
forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
String -> (String -> m a) -> m a
withSystemTempDirectory String
"latex-temp" ((String -> IO ByteString) -> IO ByteString)
-> (String -> IO ByteString) -> IO ByteString
forall a b. (a -> b) -> a -> b
$ \String
tmpDir -> do
        let texFile :: String
texFile = String
tmpDir String -> String -> String
</> String
"input.tex"
            pdfFile :: String
pdfFile = String
tmpDir String -> String -> String
</> String
"input.pdf"

        -- \| Generate LaTeX source from input
        let latexSource :: Text
latexSource = p -> Text
forall a. ToPreLaTeXM a => a -> Text
generateLaTeX p
input

        -- \| Write LaTeX source
        String -> ByteString -> IO ()
BS.writeFile String
texFile (Text -> ByteString
TE.encodeUtf8 Text
latexSource)

        -- \| Compile with pdflatex
        (ExitCode
exitCode, ByteString
stdout, ByteString
_) <- String -> String -> IO (ExitCode, ByteString, ByteString)
runLatex String
texFile String
tmpDir

        case ExitCode
exitCode of
            ExitFailure Int
_ -> ByteString -> IO ByteString
forall a. ByteString -> IO a
failLatex ByteString
stdout
            ExitCode
ExitSuccess -> String -> IO ByteString
BSL.readFile String
pdfFile

-------------------------------- Helpers -----------------------------------

-- | function that encapsules the state logic to generate latex code from
--   the ast.
generateLaTeX :: (ToPreLaTeXM a) => a -> Text
generateLaTeX :: forall a. ToPreLaTeXM a => a -> Text
generateLaTeX a
input =
    let (PreLaTeX
res, GlobalState
gs) = State GlobalState PreLaTeX
-> GlobalState -> (PreLaTeX, GlobalState)
forall s a. State s a -> s -> (a, s)
runState (a -> State GlobalState PreLaTeX
forall a. ToPreLaTeXM a => a -> State GlobalState PreLaTeX
toPreLaTeXM a
input) GlobalState
initialGlobalState
     in LaTeX -> Text
renderLaTeX (LaTeX -> Text) -> LaTeX -> Text
forall a b. (a -> b) -> a -> b
$
            Map Label Text -> PreLaTeX -> LaTeX
toLaTeX
                (Getting (Map Label Text) GlobalState (Map Label Text)
-> GlobalState -> Map Label Text
forall s (m :: * -> *) a. MonadReader s m => Getting a s a -> m a
view Getting (Map Label Text) GlobalState (Map Label Text)
Lens' GlobalState (Map Label Text)
labelToRef GlobalState
gs)
                (Getting PreLaTeX GlobalState PreLaTeX -> GlobalState -> PreLaTeX
forall s (m :: * -> *) a. MonadReader s m => Getting a s a -> m a
view Getting PreLaTeX GlobalState PreLaTeX
Lens' GlobalState PreLaTeX
preDocument GlobalState
gs PreLaTeX -> PreLaTeX -> PreLaTeX
forall a. Semigroup a => a -> a -> a
<> PreLaTeX -> PreLaTeX
document PreLaTeX
res)

-- | basically readCreateProcessWithExitCode, but written to work inside the docker container
--   because of encoding issues.
runLatex :: FilePath -> FilePath -> IO (ExitCode, BS.ByteString, BS.ByteString)
runLatex :: String -> String -> IO (ExitCode, ByteString, ByteString)
runLatex String
texFile String
workDir = do
    (Just Handle
hin, Just Handle
hout, Just Handle
herr, ProcessHandle
ph) <-
        CreateProcess
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
createProcess
            (String -> [String] -> CreateProcess
proc String
"latexmk" [String
"-pdf", String
"-interaction=nonstopmode", String
"-halt-on-error", String
texFile])
                { cwd = Just workDir
                , std_in = CreatePipe
                , std_out = CreatePipe
                , std_err = CreatePipe
                }
    Handle -> IO ()
hClose Handle
hin
    ByteString
out <- Handle -> IO ByteString
BS.hGetContents Handle
hout
    ByteString
err <- Handle -> IO ByteString
BS.hGetContents Handle
herr
    ExitCode
exitCode <- ProcessHandle -> IO ExitCode
waitForProcess ProcessHandle
ph
    (ExitCode, ByteString, ByteString)
-> IO (ExitCode, ByteString, ByteString)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (ExitCode
exitCode, ByteString
out, ByteString
err)

failLatex :: BS.ByteString -> IO a
failLatex :: forall a. ByteString -> IO a
failLatex ByteString
stdout =
    -- TODO: maybe drop preambel here
    String -> IO a
forall a. String -> IO a
forall (m :: * -> *) a. MonadFail m => String -> m a
fail (ByteString -> String
BS.unpack ByteString
stdout)

-------------------------------- Deprecated -----------------------------------

generatePDFFromSection :: Text -> IO (Either String BSL.ByteString)
generatePDFFromSection :: Text -> IO (Either String ByteString)
generatePDFFromSection = Text -> IO (Either String ByteString)
forall a. HasCallStack => a
undefined

--     let NamedType _ _ sectionT' = sectionT
--         NamedType _ _ footnoteT' = footnoteT
--      in generatePDFfromParsed
--             (nSc *> runFootnoteWriterT (sectionP sectionT' eof) [footnoteT'])
--             sectionToText
--             (input <> "\n")
--   where
--     sectionToText (sec, labelmap) =
--         let (latexSection, gs) = runState (toPreLaTeXM sec) $ initialGlobalState & labelToFootNote .~ labelmap
--          in renderLaTeX $
--                 toLaTeX (view labelToRef gs) (view preDocument gs <> document latexSection)

-- mkPDF :: FilePath -> IO (Either String BS.ByteString)
-- mkPDF filename = do
--     let
--         pdfCommand = "pdflatex -interaction=nonstopmode -halt-on-error " <> filename
--         workingDir = "./src/Language/Ltml/ToLaTeX/Auxiliary"

--     -- Run pdflatex and capture output
--     (exitCode, stdout, _) <-
--         readCreateProcessWithExitCode
--             (shell pdfCommand) {cwd = Just workingDir}
--             ""

--     case exitCode of
--         ExitSuccess -> do
--             putStrLn "PDF generated successfully."
--             pdf <- BS.readFile "output.pdf"
--             return (Right pdf)
--         ExitFailure _ -> do
--             putStrLn "LaTeX compilation failed."
--             return $ Left $ drop 3094 stdout -- omitting the preambel of the pdflatex output here.
--             -- could be different on another system and thus maybe revert later

-- withTempIn :: FilePath -> String -> (FilePath -> IO a) -> IO a
-- withTempIn parent template =
--     bracket
--         (createTempDirectory parent template)
--         removeDirectoryRecursive