{-# LANGUAGE Safe #-}

{-
Copyright (c) 2006-2011 John Goerzen <jgoerzen@complete.org>

All rights reserved.

For license and copyright information, see the file LICENSE
-}

{- |
   Module     : Data.Progress.Meter
   Copyright  : Copyright (C) 2006-2011 John Goerzen
   SPDX-License-Identifier: BSD-3-Clause

   Stability  : stable
   Portability: portable

Tool for maintaining a status bar, supporting multiple simultaneous tasks,
as a layer atop "Data.Progress.Tracker".

Written by John Goerzen, jgoerzen\@complete.org -}

module Data.Progress.Meter (-- * Types
                               ProgressMeter,
                               -- * Creation and Configuration
                               simpleNewMeter,
                               newMeter,
                               setComponents,
                               addComponent,
                               removeComponent,
                               setWidth,

                               -- * Rendering and Output
                               renderMeter,
                               displayMeter,
                               clearMeter,
                               writeMeterString,
                               autoDisplayMeter,
                               killAutoDisplayMeter
                               ) where

import safe Data.Progress.Tracker
    ( ProgressStatuses(..),
      Progress,
      ProgressStatus(totalUnits, completedUnits, trackerName),
      getSpeed,
      getETR )
import safe Control.Concurrent
    ( modifyMVar_,
      withMVar,
      newMVar,
      MVar,
      threadDelay,
      forkIO,
      myThreadId,
      yield,
      ThreadId )
import Control.Monad (when)
import Data.String.Utils (join)
import System.Time.Utils (renderSecs)
import Data.Quantity (renderNums, binaryOpts)
import safe System.IO ( Handle, hFlush, hPutStr )
import Control.Monad (filterM)

{- | The main data type for the progress meter. -}
data ProgressMeterR =
    ProgressMeterR {masterP :: Progress, -- ^ The master 'Progress' object for overall status
                    components :: [Progress], -- ^ Individual component statuses
                    width :: Int, -- ^ Width of the meter
                    unit :: String, -- ^ Units of display
                    renderer :: [Integer] -> [String], -- ^ Function to render numbers
                    autoDisplayers :: [ThreadId] -- ^ Auto-updating display
                   }

type ProgressMeter = MVar ProgressMeterR

{- | Set up a new status bar using defaults:

* The given tracker

* Width 80

* Data.Quantity.renderNums binaryOpts 1

* Unit inticator @"B"@

-}
simpleNewMeter :: Progress -> IO ProgressMeter
simpleNewMeter pt = newMeter pt "B" 80 (renderNums binaryOpts 1)

{- | Set up a new status bar. -}
newMeter :: Progress           -- ^ The top-level 'Progress'
         -> String              -- ^ Unit indicator string
          -> Int                -- ^ Width of the terminal -- usually 80
          -> ([Integer] -> [String])-- ^ A function to render sizes
          -> IO ProgressMeter
newMeter tracker u w rfunc =
    newMVar $ ProgressMeterR {masterP = tracker, components = [],
                         width = w, renderer = rfunc, autoDisplayers = [],
                         unit = u}

{- | Adjust the list of components of this 'ProgressMeter'. -}
setComponents :: ProgressMeter -> [Progress] -> IO ()
setComponents meter componentlist = modifyMVar_ meter (\m -> return $ m {components = componentlist})

{- | Add a new component to the list of components. -}
addComponent :: ProgressMeter -> Progress -> IO ()
addComponent meter component =
    modifyMVar_ meter (\m -> return $ m {components = component : components m})

{- | Remove a component by name. -}
removeComponent :: ProgressMeter -> String -> IO ()
removeComponent meter componentname = modifyMVar_ meter $ \m ->
   do newc <- filterM (\x -> withStatus x (\y -> return $ trackerName y /= componentname))
              (components m)
      return $ m {components = newc}

{- | Adjusts the width of this 'ProgressMeter'. -}
setWidth :: ProgressMeter -> Int -> IO ()
setWidth meter w = modifyMVar_ meter (\m -> return $ m {width = w})

{- | Like renderMeter, but prints it to the screen instead of returning it.

This function will output CR, then the meter.

Pass stdout as the handle for regular display to the screen. -}
displayMeter :: Handle -> ProgressMeter -> IO ()
displayMeter h r = withMVar r $ \meter ->
    do s <- renderMeterR meter
       hPutStr h ("\r" ++ s)
       hFlush h
       -- By placing this whole thing under withMVar, we can effectively
       -- lock the IO and prevent IO from stomping on each other.

{- | Clears the meter -- outputs CR, spaces equal to the width - 1,
then another CR.

Pass stdout as the handle for regular display to the screen. -}
clearMeter :: Handle -> ProgressMeter -> IO ()
clearMeter h pm = withMVar pm $ \m ->
                     do hPutStr h (clearmeterstr m)
                        hFlush h

{- | Clears the meter, writes the given string, then restores the meter.
The string is assumed to contain a trailing newline.

Pass stdout as the handle for regular display to the screen. -}
writeMeterString :: Handle -> ProgressMeter -> String -> IO ()
writeMeterString h pm msg = withMVar pm $ \meter ->
                            do s <- renderMeterR meter
                               hPutStr h (clearmeterstr meter)
                               hPutStr h msg
                               hPutStr h s
                               hFlush h

clearmeterstr :: ProgressMeterR -> String
clearmeterstr m = "\r" ++ replicate (width m - 1) ' ' ++ "\r"

{- | Starts a thread that updates the meter every n seconds by calling
the specified function.  Note: @displayMeter stdout@
is an ideal function here.

Save this threadID and use it later to call 'stopAutoDisplayMeter'.
-}
autoDisplayMeter :: ProgressMeter -- ^ The meter to display
                 -> Int         -- ^ Update interval in seconds
                 -> (ProgressMeter -> IO ()) -- ^ Function to display it
                 -> IO ThreadId -- ^ Resulting thread id
autoDisplayMeter pm delay displayfunc =
    do thread <- forkIO workerthread
       modifyMVar_ pm (\p -> return $ p {autoDisplayers = thread : autoDisplayers p})
       return thread
    where workerthread = do tid <- myThreadId
                            -- Help fix a race condition so that the above
                            -- modifyMVar can run before a check ever does
                            yield
                            loop tid
          loop tid = do displayfunc pm
                        threadDelay (delay * 1000000)
                        c <- doIContinue tid
                        when c (loop tid)
          doIContinue tid = withMVar pm $ \p ->
                               if tid `elem` autoDisplayers p
                                  then return True
                                  else return False

{- | Stops the specified meter from displaying.

You should probably call 'clearMeter' after a call to this. -}
killAutoDisplayMeter :: ProgressMeter -> ThreadId -> IO ()
killAutoDisplayMeter pm t =
    modifyMVar_ pm (\p -> return $ p {autoDisplayers = filter (/= t) (autoDisplayers p)})

{- | Render the current status. -}
renderMeter :: ProgressMeter -> IO String
renderMeter r = withMVar r $ renderMeterR

renderMeterR :: ProgressMeterR -> IO String
renderMeterR meter =
    do overallpct <- renderpct $ masterP meter
       compnnts <- mapM (rendercomponent $ renderer meter)
                     (components meter)
       let componentstr = case join " " compnnts of
                            [] -> ""
                            x -> x ++ " "
       rightpart <- renderoverall (renderer meter) (masterP meter)
       let leftpart = overallpct ++ " " ++ componentstr
       let padwidth = (width meter) - 1 - (length leftpart) - (length rightpart)
       if padwidth < 1
          then return $ take (width meter - 1) $ leftpart ++ rightpart
          else return $ leftpart ++ replicate padwidth ' ' ++ rightpart

    where
      u = unit meter
      renderpct pt =
              withStatus pt renderpctpts
      renderpctpts pts =
                  if (totalUnits pts == 0)
                     then return "0%"
                     else return $ show (((completedUnits pts) * 100) `div` (totalUnits pts)) ++ "%"
      rendercomponent :: ([Integer] -> [String]) -> Progress -> IO String
      rendercomponent rfunc pt = withStatus pt $ \pts ->
              do pct <- renderpctpts pts
                 let renders = rfunc [totalUnits pts, completedUnits pts]
                 return $ "[" ++ trackerName pts ++ " " ++
                     (renders !! 1) ++ u ++ "/" ++
                     head renders ++ u ++ " " ++ pct ++ "]"

      renderoverall :: (ProgressStatuses a (IO [Char])) => ([Integer] -> [[Char]]) -> a -> IO [Char]
      renderoverall rfunc pt = withStatus pt $ \pts ->
                                         do etr <- getETR pts
                                            speed <- getSpeed pts
                                            return $ head (rfunc [floor (speed :: Double)]) ++ u ++
                                                       "/s " ++ renderSecs etr
