-- | Simple table representation. This module is meant to be imported qualified.
module Data.Table (
  -- * Datatypes
    Alignment(..)
  , Table

  -- ** Queries
  , toList
  , alignments
  , defaultAlignment
  , columns
  , rows

  -- ** Construction/Modification
  , empty
  , fromList
  , newRow
  , setAlignment
  , appendCell
  , appendNumCell

  -- ** Cell based access
  , getCell
  , setCell
  , headerLastCell
  , alignLastCell

  -- ** Pretty Printing
  , toLaTeX
) where

import Safe
import Data.List
import qualified Data.IntMap as IM

import Extension.Prelude

------------------------------------------------------------------------------
-- Helper functions
------------------------------------------------------------------------------

-- | Round a number to the given amount of decimals.
roundDecimal :: RealFrac a 
             => Int  -- ^ Number of decimals after the decimal point
             -> a -> a
roundDecimal d n = fromIntegral ((round (multiplier * n))::Integer) / multiplier
  where 
  multiplier :: RealFrac a => a
  multiplier = 10 ^^ d

-- | The maximum key of an 'IntMap', if there is one.
maxKey :: IM.IntMap a -> Maybe Int
maxKey = fmap (fst.fst) . IM.maxViewWithKey

-- | The minimal length of an array indexable by indices from '0' to 'maxKey'.
denseLength :: IM.IntMap a -> Int
denseLength = maybe 0 succ . maxKey

-- | Convert the sparse intmap to a dense list, filling the gaps with the given
-- default element.
toDenseList :: a -> IM.IntMap a -> [a]
toDenseList def m = case maxKey m of
  Just k  -> merge [0..k] (IM.toList m)
  Nothing -> []
  where
  merge [] vs = map snd vs
  merge ks [] = replicate (length ks) def
  merge ks@(k:ks') vs@((idx,v):vs') = case compare k idx of
    LT -> def : merge ks' vs
    EQ -> v   : merge ks' vs'
    GT -> v   : merge ks  vs'

-- | Convert a dense list to an 'IntMap' assuming the list has consecutive
-- indexes starting from '0'.
fromDenseList :: [a] -> IM.IntMap a 
fromDenseList = IM.fromList . zip [0..]

------------------------------------------------------------------------------
-- Table datatype
------------------------------------------------------------------------------

type Cells a = IM.IntMap (IM.IntMap a)

-- | A column alignment.
data Alignment = AlignLeft | AlignRight
  deriving( Eq, Ord, Show )

-- | A table with aligned rows.
data Table a = Table { 
    alignments :: IM.IntMap Alignment  -- ^ The alignment for each column
  , headers    :: IM.IntMap a          -- ^ The headers for each column
  , getCells   :: Cells a              -- ^ Cells indexed first by rows then by columns
  }
  deriving( Eq, Ord, Show )

instance Functor Table where
  fmap f = mapCellsAndHeaders (IM.map (IM.map f)) (IM.map f)

------------------------------------------------------------------------------
-- Queries
------------------------------------------------------------------------------

-- | The default alignment to be used for a row.
defaultAlignment :: Alignment
defaultAlignment = AlignLeft

-- | The number of columns of the table.
columns :: Table a -> Int
columns = maximumDef 0 . map (denseLength . snd) . IM.toList . getCells

-- | The number of rows of the table.
rows :: Table a -> Int
rows = denseLength . getCells

-- | Retrieve the contents of a cell if it exists.
getCell :: (Int,Int) -> Table a -> Maybe a
getCell (rowIdx, colIdx) = (IM.lookup colIdx =<<) . IM.lookup rowIdx . getCells
  
-- | View the index and the contents of the last cell.
viewLastCell :: Table a -> Maybe ((Int, Int), a)
viewLastCell t = do
  ((rowIdx,row),  _) <- IM.maxViewWithKey $ getCells t
  ((colIdx,cell), _) <- IM.maxViewWithKey $ row
  return ((rowIdx, colIdx), cell)

-- | Retrieve the index of the last cell.
lastCellIndex :: Table a -> Maybe (Int, Int)
lastCellIndex = fmap fst . viewLastCell

toList :: a -> Table a -> [[a]]
toList def = map (toDenseList def) . toDenseList IM.empty . getCells

------------------------------------------------------------------------------
-- Table construction
------------------------------------------------------------------------------

-- Internal helper functions
----------------------------

-- | Updtate the alignments.
mapAlignments :: (IM.IntMap Alignment -> IM.IntMap Alignment) -> Table a -> Table a
mapAlignments f t = t { alignments = f (alignments t) }

-- | Updtate the headers.
mapHeaders :: (IM.IntMap a -> IM.IntMap a) -> Table a -> Table a
mapHeaders f = mapCellsAndHeaders id f

-- | Update the cells.
mapCells :: (Cells a -> Cells a) -> Table a -> Table a
mapCells f = mapCellsAndHeaders f id

