module Data.Renderable (
    RenderStrategy(..),
    Renderer,
    Rendering,
    CleanOp,
    Cache,
    CacheStats(..),
    renderPrims,
    renderPrimsDebug,
    renderPrimsWithStats,
    emptyRenderer,
    appendRenderer
) where

import Prelude hiding (lookup)
import Control.Applicative
import Control.Monad
import Control.Monad.IO.Class
import Data.Monoid
import Data.Hashable
import qualified Data.Traversable as T
import Data.IntMap (IntMap)
import Data.Foldable (foldl')
import qualified Data.IntMap as IM
--------------------------------------------------------------------------------
-- A strategy for rendering
--------------------------------------------------------------------------------
-- | A 'RenderStrategy' is a method for creating a renderer that can render
-- your primitives. Examples of primitives are are points, lines, triangles and
-- other shapes. A 'RenderStrategy' is parameterized by four types -
--
-- @m@ - the monad in which rendering calls will take place.
--
-- @t@ - type of the graphics transformation that can be applied to the
--       renderer
--
-- @r@ - type that holds static resources such as windows, shaders, etc.
--
-- @a@ - type of the primitive that can be renderered.
data RenderStrategy m t r a = RenderStrategy
    { canAllocPrimitive :: r -> a -> Bool
      -- ^ Determines whether a renderer can be allocated for the primitive.
      -- A result of 'False' will defer compilation until a later time (the next
      -- frame).

    , compilePrimitive :: r -> a -> m (Renderer m t)
      -- ^ Allocates resources for rendering the primitive and return
      -- a monadic call that renders the primitive using a transform.
      -- Tuples that with a call to clean up the allocated resources.
    }
--------------------------------------------------------------------------------
-- Rendering
--------------------------------------------------------------------------------
-- | A Rendering is an effectful computation for displaying something given a
-- transform.
type Rendering m t = t -> m ()

-- | A CleanOp is an effectfull computaton that cleans up any resources
-- allocated during the creation of an associated Rendering.
type CleanOp m = m ()

-- | A Renderer is the pairing of a Rendering and a Cleanup.
type Renderer m t = (CleanOp m, Rendering m t)

-- | Create a renderer that renders nothing and releases no resources.
emptyRenderer :: Monad m => Renderer m t
emptyRenderer = (return (), const $ return ())

-- | Appends two renderers into one.
appendRenderer :: Monad m => Renderer m t -> Renderer m t -> Renderer m t
appendRenderer (c1,r1) (c2,r2) = (c1 >> c2, \t -> r1 t >> r2 t)

-- | A cache of renderers.
type Cache m t = IntMap (Renderer m t)

findRenderer :: (Monad m, Hashable a)
             => Cache m t -> (Cache m t, IntMap a) -> a -> (Cache m t, IntMap a)
findRenderer cache (found, missing) a =
    let k = hash a in
    case IM.lookup k cache of
        Nothing -> (found, IM.insert k a missing)
        Just r  -> (IM.insert k r found, missing)

getRenderer :: (Hashable a, Monad m)
            => RenderStrategy m t r a -> r -> Cache m t -> a -> m (Cache m t)
getRenderer s rez cache a =
    if canAllocPrimitive s rez a
    then do r <- compilePrimitive s rez a
            return $ IM.insert (hash a) r cache
    else return cache

clean :: Renderer m t -> m ()
clean = fst

render :: Renderer m t -> t -> m ()
render = snd

renderElement :: (Hashable a, Monad m) => Cache m t -> t -> a -> m ()
renderElement cache t a = do
    let k = hash a
    case IM.lookup k cache of
        Nothing -> return ()
        Just r  -> render r t

-- | A sum of lists of rendering hashes between two cache states.
-- Used for debugging resource management.
data CacheStats a = CacheStats { cachedPrev    :: [Int]
                               -- ^ All the keys of the previous cache state.
                               , cachedFound   :: [Int]
                               -- ^ The keys needed for the next state that
                               -- were found in the previous cache (no need
                               -- to allocate).
                               , cachedMissing :: [Int]
                               -- ^ The keys needed for the next state that
                               -- were not found in the previous cache (these
                               -- will need allocating).
                               , cachedStale   :: [Int]
                               -- ^ The keys found in the previous cache that
                               -- are not needed for the next state (these
                               -- can be deallocated).
                               , cachedNext    :: [Int]
                               -- ^ All the keys of the next cache state.
                               }

-- | Map a 'CacheStats' into a nice readable string.
showCacheStats :: CacheStats a -> String
showCacheStats (CacheStats cache found missing stale next) = unlines
    [ "Prev:    " ++ show cache
    , "Found:   " ++ show found
    , "Missing: " ++ show missing
    , "Stale:   " ++ show stale
    , "Next:    " ++ show next
    ]

-- | Render a list of primitives using renderings stored in the given cache,
-- return a new cache that can be used to render the next list of
-- primitives, along with some info about the comparison of the given and
-- returned cache.
renderPrimsWithStats :: (Functor m, Monad m, Monoid t, Hashable a)
                     => RenderStrategy m t r a -> r -> Cache m t -> [(t, a)]
                     -> m (Cache m t, CacheStats a)
renderPrimsWithStats s rez cache prims = do
    let (found, missing) = foldl' (findRenderer cache)
                                  (mempty, mempty)
                                  (map snd prims)
        stale = cache `IM.difference` found

    -- Clean the stale renderers
    void $ T.sequence $ fmap clean stale

    -- Get the missing renderers
    new <- foldM (getRenderer s rez) mempty $ IM.elems missing

    let next = IM.union found new
        stats = CacheStats { cachedPrev = IM.keys cache
                           , cachedFound = IM.keys found
                           , cachedMissing = IM.keys missing
                           , cachedStale = IM.keys stale
                           , cachedNext = IM.keys next
                           }

    -- Render the composite
    mapM_ (uncurry $ renderElement next) prims
    return (next,stats)

-- | Render a list of primitives using renderings stored in the given cache,
-- return a new cache that can be used to render the next list of
-- primitives. Optionally print some debug info.
renderPrimsDebug :: (Functor m, MonadIO m, Monoid t, Hashable a)
                 => Bool -> RenderStrategy m t r a -> r -> Cache m t -> [(t, a)]
                 -> m (Cache m t)
renderPrimsDebug debug s rez cache prims = do
    (next, stats) <- renderPrimsWithStats s rez cache prims
    when debug $ liftIO $ putStrLn $ showCacheStats stats
    return next

-- | Render a list of primitives using renderings stored in the given cache,
-- return a new cache that can be used to render the next list of
-- primitives.
renderPrims :: (Functor m, Monad m, Monoid t, Hashable a)
            => RenderStrategy m t r a -> r -> Cache m t -> [(t, a)]
            -> m (Cache m t)
renderPrims s rez cache prims = fst <$> renderPrimsWithStats s rez cache prims