
-- | A module to represent text with very basic formatting. Values are of
--   type ['Text'] and shown with 'showText'.
--
--   As an example of the formatting:
--
-- > [Line "Cooking for hungry people."
-- > ,Line "Welcome to my cookery recipe program, I sure hope you enjoy using it!"
-- > ,Line ""
-- > ,Cols ["Omlette","  A tasty eggy treat."]
-- > ,Cols ["  -m"," --mushrooms","  Some mushrooms, or in fact any other ingredients you have in the cupboards"]
-- > ,Cols ["  -e"," --eggs", "  But always you need eggs"]
-- > ,Line ""
-- > ,Cols ["Spagetti Bolognaise", "  An Italian delight."]
-- > ,Cols ["  -s"," --spagetti","  The first word in the name"]
-- > ,Cols ["  -b"," --bolognaise","  The second word in the name"]
-- > ,Cols ["  -d"," --dolmio","  The magic ingredient!"]
-- > ,Line ""
-- > ,Line "    The author of this program explicitly disclaims any liability for poisoning people who get their recipes off the internet."]
--
--   With @putStrLn ('showText' ('Wrap' 50) demo)@ gives:
--
-- > Cooking for hungry people.
-- > Welcome to my cookery recipe program, I sure hope
-- > you enjoy using it!
-- >
-- > Omlette              A tasty eggy treat.
-- >   -m --mushrooms   Some mushrooms, or in fact
-- >                    any other ingredients you have
-- >                    in the cupboards
-- >   -e --eggs        But always you need eggs
-- >
-- > Spagetti Bolognaise  An Italian delight.
-- >   -s --spagetti    The first word in the name
-- >   -b --bolognaise  The second word in the name
-- >   -d --dolmio      The magic ingredient!
-- >
-- >     The author of this program explicitly
-- >     disclaims any liability for poisoning people
-- >     who get their recipes off the internet.
module System.Console.CmdArgs.Text(TextFormat(..), defaultWrap, Text(..), showText) where

import Data.Char
import Data.Function
import Data.List
import Data.Maybe
import System.Console.CmdArgs.Default


-- | Wrap with the default width of 80 characters.
defaultWrap :: TextFormat
defaultWrap = Wrap 80

-- | How to output the text.
data TextFormat = HTML -- ^ Display as HTML.
                | Wrap Int -- ^ Display as text wrapped at a certain width (see 'defaultWrap').
                  deriving (Read,Show,Eq,Ord)

instance Default TextFormat where def = defaultWrap

-- | The data type representing some text, typically used as @[Text]@. The formatting
--   is described by:
--
--   * 'Line' values represent a paragraph of text, and may be wrapped depending on the 'TextFormat'.
--     If a 'Line' value is wrapped then all leading space will be treated as an indent.
--
--   * 'Cols' values represent columns of text. Within any @[Text]@ all columns of the same length
--     are grouped in tabs, with the final column being wrapped if necessary. All columns are placed
--     adjacent with no space between them - for this reason most columns will start with a space.
data Text = Line String -- a single line
          | Cols [String] -- a single line with columns (always indented by 2 spaces)

instance Show Text where
    showList = showString . showText defaultWrap
    show x = showText defaultWrap [x]


-- | Show some text using the given formatting.
showText :: TextFormat -> [Text] -> String
showText HTML = showHTML
showText (Wrap x) = showWrap x


---------------------------------------------------------------------
-- TEXT OUTPUT

showWrap :: Int -> [Text] -> String
showWrap width xs = unlines $ concatMap f xs
    where
        cs :: [(Int,[Int])]
        cs = map (\x -> (fst $ head x, map maximum $ transpose $ map snd x)) $
                groupBy ((==) `on` fst) $ sortBy (compare `on` fst)
                [(length x, map length $ init x) | Cols x <- xs]
        pad n x = x ++ replicate (n - length x) ' '

        f (Line x) = map (a++) $ wrap1 (width - length a) b
            where (a,b) = span isSpace x

        f (Cols xs) = concat (zipWith pad ys xs ++ [z1]) : map (replicate n ' '++) zs
            where ys = fromJust $ lookup (length xs) cs
                  n = sum ys + length (takeWhile isSpace $ last xs)
                  z1:zs = wrap1 (width - n) (last xs)


wrap1 width x = ["" | null res] ++ res
    where res = wrap width x

-- | Split the text into strips of no-more than the given width
wrap :: Int -> String -> [String]
wrap width = concatMap (combine . split) . lines
    where
        split :: String -> [(String,Int)] -- string, amount of space after
        split "" = []
        split x = (a,length c) : split d
            where (a,b) = break isSpace x
                  (c,d) = span isSpace b

        -- combine two adjacent chunks while they are less than width
        combine :: [(String,Int)] -> [String]
        combine ((a,b):(c,d):xs) | length a + b + length c < width = combine $ (a ++ replicate b ' ' ++ c,d):xs
        combine (x:xs) = fst x : combine xs
        combine [] = []


---------------------------------------------------------------------
-- HTML OUTPUT

showHTML :: [Text] -> String
showHTML xs = unlines $
    ["<table class='cmdargs'>"] ++
    map f xs ++
    ["</table>"]
    where
        maxCols = maximum [length x | Cols x <- xs]

        f (Line x) = tr $ td maxCols x
        f (Cols xs) = tr $ concatMap (td 1) (init xs) ++ td (maxCols + 1 - length xs) (last xs)

        tr x = "<tr>" ++ x ++ "</tr>"
        td cols x = "<td" ++ (if cols == 1 then "" else " colspan='" ++ show cols ++ "'")
                          ++ (if null styles then "" else " style='" ++ unwords styles ++ "'") ++
                     ">" ++ if null b then "&nbsp;" else concatMap esc b ++ "</td>"
            where (a,b) = span isSpace x
                  -- if the first letter of the contents is '-', assume this is a flag
                  -- and be aware that HTML might try to line-break it, see #39
                  isFlag = take 1 b == "-"
                  styles = [ "padding-left:" ++ show (length a) ++ "ex;" | a /= "" ]
                        ++ [ "white-space:nowrap;" | isFlag ]

        esc '&' = "&amp;"
        esc '>' = "&gt;"
        esc '<' = "&lt;"
        esc '\n' = "<br />"
        esc x = [x]
