-- | A 'Document' is at heart 'ShowS' from the prelude
--
-- Essentially, if you give a Doc a string it'll print out whatever it
-- wants followed by that string. So @text "foo"@ makes the Doc that
-- prints @"foo"@ followed by its argument. The combinator names are taken
-- from 'Text.PrettyPrint.HughesPJ', although the behaviour of the two libraries is
-- slightly different.
--
-- The advantage of Printer over simple string appending/concatenating is
-- that the appends end up associating to the right, e.g.:
--
-- >   (text "foo" <> text "bar") <> (text "baz" <> text "quux") ""
-- > = \s -> (text "foo" <> text "bar") ((text "baz" <> text "quux") s) ""
-- > = (text "foo" <> text "bar") ((text "baz" <> text "quux") "")
-- > = (\s -> (text "foo") (text "bar" s)) ((text "baz" <> text "quux") "")
-- > = text "foo" (text "bar" ((text "baz" <> text "quux") ""))
-- > = (\s -> "foo" ++ s) (text "bar" ((text "baz" <> text "quux") ""))
-- > = "foo" ++ (text "bar" ((text "baz" <> text "quux") ""))
-- > = "foo" ++ ("bar" ++ ((text "baz" <> text "quux") ""))
-- > = "foo" ++ ("bar" ++ ((\s -> text "baz" (text "quux" s)) ""))
-- > = "foo" ++ ("bar" ++ (text "baz" (text "quux" "")))
-- > = "foo" ++ ("bar" ++ ("baz" ++ (text "quux" "")))
-- > = "foo" ++ ("bar" ++ ("baz" ++ ("quux" ++ "")))
--
-- The Empty alternative comes in because you want
--
-- > text "a" $$ vcat xs $$ text "b"
--
-- '$$' means above, 'vcat' is the list version of '$$'
-- (to be @\"a\\nb\"@ when @xs@  is @[]@), but without the concept of an
-- Empty Document each @$$@ would add a @'\n'@ and you'd end up with
-- @\"a\\n\\nb\"@.
-- Note that @Empty \/= text \"\"@ (the latter would cause two
-- @'\\n'@).
--
-- This code was made generic in the element type by Juliusz Chroboczek.
module Darcs.Util.Printer
    (
      Printable(..), Doc(Doc,unDoc), Printers, Printers'(..), Printer, Color(..)
    , RenderMode(..)
    , hPutDoc,     hPutDocLn,     putDoc,     putDocLn
    , hPutDocWith, hPutDocLnWith, putDocWith, putDocLnWith
    , hPutDocCompr
    , debugDocLn
    , renderString, renderStringWith, renderPS, renderPSWith
    , renderPSs, renderPSsWith, lineColor
    , prefix, insertBeforeLastline, colorText, invisibleText
    , prefixLines
    , hiddenText, hiddenPrefix, userchunk, text
    , printable, wrapText
    , blueText, redText, greenText, magentaText, cyanText
    , unsafeText, unsafeBoth, unsafeBothText, unsafeChar
    , invisiblePS, packedString, unsafePackedString, userchunkPS
    , simplePrinters, invisiblePrinter, simplePrinter
    , doc, empty, (<>), (<?>), (<+>), ($$), vcat, vsep, hcat
    , minus, newline, plus, space, backslash, lparen, rparen
    , parens
    , errorDoc
    ) where

import Control.Exception ( throwIO, ErrorCall(..) )
import Data.String ( IsString(..) )
import Data.List (intersperse)
import GHC.Stack ( currentCallStack )
import System.IO (Handle, stdout, hPutStr)
import System.IO.Unsafe ( unsafePerformIO )
import qualified Data.ByteString as B (ByteString, hPut, concat)
import qualified Data.ByteString.Char8 as BC (unpack, pack, singleton)

import Darcs.Util.ByteString ( linesPS, encodeLocale, gzWriteHandle )
import Darcs.Util.Global ( debugMessage )

-- | A 'Printable' is either a String, a packed string, or a chunk of
-- text with both representations.
data Printable = S !String
               | PS !B.ByteString
               | Both !String !B.ByteString

-- | 'spaceP' is the 'Printable' representation of a space.
spaceP :: Printable
spaceP   = Both " "  (BC.singleton ' ')

-- | 'newlineP' is the 'Printable' representation of a newline.
newlineP :: Printable
newlineP = S "\n"

