{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RankNTypes #-}

{-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-unticked-promoted-constructors #-}

-- | Build backend-agnostic columnar encodings that can be 
--   used to visualize tabular data.
module Colonnade
  ( -- * Example
    -- $setup
    -- * Types
    Colonnade
  , Headed(..)
  , Headless(..)
    -- * Typeclasses
  , E.Headedness(..)
    -- * Create
  , headed
  , headless
  , singleton
    -- * Transform
    -- ** Body
  , fromMaybe
  , columns
  , bool
  , replaceWhen
  , modifyWhen
    -- ** Header
  , mapHeaderContent
  , mapHeadedness
  , toHeadless
    -- * Cornice
    -- ** Types
  , Cornice
  , Pillar(..)
  , Fascia(..)
    -- ** Create
  , cap
  , recap
    -- * Ascii Table
  , ascii
  , asciiCapped
  ) where

import Colonnade.Encode (Colonnade,Cornice,
  Pillar(..),Fascia(..),Headed(..),Headless(..))
import Data.Foldable
import Control.Monad
import qualified Data.Bool
import qualified Data.Maybe
import qualified Colonnade.Encode as E
import qualified Data.List as List
import qualified Data.Vector as Vector

-- $setup
--
-- First, let\'s bring in some neccessary imports that will be
-- used for the remainder of the examples in the docs:
--
-- >>> import Data.Monoid (mconcat,(<>))
-- >>> import Data.Profunctor (lmap)
--
-- The data types we wish to encode are:
--
-- >>> data Color = Red | Green | Blue deriving (Show,Eq)
-- >>> data Person = Person { name :: String, age :: Int }
-- >>> data House = House { color :: Color, price :: Int }
--
-- One potential columnar encoding of a @Person@ would be:
--
-- >>> :{
-- let colPerson :: Colonnade Headed Person String
--     colPerson = mconcat
--       [ headed "Name" name
--       , headed "Age" (show . age)
--       ]
-- :}
--
-- The type signature on @colPerson@ is not neccessary
-- but is included for clarity. We can feed data into this encoding
-- to build a table:
--
-- >>> let people = [Person "David" 63, Person "Ava" 34, Person "Sonia" 12]
-- >>> putStr (ascii colPerson people)
-- +-------+-----+
-- | Name  | Age |
-- +-------+-----+
-- | David | 63  |
-- | Ava   | 34  |
-- | Sonia | 12  |
-- +-------+-----+
--
-- Similarly, we can build a table of houses with:
--
-- >>> let showDollar = (('$':) . show) :: Int -> String
-- >>> colHouse = mconcat [headed "Color" (show . color), headed "Price" (showDollar . price)]
-- >>> :t colHouse
-- colHouse :: Colonnade Headed House [Char]
-- >>> let houses = [House Green 170000, House Blue 115000, House Green 150000]
-- >>> putStr (ascii colHouse houses)
-- +-------+---------+
-- | Color | Price   |
-- +-------+---------+
-- | Green | $170000 |
-- | Blue  | $115000 |
-- | Green | $150000 |
-- +-------+---------+


-- | A single column with a header.
headed :: c -> (a -> c) -> Colonnade Headed a c
headed h = singleton (Headed h)

-- | A single column without a header.
headless :: (a -> c) -> Colonnade Headless a c
headless = singleton Headless

-- | A single column with any kind of header. This is not typically needed.
singleton :: h c -> (a -> c) -> Colonnade h a c
singleton h = E.Colonnade . Vector.singleton . E.OneColonnade h

-- | Map over the content in the header. This is similar performing 'fmap'
--   on a 'Colonnade' except that the body content is unaffected.
mapHeaderContent :: Functor h => (c -> c) -> Colonnade h a c -> Colonnade h a c
mapHeaderContent f (E.Colonnade v) =
  E.Colonnade (Vector.map (\(E.OneColonnade h e) -> E.OneColonnade (fmap f h) e) v)

-- | Map over the header type of a 'Colonnade'.
mapHeadedness :: (forall x. h x -> h' x) -> Colonnade h a c -> Colonnade h' a c
mapHeadedness f (E.Colonnade v) =
  E.Colonnade (Vector.map (\(E.OneColonnade h e) -> E.OneColonnade (f h) e) v)

-- | Remove the heading from a 'Colonnade'.
toHeadless :: Colonnade h a c -> Colonnade Headless a c
toHeadless = mapHeadedness (const Headless)


-- | Lift a column over a 'Maybe'. For example, if some people
--   have houses and some do not, the data that pairs them together
--   could be represented as:
--
-- >>> :{
-- let owners :: [(Person,Maybe House)]
--     owners =
--       [ (Person "Jordan" 18, Nothing)
--       , (Person "Ruth" 25, Just (House Red 125000))
--       , (Person "Sonia" 12, Just (House Green 145000))
--       ]
-- :}
--
-- The column encodings defined earlier can be reused with
-- the help of 'fromMaybe':
--
-- >>> :{
-- let colOwners :: Colonnade Headed (Person,Maybe House) String
--     colOwners = mconcat
--       [ lmap fst colPerson
--       , lmap snd (fromMaybe "" colHouse)
--       ]
-- :}
--
-- >>> putStr (ascii colOwners owners)
-- +--------+-----+-------+---------+
-- | Name   | Age | Color | Price   |
-- +--------+-----+-------+---------+
-- | Jordan | 18  |       |         |
-- | Ruth   | 25  | Red   | $125000 |
-- | Sonia  | 12  | Green | $145000 |
-- +--------+-----+-------+---------+
fromMaybe :: c -> Colonnade f a c -> Colonnade f (Maybe a) c
fromMaybe c (E.Colonnade v) = E.Colonnade $ flip Vector.map v $
  \(E.OneColonnade h encode) -> E.OneColonnade h (maybe c encode)

-- | Convert a collection of @b@ values into a columnar encoding of
--   the same size. Suppose we decide to show a house\'s color
--   by putting a check mark in the column corresponding to
--   the color instead of by writing out the name of the color:
--
-- >>> let allColors = [Red,Green,Blue]
-- >>> let encColor = columns (\c1 c2 -> if c1 == c2 then "✓" else "") (Headed . show) allColors
-- >>> :t encColor
-- encColor :: Colonnade Headed Color [Char]
-- >>> let encHouse = headed "Price" (showDollar . price) <> lmap color encColor
-- >>> :t encHouse
-- encHouse :: Colonnade Headed House [Char]
-- >>> putStr (ascii encHouse houses)
-- +---------+-----+-------+------+
-- | Price   | Red | Green | Blue |
-- +---------+-----+-------+------+
-- | $170000 |     | ✓     |      |
-- | $115000 |     |       | ✓    |
-- | $150000 |     | ✓     |      |
-- +---------+-----+-------+------+
columns :: Foldable g
  => (b -> a -> c) -- ^ Cell content function
  -> (b -> f c) -- ^ Header content function
  -> g b -- ^ Basis for column encodings
  -> Colonnade f a c
columns getCell getHeader = id
  . E.Colonnade
  . Vector.map (\b -> E.OneColonnade (getHeader b) (getCell b))
  . Vector.fromList
  . toList

bool ::
     f c -- ^ Heading
  -> (a -> Bool) -- ^ Predicate
  -> (a -> c) -- ^ Contents when predicate is false
  -> (a -> c) -- ^ Contents when predicate is true
  -> Colonnade f a c
bool h p onTrue onFalse = singleton h (Data.Bool.bool <$> onFalse <*> onTrue <*> p)

-- | Modify the contents of cells in rows whose values satisfy the
--   given predicate. Header content is unaffected. With an HTML backend, 
--   this can be used to strikethrough the contents of cells with data that is
--   considered invalid.
modifyWhen ::
     (c -> c) -- ^ Content change
  -> (a -> Bool) -- ^ Row predicate
  -> Colonnade f a c -- ^ Original 'Colonnade'
  -> Colonnade f a c
modifyWhen changeContent p (E.Colonnade v) = E.Colonnade
  ( Vector.map
    (\(E.OneColonnade h encode) -> E.OneColonnade h $ \a ->
      if p a then changeContent (encode a) else encode a
    ) v
  )

-- | Replace the contents of cells in rows whose values satisfy the
--   given predicate. Header content is unaffected.
replaceWhen ::
     c -- ^ New content
  -> (a -> Bool) -- ^ Row predicate
  -> Colonnade f a c -- ^ Original 'Colonnade'
  -> Colonnade f a c
replaceWhen = modifyWhen . const

-- | Augment a 'Colonnade' with a header spans over all of the
--   existing headers. This is best demonstrated by example. 
--   Let\'s consider how we might encode a pairing of the people 
--   and houses from the initial example:
--   
--   >>> let personHomePairs = zip people houses
--   >>> let colPersonFst = lmap fst colPerson
--   >>> let colHouseSnd = lmap snd colHouse
--   >>> putStr (ascii (colPersonFst <> colHouseSnd) personHomePairs)
--   +-------+-----+-------+---------+
--   | Name  | Age | Color | Price   |
--   +-------+-----+-------+---------+
--   | David | 63  | Green | $170000 |
--   | Ava   | 34  | Blue  | $115000 |
--   | Sonia | 12  | Green | $150000 |
--   +-------+-----+-------+---------+
--   
--   This tabular encoding leaves something to be desired. The heading
--   not indicate that the name and age refer to a person and that
--   the color and price refer to a house. Without reaching for 'Cornice',
--   we can still improve this situation with 'mapHeaderContent':
--
--   >>> let colPersonFst' = mapHeaderContent ("Person " ++) colPersonFst
--   >>> let colHouseSnd' = mapHeaderContent ("House " ++) colHouseSnd
--   >>> putStr (ascii (colPersonFst' <> colHouseSnd') personHomePairs)
--   +-------------+------------+-------------+-------------+
--   | Person Name | Person Age | House Color | House Price |
--   +-------------+------------+-------------+-------------+
--   | David       | 63         | Green       | $170000     |
--   | Ava         | 34         | Blue        | $115000     |
--   | Sonia       | 12         | Green       | $150000     |
--   +-------------+------------+-------------+-------------+
--
--   This is much better, but for longer tables, the redundancy
--   of prefixing many column headers can become annoying. The solution
--   that a 'Cornice' offers is to nest headers:
--   
--   >>> let cor = mconcat [cap "Person" colPersonFst, cap "House" colHouseSnd]
--   >>> :t cor
--   cor :: Cornice Headed ('Cap 'Base) (Person, House) [Char]
--   >>> putStr (asciiCapped cor personHomePairs)
--   +-------------+-----------------+
--   | Person      | House           |
--   +-------+-----+-------+---------+
--   | Name  | Age | Color | Price   |
--   +-------+-----+-------+---------+
--   | David | 63  | Green | $170000 |
--   | Ava   | 34  | Blue  | $115000 |
--   | Sonia | 12  | Green | $150000 |
--   +-------+-----+-------+---------+
--   
cap :: c -> Colonnade h a c -> Cornice h (Cap Base) a c
cap h = E.CorniceCap . Vector.singleton . E.OneCornice h . E.CorniceBase

-- | Add another cap to a cornice. There is no limit to how many times
--   this can be applied:
--   
--   >>> data Day = Weekday | Weekend deriving (Show)
--   >>> :{
--   let cost :: Int -> Day -> String
--       cost base w = case w of
--         Weekday -> showDollar base
--         Weekend -> showDollar (base + 1)
--       colStandard = foldMap (\c -> headed c (cost 8)) ["Yt","Ad","Sr"]
--       colSpecial = mconcat [headed "Stud" (cost 6), headed "Mltry" (cost 7)]
--       corStatus = mconcat
--         [ cap "Standard" colStandard
--         , cap "Special" colSpecial
--         ] 
--       corShowtime = mconcat
--         [ recap "" (cap "" (headed "Day" show))
--         , foldMap (\c -> recap c corStatus) ["Matinee","Evening"]
--         ]
--   :}
--
--   >>> putStr (asciiCapped corShowtime [Weekday,Weekend])
--   +---------+-----------------------------+-----------------------------+
--   |         | Matinee                     | Evening                     |
--   +---------+--------------+--------------+--------------+--------------+
--   |         | Standard     | Special      | Standard     | Special      |
--   +---------+----+----+----+------+-------+----+----+----+------+-------+
--   | Day     | Yt | Ad | Sr | Stud | Mltry | Yt | Ad | Sr | Stud | Mltry |
--   +---------+----+----+----+------+-------+----+----+----+------+-------+
--   | Weekday | $8 | $8 | $8 | $6   | $7    | $8 | $8 | $8 | $6   | $7    |
--   | Weekend | $9 | $9 | $9 | $7   | $8    | $9 | $9 | $9 | $7   | $8    |
--   +---------+----+----+----+------+-------+----+----+----+------+-------+
recap :: c -> Cornice h p a c -> Cornice h (Cap p) a c
recap h cor = E.CorniceCap (Vector.singleton (E.OneCornice h cor))

asciiCapped :: Foldable f
  => Cornice Headed p a String -- ^ columnar encoding
  -> f a -- ^ rows
  -> String
asciiCapped cor xs =
  let annCor = E.annotateFinely (\x y -> x + y + 3) id
        List.length xs cor
      sizedCol = E.uncapAnnotated annCor
   in E.headersMonoidal
        Nothing
        [ ( \msz _ -> case msz of
              Just sz -> "+" ++ hyphens (sz + 2)
              Nothing -> ""
          , \s -> s ++ "+\n"
          )
        , ( \msz c -> case msz of
              Just sz -> "| " ++ rightPad sz ' ' c ++ " "
              Nothing -> ""
          , \s -> s ++ "|\n"
          )
        ] annCor ++ asciiBody sizedCol xs


-- | Render a collection of rows as an ascii table. The table\'s columns are
-- specified by the given 'Colonnade'. This implementation is inefficient and
-- does not provide any wrapping behavior. It is provided so that users can
-- try out @colonnade@ in ghci and so that @doctest@ can verify example
-- code in the haddocks.
ascii :: Foldable f
  => Colonnade Headed a String -- ^ columnar encoding
  -> f a -- ^ rows
  -> String
ascii col xs =
  let sizedCol = E.sizeColumns List.length xs col
      divider = concat
        [ E.headerMonoidalFull sizedCol
             (\(E.Sized msz _) -> case msz of
               Just sz -> "+" ++ hyphens (sz + 2)
               Nothing -> ""
             )
        , "+\n"
        ]
   in List.concat
      [ divider
      , concat
         [ E.headerMonoidalFull sizedCol
             (\(E.Sized msz (Headed h)) -> case msz of
               Just sz -> "| " ++ rightPad sz ' ' h ++ " "
               Nothing -> ""
             )
         , "|\n"
         ]
      , asciiBody sizedCol xs
      ]

asciiBody :: Foldable f
  => Colonnade (E.Sized (Maybe Int) Headed) a String
  -> f a
  -> String
asciiBody sizedCol xs =
  let divider = concat
        [ E.headerMonoidalFull sizedCol
             (\(E.Sized msz _) -> case msz of
               Just sz -> "+" ++ hyphens (sz + 2)
               Nothing -> ""
             )
        , "+\n"
        ]
      rowContents = foldMap
        (\x -> concat
           [ E.rowMonoidalHeader
               sizedCol
               (\(E.Sized msz _) c -> case msz of
                 Nothing -> ""
                 Just sz -> "| " ++ rightPad sz ' ' c ++ " "
               )
               x
           , "|\n"
           ]
        ) xs
   in List.concat
      [ divider
      , rowContents
      , divider
      ]

hyphens :: Int -> String
hyphens n = List.replicate n '-'

rightPad :: Int -> a -> [a] -> [a]
rightPad m a xs = take m $ xs ++ repeat a

-- data Company = Company String String Int
-- 
-- data Company = Company
--   { companyName :: String
--   , companyCountry :: String
--   , companyValue :: Int
--   } deriving (Show)
-- 
-- myCompanies :: [Company]
-- myCompanies =
--   [ Company "eCommHub" "United States" 50
--   , Company "Layer 3 Communications" "United States" 10000000
--   , Company "Microsoft" "England" 500000000
--   ]