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

-- | Write PDF files
--
-- It could be used to generate new PDF file
-- or to incrementally update the existent one
--
-- To generate new file, first call 'writePdfHeader',
-- then a number of 'writeObject' and finally 'writeXRefTable'
--
-- To incrementally update PDF file just ommit the
-- `writePdfHeader` and append the result to the existent file

module Pdf.Toolbox.Core.Writer
(
  PdfWriter,
  runPdfWriter,
  writePdfHeader,
  writeObject,
  deleteObject,
  writeXRefTable
)
where

import Data.Int
import Data.Set (Set)
import qualified Data.Set as Set
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as BSL
import Data.ByteString.Lazy.Builder
import Data.ByteString.Lazy.Builder.ASCII
import Data.Function
import Data.Monoid
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.State
import Control.Monad.IO.Class
import System.IO.Streams (OutputStream)
import qualified System.IO.Streams as Streams

import Pdf.Toolbox.Core.Object.Types
import Pdf.Toolbox.Core.Object.Builder

-- | The monad
newtype PdfWriter m a = PdfWriter (StateT PdfState m a)
  deriving (Monad, MonadIO, MonadTrans)

-- | Execute writer action
runPdfWriter :: MonadIO m
             => OutputStream ByteString    -- ^ streams to write to
             -> PdfWriter m a              -- ^ action to run
             -> m a
runPdfWriter output (PdfWriter action) = do
  (out, count) <- liftIO $ Streams.countOutput output
  let emptyState = PdfState {
        stOutput = out,
        stObjects = Set.empty,
        stCount = count,
        stOffset = 0
        }
  evalStateT action emptyState