-- | Minimal 'Doc's representing the common characters 'space', 'newline'
-- 'minus', 'plus', and 'backslash'.
space, newline, plus, minus, backslash :: Doc
space     = unsafeBoth " "  (BC.singleton ' ')
newline   = unsafeChar '\n'
minus     = unsafeBoth "-"  (BC.singleton '-')
plus      = unsafeBoth "+"  (BC.singleton '+')
backslash = unsafeBoth "\\" (BC.singleton '\\')

-- | 'lparen' is the 'Doc' that represents @\"(\"@
lparen :: Doc
lparen = unsafeBoth  "(" (BC.singleton '(')

-- | 'rparen' is the 'Doc' that represents @\")\"@
rparen :: Doc
rparen = unsafeBoth ")" (BC.singleton ')')

-- | @'parens' doc@ returns a 'Doc' with the content of @doc@ put within
-- a pair of parenthesis.
parens :: Doc -> Doc
parens d = lparen <> d <> rparen

errorDoc :: Doc -> a
errorDoc x = unsafePerformIO $ do
   stack <- currentCallStack
   throwIO $ ErrorCall $ renderString Encode $ x $$ vcat (map text stack)

-- | 'putDocWith' puts a doc on stdout using the given printer.
putDocWith :: Printers -> Doc -> IO ()
putDocWith prs = hPutDocWith prs Encode stdout

-- | 'putDocLnWith' puts a doc, followed by a newline on stdout using
-- the given printer.
putDocLnWith :: Printers -> Doc -> IO ()
putDocLnWith prs = hPutDocLnWith prs Encode stdout

-- | 'putDoc' puts a doc on stdout using the simple printer 'simplePrinters'.
putDoc :: Doc -> IO ()
putDoc = hPutDoc Encode stdout

-- | 'putDocLn' puts a doc, followed by a newline on stdout using
-- 'simplePrinters'
putDocLn :: Doc -> IO ()
putDocLn = hPutDocLn Encode stdout

-- | 'hputDocWith' puts a doc on the given handle using the given printer.
hPutDocWith :: Printers -> RenderMode -> Handle -> Doc -> IO ()
hPutDocWith prs target h d = hPrintPrintables target h (renderWith (prs h) d)

-- | 'hputDocLnWith' puts a doc, followed by a newline on the given
-- handle using the given printer.
hPutDocLnWith :: Printers -> RenderMode -> Handle -> Doc -> IO ()
hPutDocLnWith prs target h d = hPutDocWith prs target h (d <?> newline)

-- |'hputDoc' puts a doc on the given handle using 'simplePrinters'
hPutDoc :: RenderMode -> Handle -> Doc -> IO ()
hPutDoc = hPutDocWith simplePrinters

-- 'hputDocLn' puts a doc, followed by a newline on the given handle using
-- 'simplePrinters'.
hPutDocLn :: RenderMode -> Handle -> Doc -> IO ()
hPutDocLn = hPutDocLnWith simplePrinters

-- | like 'hPutDoc' but with compress data before writing
hPutDocCompr :: RenderMode -> Handle -> Doc -> IO ()
hPutDocCompr target h = gzWriteHandle h . renderPSs target

-- | Write a 'Doc' to stderr if debugging is turned on.
debugDocLn :: Doc -> IO ()
debugDocLn = debugMessage . renderString Standard

-- | @'hPrintPrintables' h@ prints a list of 'Printable's to the handle h
hPrintPrintables :: RenderMode -> Handle -> [Printable] -> IO ()
hPrintPrintables target h = mapM_ (hPrintPrintable target h)

-- | @hPrintPrintable h@ prints a 'Printable' to the handle h.
hPrintPrintable :: RenderMode -> Handle -> Printable -> IO ()
hPrintPrintable Standard h (S ps) = hPutStr h ps
hPrintPrintable Encode  h (S ps) = B.hPut h (encodeLocale ps)
hPrintPrintable Standard h (PS ps) = B.hPut h ps
hPrintPrintable Encode  h (PS ps) = B.hPut h ps
hPrintPrintable Standard h (Both _ ps) = B.hPut h ps
hPrintPrintable Encode  h (Both _ ps) = B.hPut h ps

-- | a 'Doc' is a bit of enriched text. 'Doc's get concatanated using
-- '<>', which is right-associative.
newtype Doc = Doc { unDoc :: St -> Document }

