{-| This module provides some half-ready solutions to visualise heap
profiles both during and after execution with the help of OpenGL.  All
the rendering functions will fill the viewport if the model view
matrix is the identity (they also change the matrix), assuming the
projection matrix is the following:

@
 matrixMode $= Projection
 loadIdentity
 translate $ Vector3 (-1) (-1) 0
 scale 2 2 1
@

In other words, these functions fill the unit square at the origin. -}

module Profiling.Heap.OpenGL 
    ( colours
    , backgroundColour
    , otherColour
      -- * Processing raw samples (full profiles)
    , SamplePair(..)
    , prepareSamples
    , renderSamples
    , addSample
      -- * Processing optimised renders (profile streams)
    , GraphData
    , graphNames
    , emptyGraph
    , growGraph
    , renderGraph
    , GraphMode(..)
    , nextGraphMode
    ) where

import Control.Applicative
import Control.Monad
import qualified Data.ByteString.Char8 as S
import Data.IntMap (IntMap)
import qualified Data.IntMap as IM
import Data.List
import Graphics.Rendering.OpenGL hiding (samples)
import Graphics.Rendering.OpenGL.GL.DisplayLists
import Profiling.Heap.Types

{-| Two heap profile samples which contain the exact same cost centres
in the exact same order. -}

data SamplePair = SP
    { spTime1 :: !Time
    , spTime2 :: !Time
    , spData1 :: !ProfileSample
    , spData2 :: !ProfileSample
    } deriving Show

{-| An optimised graph rendering designed to be easily updated when a
new sample arrives. -}

data GraphData = GD
    { gdNames :: IntMap String                   -- ^ Cost centre id to name mapping.
    , gdSamples :: [SamplePair]                  -- ^ List of pairwise aligned samples.
    , gdLists :: [(Int,DisplayList,DisplayList)] -- ^ Display lists caching rendering in all modes.
    , gdMinTime :: Time                          -- ^ The time of the first sample.
    }

{-| The names of cost centres in a graph rendering. -}
graphNames :: GraphData -> IntMap String
graphNames = gdNames

{-| An empty rendering. -}

emptyGraph :: GraphData
emptyGraph = GD
             { gdNames = IM.singleton 0 "Other"
             , gdSamples = [SP 0 0 [] []]
             , gdLists = []
             , gdMinTime = 0
             }

{-| The possible ways of displaying heap profiles. -}

data GraphMode
    -- | Cost centres are stacked on top of each other without
    -- overlapping.
    = Accumulated
    -- | Each cost centre yields a separate line graph on the same
    -- scale.
    | Separate
      deriving Eq

{-| A cyclic successor function for graph modes. -}

nextGraphMode :: GraphMode -> GraphMode
nextGraphMode Accumulated = Separate
nextGraphMode Separate    = Accumulated

{-| A list of highly different colours, where the differences diminish
as we advance in the list.  The first element is black, and there is
no white. -}

colours :: [Color3 GLubyte]
colours = concatMap makeCol [0..]
    where comps = 0 : 255 : unfoldr cnext (256,127 :: Int)
          cnext (s,c) = Just (fromIntegral c,if s+fromIntegral c >= 255 then (s `div` 2,s `div` 4-1) else (s,s+c))
          makeCol n = if n == 1 then init res else res
              where res = [Color3 (comps !! rn) (comps !! gn) (comps !! bn) |
                           rn <- [0..n], gn <- [0..n], bn <- [0..n],
                           rn == n || gn == n || bn == n]

{-| The colour of the background (white).  It is not a member of
'colours'. -}

backgroundColour :: Color3 GLubyte
backgroundColour = Color3 255 255 255

{-| The colour used for unimportant cost centres (black).  It is the
first element of 'colours'. -}

otherColour :: Color3 GLubyte
otherColour = Color3 0 0 0

