{-# LANGUAGE TemplateHaskell #-}

-- | Generic heap with reference counting.
-- This module provides relatively low-level interface to the heap data structure, while keeping its internal representation hidden and consistent.
module Language.Boogie.Heap (
  Ref,
  refDoc,
  Heap,
  emptyHeap,
  at,
  alloc,
  hasGarbage,
  dealloc,
  update,
  incRefCount,
  decRefCount,
  heapDoc
) where

import Language.Boogie.AST
import Language.Boogie.PrettyPrinter
import Language.Boogie.Util
import Data.Map (Map, (!))
import qualified Data.Map as M
import Data.Set (Set)
import qualified Data.Set as S
import Text.PrettyPrint
import Control.Lens hiding (Context, at)

-- | Reference (index in the heap)
type Ref = Int

-- | Pretty-printed reference
refDoc :: Ref -> Doc
refDoc r = text ("ref_" ++ show r)

-- | Heap
data Heap a = Heap {
    _hValCounts :: Map Ref (a, Int),   -- ^ Mapping of references of values and reference counts
    _hGarbage :: Set Ref,              -- ^ Set of unused references (exactly those references for which snd hValCounts = 0, stored for efficiency)
    _hFree :: Set Ref,                 -- ^ Set of references that have been removed from the heap and are ready to be reused (stored for efficiency)
    _hFresh :: Ref                     -- ^ Smallest reference that has never been used
  } deriving Eq
  
makeLenses ''Heap  

{- Initialization -}

-- | Empty heap
emptyHeap = Heap {
  _hValCounts = M.empty,
  _hGarbage = S.empty,
  _hFree = S.empty,
  _hFresh = 0
}

{- Access -}

-- | 'at' @h r@: value of @r@ in heap @h@
at :: Show a => Heap a -> Ref -> a
at h r = case M.lookup r (h^.hValCounts) of
  Nothing -> internalError . show $ text "Cannot find reference" <+> refDoc r <+> text "in heap" $+$ heapDoc h
  Just (v, c) -> v
  
-- | Does the heap have any garbage?
hasGarbage :: Heap a -> Bool
hasGarbage h = h ^. hGarbage . to S.null . to not

{- Modification -}  
  
-- | 'alloc' @v h@ : choose a free reference in heap @h@ and store value @v@ in there; return the reference and the updated heap
alloc :: a -> Heap a -> (Ref, Heap a)
alloc v h = let (r, h') = getFreshRef h in (r, insert r v h')
  where
    getFreshRef h = if h ^. hFree . to S.null
      then let r = h^.hFresh in (r, h & hFresh .~ r + 1)
      else let (r, f') = S.deleteFindMin (h^.hFree) in (r, h & hFree .~ f')
    insert r v h = h & (over hValCounts (M.insert r (v, 0))) . (over hGarbage (S.insert r))

-- | Collect some garbage reference in the heap and return that reference and the new heap;
-- the heap must have garbage
dealloc :: Heap a -> (Ref, Heap a)
dealloc h = let (r, g') = S.deleteFindMin (h^.hGarbage) in (r, 
  h & (over hValCounts (M.delete r)) .
      (hGarbage .~ g') .
      (over hFree (S.insert r)) 
  )
    
-- | 'update' @r v h@ : set the value at reference @r@ to @v@ in @h@;
-- @r@ must be present in @h@
update :: Ref -> a -> Heap a -> Heap a
update r v = over hValCounts (M.adjust (over _1 (const v)) r)

-- | 'incRefCount' @r h@ : increase reference count of @r@ in @h@;
-- @r@ must be present in @h@
incRefCount :: Ref -> Heap a -> Heap a
incRefCount r h = let (v, c) = (h^.hValCounts) ! r
  in h & (over hValCounts (M.insert r (v, c + 1))) .
         (over hGarbage (if c == 0 then S.delete r else id)
     )

-- | 'decRefCount' @r h@ : decrease reference count of @r@ in @h@;
-- @r@ must be present in @h@          
decRefCount :: Ref -> Heap a -> Heap a
decRefCount r h = let (v, c) = (h^.hValCounts) ! r
  in h & (over hValCounts (M.insert r (v, c - 1))) .
         (over hGarbage (if c == 1 then S.insert r else id))
          
{- Ouput -}          

-- | Pretty-printed heap
heapDoc :: Show a => Heap a -> Doc
heapDoc h = (vsep $ map entryDoc (M.toList (h^.hValCounts))) $+$
  text "Garbage" <+> braces (commaSep (map refDoc (S.toList (h^.hGarbage)))) $+$
  text "Free" <+> braces (commaSep (map refDoc (S.toList (h^.hFree))))
  where entryDoc (ref, (val, count)) = refDoc ref <> braces (int count) <+> text "->" <+> text (show val)
  
instance Show a => Show (Heap a) where
  show h = show $ heapDoc h