{-# LANGUAGE TupleSections #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Compact pretty-printer.
--
-- == Examples
--
-- Assume that we want to pretty print S-Expressions, which can either be atom or a list of S-Expressions.
--
-- >>> data SExpr = SExpr [SExpr] | Atom String deriving Show
-- >>> let pretty :: SExpr -> Doc (); pretty (Atom s) = text s; pretty (SExpr xs) = text "(" <> (sep $ map pretty xs) <> text ")"
--
-- Using the above representation, the S-Expression @(a b c d)@ has the following encoding:
--
-- >>> let abcd = SExpr [Atom "a",Atom "b",Atom "c",Atom "d"]
--
-- The legible layouts of the @abcd@ S-Expression defined above would be either
--
-- >>> putStrLn $ render $ pretty abcd
-- (a b c d)
--
-- or
--
-- >>> putStrLn $ renderWith defaultOptions { optsPageWidth = 5 } $ pretty abcd
-- (a
--  b
--  c
--  d)
--
-- The @testData@ S-Expression is specially crafted to
-- demonstrate general shortcomings of both Hughes and Wadler libraries.
--
-- >>> let abcd4 = SExpr [abcd,abcd,abcd,abcd]
-- >>> let testData = SExpr [ SExpr [Atom "abcde", abcd4], SExpr [Atom "abcdefgh", abcd4]]
-- >>> putStrLn $ render $ pretty testData
-- ((abcde ((a b c d) (a b c d) (a b c d) (a b c d)))
--  (abcdefgh ((a b c d) (a b c d) (a b c d) (a b c d))))
--
-- on 20-column-wide page
--
-- >>> putStrLn $ renderWith defaultOptions { optsPageWidth = 20 } $ pretty testData
-- ((abcde ((a b c d)
--          (a b c d)
--          (a b c d)
--          (a b c d)))
--  (abcdefgh
--   ((a b c d)
--    (a b c d)
--    (a b c d)
--    (a b c d))))
--
-- Yet, neither Hughes' nor Wadler's library can deliver those results.
--
-- === Annotations
--
-- For example we can annotate every /car/ element of S-Expressions,
-- and in the rendering phase emphasise them by rendering them in uppercase.
--
-- >>> let pretty' :: SExpr -> Doc Any; pretty' (Atom s) = text s; pretty' (SExpr []) = text "()"; pretty' (SExpr (x:xs)) = text "(" <> (sep $ annotate (Any True) (pretty' x) : map pretty' xs) <> text ")"
-- >>> let render' = renderWith defaultOptions { optsAnnotate  = \a x -> if a == Any True then map toUpper x else x }
-- >>> putStrLn $ render' $ pretty' testData
-- ((ABCDE ((A B C D) (A B C D) (A B C D) (A B C D)))
--  (ABCDEFGH ((A B C D) (A b c d) (A b c d) (A b c d))))
--
module Text.PrettyPrint.Compact (
   -- * Documents
   Doc,

   -- * Basic combinators
   module Data.Monoid, text, flush, char,

   hang, hangWith, encloseSep, list, tupled, semiBraces,

   -- * Operators
   (<+>), ($$), (</>), (<//>), (<$$>),

   -- * List combinators
   hsep, sep, hcat, vcat, cat, punctuate,

   -- * Fill combiantors
   -- fillSep, fillCat,

   -- * Bracketing combinators
   enclose, squotes, dquotes, parens, angles, braces, brackets,

   -- * Character documents
   lparen, rparen, langle, rangle, lbrace, rbrace, lbracket, rbracket,
   squote, dquote, semi, colon, comma, space, dot, backslash, equals,

   -- * Primitive type documents
   string, int, integer, float, double, rational,
   bool,

   -- * Rendering
   renderWith,
   render,
   Options(..),
   defaultOptions,

   -- * Annotations
   annotate,

   -- * Undocumented
   -- column, nesting, width
   ) where

import Data.Monoid

import Text.PrettyPrint.Compact.Core as Text.PrettyPrint.Compact

-- | Render the 'Doc' into 'String' omitting all annotations.
render :: Annotation a => Doc a -> String
render = renderWith defaultOptions

defaultOptions :: Options a String
defaultOptions = Options
    { optsAnnotate = \_ s -> s
    , optsPageWidth = 80
    }

-- | The document @(list xs)@ comma separates the documents @xs@ and
-- encloses them in square brackets. The documents are rendered
-- horizontally if that fits the page. Otherwise they are aligned
-- vertically. All comma separators are put in front of the elements.
list :: Annotation a => [Doc a] -> Doc a
list            = encloseSep lbracket rbracket comma

-- | The document @(tupled xs)@ comma separates the documents @xs@ and
-- encloses them in parenthesis. The documents are rendered
-- horizontally if that fits the page. Otherwise they are aligned
-- vertically. All comma separators are put in front of the elements.
tupled :: Annotation a => [Doc a] -> Doc a
tupled          = encloseSep lparen   rparen  comma


-- | The document @(semiBraces xs)@ separates the documents @xs@ with
-- semi colons and encloses them in braces. The documents are rendered
-- horizontally if that fits the page. Otherwise they are aligned
-- vertically. All semi colons are put in front of the elements.
semiBraces :: Annotation a => [Doc a] -> Doc a
semiBraces      = encloseSep lbrace   rbrace  semi

-- | The document @(enclosure l r sep xs)@ concatenates the documents
-- @xs@ separated by @sep@ and encloses the resulting document by @l@
-- and @r@. The documents are rendered horizontally if that fits the
-- page. Otherwise they are aligned vertically. All separators are put
-- in front of the elements. For example, the combinator 'list' can be
-- defined with @enclosure@:
--
-- > list xs = enclosure lbracket rbracket comma xs
-- > test    = text "list" <+> (list (map int [10,200,3000]))
--
-- Which is layed out with a page width of 20 as:
--
-- @
-- list [10,200,3000]
-- @
--
-- But when the page width is 15, it is layed out as:
--
-- @
-- list [10
--      ,200
--      ,3000]
-- @
encloseSep :: Annotation a => Doc a -> Doc a -> Doc a -> [Doc a] -> Doc a
encloseSep left right separator ds
    = (<> right) $ case ds of
        []  -> left
        [d] -> left <> d
        (d:ds') -> cat (left <> d:map (separator <>) ds')

-----------------------------------------------------------
-- punctuate p [d1,d2,...,dn] => [d1 <> p,d2 <> p, ... ,dn]
-----------------------------------------------------------


-- | @(punctuate p xs)@ concatenates all documents in @xs@ with
-- document @p@ except for the last document.
--
-- > someText = map text ["words","in","a","tuple"]
-- > test     = parens (align (cat (punctuate comma someText)))
--
-- This is layed out on a page width of 20 as:
--
-- @
-- (words,in,a,tuple)
-- @
--
-- But when the page width is 15, it is layed out as:
--
-- @
-- (words,
--  in,
--  a,
--  tuple)
-- @
--
-- (If you want put the commas in front of their elements instead of
-- at the end, you should use 'tupled' or, in general, 'encloseSep'.)
punctuate :: Annotation a => Doc a -> [Doc a] -> [Doc a]
punctuate _p []      = []
punctuate _p [d]     = [d]
punctuate p (d:ds)  = (d <> p) : punctuate p ds


-----------------------------------------------------------
-- high-level combinators
-----------------------------------------------------------


-- | The document @(sep xs)@ concatenates all documents @xs@ either
-- horizontally with @(\<+\>)@, if it fits the page, or vertically
-- with @(\<$$\>)@. Documents on the left of horizontal concatenation
-- must fit on a single line.
--
sep :: Annotation a => [Doc a] -> Doc a
sep xs = groupingBy " " (map (0,) xs)


-- -- | The document @(fillSep xs)@ concatenates documents @xs@
-- -- horizontally with @(\<+\>)@ as long as its fits the page, than
-- -- inserts a @line@ and continues doing that for all documents in
-- -- @xs@.
-- --
-- -- > fillSep xs  = foldr (\<\/\>) empty xs
-- fillSep :: Annotation a => [Doc a] -> Doc a
-- fillSep         = foldDoc (</>)

-- | The document @(hsep xs)@ concatenates all documents @xs@
-- horizontally with @(\<+\>)@.
hsep :: Annotation a => [Doc a] -> Doc a
hsep            = foldDoc (<+>)

-- | The document @(cat xs)@ concatenates all documents @xs@ either
-- horizontally with @(\<\>)@, if it fits the page, or vertically with
-- @(\<$$\>)@.
--
cat :: Annotation a => [Doc a] -> Doc a
cat xs = groupingBy "" (map (0,) xs)

-- -- | The document @(fillCat xs)@ concatenates documents @xs@
-- -- horizontally with @(\<\>)@ as long as its fits the page, than inserts
-- -- a @linebreak@ and continues doing that for all documents in @xs@.
-- --
-- -- > fillCat xs  = foldr (\<\/\/\>) empty xs
-- fillCat :: Annotation a => [Doc a] -> Doc a
-- fillCat         = foldDoc (<//>)

-- | The document @(hcat xs)@ concatenates all documents @xs@
-- horizontally with @(\<\>)@.
hcat :: Annotation a => [Doc a] -> Doc a
hcat            = foldDoc (<>)

-- | The document @(vcat xs)@ concatenates all documents @xs@
-- vertically with @($$)@.
vcat :: Annotation a => [Doc a] -> Doc a
vcat            = foldDoc ($$)

foldDoc :: Annotation a => (Doc a -> Doc a -> Doc a) -> [Doc a] -> Doc a
foldDoc _ []       = mempty
foldDoc f ds       = foldr1 f ds

-- | The document @(x \<+\> y)@ concatenates document @x@ and @y@ with a
-- @space@ in between.  (infixr 6)
(<+>) :: Annotation a => Doc a -> Doc a -> Doc a
x <+> y         = x <> space <> y

-- | The document @(x \<\/\> y)@ puts @x@ and @y@ either next to each other
-- (with a @space@ in between) or underneath each other. (infixr 5)
(</>) :: Annotation a => Doc a -> Doc a -> Doc a
x </> y         = hang 0 x y

-- | The document @(x \<\/\/\> y)@ puts @x@ and @y@ either right next
-- to each other (if @x@ fits on a single line) or underneath each
-- other. (infixr 5)
(<//>) :: Annotation a => Doc a -> Doc a -> Doc a
x <//> y        = hangWith "" 0 x y

-- | The document @(x \<$$\> y)@ concatenates document @x@ and @y@ with
-- a linebreak in between. (infixr 5)
(<$$>) :: Annotation a => Doc a -> Doc a -> Doc a
(<$$>) = ($$)

-- | Document @(squotes x)@ encloses document @x@ with single quotes
-- \"'\".
squotes :: Annotation a => Doc a -> Doc a
squotes         = enclose squote squote

-- | Document @(dquotes x)@ encloses document @x@ with double quotes
-- '\"'.
dquotes :: Annotation a => Doc a -> Doc a
dquotes         = enclose dquote dquote

-- | Document @(braces x)@ encloses document @x@ in braces, \"{\" and
-- \"}\".
braces :: Annotation a => Doc a -> Doc a
braces          = enclose lbrace rbrace

-- | Document @(parens x)@ encloses document @x@ in parenthesis, \"(\"
-- and \")\".
parens :: Annotation a => Doc a -> Doc a
parens          = enclose lparen rparen

-- | Document @(angles x)@ encloses document @x@ in angles, \"\<\" and
-- \"\>\".
angles :: Annotation a => Doc a -> Doc a
angles          = enclose langle rangle

-- | Document @(brackets x)@ encloses document @x@ in square brackets,
-- \"[\" and \"]\".
brackets :: Annotation a => Doc a -> Doc a
brackets        = enclose lbracket rbracket

-- | The document @(enclose l r x)@ encloses document @x@ between
-- documents @l@ and @r@ using @(\<\>)@.
enclose :: Annotation a => Doc a -> Doc a -> Doc a -> Doc a
enclose l r x   = l <> x <> r

char :: Annotation a => Char -> Doc a
char x = text [x]

-- | The document @lparen@ contains a left parenthesis, \"(\".
lparen :: Annotation a => Doc a
lparen          = char '('
-- | The document @rparen@ contains a right parenthesis, \")\".
rparen :: Annotation a => Doc a
rparen          = char ')'
-- | The document @langle@ contains a left angle, \"\<\".
langle :: Annotation a => Doc a
langle          = char '<'
-- | The document @rangle@ contains a right angle, \">\".
rangle :: Annotation a => Doc a
rangle          = char '>'
-- | The document @lbrace@ contains a left brace, \"{\".
lbrace :: Annotation a => Doc a
lbrace          = char '{'
-- | The document @rbrace@ contains a right brace, \"}\".
rbrace :: Annotation a => Doc a
rbrace          = char '}'
-- | The document @lbracket@ contains a left square bracket, \"[\".
lbracket :: Annotation a => Doc a
lbracket        = char '['
-- | The document @rbracket@ contains a right square bracket, \"]\".
rbracket :: Annotation a => Doc a
rbracket        = char ']'


-- | The document @squote@ contains a single quote, \"'\".
squote :: Annotation a => Doc a
squote          = char '\''
-- | The document @dquote@ contains a double quote, '\"'.
dquote :: Annotation a => Doc a
dquote          = char '"'
-- | The document @semi@ contains a semi colon, \";\".
semi :: Annotation a => Doc a
semi            = char ';'
-- | The document @colon@ contains a colon, \":\".
colon :: Annotation a => Doc a
colon           = char ':'
-- | The document @comma@ contains a comma, \",\".
comma :: Annotation a => Doc a
comma           = char ','

-- | The document @dot@ contains a single dot, \".\".
dot :: Annotation a => Doc a
dot             = char '.'
-- | The document @backslash@ contains a back slash, \"\\\".
backslash :: Annotation a => Doc a
backslash       = char '\\'
-- | The document @equals@ contains an equal sign, \"=\".
equals :: Annotation a => Doc a
equals          = char '='

-----------------------------------------------------------
-- Combinators for prelude types
-----------------------------------------------------------

-- string is like "text" but replaces '\n' by "line"

-- | The document @(string s)@ concatenates all characters in @s@
-- using @line@ for newline characters and @char@ for all other
-- characters. It is used instead of 'text' whenever the text contains
-- newline characters.
string :: Annotation a => String -> Doc a
string = vcat . map text . lines

bool :: Annotation a => Bool -> Doc a
bool b          = text (show b)

-- | The document @(int i)@ shows the literal integer @i@ using
-- 'text'.
int :: Annotation a => Int -> Doc a
int i           = text (show i)

-- | The document @(integer i)@ shows the literal integer @i@ using
-- 'text'.
integer :: Annotation a => Integer -> Doc a
integer i       = text (show i)

-- | The document @(float f)@ shows the literal float @f@ using
-- 'text'.
float :: Annotation a => Float -> Doc a
float f         = text (show f)

-- | The document @(double d)@ shows the literal double @d@ using
-- 'text'.
double :: Annotation a => Double -> Doc a
double d        = text (show d)

-- | The document @(rational r)@ shows the literal rational @r@ using
-- 'text'.
rational :: Annotation a => Rational -> Doc a
rational r      = text (show r)



-- | The hang combinator implements hanging indentation. The document
-- @(hang i x y)@ either @x@ and @y@ concatenated with @\<+\>@ or @y@
-- below @x@ with an additional indentation of @i@.

hang :: Annotation a => Int -> Doc a -> Doc a -> Doc a
hang = hangWith " "


-- | The hang combinator implements hanging indentation. The document
-- @(hang separator i x y)@ either @x@ and @y@ concatenated with @\<\>
-- text separator \<\>@ or @y@ below @x@ with an additional
-- indentation of @i@.
hangWith :: Annotation a => String -> Int -> Doc a -> Doc a -> Doc a
hangWith separator n x y = groupingBy separator [(0,x), (n,y)]

space :: Annotation a => Doc a
space = text " "

-- $setup
-- >>> import Data.Monoid
-- >>> import Data.Char