instance IsString Doc where
   fromString = text

-- TODO this is a rather ad-hoc hack that further complicates
-- some already confusing code. We should find a more general
-- solution. See the discussion on issue1639.
-- | Used when rendering a 'Doc' to indicate if the result
-- should be encoded to the current locale or left alone.
-- In practice this only affects output when a relevant
-- DARCS_DONT_ESCAPE_XXX option is set (see Darcs.Util.Printer.Color)
-- If in doubt, choose 'Standard'.
data RenderMode =
      Encode       -- ^Encode Strings with the current locale.
                   -- At present ByteStrings are assumed to be in
                   -- UTF8 and are left alone, so will be mis-encoded
                   -- in non-UTF8 locales.
    | Standard     -- ^Don't encode.

-- | The State associated with a doc. Contains a set of printers for each
-- hanlde, and the current prefix of the document.
data St = St { printers :: !Printers',
               currentPrefix :: !([Printable] -> [Printable]) }
type Printers = Handle -> Printers'

-- | A set of printers to print different types of text to a handle.
data Printers' = Printers {colorP :: !(Color -> Printer),
                           invisibleP :: !Printer,
                           hiddenP :: !Printer,
                           userchunkP :: !Printer,
                           defP :: !Printer,
                           lineColorT :: !(Color -> Doc -> Doc),
                           lineColorS :: !([Printable] -> [Printable])
                          }
type Printer = Printable -> St -> Document

data Color = Blue | Red | Green | Cyan | Magenta

-- | 'Document' is a wrapper around '[Printable] -> [Printable]' which allows
-- for empty Documents. The simplest 'Documents' are built from 'String's
-- using 'text'.
data Document = Document ([Printable] -> [Printable])
              | Empty

-- | renders a 'Doc' into a 'String' with control codes for the
-- special features of the doc.
renderString :: RenderMode -> Doc -> String
renderString = renderStringWith simplePrinters'

-- | renders a 'Doc' into a 'String' using a given set of printers.
renderStringWith :: Printers' -> RenderMode -> Doc -> String
renderStringWith prs target d = concatMap (toString target) $ renderWith prs d
    where toString Standard (S s) = s
          toString Encode  (S s) = BC.unpack . encodeLocale $ s
          toString Standard (PS ps) = BC.unpack ps
          toString Encode  (PS ps) = BC.unpack ps
          toString Standard (Both s _) = s
          toString Encode  (Both s _) = BC.unpack . encodeLocale $ s

-- | renders a 'Doc' into 'B.ByteString' with control codes for the
-- special features of the Doc. See also 'readerString'.
renderPS :: RenderMode -> Doc -> B.ByteString
renderPS = renderPSWith simplePrinters'

-- | renders a 'Doc' into a list of 'PackedStrings', one for each line.
renderPSs :: RenderMode -> Doc -> [B.ByteString]
renderPSs = renderPSsWith simplePrinters'

-- | renders a doc into a 'B.ByteString' using a given set of printers.
renderPSWith :: Printers' -> RenderMode -> Doc -> B.ByteString
renderPSWith prs target d = B.concat $ renderPSsWith prs target d

-- | renders a 'Doc' into a list of 'PackedStrings', one for each
-- chunk of text that was added to the doc, using the given set of
-- printers.
renderPSsWith :: Printers' -> RenderMode -> Doc -> [B.ByteString]
renderPSsWith prs target d = map (toPS target) $ renderWith prs d
    where toPS Standard (S s)        = BC.pack s
          toPS Encode  (S s)        = encodeLocale s
          toPS Standard (PS ps)      = ps
          toPS Encode  (PS ps)      = ps
          toPS Standard (Both _ ps)  = ps
          toPS Encode  (Both _ ps)  = ps

-- | renders a 'Doc' into a list of 'Printables' using a set of
-- printers. Each item of the list corresponds to a string that was
-- added to the doc.
renderWith :: Printers' -> Doc -> [Printable]
renderWith ps (Doc d) = case d (initState ps) of
                        Empty -> []
                        Document f -> f []

initState :: Printers' -> St
initState prs = St { printers = prs, currentPrefix = id }

prefix :: String -> Doc -> Doc
prefix s (Doc d) = Doc $ \st ->
                   let p = S s
                       st' = st { currentPrefix = currentPrefix st . (p:) } in
                   case d st' of
                     Document d'' -> Document $ (p:) . d''
                     Empty -> Empty

