module Data.Progress.Meter (
                               ProgressMeter,
                               
                               simpleNewMeter,
                               newMeter,
                               setComponents,
                               addComponent,
                               removeComponent,
                               setWidth,
                               
                               renderMeter,
                               displayMeter,
                               clearMeter,
                               writeMeterString,
                               autoDisplayMeter,
                               killAutoDisplayMeter
                               ) where
import Data.Progress.Tracker
import Control.Concurrent
import Control.Monad (when)
import Data.String.Utils (join)
import System.Time.Utils (renderSecs)
import Data.Quantity (renderNums, binaryOpts)
import System.IO
import Control.Monad (filterM)
data ProgressMeterR =
    ProgressMeterR {masterP :: Progress, 
                    components :: [Progress], 
                    width :: Int, 
                    unit :: String, 
                    renderer :: [Integer] -> [String], 
                    autoDisplayers :: [ThreadId] 
                   }
type ProgressMeter = MVar ProgressMeterR
simpleNewMeter :: Progress -> IO ProgressMeter
simpleNewMeter pt = newMeter pt "B" 80 (renderNums binaryOpts 1)
newMeter :: Progress           
         -> String              
          -> Int                
          -> ([Integer] -> [String])
          -> IO ProgressMeter
newMeter tracker u w rfunc =
    newMVar $ ProgressMeterR {masterP = tracker, components = [],
                         width = w, renderer = rfunc, autoDisplayers = [],
                         unit = u}
setComponents :: ProgressMeter -> [Progress] -> IO ()
setComponents meter componentlist = modifyMVar_ meter (\m -> return $ m {components = componentlist})
addComponent :: ProgressMeter -> Progress -> IO ()
addComponent meter component =
    modifyMVar_ meter (\m -> return $ m {components = component : components m})
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}
setWidth :: ProgressMeter -> Int -> IO ()
setWidth meter w = modifyMVar_ meter (\m -> return $ m {width = w})
displayMeter :: Handle -> ProgressMeter -> IO ()
displayMeter h r = withMVar r $ \meter ->
    do s <- renderMeterR meter
       hPutStr h ("\r" ++ s)
       hFlush h
       
       
clearMeter :: Handle -> ProgressMeter -> IO ()
clearMeter h pm = withMVar pm $ \m ->
                     do hPutStr h (clearmeterstr m)
                        hFlush h
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"
autoDisplayMeter :: ProgressMeter 
                 -> Int         
                 -> (ProgressMeter -> IO ()) 
                 -> IO ThreadId 
autoDisplayMeter pm delay displayfunc =
    do thread <- forkIO workerthread
       modifyMVar_ pm (\p -> return $ p {autoDisplayers = thread : autoDisplayers p})
       return thread
    where workerthread = do tid <- myThreadId
                            
                            
                            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
killAutoDisplayMeter :: ProgressMeter -> ThreadId -> IO ()
killAutoDisplayMeter pm t =
    modifyMVar_ pm (\p -> return $ p {autoDisplayers = filter (/= t) (autoDisplayers p)})
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