-- |
-- Module      : Test.FitSpec.PrettyPrint
-- Copyright   : (c) 2015-2017 Rudy Matela
-- License     : 3-Clause BSD  (see the file LICENSE)
-- Maintainer  : Rudy Matela <rudy@matela.com.br>
--
-- A very simple pretty printing library used to generate 'Test.FitSpec' reports.
module Test.FitSpec.PrettyPrint
  ( beside
  , above
  , showTuple
  , table
  , columns
  , showQuantity
  , showEach
  , headToUpper
  )
where
-- TODO: Fix somewhat inefficient implementations, i.e.: heavy use of '(++)'.

import Data.List (intercalate,transpose,isSuffixOf)
import Data.Char (toUpper)

showQuantity :: Int -> String -> String
showQuantity 1 what = "1 " ++ what
showQuantity n what = show n ++ " " ++ pluralize what

showEach :: Show a => String -> [a] -> String
showEach what [x] = what ++ " " ++ show x
showEach what xs  = "each of " ++ pluralize what ++ " "
                 ++ intercalate ", " (map show $ init xs)
                 ++ " and "
                 ++ show (last xs)

-- | Pluralizes a word.
-- Is not comprehensive (and may never be),
-- add missing cases as they are found.
--
-- > pluralize "test case" == "test cases"
-- > pluralize "property"  == "properties"
pluralize :: String -> String
pluralize s | s `ew` "se" = s ++ "s"
            | s `ew` "n"  = s ++ "s"
            | s `ew` "y"  = init s ++ "ies"
            | otherwise   = s
  where ew = flip isSuffixOf


-- | Appends two Strings side by side, line by line
--
-- > beside ["asdf\nqw\n","zxvc\nas"] ==
-- >  "asdfzxvc\n\
-- >  \qw  as\n"
beside :: String -> String -> String
beside cs ds = unlines $ zipWith (++) (normalize ' ' css) dss
  where [css,dss] = normalize "" [lines cs,lines ds]

-- | Append two Strings on top of each other, adding line breaks *when needed*.
above :: String -> String -> String
above cs ds = if last cs == '\n' || head ds == '\n'
                then cs ++ ds
                else cs ++ '\n':ds

-- | Show elements of a list as a tuple.  If there are multiple lines in any of
--   the strings, tuple is printed multiline.
--
-- > showTuple ["asdf\nqwer\n","zxvc\nasdf\n"] ==
-- >   "( asdf\n\
-- >   \  qwer\n\
-- >   \, zxvc\n\
-- >   \  asdf )\n"
--
-- > showTuple ["asdf","qwer"] == "(asdf,qwer)"
showTuple :: [String] -> String
showTuple []  = ""
showTuple [s] = s
showTuple (s:ss) =
  if any ('\n' `elem`) (s:ss)
    then "( " `beside` s
      ++ init (concatMap (", " `beside`) ss)
      ++ " )\n"
    else "(" ++ intercalate "," (s:ss) ++ ")"

-- | Formats a table using a given separator.
--
-- > table "  " [ ["asdf", "qwer",     "zxvc\nzxvc"]
-- >            , ["0",    "1",        "2"]
-- >            , ["123",  "456\n789", "3"] ] ==
-- >   "asdf  qwer  zxvc\n\
-- >   \            zxvc\n\
-- >   \0     1     2\n\
-- >   \123   456   3\n\
-- >   \      789\n\"
table :: String -> [[String]] -> String
table s []  = ""
table s sss = unlines
            . map (removeTrailing ' ')
            . map (intercalate s)
            . transpose
            . map (normalize ' ')
            . foldr1 (zipWith (++))
            . map (normalize "" . map lines)
            . normalize ""
            $ sss

-- | Given a separator, format strings in columns
--
-- > columns " | " ["asdf", "qw\nzxcv", "as\ndf"] ==
-- >   "asdf | qw   | as\n\
-- >   \     | zxcv | df\n"
columns :: String -> [String] -> String
columns s = unlines
          . map (removeTrailing ' ')
          . map (intercalate s)
          . transpose
          . map (normalize ' ')
          . normalize ""
          . map lines

-- | Fits a list to a certain width by appending a certain value
--
-- > fit ' ' 6 "str" == "str   "
--
-- > fit 0 6 [1,2,3] == [1,2,3,0,0,0]
fit :: a -> Int -> [a] -> [a]
fit x n xs = xs ++ replicate (n - length xs) x

-- | normalize makes all list the same length by adding a value
--
-- > normalize ["asdf","qw","er"] == normalize ["asdf","qw  ","er  "]
normalize :: a -> [[a]] -> [[a]]
normalize x xs = map (x `fit` maxLength xs) xs

-- | Given a list of lists returns the maximum length
maxLength :: [[a]] -> Int
maxLength = maximum . (0:) . map length

removeTrailing :: Eq a => a -> [a] -> [a]
removeTrailing x = reverse
                 . dropWhile (==x)
                 . reverse

headToUpper :: [Char] -> [Char]
headToUpper [] = []
headToUpper (c:cs) = toUpper c : cs