-- | This library is for drawing text tables.
--
-- Pass a 2D-list of strings and get a single string with table contents.
--
-- @
-- makeDefaultSimpleTable :: [[String]] -> String
-- @
module Text.SimpleTableGenerator (
  makeSimpleTable,
  makeDefaultSimpleTable,
  --
  SimpleTableConfig (..),
  --
  simpleTableConfig,
  --
  simpleTableLeftPad,
  simpleTableCenterPad,
  simpleTableRightPad,
  --
  simpleTableBottomPad,
  simpleTableMiddlePad,
  simpleTableTopPad,
  ) where

import Data.List.Split (splitOn)
import Data.List (transpose)

-- types:
type CellLine = String  -- cells can be multiline
type Cell  = [CellLine]
type Row   = [Cell]
type Table = [Row]
type CellSize  = (Int, Int)
type CellSizeTable = [[CellSize]]
type TextTable = [[String]]

-- wouldn't be exported
data CellWrapper =
  CellWrapper {
    cell :: Cell,
    rowNum :: Int, colNum :: Int, cellWidth :: Int, cellHeight :: Int,
    topLeft :: String, top :: String, topRight :: String, right :: String, bottomRight :: String,
    bottom :: String, bottomLeft :: String, left :: String
  } deriving (Show)

-- | Data type that represents table configuration.
data SimpleTableConfig =
  SimpleTableConfig {
  -- | String containing table border characters, in order like this: "┌┬┐├┼┤└┴┘─│".
  -- Must be exactly 11 characters long, otherwise error will be thrown.
  tableBorders :: String,
  -- | Minimum widths for each column, from left to right. Padding size is not counted.
  -- List's length may not match the actual count of columns in a table.
  colMinWidths :: [Int],
  -- | Minimum heights for each row, from top to bottom. Padding size is not counted.
  -- List's length may not match the actual count of rows in a table.
  rowMinHeights :: [Int],
  padFunction :: String -> Int -> String -> String,
  cellPadFunction :: String -> Int -> [String] -> [String],
  -- | Width of left and right margins.
  horizontalPadding :: Int,
  -- | Height of top and bottom margins.
  verticalPadding :: Int,
  -- | String used as padding. \" \" (space) by default.
  paddingStr :: String,
  -- | String used to fill in empty cells. \"\" by default.
  -- Empty cells will be padded with 'paddingStr' after placing 'emptyCellStr' into each them.
  emptyCellStr :: String
  }

-- | Default table config.
simpleTableConfig =
  SimpleTableConfig {
  tableBorders = "┌┬┐├┼┤└┴┘─│",
  colMinWidths  = [],
  rowMinHeights = [],
  padFunction   = simpleTableRightPad,
  cellPadFunction = simpleTableBottomPad,
  horizontalPadding = 1,
  verticalPadding = 0,
  paddingStr = " ",
  emptyCellStr = ""
  }

