{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}

----------------------------------------------------------------------------
-- |
-- Module      :  System.Texrunner.Online
-- Copyright   :  (c) 2015 Christopher Chalmers
-- License     :  BSD-style (see LICENSE)
-- Maintainer  :  c.chalmers@me.com
--
-- Functions for running and parsing using Tex's online interface. This is
-- mostly used for getting measurements like hbox dimensions and textwidth.
--
-- Tex's online interface is basically running the command line. You can
-- see it by running @pdflatex@ without any arguments. The contents can
-- be writen line by and tex can give feedback though stdout, which gets
-- parsed in by this module. This is the only way I know to get info
-- like hbox sizes. Please let me know if you know a better way.
--
-----------------------------------------------------------------------------

module System.Texrunner.Online
  ( OnlineTex
  -- * Running Tex online
  , runOnlineTex

  , runOnlineTex'
  -- * Interaction
  , hbox
  , hsize
  , showthe
  , onlineTexParser
  , texPutStrLn

  -- * Low level
  -- | These functions allow give you direct access to the iostreams
  --   with tex. The implementation is likely to change in the future
  --   and using them directly is not recommended.
  , TexStreams
  , getInStream
  , getOutStream
  , clearUnblocking
  ) where

import           Control.Applicative
import           Control.Monad.Reader
import qualified Data.Attoparsec.ByteString   as A
import           Data.ByteString.Char8        (ByteString)
import qualified Data.ByteString.Char8        as C8
import qualified Data.ByteString.Lazy.Char8   as LC8
import           Data.List                    (find)
import           Data.Maybe
import           Data.Monoid
import qualified Data.Traversable             as T

import           System.Directory
import           System.FilePath
import           System.IO
import           System.IO.Streams            as Streams
import           System.IO.Streams.Attoparsec
import           System.IO.Temp
import           System.Process               as P (runInteractiveProcess)

import           System.Texrunner.Parse

-- | Type for dealing with Tex's piping interface; the current streams
--   are available though the 'MonadReader' instance.
newtype OnlineTex a = OnlineTex {runOnlineTexT :: ReaderT TexStreams IO a}
  deriving (Functor, Applicative, Monad, MonadIO, MonadReader TexStreams)

-- | Run a tex process, discarding the resulting PDF.
runOnlineTex :: String      -- ^ tex command
             -> [String]    -- ^ tex command arguments
             -> ByteString  -- ^ preamble
             -> OnlineTex a -- ^ Online Tex to be Run
             -> IO a
runOnlineTex command args preamble process =
  (\(a,_,_) -> a) <$> runOnlineTex' command args preamble process

-- | Run a tex process, keeping the resulting PDF. The OnlineTex must receive
--   the terminating control sequence (\\bye, \\end{document}, \\stoptext).
runOnlineTex' :: String
              -> [String]
              -> ByteString
              -> OnlineTex a
              -> IO (a, TexLog, Maybe LC8.ByteString)
runOnlineTex' command args preamble process =
  withSystemTempDirectory "onlinetex." $ \path -> do
    (outS, inS, h) <- mkTexHandles path Nothing command args preamble
    a              <- flip runReaderT (outS, inS) . runOnlineTexT $ process

    write Nothing outS
    _ <- waitForProcess h

    -- it's normally texput.pdf but some (Context) choose random names
    pdfPath  <- find ((==".pdf") . takeExtension) <$> getDirectoryContents path
    pdfFile  <- T.mapM (LC8.readFile . (path </>)) pdfPath

    logPath  <- find ((==".log") . takeExtension) <$> getDirectoryContents path
    logFile  <- T.mapM (C8.readFile . (path </>)) logPath

    return (a, parseLog $ fromMaybe "" logFile, pdfFile)

-- | Get the dimensions of a hbox.
hbox :: Fractional n => ByteString -> OnlineTex (Box n)
hbox str = do
  clearUnblocking
  texPutStrLn $ "\\setbox0=\\hbox{" <> str <> "}\n\\showbox0\n"
  onlineTexParser parseBox

-- | Parse result from @\showthe@.
showthe :: Fractional n => ByteString -> OnlineTex n
showthe str = do
  clearUnblocking
  texPutStrLn $ "\\showthe" <> str
  onlineTexParser parseUnit

-- | Dimensions from filling the current line.
hsize :: Fractional n => OnlineTex n
hsize = boxWidth <$> hbox "\\line{\\hfill}"

-- | Run an Attoparsec parser on Tex's output.
onlineTexParser :: A.Parser a -> OnlineTex a
onlineTexParser p = getInStream >>= liftIO . parseFromStream p
  -- TODO: have a timeout

texPutStrLn :: ByteString -> OnlineTex ()
texPutStrLn a = getOutStream >>= liftIO . write (Just $ C8.append a "\n")

-- * Internal
-- These functions should be used with caution.

type TexStreams = (OutputStream ByteString, InputStream ByteString)

-- | Get the output stream to read tex's output.
getOutStream :: OnlineTex (OutputStream ByteString)
getOutStream = reader fst

-- | Get the input stream to give text to tex.
getInStream :: OnlineTex (InputStream ByteString)
getInStream = reader snd

-- | Clear any output tex has already given.
clearUnblocking :: OnlineTex ()
clearUnblocking = getInStream >>= void . liftIO . Streams.read

-- | Uses a surface to open an interface with Tex.
mkTexHandles :: FilePath
             -> Maybe [(String, String)]
             -> String
             -> [String]
             -> ByteString
             -> IO (OutputStream ByteString,
                    InputStream ByteString,
                    ProcessHandle)
mkTexHandles dir env command args preamble = do

  -- Tex doesn't send anything to stderr
  (outStream, inStream, _, h) <- runInteractiveProcess'
                                   command
                                   args
                                   (Just dir)
                                   env

  -- inStream <- debugStream inStream'

  -- commands to get Tex to play nice
  write (Just $ "\\tracingonline=1"  -- \showbox is echoed to stdout
             <> "\\showboxdepth=1"   -- show boxes one deep
             <> "\\showboxbreadth=1"
             <> "\\scrollmode\n"     -- don't pause after showing something
        ) outStream
  write (Just preamble) outStream

  return (outStream, inStream, h)

-- Adapted from io-streams. Sets input handle to line buffering.
runInteractiveProcess'
    :: FilePath                 -- ^ Filename of the executable (see 'proc' for details)
    -> [String]                 -- ^ Arguments to pass to the executable
    -> Maybe FilePath           -- ^ Optional path to the working directory
    -> Maybe [(String,String)]  -- ^ Optional environment (otherwise inherit)
    -> IO (OutputStream ByteString,
           InputStream ByteString,
           InputStream ByteString,
           ProcessHandle)
runInteractiveProcess' cmd args wd env = do
    (hin, hout, herr, ph) <- P.runInteractiveProcess cmd args wd env

    -- it is possible to flush using write (Just "") but this seems nicer
    -- is there a better way?
    hSetBuffering hin LineBuffering

    sIn  <- Streams.handleToOutputStream hin >>=
            Streams.atEndOfOutput (hClose hin) >>=
            Streams.lockingOutputStream
    sOut <- Streams.handleToInputStream hout >>=
            Streams.atEndOfInput (hClose hout) >>=
            Streams.lockingInputStream
    sErr <- Streams.handleToInputStream herr >>=
            Streams.atEndOfInput (hClose herr) >>=
            Streams.lockingInputStream

    return (sIn, sOut, sErr, ph)

-- debugStream :: InputStream ByteString -> IO (InputStream ByteString)
-- debugStream = debugInput id "tex" Streams.stdout