-- TODO try to find another way to do this, it's rather a violation
-- of the Doc abstraction
prefixLines :: Doc -> Doc -> Doc
prefixLines prefixer prefixee =
   vcat $ map (prefixer <+>) $ map packedString $ linesPS $
   -- this will just get round-tripped back into a Doc,
   renderPS Standard prefixee

-- TODO try to find another way to do this, it's rather a violation
-- of the Doc abstraction
insertBeforeLastline :: Doc -> Doc -> Doc
insertBeforeLastline a b =
   -- as this will just get round-tripped back into a Doc,
   -- we use 'Standard' as the Target type so the encoding
   -- is left alone
   case reverse $ map packedString $ linesPS $ renderPS Standard a of
   (ll:ls) -> vcat (reverse ls) $$ b $$ ll
   [] -> error "empty Doc given as first argument of Printer.insert_before_last_line"


lineColor :: Color -> Doc -> Doc
lineColor c d = Doc $ \st -> case lineColorT (printers st) c d of
                             Doc d' -> d' st

hiddenPrefix :: String -> Doc -> Doc
hiddenPrefix s (Doc d) =
    Doc $ \st -> let pr = printers st
                     p = S (renderStringWith pr Standard $ hiddenText s)
                     st' = st { currentPrefix = currentPrefix st . (p:) }
                 in case d st' of
                      Document d'' -> Document $ (p:) . d''
                      Empty -> Empty

-- | 'unsafeBoth' builds a Doc from a 'String' and a 'B.ByteString' representing
-- the same text, but does not check that they do.
unsafeBoth :: String -> B.ByteString -> Doc
unsafeBoth s ps = Doc $ simplePrinter (Both s ps)

-- | 'unsafeBothText' builds a 'Doc' from a 'String'. The string is stored in the
-- Doc as both a String and a 'B.ByteString'.
unsafeBothText :: String -> Doc
unsafeBothText s = Doc $ simplePrinter (Both s (BC.pack s))

-- | 'packedString' builds a 'Doc' from a 'B.ByteString' using 'printable'
packedString :: B.ByteString -> Doc
packedString = printable . PS

-- | 'unsafePackedString' builds a 'Doc' from a 'B.ByteString' using 'simplePrinter'
unsafePackedString :: B.ByteString -> Doc
unsafePackedString = Doc . simplePrinter . PS

-- | 'invisiblePS' creates a 'Doc' with invisible text from a 'B.ByteString'
invisiblePS :: B.ByteString -> Doc
invisiblePS = invisiblePrintable . PS

-- | 'userchunkPS' creates a 'Doc' representing a user chunk from a 'B.ByteString'.
userchunkPS :: B.ByteString -> Doc
userchunkPS = userchunkPrintable . PS

-- | 'unsafeChar' creates a Doc containing just one character.
unsafeChar :: Char -> Doc
unsafeChar = unsafeText . (:"")

-- | 'text' creates a 'Doc' from a @String@, using 'printable'.
text :: String -> Doc
text = printable . S

-- | 'unsafeText' creates a 'Doc' from a 'String', using 'simplePrinter' directly
unsafeText :: String -> Doc
unsafeText = Doc . simplePrinter . S

-- | 'invisibleText' creates a 'Doc' containing invisible text from a @String@
invisibleText :: String -> Doc
invisibleText = invisiblePrintable . S

-- | 'hiddenText' creates a 'Doc' containing hidden text from a @String@
hiddenText :: String -> Doc
hiddenText = hiddenPrintable . S

-- | 'userchunk' creates a 'Doc' containing a user chunk from a @String@
userchunk :: String -> Doc
userchunk = userchunkPrintable . S

-- | 'blueText' creates a 'Doc' containing blue text from a @String@
blueText, redText, greenText, magentaText, cyanText :: String -> Doc
blueText = colorText Blue
redText = colorText Red
greenText = colorText Green
magentaText = colorText Magenta
cyanText = colorText Cyan

-- | 'colorText' creates a 'Doc' containing colored text from a @String@
colorText :: Color -> String -> Doc
colorText c = mkColorPrintable c . S

-- | @'wrapText' n s@ is a 'Doc' representing @s@ line-wrapped at 'n' characters
wrapText :: Int -> String -> Doc
wrapText n s =
    vcat . map text . reverse $ "" : foldl add_to_line [] (words s)
  where add_to_line [] a = [a]
        add_to_line ("":d) a = a:d
        add_to_line (l:ls) new | length l + length new > n = new:l:ls
        add_to_line (l:ls) new = (l ++ " " ++ new):ls