-- | Create table using 'simpleTableConfig'.
-- Example usage:
--
-- @
-- putStrLn $ makeDefaultSimpleTable [["1","2","3"], [\"One\",\"Two\",\"Three\"], [\"First\", \"Second\"]]
-- @
makeDefaultSimpleTable table =
  makeSimpleTable simpleTableConfig table

-- | Example usage:
--
-- @
-- putStrLn $ makeSimpleTable simpleTableConfig {
--   tableBorders = "+++++++++-|",
--   colMinWidths  = [3, 4],
--   rowMinHeights = [2],
--   padFunction   = 'simpleTableLeftPad',
--   cellPadFunction = 'simpleTableBottomPad',
--   horizontalPadding = 0,
--   verticalPadding = 1,
--   paddingStr = ".,`"
-- } [["a"], ["b", "c"]]
-- @
makeSimpleTable :: SimpleTableConfig -> [[String]] -> String
makeSimpleTable config table =
    showTable $
    map2d cell $
    appendBorders $
    normalizeBorderLengths $
    wrapTable processedConfig $
    padTableCells processedConfig $
    makeCells $
    normalizeColumnCount processedConfig $ table
    where
      processedConfig =
        constructPaddingFunctions $ validateConfig config

-- | Put something in empty cells
normalizeColumnCount :: SimpleTableConfig -> TextTable -> TextTable
normalizeColumnCount config = normalizeColumnCountWithStr (emptyCellStr config)

normalizeColumnCountWithStr :: String -> TextTable -> TextTable
normalizeColumnCountWithStr emptyCellStr textTable =
    map (\row -> addExtraCells row) textTable
    where
        addExtraCells row
            | length row < columnCount = row ++ (take (columnCount - length row)
                $ repeat emptyCellStr)
            | otherwise = row
        columnCount = fst $ get2DListSize textTable

-- | convert TextTable to Table by splitting each cell
-- line by line
makeCells :: TextTable -> Table
makeCells textTable =
    map (\rowStr  -> map
            (\cellStr -> splitOn "\n" cellStr) rowStr) textTable
    where
        splitCell :: String -> Cell
        splitCell cellStr = splitOn "\n" cellStr

get2DListSize :: [[a]] -> (Int, Int)
get2DListSize list2d = (maximum $ map length list2d, length list2d)

padTableCells :: SimpleTableConfig -> Table -> Table
padTableCells config table = (padCellLines . addCellLines) table
  where
    -- add empty `CellLine`s to each `Cell`
    addCellLines table = zipWith addExtraCellLines table realRowHeights
    addExtraCellLines row height = map
      (\cell -> (cellPadFunction config) "\n" height cell) row
    -- adds extra spaces to each `CellLine`
    padCellLines table = transpose $
      zipWith padCellList (transpose table) realColWidths

    padCellList col width = map (\cell -> map
                                  (\celLine ->
                                   (padFunction config)
                                   (paddingStr config)
                                   width celLine)
                                  cell) col

    -- Calculate real column widths.
    -- Minimum column widths are limited by values from config.
    realColWidths = zipWith max (colWidths cellSizeTable) ((colMinWidths config) ++ (repeat 0))
    realRowHeights = zipWith max (rowHeights cellSizeTable) ((rowMinHeights config) ++ (repeat 0))
    cellSizeTable = map2d get2DListSize table

    -- list of heights for each row
    rowHeights :: CellSizeTable -> [Int]
    rowHeights sizeTable = maxOfMap2 snd sizeTable

    -- list of widths for each column
    colWidths :: CellSizeTable -> [Int]
    colWidths sizeTable  = maxOfMap2 fst $ transpose sizeTable

    maxOfMap2 :: (a -> Int) -> [[a]] -> [Int]
    maxOfMap2 f = map (\sth -> maximum $ map f sth)

-- | Join 'CellLine's
wrapTable config table = wrapCells $ addCellCoords table
  where
    -- Add cell coordinates for each cell.
    -- e.g. [["a", "b"]] will be transformed to [[(1,1,"a"), (1,2,"b")]]
    addCellCoords :: Table -> [[(Int, Int, Cell)]]
    addCellCoords table = zipWith
      (\ rowNum list ->
       -- Insert row numbers.
        map (\ (colNum, cell) ->
                 (rowNum, colNum, cell)) list) [1..] $
      -- Enumerate columns
      map (zip [1..]) table
    wrapCells :: [[(Int, Int, Cell)]] -> [[CellWrapper]]
    wrapCells = map2d wrapCell
    wrapCell :: (Int, Int, Cell) -> CellWrapper
    wrapCell (rowNum, colNum, cell) =
      (CellWrapper cell rowNum colNum
       -- cell width
       (maximum $ map length cell)
       -- cell height
       (length cell)
       topLeft top topRight right bottomRight bottom bottomLeft left)
      where
        (width, height) = get2DListSize table
        borders =  tableBorders config
        topLeft
          | rowNum == 1 && colNum == 1 = [borders !! 0]          -- ┌
          | colNum == 1 = [borders !! 3]                        -- ├
          | rowNum == 1 = [borders !! 1]                        -- ┬
          | otherwise = [borders !! 4]                          -- ┼
        topRight
          | rowNum == 1 && colNum == width = [borders !! 2]      -- ┐
          | rowNum /= 1 && colNum == width = [borders !! 5]      -- ┤
          | otherwise = ""
        right
          | colNum == width = [borders !! 10]
          | otherwise = ""
        bottomRight
          | rowNum == height && colNum == width = [borders !! 8] -- ┘
          | rowNum == height = [borders !! 7]                   -- ┴
          | colNum == width = [borders !! 5]                    -- ┤
          | otherwise = ""
        bottom
          | rowNum == height = [borders !! 9]                   -- ─
          | otherwise = ""
        bottomLeft
          | rowNum == height && colNum == 1 = [borders !! 6]     -- └
          | rowNum == height = [borders !! 7]                   -- └
          | otherwise  = ""
        top = [borders !! 9]                                    -- ─
        left = [borders !! 10]                                  -- │

normalizeBorderLengths :: [[CellWrapper]] -> [[CellWrapper]]
normalizeBorderLengths =
  map2d normalizeBorderLength
  where
    normalizeBorderLength :: CellWrapper -> CellWrapper
    normalizeBorderLength
      (CellWrapper cell rowNum colNum cellWidth cellHeight topLeft
       top topRight right bottomRight bottom bottomLeft left) =
      (CellWrapper cell rowNum colNum cellWidth cellHeight topLeft
       (takeOf cellWidth top) topRight
       right bottomRight
       (takeOf cellWidth bottom) bottomLeft
       left)
      where
        takeOf n sth =
          take n $ concat $ repeat sth

appendBorders :: [[CellWrapper]] -> [[CellWrapper]]
appendBorders table =
  map2d appendAll table
  where
    appendAll
      (CellWrapper cell rowNum colNum cellWidth cellHeight topLeft
       top topRight right bottomRight bottom bottomLeft left) =
      (CellWrapper
       ((
         -- append bottomLeft
         (\ cell ->
           if rowNum == height then
             (init cell) ++ [bottomLeft ++ last cell]
           else
             cell) .
         -- append bottomRight
         (\ cell ->
          if colNum == width && rowNum == height then
            (init cell) ++ [last cell ++ bottomRight]
            else
          cell) .
         -- append bottom
         (\ cell ->
          if rowNum == height then
            cell ++ [bottom]
            else
            cell) .
         -- append right
         (\ cell ->
          if colNum == width then
             [head cell] ++ (zipWith (++) (tail cell) (repeat right))
           else
             cell) .
         -- append topRight
         (\ cell ->
           (head cell ++ topRight):(tail cell)) .
         -- appendt left
         (\ cell ->
           [head cell] ++ zipWith (++) (repeat left) (tail cell)) .
         -- append topLeft
         (\ cell -> (concat (topLeft : [head cell])):(tail cell)) .
         -- appent top
         (\ cell -> top:cell))
         cell)
       rowNum colNum cellWidth cellHeight topLeft
       top topRight right bottomRight bottom bottomLeft left)
    width  = fst $ get2DListSize table
    height = snd $ get2DListSize table


-- part 5: join cells & rows

showTable :: [[[String]]] -> String
showTable textTable = strJoin "\n" $
  map (strJoin "\n") $
  map2d (strJoin "") $
  map transpose textTable
    where
      strJoin :: String -> [String] -> String
      strJoin separator lst = if null lst then
                                ""
                              else
                                foldr1 (\x y -> x ++ separator ++ y) lst

-- | Horizontal padding function.
-- Appends padding string (first argument) to the left of the given string to make
-- it's length equal to the second argument.
simpleTableLeftPad :: String -> Int -> String -> String
simpleTableLeftPad paddingStr width str
  | length str > width = error "SimpleTableGenerator: String's length is greater than maximum!"
  | otherwise = padding ++ str
    where
      padding = take (width - length str) $ concat $ repeat paddingStr

-- | Horizontal padding function.
-- Appends padding string (first argument) to the right of the given string to make
-- it's length equal to the second argument.
simpleTableRightPad :: String -> Int -> String -> String
simpleTableRightPad paddingStr width str
  | length str > width = error "SimpleTableGenerator: String's length is greater than maximum!"
  | otherwise = str ++ padding
    where
      padding = take (width - length str) $ concat $ repeat paddingStr

-- | Horizontal padding function.
-- Appends padding string (first argument) both to the right and left of the given string to make
-- it's length equal to the second argument.
simpleTableCenterPad :: String -> Int -> String -> String
simpleTableCenterPad paddingStr width str
  | length str > width = error "SimpleTableGenerator: String's length is greater than maximum!"
  | even (width - length str) = halfPadding ++ str ++ halfPadding
  | otherwise = halfPadding ++ str ++ take (halfWidth + 1) padding
    where
      halfWidth = ((width - length str) `div` 2)
      padding = concat $ repeat paddingStr
      halfPadding = take halfWidth padding

-- | Vertical padding function.
-- Appends padding to the bottom of given 'Cell'
simpleTableBottomPad :: String -> Int -> Cell -> [String]
simpleTableBottomPad cellStr height cell
  | length cell > height = error "SimpleTableGenerator: Cell's height is greater than maximum!"
  | length cell == height = cell
  | otherwise = cell ++ padding
    where
      padding = replicate (height - length cell) ""

-- | Vertical padding function.
-- Appends padding to the top of given 'Cell'
simpleTableTopPad :: String -> Int -> Cell -> [String]
simpleTableTopPad cellStr height cell
  | length cell > height = error "SimpleTableGenerator: Cell's height is greater than maximum!"
  | length cell == height = cell
  | otherwise = padding ++ cell
    where
      padding = replicate (height - length cell) ""

-- | Vertical padding function.
-- Appends padding both to top and bottom of given 'Cell'
simpleTableMiddlePad :: String -> Int -> Cell -> [String]
simpleTableMiddlePad cellStr height cell
  | length cell > height = error "SimpleTableGenerator: Cell's height is greater than maximum!"
  | length cell == height = cell
  | even (height - length cell) = halfPadding ++ cell ++ halfPadding
  | otherwise = halfPadding ++ cell ++ halfPadding ++ [""]
    where
      halfPadding = replicate ((height - length cell) `div` 2) ""

constructPaddingFunctions :: SimpleTableConfig -> SimpleTableConfig
constructPaddingFunctions config = config {
  padFunction = (\ f padStr width ->
                     let padding = take (horizontalPadding config) $
                               concat $ repeat (paddingStr config) in
                       padding  ++ (f padStr width) ++ padding)
                . (padFunction config),
  cellPadFunction =
      (\ f cellStr height ->
           let padding = (concat
                          $ take (verticalPadding config)
                                $ repeat [""]) in
           padding ++ (f cellStr height) ++ padding) .
      (cellPadFunction config),
  horizontalPadding = 0,
  verticalPadding = 0
  }

validateConfig config
  | 0 == length (paddingStr config) = error "SimpleTableGenerator: paddingStr is empty!"
  | 11 /= length (tableBorders config) = error "SimpleTableGenerator: tableBorders must be a string of 11 characters!"
  | 0 > horizontalPadding config = error "SimpleTableGenerator: horizontalPadding must be >= 0!"
  | 0 > verticalPadding config = error "SimpleTableGenerator: verticalPadding must be >= 0!"
  | otherwise = config

-- misc functions

map2d :: (a -> b) -> [[a]] -> [[b]]
map2d =  map . map