{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE BangPatterns #-}

-- | Build HTML tables using @blaze-html@ and @colonnade@. The bottom
--   of this page has a tutorial that walks through a full example,
--   illustrating how to meet typical needs with this library. It is
--   recommended that users read the documentation for @colonnade@ first,
--   since this library builds on the abstractions introduced there.
--   A concise example of this library\'s use:
--
-- >>> :set -XOverloadedStrings
-- >>> :module + Colonnade Text.Blaze.Html Text.Blaze.Colonnade
-- >>> let col = headed "Grade" (toHtml . fst) <> headed "Letter" (toHtml . snd)
-- >>> let rows = [("90-100",'A'),("80-89",'B'),("70-79",'C')]
-- >>> printVeryCompactHtml (encodeHtmlTable mempty col rows)
-- <table>
--     <thead>
--         <tr><th>Grade</th><th>Letter</th></tr>
--     </thead>
--     <tbody>
--         <tr><td>90-100</td><td>A</td></tr>
--         <tr><td>80-89</td><td>B</td></tr>
--         <tr><td>70-79</td><td>C</td></tr>
--     </tbody>
-- </table>
module Text.Blaze.Colonnade
  ( -- * Apply
    encodeHtmlTable
  , encodeCellTable
  , encodeTable
  , encodeCappedTable
    -- * Cell
    -- $build
  , Cell(..)
  , htmlCell
  , stringCell
  , textCell
  , lazyTextCell
  , builderCell
  , htmlFromCell
    -- * Interactive
  , printCompactHtml
  , printVeryCompactHtml
    -- * Tutorial
    -- $setup

    -- * Discussion
    -- $discussion
  ) where

import Text.Blaze (Attribute,(!))
import Text.Blaze.Html (Html, toHtml)
import Colonnade (Colonnade,Headed,Headless,Fascia,Cornice)
import Data.Text (Text)
import Control.Monad
import Data.Monoid
import Data.Foldable
import Data.String (IsString(..))
import Data.Maybe (listToMaybe)
import Data.Char (isSpace)
import qualified Data.List as List
import qualified Text.Blaze.Html.Renderer.Pretty as Pretty
import qualified Text.Blaze as Blaze
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as HA
import qualified Colonnade.Encode as E
import qualified Data.Text as Text
import qualified Data.Text.Lazy as LText
import qualified Data.Text.Lazy.Builder as TBuilder

-- $setup
-- We start with a few necessary imports and some example data
-- types:
-- 
-- >>> :set -XOverloadedStrings
-- >>> import Data.Monoid (mconcat,(<>))
-- >>> import Data.Char (toLower)
-- >>> import Data.Profunctor (Profunctor(lmap))
-- >>> import Colonnade (Colonnade,Headed,Headless,headed,cap,Fascia(..))
-- >>> import Text.Blaze.Html (Html, toHtml, toValue)
-- >>> import qualified Text.Blaze.Html5 as H
-- >>> data Department = Management | Sales | Engineering deriving (Show,Eq)
-- >>> data Employee = Employee { name :: String, department :: Department, age :: Int }
-- 
-- We define some employees that we will display in a table:
--
-- >>> :{
-- let employees = 
--       [ Employee "Thaddeus" Sales 34
--       , Employee "Lucia" Engineering 33
--       , Employee "Pranav" Management 57
--       ]
-- :}
-- 
-- Let's build a table that displays the name and the age
-- of an employee. Additionally, we will emphasize the names of
-- engineers using a @\<strong\>@ tag.
--
-- >>> :{
-- let tableEmpA :: Colonnade Headed Employee Html
--     tableEmpA = mconcat
--       [ headed "Name" $ \emp -> case department emp of
--           Engineering -> H.strong (toHtml (name emp))
--           _ -> toHtml (name emp)
--       , headed "Age" (toHtml . show . age)
--       ]
-- :}
--
-- The type signature of @tableEmpA@ is inferrable but is written
-- out for clarity in this example. Additionally, note that the first
-- argument to 'headed' is of type 'Html', so @OverloadedStrings@ is
-- necessary for the above example to compile. To avoid using this extension,
-- it is possible to instead use 'toHtml' to convert a 'String' to 'Html'.
-- Let\'s continue:
--
-- >>> let customAttrs = HA.class_ "stylish-table" <> HA.id "main-table"
-- >>> printCompactHtml (encodeHtmlTable customAttrs tableEmpA employees)
-- <table class="stylish-table" id="main-table">
--     <thead>
--         <tr>
--             <th>Name</th>
--             <th>Age</th>
--         </tr>
--     </thead>
--     <tbody>
--         <tr>
--             <td>Thaddeus</td>
--             <td>34</td>
--         </tr>
--         <tr>
--             <td><strong>Lucia</strong></td>
--             <td>33</td>
--         </tr>
--         <tr>
--             <td>Pranav</td>
--             <td>57</td>
--         </tr>
--     </tbody>
-- </table>
-- 
-- Excellent. As expected, Lucia\'s name is wrapped in a @\<strong\>@ tag 
-- since she is an engineer.
--
-- One limitation of using 'Html' as the content
-- type of a 'Colonnade' is that we are unable to add attributes to
-- the @\<td\>@ and @\<th\>@ elements. This library provides the 'Cell' type
-- to work around this problem. A 'Cell' is just 'Html' content and a set
-- of attributes to be applied to its parent @<th>@ or @<td>@. To illustrate
-- how its use, another employee table will be built. This table will
-- contain a single column indicating the department of each employ. Each
-- cell will be assigned a class name based on the department. To start off,
-- let\'s build a table that encodes departments:
--
-- >>> :{
-- let tableDept :: Colonnade Headed Department Cell
--     tableDept = mconcat
--       [ headed "Dept." $ \d -> Cell
--           (HA.class_ (toValue (map toLower (show d))))
--           (toHtml (show d))
--       ]
-- :}
--
-- Again, @OverloadedStrings@ plays a role, this time allowing the
-- literal @"Dept."@ to be accepted as a value of type 'Cell'. To avoid
-- this extension, 'stringCell' could be used to upcast the 'String'.
-- To try out our 'Colonnade' on a list of departments, we need to use
-- 'encodeCellTable' instead of 'encodeHtmlTable':
--
-- >>> let twoDepts = [Sales,Management]
-- >>> printVeryCompactHtml (encodeCellTable customAttrs tableDept twoDepts)
-- <table class="stylish-table" id="main-table">
--     <thead>
--         <tr><th>Dept.</th></tr>
--     </thead>
--     <tbody>
--         <tr><td class="sales">Sales</td></tr>
--         <tr><td class="management">Management</td></tr>
--     </tbody>
-- </table>
-- 
-- The attributes on the @\<td\>@ elements show up as they are expected to.
-- Now, we take advantage of the @Profunctor@ instance of 'Colonnade' to allow
-- this to work on @Employee@\'s instead:
--
-- >>> :t lmap
-- lmap :: Profunctor p => (a -> b) -> p b c -> p a c
-- >>> let tableEmpB = lmap department tableDept
-- >>> :t tableEmpB
-- tableEmpB :: Colonnade Headed Employee Cell
-- >>> printVeryCompactHtml (encodeCellTable customAttrs tableEmpB employees)
-- <table class="stylish-table" id="main-table">
--     <thead>
--         <tr><th>Dept.</th></tr>
--     </thead>
--     <tbody>
--         <tr><td class="sales">Sales</td></tr>
--         <tr><td class="engineering">Engineering</td></tr>
--         <tr><td class="management">Management</td></tr>
--     </tbody>
-- </table>
-- 
-- This table shows the department of each of our three employees, additionally
-- making a lowercased version of the department into a class name for the @\<td\>@.
-- This table is nice for illustrative purposes, but it does not provide all the
-- information that we have about the employees. If we combine it with the
-- earlier table we wrote, we can present everything in the table. One small
-- roadblock is that the types of @tableEmpA@ and @tableEmpB@ do not match, which
-- prevents a straightforward monoidal append:
--
-- >>> :t tableEmpA
-- tableEmpA :: Colonnade Headed Employee Html
-- >>> :t tableEmpB
-- tableEmpB :: Colonnade Headed Employee Cell
--
-- We can upcast the content type with 'fmap'.
-- Monoidal append is then well-typed, and the resulting 'Colonnade'
-- can be applied to the employees:
--
-- >>> let tableEmpC = fmap htmlCell tableEmpA <> tableEmpB
-- >>> :t tableEmpC
-- tableEmpC :: Colonnade Headed Employee Cell
-- >>> printCompactHtml (encodeCellTable customAttrs tableEmpC employees)
-- <table class="stylish-table" id="main-table">
--     <thead>
--         <tr>
--             <th>Name</th>
--             <th>Age</th>
--             <th>Dept.</th>
--         </tr>
--     </thead>
--     <tbody>
--         <tr>
--             <td>Thaddeus</td>
--             <td>34</td>
--             <td class="sales">Sales</td>
--         </tr>
--         <tr>
--             <td><strong>Lucia</strong></td>
--             <td>33</td>
--             <td class="engineering">Engineering</td>
--         </tr>
--         <tr>
--             <td>Pranav</td>
--             <td>57</td>
--             <td class="management">Management</td>
--         </tr>
--     </tbody>
-- </table>

-- $build
--
-- The 'Cell' type is used to build a 'Colonnade' that 
-- has 'Html' content inside table cells and may optionally
-- have attributes added to the @\<td\>@ or @\<th\>@ elements
-- that wrap this HTML content.

-- | The attributes that will be applied to a @\<td\>@ and
--   the HTML content that will go inside it. When using
--   this type, remember that 'Attribute', defined in @blaze-markup@,
--   is actually a collection of attributes, not a single attribute.
data Cell = Cell
  { cellAttribute :: !Attribute
  , cellHtml :: !Html
  }

instance IsString Cell where
  fromString = stringCell

instance Monoid Cell where
  mempty = Cell mempty mempty
  mappend (Cell a1 c1) (Cell a2 c2) = Cell (mappend a1 a2) (mappend c1 c2)

-- | Create a 'Cell' from a 'Widget'
htmlCell :: Html -> Cell
htmlCell = Cell mempty

-- | Create a 'Cell' from a 'String'
stringCell :: String -> Cell
stringCell = htmlCell . fromString

-- | Create a 'Cell' from a 'Char'
charCell :: Char -> Cell
charCell = stringCell . pure

-- | Create a 'Cell' from a 'Text'
textCell :: Text -> Cell
textCell = htmlCell . toHtml

-- | Create a 'Cell' from a lazy text
lazyTextCell :: LText.Text -> Cell
lazyTextCell = textCell . LText.toStrict

-- | Create a 'Cell' from a text builder
builderCell :: TBuilder.Builder -> Cell
builderCell = lazyTextCell . TBuilder.toLazyText

-- | Encode a table. This handles a very general case and
--   is seldom needed by users. One of the arguments provided is
--   used to add attributes to the generated @\<tr\>@ elements.
encodeTable :: forall h f a c. (Foldable f, E.Headedness h)
  => h (Attribute,Attribute) -- ^ Attributes of @\<thead\>@ and its @\<tr\>@, pass 'Nothing' to omit @\<thead\>@
  -> Attribute -- ^ Attributes of @\<tbody\>@ element
  -> (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element
  -> ((Html -> Html) -> c -> Html) -- ^ Wrap content and convert to 'Html'
  -> Attribute -- ^ Attributes of @\<table\>@ element
  -> Colonnade h a c -- ^ How to encode data as a row
  -> f a -- ^ Collection of data
  -> Html
encodeTable mtheadAttrs tbodyAttrs trAttrs wrapContent tableAttrs colonnade xs =
  H.table ! tableAttrs $ do
    case E.headednessExtractForall of
      Nothing -> return mempty
      Just extractForall -> do
        let (theadAttrs,theadTrAttrs) = extract mtheadAttrs
        H.thead ! theadAttrs $ H.tr ! theadTrAttrs $ do
          -- E.headerMonoidalGeneral colonnade (wrapContent H.th)
          foldlMapM' (wrapContent H.th . extract . E.oneColonnadeHead) (E.getColonnade colonnade)
        where
        extract :: forall y. h y -> y
        extract = E.runExtractForall extractForall
    encodeBody trAttrs wrapContent tbodyAttrs colonnade xs

foldlMapM' :: forall g b a m. (Foldable g, Monoid b, Monad m) => (a -> m b) -> g a -> m b
foldlMapM' f xs = foldr f' pure xs mempty
  where
  f' :: a -> (b -> m b) -> b -> m b
  f' x k bl = do
    br <- f x
    let !b = mappend bl br
    k b

-- | Encode a table with tiered header rows.
-- >>> let cor = mconcat [cap "Personal" (fmap htmlCell tableEmpA), cap "Work" tableEmpB]
-- >>> let fascia = FasciaCap (HA.class_ "category") (FasciaBase (HA.class_ "subcategory"))
-- >>> printCompactHtml (encodeCappedCellTable mempty fascia cor [head employees])
-- <table>
--     <thead>
--         <tr class="category">
--             <th colspan="2">Personal</th>
--             <th colspan="1">Work</th>
--         </tr>
--         <tr class="subcategory">
--             <th colspan="1">Name</th>
--             <th colspan="1">Age</th>
--             <th colspan="1">Dept.</th>
--         </tr>
--     </thead>
--     <tbody>
--         <tr>
--             <td>Thaddeus</td>
--             <td>34</td>
--             <td class="sales">Sales</td>
--         </tr>
--     </tbody>
-- </table>

encodeCappedCellTable :: Foldable f
  => Attribute -- ^ Attributes of @\<table\>@ element
  -> Fascia p Attribute -- ^ Attributes for @\<tr\>@ elements in the @\<thead\>@
  -> Cornice Headed p a Cell 
  -> f a -- ^ Collection of data
  -> Html
encodeCappedCellTable = encodeCappedTable mempty mempty (const mempty) htmlFromCell

-- | Encode a table with tiered header rows. This is the most general function
--   in this library for encoding a 'Cornice'.
--
encodeCappedTable :: Foldable f
  => Attribute -- ^ Attributes of @\<thead\>@ 
  -> Attribute -- ^ Attributes of @\<tbody\>@ element
  -> (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element in the @\<tbody\>@
  -> ((Html -> Html) -> c -> Html) -- ^ Wrap content and convert to 'Html'
  -> Attribute -- ^ Attributes of @\<table\>@ element
  -> Fascia p Attribute -- ^ Attributes for @\<tr\>@ elements in the @\<thead\>@
  -> Cornice Headed p a c 
  -> f a -- ^ Collection of data
  -> Html
encodeCappedTable theadAttrs tbodyAttrs trAttrs wrapContent tableAttrs fascia cornice xs = do
  let colonnade = E.discard cornice
      annCornice = E.annotate cornice
  H.table ! tableAttrs $ do
    H.thead ! theadAttrs $ do
      E.headersMonoidal 
        (Just (fascia, \attrs theHtml -> H.tr ! attrs $ theHtml))
        [ ( \msz c -> case msz of
              Just sz -> wrapContent H.th c ! HA.colspan (H.toValue (show sz))
              Nothing -> mempty
          , id
          )
        ]
        annCornice
      -- H.tr ! trAttrs $ do
      -- E.headerMonoidalGeneral colonnade (wrapContent H.th)
    encodeBody trAttrs wrapContent tbodyAttrs colonnade xs

encodeBody :: Foldable f
  => (a -> Attribute) -- ^ Attributes of each @\<tr\>@ element
  -> ((Html -> Html) -> c -> Html) -- ^ Wrap content and convert to 'Html'
  -> Attribute -- ^ Attributes of @\<tbody\>@ element
  -> Colonnade h a c -- ^ How to encode data as a row
  -> f a -- ^ Collection of data
  -> Html
encodeBody trAttrs wrapContent tbodyAttrs colonnade xs = do
  H.tbody ! tbodyAttrs $ do
    forM_ xs $ \x -> do
      H.tr ! trAttrs x $ E.rowMonoidal colonnade (wrapContent H.td) x
  

-- | Encode a table. Table cells may have attributes
--   applied to them.
encodeCellTable :: 
     Foldable f
  => Attribute -- ^ Attributes of @\<table\>@ element
  -> Colonnade Headed a Cell -- ^ How to encode data as columns
  -> f a -- ^ Collection of data
  -> Html
encodeCellTable = encodeTable
  (E.headednessPure (mempty,mempty)) mempty (const mempty) htmlFromCell 

-- | Encode a table. Table cell element do not have 
--   any attributes applied to them.
encodeHtmlTable :: 
     (Foldable f, E.Headedness h)
  => Attribute -- ^ Attributes of @\<table\>@ element
  -> Colonnade h a Html -- ^ How to encode data as columns
  -> f a -- ^ Collection of data
  -> Html
encodeHtmlTable = encodeTable
  (E.headednessPure (mempty,mempty)) mempty (const mempty) ($) 

-- | Convert a 'Cell' to 'Html' by wrapping the content with a tag
-- and applying the 'Cell' attributes to that tag.
htmlFromCell :: (Html -> Html) -> Cell -> Html
htmlFromCell f (Cell attr content) = f ! attr $ content

data St = St
  { stContext :: [String]
  , stTagStatus :: TagStatus
  , stResult :: String -> String -- ^ difference list
  }

data TagStatus 
  = TagStatusSomeTag 
  | TagStatusOpening (String -> String)
  | TagStatusOpeningAttrs 
  | TagStatusNormal 
  | TagStatusClosing (String -> String)
  | TagStatusAfterTag

removeWhitespaceAfterTag :: String -> String -> String
removeWhitespaceAfterTag chosenTag = 
  either id (\st -> stResult st "") . foldlM (flip f) (St [] TagStatusNormal id)
  where
  f :: Char -> St -> Either String St
  f c (St ctx status res) = case status of
    TagStatusNormal
      | c == '<' -> Right (St ctx TagStatusSomeTag likelyRes)
      | isSpace c -> if Just chosenTag == listToMaybe ctx
          then Right (St ctx TagStatusNormal res) -- drops the whitespace
          else Right (St ctx TagStatusNormal likelyRes)
      | otherwise -> Right (St ctx TagStatusNormal likelyRes)
    TagStatusSomeTag
      | c == '/' -> Right (St ctx (TagStatusClosing id) likelyRes)
      | c == '>' -> Left "unexpected >"
      | c == '<' -> Left "unexpected <"
      | otherwise -> Right (St ctx (TagStatusOpening (c:)) likelyRes)
    TagStatusOpening tag
      | c == '>' -> Right (St (tag "" : ctx) TagStatusAfterTag likelyRes)
      | isSpace c -> Right (St (tag "" : ctx) TagStatusOpeningAttrs likelyRes)
      | otherwise -> Right (St ctx (TagStatusOpening (tag . (c:))) likelyRes)
    TagStatusOpeningAttrs
      | c == '>' -> Right (St ctx TagStatusAfterTag likelyRes)
      | otherwise -> Right (St ctx TagStatusOpeningAttrs likelyRes)
    TagStatusClosing tag
      | c == '>' -> do
          otherTags <- case ctx of
            [] -> Left "closing tag without any opening tag"
            closestTag : otherTags -> if closestTag == tag ""
              then Right otherTags
              else Left $ "closing tag <" ++ tag "" ++ "> did not match opening tag <" ++ closestTag ++ ">"
          Right (St otherTags TagStatusAfterTag likelyRes)
      | otherwise -> Right (St ctx (TagStatusClosing (tag . (c:))) likelyRes)
    TagStatusAfterTag
      | c == '<' -> Right (St ctx TagStatusSomeTag likelyRes)
      | isSpace c -> if Just chosenTag == listToMaybe ctx
          then Right (St ctx TagStatusAfterTag res) -- drops the whitespace
          else Right (St ctx TagStatusNormal likelyRes)
      | otherwise -> Right (St ctx TagStatusNormal likelyRes)
    where 
    likelyRes :: String -> String
    likelyRes = res . (c:)

-- | Pretty print an HTML table, stripping whitespace from inside @\<td\>@,
--   @\<th\>@, and common inline tags. The implementation is inefficient and is
--   incorrect in many corner cases. It is only provided to reduce the line
--   count of the HTML printed by GHCi examples in this module\'s documentation.
--   Use of this function is discouraged.
printCompactHtml :: Html -> IO ()
printCompactHtml = putStrLn 
  . List.dropWhileEnd (== '\n')
  . removeWhitespaceAfterTag "td" 
  . removeWhitespaceAfterTag "th" 
  . removeWhitespaceAfterTag "strong" 
  . removeWhitespaceAfterTag "span" 
  . removeWhitespaceAfterTag "em" 
  . Pretty.renderHtml

-- | Similar to 'printCompactHtml'. Additionally strips all whitespace inside
--   @\<tr\>@ elements and @\<thead\>@ elements.
printVeryCompactHtml :: Html -> IO ()
printVeryCompactHtml = putStrLn 
  . List.dropWhileEnd (== '\n')
  . removeWhitespaceAfterTag "td" 
  . removeWhitespaceAfterTag "th" 
  . removeWhitespaceAfterTag "strong" 
  . removeWhitespaceAfterTag "span" 
  . removeWhitespaceAfterTag "em" 
  . removeWhitespaceAfterTag "tr" 
  . Pretty.renderHtml


-- $discussion
--
-- In this module, some of the functions for applying a 'Colonnade' to
-- some values to build a table have roughly this type signature:
--
-- > Foldable a => Colonnade Headedness Cell a -> f a -> Html
--
-- The 'Colonnade' content type is 'Cell', but the content
-- type of the result is 'Html'. It may not be immidiately clear why
-- this is useful done. Another strategy, which this library also
-- uses, is to write
-- these functions to take a 'Colonnade' whose content is 'Html':
--
-- > Foldable a => Colonnade Headedness Html a -> f a -> Html
--
-- When the 'Colonnade' content type is 'Html', then the header
-- content is rendered as the child of a @\<th\>@ and the row
-- content the child of a @\<td\>@. However, it is not possible
-- to add attributes to these parent elements. To accomodate this
-- situation, it is necessary to introduce 'Cell', which includes
-- the possibility of attributes on the parent node.