    -- | 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]