data Elem = Elem {
  elemIndex :: {-# UNPACK #-} !Int,
  elemGen :: {-# UNPACK #-} !Int,
  elemOffset :: {-# UNPACK #-} !Int64,
  elemFree :: !Bool
  }

instance Eq Elem where
  (==) = (==) `on` elemIndex

instance Ord Elem where
  compare = compare `on` elemIndex

data PdfState = PdfState {
  stOutput :: OutputStream ByteString,
  stObjects :: !(Set Elem),
  stCount :: IO Int64,
  stOffset :: {-# UNPACK #-} !Int64
  }

-- | Write PDF header. Used for generating new PDF files.
-- Should be the first call. Not used fo incremental updates
writePdfHeader :: MonadIO m => PdfWriter m ()
writePdfHeader = do
  output <- PdfWriter $ gets stOutput
  liftIO $ Streams.write (Just "%PDF-1.7\n") output

-- | Write object
writeObject :: MonadIO m => Ref -> Object BSL.ByteString -> PdfWriter m ()
writeObject ref@(Ref index gen) obj = do
  st <- PdfWriter get
  pos <- countWritten
  addElem $ Elem index gen pos False
  dumpObject (stOutput st) ref obj
  return ()

-- | Delete object
deleteObject :: MonadIO m => Ref -> Int64 -> PdfWriter m ()
deleteObject (Ref index gen) nextFree =
  addElem $ Elem index gen nextFree True

-- | Write xref table. Should be the last call.
-- Used for generating and incremental updates.
writeXRefTable :: MonadIO m
               => Int64           -- ^ size of the original PDF file. Should be 0 for new file
               -> Dict            -- ^ trailer
               -> PdfWriter m ()
writeXRefTable offset tr = do
  st <- PdfWriter get
  off <- (+ offset) `liftM` countWritten
  let elems = Set.mapMonotonic (\e -> e {elemOffset = elemOffset e + offset})  $ stObjects st
      content = byteString "xref\n" `mappend`
                buildXRefTable (Set.toAscList elems) `mappend`
                byteString "trailer\n" `mappend`
                buildDict tr `mappend`
                byteString "\nstartxref\n" `mappend`
                int64Dec off `mappend`
                byteString "\n%%EOF\n"
  liftIO $ Streams.writeLazyByteString (toLazyByteString content) (stOutput st)

countWritten :: MonadIO m => PdfWriter m Int64
countWritten = do
  st <- PdfWriter get
  c <- (stOffset st +) `liftM` liftIO (stCount st)
  PdfWriter $ put $ st {stOffset = c}
  return $! c

addElem :: Monad m => Elem -> PdfWriter m ()
addElem e = do
  st <- PdfWriter get
  when (Set.member e $ stObjects st) $ error $ "PdfWriter: attempt to write object with the same index: " ++ show (elemIndex e)
  PdfWriter $ put st {stObjects = Set.insert e $ stObjects st}

dumpObject :: MonadIO m => OutputStream ByteString -> Ref -> Object BSL.ByteString -> m ()
dumpObject out ref o = liftIO $ Streams.writeLazyByteString (toLazyByteString $ buildIndirectObject ref o) out

buildXRefTable :: [Elem] -> Builder
buildXRefTable entries =
  mconcat (map buildXRefSection $ sections entries)
  where
  sections :: [Elem] -> [[Elem]]
  sections [] = []
  sections xs = let (s, rest) = section xs in s : sections rest
  section [] = error "impossible"
  section (x:xs) = go (elemIndex x + 1) [x] xs
    where
    go _ res [] = (reverse res, [])
    go i res (y:ys) =
      if i == elemIndex y
        then go (i + 1) (y : res) ys
        else (reverse res, y:ys)

buildXRefSection :: [Elem] -> Builder
buildXRefSection [] = error "impossible"
buildXRefSection s@(e:_) =
  intDec (elemIndex e) `mappend`
  char7 ' ' `mappend`
  intDec (length s) `mappend`
  char7 '\n' `mappend`
  loop s
  where
  loop (x:xs) =
    buildFixed 10 '0' (elemOffset x) `mappend`
    char7 ' ' `mappend`
    buildFixed 5 '0' (elemGen x) `mappend`
    char7 ' ' `mappend`
    char7 (if elemFree x then 'f' else 'n') `mappend`
    string7 "\r\n" `mappend`
    loop xs
  loop [] = mempty

buildFixed :: Show a => Int -> Char -> a -> Builder
buildFixed len c i =
  let v = take len $ show i
      l = length v
  in string7 $ replicate (len - l) c ++ v

{-
-- At attempt to do it directly with Set.
-- Actually uses 2x memory...
buildXRefTable :: Set Elem -> Builder
buildXRefTable elems
  | Set.null elems = mempty
  | otherwise = buildXRefSection elems

buildXRefSection :: Set Elem -> Builder
buildXRefSection elems =
  intDec (elemIndex start) `mappend`
  char7 ' ' `mappend`
  intDec len `mappend`
  char7 '\n' `mappend`
  section `mappend`
  buildXRefTable rest
  where
  (start, len, rest) = sectionLength elems
  section = buildSection len elems

buildSection :: Int -> Set Elem -> Builder
buildSection 0 _ = mempty
buildSection l els =
  let (x, xs) = Set.deleteFindMin els
  in buildFixed 10 '0' (elemOffset x) `mappend`
     char7 ' ' `mappend`
     buildFixed 5 '0' (elemGen x) `mappend`
     char7 ' ' `mappend`
     char7 (if elemFree x then 'f' else 'n') `mappend`
     string7 "\r\n" `mappend`
     buildSection (l - 1) xs

sectionLength :: Set Elem -> (Elem, Int, Set Elem)
sectionLength els =
  let (x, xs) = Set.deleteFindMin els
      (count, rest) = go 1 (elemIndex x) xs
  in (x, count, rest)
  where
  go n val xs
    | Set.null xs = (n, xs)
    | otherwise = let (next, rest) = Set.deleteFindMin xs
                  in if elemIndex next == val + 1
                       then go (n + 1) (val + 1) rest
                       else (n, xs)
-}