-- |
-- Module      : Console.Display
-- License     : BSD-style
-- Maintainer  : Vincent Hanquez <vincent@snarc.org>
-- Stability   : experimental
-- Portability : Good
--
-- Displaying utilities
--

{-# LANGUAGE NoImplicitPrelude #-}

module Console.Display
    ( TerminalDisplay
    -- * Basic
    , displayInit
    , display
    , displayTextColor
    , displayLn
    -- * Progress Bar
    , ProgressBar
    , progress
    , progressTick
    -- * Summary line
    , Summary
    , summary
    , summarySet
    -- * Attributes
    , ColorComponent
    , OutputElem(..)
    , termText
    , justify
    -- * Table
    , Justify(..)
    , Table
    , Column
    , columnNew
    , tableCreate
    , tableHeaders
    , tableAppend
    ) where

import Basement.Terminal
import Basement.Terminal.ANSI
import Basement.Types.OffsetSize

import Foundation
import Foundation.Numerical
import Foundation.IO
import Foundation.IO.Terminal
import Foundation.String
import Foundation.Collection

import           System.IO (Handle, hSetBuffering, BufferMode(NoBuffering))

import           Control.Applicative
import           Control.Monad (when)
import           Control.Concurrent.MVar

-- | Element to output text and attributes to the display
data OutputElem =
      Bg ColorComponent
    | Fg ColorComponent
    | T  String
    | LeftT (CountOf Char) String      -- ^ Left-aligned text
    | RightT (CountOf Char) String     -- ^ Right-aligned text
    | CenterT (CountOf Char) String    -- ^ Centered text
    | JustifiedT (CountOf Char) String -- ^ Justified text
    | NA
    deriving (Show,Eq)

-- | Terminal display state
data TerminalDisplay = TerminalDisplay (MVar Bool) Handle

hPutStr :: Handle -> String -> IO ()
hPutStr h = hPut h . toBytes UTF8

termText :: String -> String
termText = id

-- | Create a new display
displayInit :: IO TerminalDisplay
displayInit = do
    initialize
    hSetBuffering stdout NoBuffering
    cf <- newMVar False
    pure $ TerminalDisplay cf stdout

-- | Display
display :: TerminalDisplay -> [OutputElem] -> IO ()
display tdisp@(TerminalDisplay clearFirst term) oelems = do
    cf <- modifyMVar clearFirst $ \cf -> return (False, cf)
    when cf $ hPutStr term eraseLineFromCursor
    hPutStr term $ renderOutput tdisp oelems
  where
        clearLineFirst = eraseLineFromCursor

renderOutput :: TerminalDisplay -> [OutputElem] -> String
renderOutput (TerminalDisplay _ term) to = mconcat $ fmap toString to
  where
        wF = flip sgrForeground False
        wB = flip sgrBackground False
        rD = sgrReset

        toString (Fg c) = wF c
        toString (Bg c) = wB c
        toString (T t)  = t
        toString (LeftT size t)      = justify JustifyLeft      size t
        toString (RightT size t)     = justify JustifyRight     size t
        toString (CenterT size t)    = justify JustifyCenter    size t
        toString (JustifiedT size t) = justify JustifyJustified size t
        toString NA     = rD

-- | A simple utility that display a @msg@ in @color@
displayTextColor :: TerminalDisplay -> ColorComponent -> String -> IO ()
displayTextColor term color msg =
    display term [Fg color, T msg]

-- | A simple utility that display a @msg@ in @color@ and newline at the end.
displayLn :: TerminalDisplay -> ColorComponent -> String -> IO ()
displayLn disp color msg = displayTextColor disp color (msg <> "\n")

white :: ColorComponent
white = 7

-- | A progress bar widget created and used within the `progress` function.
data ProgressBar = ProgressBar TerminalDisplay ProgressBackend (MVar ProgressState)

type ProgressBackend = String -> IO ()

-- | Summary
data Summary = Summary SummaryBackend
type SummaryBackend = [OutputElem] -> IO ()

data ProgressState = ProgressState
    { pgLhs     :: String
    , pgRhs     :: String
    , pgMax     :: Int
    , pgCurrent :: Int
    }

initProgressState :: Int -> ProgressState
initProgressState maxItems = ProgressState
    { pgLhs     = ""
    , pgRhs     = ""
    , pgMax     = maxItems
    , pgCurrent = 0
    }

-- | Create a new progress bar context from a terminal display, a number of
-- items, and a progress update function.
--
-- The progress bar update function should perform the desired actions on each
-- of the items, and call `progressTick` whenever an item is fully processed.
--
-- Each time the given update function calls progressTick the progress bar
-- fills by one item until the given number of items is matched. Once the bar
-- is filled it will not fill any further even if progressTick is called
-- again.
--
-- The progress bar will disappear when the given update function completes
-- running, even if the progress bar is not yet entirely filled.
--
-- For example, the following function will create a progress bar of 100
-- items, and complete one of the items every tenth of a second. Once all of
-- the items are completed the progress bar is removed and a completion String
-- is returned.
--
-- @
-- runProgressBar :: IO String
-- runProgressBar = do
--     terminalDisplay <- displayInit
--     progress terminalDisplay 100 (progressFunction 100)
--   where
--     progressFunction :: Int -> ProgressBar -> IO String
--     progressFunction 0 _   = return "Completed!"
--     progressFunction n bar = do
--       threadDelay 100000
--       progressTick bar
--       progressFunction (n - 1) bar
-- @
progress :: TerminalDisplay       -- ^ The terminal display to display the progress bar on.
         -> Int                   -- ^ The number of items the progress bar represents.
         -> (ProgressBar -> IO a) -- ^ The progression bar update function.
         -> IO a                  -- ^ The results of the progress bar update function.
progress tdisp@(TerminalDisplay cf term) numberItems f = do
    let b = backend (Just cursorDown)
                    (Just "\r")
                    (Just eraseLineFromCursor)
    pbar <- ProgressBar tdisp b <$> newMVar (initProgressState numberItems)

    progressStart pbar
    a <- f pbar
    displayLn tdisp white ""
    return a
  where
    backend :: Maybe (Word64 -> String)
            -> Maybe String
            -> Maybe String
            -> ProgressBackend
    backend _ (Just goHome) (Just clearEol) = \msg -> do
        hPutStr term $ mconcat [clearEol, msg, goHome]
        modifyMVar_ cf $ return . const True
    backend _ _ _ = displayLn tdisp white

showBar :: ProgressBar -> IO ()
showBar (ProgressBar _ backend pgsVar) = do
    pgs <- readMVar pgsVar
    let bar = getBar pgs
    backend bar
  where
    getBar (ProgressState lhs rhs maxItems current) =
            lhs `sep` bar `sep`
            (show current <> "/" <> show maxItems) `sep`
            rhs
      where
        sep s1 s2
            | null s1   = s2
            | null s2   = s1
            | otherwise = s1 <> " " <> s2

        bar
            | maxItems == current = "[" <> replicate (CountOf szMax) fillingChar <> "]"
            | otherwise           = "[" <> replicate (CountOf filled) fillingChar <> ">" <> replicate unfilled' ' ' <> "]"

        fillingChar = '='

        unfilled   = CountOf $ szMax - filled
        unfilled'  = CountOf $ szMax - filled - 1
        filled     = roundDown numberChar

        numberChar = fromIntegral szMax / currentProgress
        szMax      = 40 :: Int

        currentProgress :: Double
        currentProgress = fromIntegral maxItems / fromIntegral current

-- | Start displaying the progress bar
progressStart :: ProgressBar -> IO ()
progressStart pbar = do
    showBar pbar
    return ()

-- | Ticks an element on the given progress bar. Should be used within a
-- progress bar update function that is passed into `progress`.
--
-- @
-- progressFunction :: ProgressBar -> IO String
-- progressFunction bar = do
--   threadDelay 100000
--   progressTick bar
--   threadDelay 200000
--   progressTick bar
--   return "Completed!"
-- @
progressTick :: ProgressBar -> IO ()
progressTick pbar@(ProgressBar _ _ st) = do
    modifyMVar_ st $ \pgs -> return $ pgs { pgCurrent = min (pgMax pgs) (pgCurrent pgs + 1) }
    showBar pbar
    return ()

-- | Create a summary
summary :: TerminalDisplay -> IO Summary
summary tdisp@(TerminalDisplay cf term) = do
    let b = backend (Just cursorDown)
                    (Just "\r")
                    (Just eraseLineFromCursor)
    return $ Summary b
  where
    backend :: Maybe (Word64 -> String)
            -> Maybe String
            -> Maybe String
            -> SummaryBackend
    backend _ (Just goHome) (Just clearEol) = \msg -> do
        hPutStr term $ mconcat [clearEol, renderOutput tdisp msg, goHome]
        modifyMVar_ cf $ return . const True
    backend _ _ _ = \msg ->
        hPutStr term $ mconcat [renderOutput tdisp msg]

-- | Set the summary
summarySet :: Summary -> [OutputElem] -> IO ()
summarySet (Summary backend) = backend

-- | A justification of text.
data Justify = JustifyLeft       -- ^ Left align text
             | JustifyRight      -- ^ Right align text
             | JustifyCenter     -- ^ Center text
             | JustifyJustified  -- ^ Text fills the whole line width

-- | Boxes a string to a given size using the given justification.
--
-- If the size of the given string is greater than or equal to the given boxing
-- size, then the original string is returned.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> justify JustifyRight 35 "Lorem ipsum dolor sit amet"
-- "Lorem ipsum dolor sit amet         "
--
-- >>> justify JustifyLeft 35 "Lorem ipsum dolor sit amet"
-- "         Lorem ipsum dolor sit amet"
--
-- >>> justify JustifyCenter 35 "Lorem ipsum dolor sit amet"
-- "    Lorem ipsum dolor sit amet     "
--
-- >>> justify JustifyJustified 35 "Lorem ipsum dolor sit amet"
-- "Lorem    ipsum   dolor   sit   amet"
--
-- Apply a justified justification to a one word string, resulting in a string
-- of the given length with the word at the left followed by the remaining
-- space.
--
-- >>> justify JustifyJustified 10 "Hello."
-- "Hello.    "
--
-- Attempt to box a string that is larger than the given box, yielding the
-- original string.
--
-- >>> justify JustifyRight 5 "Hello, World!"
-- "Hello, World!"
justify :: Justify -> CountOf Char -> String -> String
justify justification size string
    | size <= stringSize = string
    | otherwise = case justification of
        JustifyLeft      -> padding <> string
        JustifyRight     -> string <> padding
        JustifyCenter    -> if even sizeDifference
          then halfPadding <> string <>       halfPadding
          else halfPadding <> string <> ' ' `cons` halfPadding
        JustifyJustified -> justifyJustified size string
  where
    padding        = replicate sizeDifference ' '
    halfPadding    = replicate (toCount $ fromCount sizeDifference `div` 2) ' '
    sizeDifference = fromMaybe 0 $ size - stringSize
    stringSize     = length string
    even (CountOf n) = (n `mod` 2) == 0

-- | Justifies the given string so that it takes up the space of the entire
-- given box size.
justifyJustified :: CountOf Char -> String -> String
justifyJustified size string
    | numberOfWords == 1 = string <> replicate (toCount lengthDifference) ' '
    | otherwise          = justifiedString
  where
    justifiedString  = intercalate spaces $ insertExcessSpaces (toCount excessChars) stringWords
    spaces           = replicate (toCount spacing) ' '
    excessChars      = lengthDifference `mod` (numberOfWords - 1)
    spacing          = lengthDifference `div` (numberOfWords - 1)
    lengthDifference = fromCount size - wordsLength
    wordsLength      = foldl' (+) 0 $ fmap (fromCount . length) stringWords
    numberOfWords    = fromCount (length stringWords)
    stringWords      = words string

-- | Inserts excess spaces between words for the process of justifying a
-- string.
insertExcessSpaces :: CountOf Char -> [String] -> [String]
insertExcessSpaces _ []     = []
insertExcessSpaces 0 w      = w
insertExcessSpaces n (x:xs) = (x <> " ") : insertExcessSpaces (fromMaybe 0 $ n - 1) xs

{-
data Attr = Attr
    { attrHasSize    :: Maybe Int
    , attrHasJustify :: Maybe Justify
    , attrHasColor   :: Maybe Color
    , attrHasBgColor :: Maybe Color
    , attrText       :: String
    }

instance Monoid Attr where
    mempty = Attr Nothing Nothing Nothing Nothing ""
    mappend a1 a2 =
        Attr { attrHasSize    = attrHasSize a2
             , attrHasJustify = attrHasJustify a2
             , attrHasColor   = attrHasColor a2
             , attrHasBgColor = attrHasBgColor a2
             , attrText       = attrText a1 ++ attrText a2
             }

attrColor :: Color -> Attr -> Attr
attrColor c a = a { attrHasColor = Just c }

attrSize :: Int -> Attr -> Attr
attrSize 0 a = a { attrHasSize = Nothing }
attrSize n a = a { attrHasSize = Just n }

attrJustify :: Justify -> Attr -> Attr
attrJustify j a = a { attrHasJustify = Just j }

text :: String -> Attr
text s = mempty { attrText = s }

toElem :: Attr -> [OutputElem]
toElem attr =
    let j = case attrHasSize attr of
            Just n ->
                case attrHasJustify attr of
                    Nothing           -> [LeftT n " "]
                    Just JustifyLeft  -> [LeftT n " "]
                    Just JustifyRight -> [RightT n " "]
            Nothing ->
                []
        fwc = case attrHasColor attr of
                    Nothing -> []
                    Just c  -> [Fg c]
        bgc = case attrHasBgColor attr of
                    Nothing -> []
                    Just c  -> [Bg c]
     in mconcat [j,fwc,bgc, [T $ attrText attr,NA]]
-}

-- | Column for a table
data Column = Column
    { columnSize    :: CountOf Char     -- ^ Size of the column
    , columnName    :: String  -- ^ Name of the column. used in 'tableHeaders'
    , columnJustify :: Justify -- ^ The justification to use for the cell of this column
    , columnWrap    :: Bool    -- ^ if needed to wrap, whether text disappear or use multi lines row (unimplemented)
    }

-- | Create a new column setting the right default parameters
columnNew :: CountOf Char -> String -> Column
columnNew n name = Column
    { columnSize    = n
    , columnName    = name
    , columnJustify = JustifyLeft
    , columnWrap    = False
    }

-- | Table widget
data Table = Table
    { tColumns     :: [Column]
    , rowSeparator :: String
    }

-- | Create a new table
tableCreate :: [Column] -> Table
tableCreate cols = Table { tColumns = cols, rowSeparator = "" }

-- | Show the table headers
tableHeaders :: TerminalDisplay -> Table -> IO ()
tableHeaders td t =
    tableAppend td t $ columnName <$> tColumns t

-- | Append a row to the table.
--
-- if the number of elements is greater than the amount of
-- column the table has been configured with, the extra
-- elements are dropped.
tableAppend :: TerminalDisplay -> Table -> [String] -> IO ()
tableAppend td (Table cols rowSep) l = do
    let disp = case compare numelems numcols of
                        LT -> zip cols (l <> replicate (fromMaybe 0 $ numcols - numelems) "")
                        _  -> zip cols l :: [(Column, String)]
    mapM_ printColRow $ intersperse Nothing $ fmap Just disp
  where
    numelems = length l
    numcols  = sizeCast Proxy (length cols) :: CountOf String
    printColRow Nothing =
        display td [T rowSep]
    printColRow (Just (c, fieldElement)) = do
        let oe = case columnJustify c of
                   JustifyLeft      -> RightT     (columnSize c) fieldElement
                   JustifyRight     -> LeftT      (columnSize c) fieldElement
                   JustifyCenter    -> CenterT    (columnSize c) fieldElement
                   JustifyJustified -> JustifiedT (columnSize c) fieldElement
        display td [oe,T "\n"]