{-| The limit under which cost centres are filtered out (grouped under
the name \"Other\"). -}

costLimit :: Cost
costLimit = 256

{-| Create a list of sample pairs where each cost centre is paired up
with the consecutive one, so it is easier to render them.  Cost
centres with small costs (below 'costLimit') are lumped together under
identifier 0, reserved for \"Other\". -}

prepareSamples :: ProfileQuery p => p -> [SamplePair]
prepareSamples prof = foldl addSample [SP 0 0 [] []] (samples prof)

-- Must be called within "renderPrimitive Quads".
renderSampleAccumulated :: SamplePair -> IO ()
renderSampleAccumulated (SP t1 t2 smp1 smp2) = do
  let acc s1 s2 = scanl accCost (undefined,0,0) (zip s1 s2)
      accCost (_,c1,c2) ((ccid,c1'),(_,c2')) = (ccid,c1+c1',c2+c2')

  forM_ (zip <*> tail $ acc smp1 smp2) $ \((_,c1,c2),(ccid,c1',c2')) -> do
    color (colours !! ccid)
    vertex2 (realToFrac t1) (fromIntegral c1)
    vertex2 (realToFrac t2) (fromIntegral c2)
    vertex2 (realToFrac t2) (fromIntegral c2')
    vertex2 (realToFrac t1) (fromIntegral c1')

-- Must be called within "renderPrimitive Lines".
renderSampleSeparate :: SamplePair -> IO ()
renderSampleSeparate (SP t1 t2 smp1 smp2) = do
  forM_ (zip smp1 smp2) $ \((ccid,cost1),(_,cost2)) -> do
    color (colours !! ccid)
    vertex2 (realToFrac t1) (fromIntegral cost1)
    vertex2 (realToFrac t2) (fromIntegral cost2)

{-| Render a given list of prepared samples in the given mode.  The
third argument is the maximum time of the graph, which affects
horizontal scaling. -}

renderSamples :: GraphMode -> [SamplePair] -> Time -> IO ()
renderSamples Accumulated smps tmax = do
  let cmax = fromIntegral . maximum $ [sum (map snd smp) | SP _ _ _ smp <- smps]

  scale2 (1/realToFrac tmax) (1/cmax)

  renderPrimitive Quads $ forM_ smps renderSampleAccumulated

renderSamples Separate smps tmax = do
  let cmax = fromIntegral . maximum $ [cost | SP _ _ _ smp <- smps, (_,cost) <- smp]

  scale2 (1/realToFrac tmax) (1/cmax)

  renderPrimitive Lines $ forM_ smps renderSampleSeparate

{-| Integrating a new sample into the list of merged sample pairs we
have so far.  The input list should start with the latest sample, and
the new sample pair will be the head of the result. -}

addSample :: [SamplePair] -> (Time,ProfileSample) -> [SamplePair]
addSample smps (t,smp) = newSample : smps
    where newSample = mergeSamples (head smps) t (groupSmalls smp)
          mergeSamples (SP _ t1 _ smp1) t2 smp2 =
              SP { spTime1 = t1, spTime2 = t2, spData1 = smp1', spData2 = smp2' }
              where (smp1',smp2') = mergeSample smp1 (sort smp2)

          groupSmalls s = (0,sum . map snd $ sn) : map (\(ccid,cost) -> (ccid+1,cost)) sy
              where (sy,sn) = partition (\(_,c) -> c >= costLimit) s

          -- Merging key-value lists ordered by the key.  For each key that is
          -- present in only one of the lists we insert it with value 0 in the
          -- other list.
          mergeSample [] s = (map (\(ccid,_) -> (ccid,0)) s,s)
          mergeSample s [] = (s,map (\(ccid,_) -> (ccid,0)) s)
          mergeSample (s1@(cid1,cost1):ss1) (s2@(cid2,cost2):ss2) =
              if cid1 == cid2 then
                  let (smp1,smp2) = mergeSample ss1 ss2
                  in (s1:smp1,s2:smp2)
              else if cid1 > cid2 then
                       let (smp1,smp2) = mergeSample (s1:ss1) ss2
                       in if cost2 > 0 then ((cid2,0):smp1,s2:smp2)
                          else (smp1,smp2)
                   else let (smp1,smp2) = mergeSample ss1 (s2:ss2)
                        in if cost1 > 0 then (s1:smp1,(cid1,0):smp2)
                           else (smp1,smp2)

{-| Integrate a new sample in an extensible graph. -}

growGraph :: GraphData -> SinkInput -> IO GraphData
growGraph graph SinkStop = return graph
growGraph graph (SinkId ccid ccname) = return (modNames (IM.insert (ccid+1) (S.unpack ccname)) graph)
    where modNames f g = g { gdNames = f (gdNames g) }
growGraph graph (SinkSample t smp) = do
  let graph' = graph { gdSamples = addSample (gdSamples graph) (t,smp) }
      graph'' = if gdMinTime graph' <= 0 then graph' { gdMinTime = t } else graph'
      smps = gdSamples graph''

      -- The heart of the optimisation: compiling a tree of display lists as we go.
      dlUnion []             = return []
      dlUnion xs@((x,_,_):_) = if length prefix >= 20 then do
                                   dlAcc <- defineNewList Compile $ mapM_ clAcc prefix
                                   dlSep <- defineNewList Compile $ mapM_ clSep prefix
                                   dlUnion ((x+1,dlAcc,dlSep):rest)
                                 else return xs
          where (prefix,rest) = span (\(y,_,_) -> y == x) xs

      clAcc = \(_,dl,_) -> callList dl
      clSep = \(_,_,dl) -> callList dl

  dlAcc <- defineNewList Compile $ renderPrimitive Quads $ renderSampleAccumulated $ head smps
  dlSep <- defineNewList Compile $ renderPrimitive Lines $ renderSampleSeparate $ head smps

  dls' <- dlUnion ((0,dlAcc,dlSep):gdLists graph'')

  return (graph'' { gdLists = dls' })

{-| Render a stream in the given graph mode. -}

renderGraph :: GraphMode -> GraphData -> IO ()

renderGraph Accumulated graph = do
  let smps = gdSamples graph
      tmin = realToFrac $ gdMinTime graph
      tmax = realToFrac . spTime2 . head $ smps
      cmax = fromIntegral . maximum $ [sum (map snd smp) | SP _ _ _ smp <- take 50 smps]

  scale2 (1/(tmax-tmin)) (1/cmax)
  translate2 (-tmin) 0
  mapM_ (\(_,dl,_) -> callList dl) . gdLists $ graph

renderGraph Separate graph = do
  let smps = gdSamples graph
      tmin = realToFrac $ gdMinTime graph
      tmax = realToFrac . spTime2 . head $ smps
      cmax = fromIntegral . maximum $ [cost | SP _ _ _ smp <- take 50 smps, (_,cost) <- smp]

  scale2 (1/(tmax-tmin)) (1/cmax)
  translate2 (-tmin) 0

  mapM_ (\(_,_,dl) -> callList dl) . gdLists $ graph

-- Helper functions to make type disambiguation easier.

vertex2 :: GLfloat -> GLfloat -> IO ()
vertex2 x y = vertex $ Vertex2 x y

scale2 :: GLfloat -> GLfloat -> IO ()
scale2 x y = scale x y 1

translate2 :: GLfloat -> GLfloat -> IO ()
translate2 x y = translate $ Vector3 x y 0