-- | Updtate the headers and the cells
mapCellsAndHeaders :: (Cells a -> Cells b) 
                   -> (IM.IntMap a -> IM.IntMap b) 
                   -> Table a -> Table b
mapCellsAndHeaders fCells fHeaders t = 
  t { headers = fHeaders (headers t), getCells = fCells (getCells t) }


-- Externally available functions
---------------------------------

-- | Emtpy table.
empty :: Table a
empty = Table IM.empty IM.empty IM.empty

-- | Convert a list of rows to a table.
fromList :: [[a]] -> Table a
fromList = Table IM.empty IM.empty . fromDenseList . map fromDenseList

-- | Set the alignment of the given column.
setAlignment :: Int -> Alignment -> Table a -> Table a
setAlignment colIdx alignment = mapAlignments (IM.insert colIdx alignment)

-- | Set the header of the given column.
setHeader :: Int -> a -> Table a -> Table a
setHeader colIdx header = mapHeaders (IM.insert colIdx header)

-- | Set the contents of a cell.
setCell :: (Int,Int) -> a -> Table a -> Table a
setCell idx@(rowIdx, colIdx) x 
  | rowIdx < 0 || colIdx < 0 = error $ "setCell: index out of range" ++ show idx
  | otherwise                = mapCells (IM.alter changeRow rowIdx)
  where
  changeRow Nothing    = Just $ IM.singleton colIdx x
  changeRow (Just row) = Just $ IM.insert colIdx x row

-- | Add a new empty row to the table.
newRow :: Table a -> Table a
newRow = mapCells $ \cells -> case maxKey cells of
  Just rowIdx -> IM.insert (succ rowIdx) IM.empty cells
  Nothing     -> IM.singleton 0 IM.empty

-- | Append a cell at the end of the last row 
appendCell :: a -> Table a -> Table a
appendCell cell = mapCells $ \cells -> 
  case IM.maxViewWithKey cells of
    Just ((rowIdx,row),cellsNoRow) ->
      case maxKey row of
        Just colIdx -> IM.insert rowIdx (IM.insert (succ colIdx) cell row) cellsNoRow
        Nothing     -> IM.insert rowIdx (IM.singleton 0 cell)              cellsNoRow
    Nothing -> IM.singleton 0 (IM.singleton 0 cell)

-- | Set the alignemnt of the last cell.
alignLastCell :: Alignment -> Table a -> Table a
alignLastCell al t = case lastCellIndex t of
  Just (_, colIdx) -> setAlignment colIdx al t
  Nothing          ->                        t

headerLastCell :: a -> Table a -> Table a
headerLastCell header t = case lastCellIndex t of
  Just (_, colIdx) -> setHeader colIdx header t
  Nothing          ->                         t

-- | Append a cell containing a number; i.e. round and right align.
appendNumCell :: RealFrac a 
              => Int -- ^ Number of digits after the decimal point
              -> a -> Table String -> Table String
appendNumCell dec x = 
  alignLastCell AlignRight . appendCell (show $ roundDecimal dec x)


------------------------------------------------------------------------------
-- Pretty printing
------------------------------------------------------------------------------

type Aligner a =  Int  -- ^ The length to achieve.
               -> [a]  -- ^ The current list.
               -> [a]  -- ^ The aligned list.

-- | Align the number of elements in each list using the given alignment function.
alignBy :: Aligner a -> [[a]] -> [[a]]
alignBy _      [] = []
alignBy expand rs = map (expand maxLength) rs
  where
  maxLength = maximum $ map length rs

-- | Align the contents of each cell using the given aligment for each row.
alignCellsBy :: [Aligner a] -> [[[a]]] -> [[[a]]]
alignCellsBy expanders = 
  transpose . zipWith alignBy (cycle expanders) . transpose . alignBy (flushLeftBy [[]])

-- | Convert a table to the body of a LaTeX table.
toLaTeX :: (a -> String) -> Table a -> String
toLaTeX toStr t = 
  formatLines . map formatLine . alignCells . addHeaders . toList "" . fmap toStr $ t
  where
  addHeaders
    | IM.null $ headers t = id
    | otherwise           = ((toDenseList "" . IM.map toStr $ headers t):)
  doAlign AlignLeft  = flushLeft
  doAlign AlignRight = flushRight
  aligns = toDenseList defaultAlignment (alignments t) ++ repeat defaultAlignment
  alignCells = alignCellsBy (map doAlign aligns)
  formatLine = (++"\\\\") . concat . intersperse " & "
  formatLines = unlines


------------------------------------------------------------------------------
-- Some (manual) testing
------------------------------------------------------------------------------

{-
test = fromList $ [["simon","where","a"],["new","","world"],[],["order"]]
test1 = setHeader 1 "URGH" .setHeader 1 "WORDL" . appendNumCell 2 10.2 . alignLastCell AlignRight . appendCell "w" $ test

latex = putStrLn . toLaTeX id 
-}