{-# LANGUAGE ScopedTypeVariables #-}
module System.Taffybar.Widget.Generic.Graph (
  
    GraphHandle
  , GraphConfig(..)
  , GraphDirection(..)
  , GraphStyle(..)
  
  , graphNew
  , graphAddSample
  , defaultGraphConfig
  ) where
import           Control.Concurrent
import           Control.Monad ( when )
import           Control.Monad.IO.Class
import           Data.Foldable ( mapM_ )
import           Data.Sequence ( Seq, (<|), viewl, ViewL(..) )
import qualified Data.Sequence as S
import qualified Data.Text as T
import qualified GI.Cairo.Render as C
import           GI.Cairo.Render.Connector
import qualified GI.Cairo.Render.Matrix as M
import qualified GI.Gtk as Gtk
import           Prelude hiding ( mapM_ )
import           System.Taffybar.Util
import           System.Taffybar.Widget.Util
newtype GraphHandle = GH (MVar GraphState)
data GraphState =
  GraphState { graphIsBootstrapped :: Bool
             , graphHistory :: [Seq Double]
             , graphCanvas :: Gtk.DrawingArea
             , graphConfig :: GraphConfig
             }
data GraphDirection = LEFT_TO_RIGHT | RIGHT_TO_LEFT deriving (Eq)
type RGBA = (Double, Double, Double, Double)
data GraphStyle
    = Area 
    | Line 
data GraphConfig = GraphConfig {
  
    graphPadding :: Int
  
  , graphBackgroundColor :: RGBA
  
  , graphBorderColor :: RGBA
  
  , graphBorderWidth :: Int
  
  , graphDataColors :: [RGBA]
  
  , graphDataStyles :: [GraphStyle]
  
  , graphHistorySize :: Int
  
  , graphLabel :: Maybe T.Text
  
  , graphWidth :: Int
  
  , graphDirection :: GraphDirection
  }
defaultGraphConfig :: GraphConfig
defaultGraphConfig =
  GraphConfig
  { graphPadding = 2
  , graphBackgroundColor = (0.0, 0.0, 0.0, 1.0)
  , graphBorderColor = (0.5, 0.5, 0.5, 1.0)
  , graphBorderWidth = 1
  , graphDataColors = cycle [(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0)]
  , graphDataStyles = repeat Area
  , graphHistorySize = 20
  , graphLabel = Nothing
  , graphWidth = 50
  , graphDirection = LEFT_TO_RIGHT
  }
graphAddSample :: GraphHandle -> [Double] -> IO ()
graphAddSample (GH mv) rawData = do
  s <- readMVar mv
  let drawArea = graphCanvas s
      histSize = graphHistorySize (graphConfig s)
      histsAndNewVals = zip pcts (graphHistory s)
      newHists = case graphHistory s of
        [] -> map S.singleton pcts
        _ -> map (\(p,h) -> S.take histSize $ p <| h) histsAndNewVals
  when (graphIsBootstrapped s) $ do
    modifyMVar_ mv (\s' -> return s' { graphHistory = newHists })
    postGUIASync $ Gtk.widgetQueueDraw drawArea
  where
    pcts = map (clamp 0 1) rawData
clamp :: Double -> Double -> Double -> Double
clamp lo hi d = max lo $ min hi d
outlineData :: (Double -> Double) -> Double -> Double -> C.Render ()
outlineData pctToY xStep pct = do
  (curX,_) <- C.getCurrentPoint
  C.lineTo (curX + xStep) (pctToY pct)
renderFrameAndBackground :: GraphConfig -> Int -> Int -> C.Render ()
renderFrameAndBackground cfg w h = do
  let (backR, backG, backB, backA) = graphBackgroundColor cfg
      (frameR, frameG, frameB, frameA) = graphBorderColor cfg
      pad = graphPadding cfg
      fpad = fromIntegral pad
      fw = fromIntegral w
      fh = fromIntegral h
  
  C.setSourceRGBA backR backG backB backA
  C.rectangle fpad fpad (fw - 2 * fpad) (fh - 2 * fpad)
  C.fill
  
  
  when (graphBorderWidth cfg > 0) $ do
    let p = fromIntegral (graphBorderWidth cfg)
    C.setLineWidth p
    C.setSourceRGBA frameR frameG frameB frameA
    C.rectangle (fpad + (p / 2)) (fpad + (p / 2))
       (fw - 2 * fpad - p) (fh - 2 * fpad - p)
    C.stroke
renderGraph :: [Seq Double] -> GraphConfig -> Int -> Int -> Double -> C.Render ()
renderGraph hists cfg w h xStep = do
  renderFrameAndBackground cfg w h
  C.setLineWidth 0.1
  let pad = fromIntegral $ graphPadding cfg
  let framePad = fromIntegral $ graphBorderWidth cfg
  
  
  
  C.translate (pad + framePad) (pad + framePad)
  let xS = (fromIntegral w - 2 * pad - 2 * framePad) / fromIntegral w
      yS = (fromIntegral h - 2 * pad - 2 * framePad) / fromIntegral h
  C.scale xS yS
  
  
  
  when (graphDirection cfg == RIGHT_TO_LEFT) $
      C.transform $ M.Matrix (-1) 0 0 1 (fromIntegral w) 0
  let pctToY pct = fromIntegral h * (1 - pct)
      renderDataSet hist color style
        | S.length hist <= 1 = return ()
        | otherwise = do
          let (r, g, b, a) = color
              originY = pctToY newestSample
              originX = 0
              newestSample :< hist' = viewl hist
          C.setSourceRGBA r g b a
          C.moveTo originX originY
          mapM_ (outlineData pctToY xStep) hist'
          case style of
            Area -> do
              (endX, _) <- C.getCurrentPoint
              C.lineTo endX (fromIntegral h)
              C.lineTo 0 (fromIntegral h)
              C.fill
            Line -> do
              C.setLineWidth 1.0
              C.stroke
  sequence_ $ zipWith3 renderDataSet hists (graphDataColors cfg)
            (graphDataStyles cfg)
drawBorder :: MVar GraphState -> Gtk.DrawingArea -> C.Render ()
drawBorder mv drawArea = do
  (w, h) <- widgetGetAllocatedSize drawArea
  s <- liftIO $ readMVar mv
  let cfg = graphConfig s
  renderFrameAndBackground cfg w h
  liftIO $ modifyMVar_ mv (\s' -> return s' { graphIsBootstrapped = True })
  return ()
drawGraph :: MVar GraphState -> Gtk.DrawingArea ->  C.Render ()
drawGraph mv drawArea = do
  (w, h) <- widgetGetAllocatedSize drawArea
  drawBorder mv drawArea
  s <- liftIO $ readMVar mv
  let hist = graphHistory s
      cfg = graphConfig s
      histSize = graphHistorySize cfg
      
      
      xStep = fromIntegral w / fromIntegral (histSize - 1)
  case hist of
    [] -> renderFrameAndBackground cfg w h
    _ -> renderGraph hist cfg w h xStep
graphNew :: MonadIO m => GraphConfig -> m (Gtk.Widget, GraphHandle)
graphNew cfg = liftIO $ do
  drawArea <- Gtk.drawingAreaNew
  mv <- newMVar GraphState { graphIsBootstrapped = False
                           , graphHistory = []
                           , graphCanvas = drawArea
                           , graphConfig = cfg
                           }
  Gtk.widgetSetSizeRequest drawArea (fromIntegral $ graphWidth cfg) (-1)
  _ <- Gtk.onWidgetDraw drawArea $ \ctx -> renderWithContext
       (drawGraph mv drawArea) ctx >> return True
  box <- Gtk.boxNew Gtk.OrientationHorizontal 1
  case graphLabel cfg of
    Nothing  -> return ()
    Just lbl -> do
      l <- Gtk.labelNew (Nothing :: Maybe T.Text)
      Gtk.labelSetMarkup l lbl
      Gtk.boxPackStart box l False False 0
  Gtk.widgetSetVexpand drawArea True
  Gtk.widgetSetVexpand box True
  Gtk.boxPackStart box drawArea True True 0
  Gtk.widgetShowAll box
  giBox <- Gtk.toWidget box
  return (giBox, GH mv)