{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Data.Renderable where

import Prelude hiding (lookup)
import Control.Monad
import Data.Hashable
import Data.IntMap (IntMap)
import Data.Maybe
import Data.Monoid
import Data.List (intercalate)
import qualified Data.IntSet as S
import qualified Data.IntMap as IM
import GHC.Stack

--------------------------------------------------------------------------------
-- Decomposable Instances
--------------------------------------------------------------------------------
-- | Any element is decomposable by returning a list consisting of itself.
instance Decomposable (Element m r t) m r t where
    decompose e = [e]
--------------------------------------------------------------------------------
-- Renderable Instances
--------------------------------------------------------------------------------
-- | Any Element is renderable by rendering its contained datatype.
instance Renderable (Element m r t) where
    type RenderMonad (Element m r t) = m
    type RenderRsrc (Element m r t) = r
    type RenderTfrm (Element m r t) = t
    cache rz rs (Element a)   = attachIfNeeded rz rs a
    nameOf (Element a)        = "Element " ++ nameOf a
    composite (Element a) = composite a

-- | A tuple is renderable when it is a pairing of a transform and another
-- renderable datatype.
instance ( t ~ RenderTfrm a, Show t, Monoid t
         , Hashable a, Renderable a) => Renderable (t,a) where
    type RenderMonad (t,a) = RenderMonad a
    type RenderTfrm (t,a) = RenderTfrm a
    type RenderRsrc (t,a) = RenderRsrc a
    cache rz rs (_,a) = attachIfNeeded rz rs a
    nameOf (t,a) = "(" ++ show t ++ ", " ++ nameOf a ++ ")"
    composite (t,a) = map (fmap $ fmap (t <>)) $ composite a

-- | A Maybe is renderable by rendering the datatype contained in the Just
-- constructor or by rendering nothing.
instance (Renderable a, Hashable a, Show a) => Renderable (Maybe a) where
    type RenderMonad (Maybe a) = RenderMonad a
    type RenderTfrm (Maybe a) = RenderTfrm a
    type RenderRsrc (Maybe a) = RenderRsrc a
    cache rz rs (Just a) = attachIfNeeded rz rs a
    cache _ rs _         = return rs
    nameOf (Just a) = "Just " ++ nameOf a
    nameOf _        = "Nothing"
    composite (Just a) = composite a
    composite _ = []

-- | A list of renderable instances is renderable by rendering each
-- instance.
instance (Renderable a, Hashable a) => Renderable [a] where
    type RenderMonad [a] = RenderMonad a
    type RenderTfrm [a] = RenderTfrm a
    type RenderRsrc [a] = RenderRsrc a
    cache = foldM . attachIfNeeded
    nameOf as = "[ " ++ (intercalate ", " names) ++ " ]"
        where names = map nameOf as
    composite = concatMap composite
--------------------------------------------------------------------------------
-- Rendering and cacheing
--------------------------------------------------------------------------------
-- | Render a datatype using renderings stored in the given cache.
renderData :: (Monad m, Renderable a, Monoid (RenderTfrm a))
           => Cache m (RenderTfrm a) -> a -> m ()
renderData c = renderComposite c mempty . composite

-- | Render only the hidden layers of a datatype using renderings stored in
-- the given cache. This is sometimes useful for debugging.
renderDataHidden :: (Renderable a, Monad m, Monoid (RenderTfrm a))
                 => Cache m (RenderTfrm a) -> (RenderTfrm a) -> a -> m ()
renderDataHidden c t = renderComposite c t . catMaybes . map f . composite
    where f (i, Nothing) = Just (i, Just mempty)
          f _ = Nothing

-- | Render the composite of a datatype using renderings stored in the
-- given cache.
renderComposite :: (Monad m, Monoid t) => Cache m t -> t -> Composite t -> m ()
renderComposite rs t = mapM_ (uncurry go)
    where go k (Just t') = maybe (err k) (rend t') $ IM.lookup k rs
          go _ _ = return ()
          rend t' (Rendering f _) = f $ t <> t'
          err k = errorWithStackTrace $ unwords [ "Fatal error! Could not find"
                                                , "rendering (from a layer)"
                                                , show k
                                                ]

-- | If needed, create a new rendering given some resources, insert it in
-- the cache and return the new cache.
attachIfNeeded :: ( Renderable a, Monad (RenderMonad a)
                 , Monoid (RenderTfrm a), Hashable a)
              => RenderRsrc a -> Cache (RenderMonad a) (RenderTfrm a)
              -> a -> (RenderMonad a) (Cache (RenderMonad a) (RenderTfrm a))
attachIfNeeded rz cache' a =
    maybe (cache rz cache' a) (const $ return cache') $ IM.lookup (hash a) cache'

-- | Detach any renderings that are not needed to render the
-- given data.
detachUnused :: (Monad m, Renderable a) => Cache m t -> a -> m (Cache m t)
detachUnused c a =
    -- Get the hashes listed in the composite (these are used)
    let hashes = S.fromList $ map fst $ composite a
        -- Get the hashes currently in the cache
        keys = IM.keysSet c
        -- Diff them
        diff = S.difference keys hashes
        -- Detach them
    in foldM detach c $ S.toList diff

-- | Remove a rendering from a cache and clean up the resources allocated
-- for that rendering.
detach :: Monad m => Cache m t -> Int -> m (Cache m t)
detach c k = do
    case IM.lookup k c of
        Nothing        -> let s = "Could not find rendering for " ++ show k
                          in errorWithStackTrace s
        Just rendering -> clean rendering
    return $ IM.delete k c
--------------------------------------------------------------------------------
-- Decomposition
--------------------------------------------------------------------------------
-- | An instance of Decomposable can be broken down into a number of elements.
class Decomposable a m r t where
    decompose :: a -> [Element m r t]
--------------------------------------------------------------------------------
-- Element
--------------------------------------------------------------------------------
instance Hashable (Element m r t) where
    hashWithSalt s (Element a) = s `hashWithSalt` "Element" `hashWithSalt` a

instance Eq (Element m r t) where
    a == b = hash a == hash b

instance Show (Element m r t) where
    show (Element a) = "Element{ " ++ show a ++ " }"

-- | Element is a generic existential type that can be used to enclose
-- instances of Renderable in order to contain them all in a heterogeneous list.
-- 'm', 'r' and 't' must be shared with all Renderable instances stored in
-- a heterogeneous list of Elements.
data Element m r t where
    Element  :: ( Monad m, Show a, Hashable a, Renderable a
                , m ~ RenderMonad a
                , r ~ RenderRsrc a
                , t ~ RenderTfrm a)
             => a -> Element m r t
--------------------------------------------------------------------------------
-- Renderable
--------------------------------------------------------------------------------
class Renderable a where
    -- | The monad needed to render the datatype.  In most cases this is
    -- probably IO.
    type RenderMonad a :: * -> *
    -- | The datatype that is used to transform renderings.
    type RenderTfrm a  :: *
    -- | The datatype that holds cached resources that will be used to
    -- composite and render the datatype.
    type RenderRsrc a  :: *
    -- | The name of a renderable datatype. This is mostly for debugging.
    nameOf :: a -> String
    -- | Store the rendering of a datatype in a cache keyed by the hash of that
    -- datatype. Returns the new cache.
    cache :: (Monad (RenderMonad a), Monoid (RenderTfrm a))
          => RenderRsrc a -> Cache (RenderMonad a) (RenderTfrm a) -> a
          -> (RenderMonad a) (Cache (RenderMonad a) (RenderTfrm a))
    -- | The entire composite list of renderings for a given datatype.
    composite :: a -> Composite (RenderTfrm a)

-- | A cache of renderings.
type Cache m t = IntMap (Rendering m t)

instance Monad m => Monoid (Rendering m t) where
    (Rendering a b) `mappend` (Rendering c d) =
        Rendering (\t -> a t >> c t) (b >> d)
    mempty = Rendering (const $ return ()) (return ())

-- | A rendering is a type that contains some effectful computation for
-- displaying something given a transform. It also contains an effectful
-- computation for cleaning up any resources allocated during its creation.
data Rendering m t = Rendering { render :: t -> m ()
                               , clean  :: m ()
                               }

-- | A composite is a representation of the entire rendered datatype. It is
-- a flattened list of all the renderings (denoted by hash), along with
-- that rendering\'s local transformation. If a rendering is explicitly run
-- by another rendering (as in a Renderable class definition) then the
-- transformation for that rendering should be Nothing, which will keep
-- 'renderComposite' from running that rendering in addition to the
-- rendering its included in. For example:
-- @
-- [(0, Just $ Transform (10,10) (0.5,0.5) 0)
-- ,(1, Nothing)
-- ]
-- @
-- The above is a composite of two renderings, the first will be rendered
-- by 'renderComposite' using the given transform while the second is
-- effectively hidden but present. Being present in the composite will keep
-- 'detachUnused' from detaching and cleaning the rendering.
type Composite a = [(Int, Maybe a)]