-- | 'printable x' creates a 'Doc' from any 'Printable'.
printable, invisiblePrintable, hiddenPrintable, userchunkPrintable :: Printable -> Doc
printable x = Doc $ \st -> defP (printers st) x st

mkColorPrintable :: Color -> Printable -> Doc
mkColorPrintable c x = Doc $ \st -> colorP (printers st) c x st
invisiblePrintable x = Doc $ \st -> invisibleP (printers st) x st
hiddenPrintable x = Doc $ \st -> hiddenP (printers st) x st
userchunkPrintable x = Doc $ \st -> userchunkP (printers st) x st

-- | 'simplePrinters' is a 'Printers' which uses the set 'simplePriners\'' on any
-- handle.
simplePrinters :: Printers
simplePrinters _ = simplePrinters'

-- | A set of default printers suitable for any handle. Does not use color.
simplePrinters' :: Printers'
simplePrinters'  = Printers { colorP = const simplePrinter,
                              invisibleP = simplePrinter,
                              hiddenP = invisiblePrinter,
                              userchunkP = simplePrinter,
                              defP = simplePrinter,
                              lineColorT = const id,
                              lineColorS = id
                            }

-- | 'simplePrinter' is the simplest 'Printer': it just concatenates together
-- the pieces of the 'Doc'
simplePrinter :: Printer
simplePrinter x = unDoc $ doc (\s -> x:s)

-- | 'invisiblePrinter' is the 'Printer' for hidden text. It just replaces
-- the document with 'empty'.  It's useful to have a printer that doesn't
-- actually do anything because this allows you to have tunable policies,
-- for example, only printing some text if it's to the terminal, but not
-- if it's to a file or vice-versa.
invisiblePrinter :: Printer
invisiblePrinter _ = unDoc empty

infixr 6 <>
infixr 6 <+>
infixr 5 $$

-- | The empty 'Doc'.
empty :: Doc
empty = Doc $ const Empty
doc :: ([Printable] -> [Printable]) -> Doc
doc f = Doc $ const $ Document f

-- | '(<>)' is the concatenation operator for 'Doc's
(<>) :: Doc -> Doc -> Doc
-- | @a '<?>' b@ is @a <> b@ if @a@ is not empty, else empty.
(<?>) :: Doc -> Doc -> Doc
-- | @a '<+>' b@ is @a@ followed by a space, then @b@.
(<+>) :: Doc -> Doc -> Doc
-- | @a '$$' b@ is @a@ above @b@.
($$) :: Doc -> Doc -> Doc
-- a then b
Doc a <> Doc b =
   Doc $ \st -> case a st of
                Empty -> b st
                Document af ->
                    Document (\s -> af $ case b st of
                                         Empty -> s
                                         Document bf -> bf s)

-- empty if a empty, else a then b
Doc a <?> Doc b =
    Doc $ \st -> case a st of
                 Empty -> Empty
                 Document af -> Document (\s -> af $ case b st of
                                                     Empty -> s
                                                     Document bf -> bf s)

-- a then space then b
Doc a <+> Doc b =
    Doc $ \st -> case a st of
                 Empty -> b st
                 Document af -> Document (\s -> af $ case b st of
                                                     Empty -> s
                                                     Document bf ->
                                                         spaceP:bf s)

-- a above b
Doc a $$ Doc b =
   Doc $ \st -> case a st of
                Empty -> b st
                Document af ->
                    Document (\s -> af $ case b st of
                                         Empty -> s
                                         Document bf -> sf (newlineP:pf (bf s)))
                        where pf = currentPrefix st
                              sf = lineColorS $ printers st

-- | 'vcat' piles vertically a list of 'Doc's.
vcat :: [Doc] -> Doc
vcat [] = empty
vcat ds = foldr1 ($$) ds

-- | 'vsep' piles vertically a list of 'Doc's leaving a blank line between each.
vsep :: [Doc] -> Doc
vsep [] = empty
vsep ds = foldr1 ($$) $ intersperse (text "") ds

-- | 'hcat' concatenates (horizontally) a list of 'Doc's
hcat :: [Doc] -> Doc
hcat [] = empty
hcat ds = foldr1 (<>) ds