-- Simple and efficient printing combinators
module PPrint where

infixr 2 &,!
infixr 1 !/

data Output = Sep | Str String | Nl | Indent Int

newtype Document = P ([Output] -> [Output])

class Printable a where
  pr :: a -> Document
  prList :: [a] -> Document

  prList = wpr

instance Printable Document where
  pr = id

nil = P id

nl = P (Nl:)
sep = P (Sep:)

indented x = indented' 2 x
indented' n x = P (Indent n:) ! x ! P (Indent (-n):)

vpr xs = foldr (!/) nil xs
wpr xs = prsep sep xs
hpr xs = foldr (!) nil xs

vmap f = foldr ((!/) . f) nil
wmap f xs = wpr (map f xs)
hmap f = foldr ((!) . f) nil

x !/ y = x ! nl ! y

x ! y = comp (pr x) (pr y)
  where comp (P x) (P y) = P (x . y)

x & y = x ! sep ! y

prsep s [] = nil
prsep s (x:xs) = x ! prpre s xs

prpre s [] = nil
prpre s (x:xs) = s ! x ! prpre s xs

instance Printable Char where
  pr c = P (Str [c]:)
  prList s = P (Str s:)

instance Printable a => Printable [a] where
  pr = prList

instance Printable Int where
  pr x = pr (show (x `asTypeOf` 1))

{-
instance Printable a => Printable (Maybe a) where
  pr Nothing = nil
  pr (Just x) = pr x
-}

pprint x = fmt0 0 (apply (pr x) [])
  where
    apply (P pr) = pr

    -- The printer is designed to avoid producing redundant spaces:
    --  + No indentation space on blank lines.
    --  + No trailing spaces at the end of lines.
    --  + No double spaces between items.
    
    -- fmt0: at the beginning of a line, before indentation has been made
    fmt0 n [] = []
    fmt0 n (Nl:os) = "\n"++fmt0 n os
    fmt0 n (Indent i:os) = fmt0 (n+i) os
    fmt0 n (Sep:os) = fmt0 n os
    fmt0 n (Str s:os) = space n++s++fmt n os

    space n = replicate (n `div` 8) '\t' ++ replicate (n `mod` 8) ' '

    -- fmt: in the middle of a line, after indentation and some text
    fmt n [] = []
    fmt n (o:os) =
      case o of
	Str s -> s++fmt n os
	Nl -> "\n"++fmt0 n os
	Indent i -> fmt (n+i) os
	Sep -> fmt1 n os

    -- fmt1: in the middle of a line, a space is to be inserted before next item
    fmt1 n [] = []
    fmt1 n (o:os) =
      case o of
        Str s -> ' ':s++fmt n os
	Nl -> "\n"++fmt0 n os
	Indent i -> fmt1 (n+i) os
	Sep -> fmt1 n os