{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE StandaloneDeriving         #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TupleSections              #-}
{-# LANGUAGE ViewPatterns               #-}
{-|
Copyright   : (c) Christian Gram Kalhauge, 2018
License     : MIT
Maintainer  : kalhuage@cs.ucla.edu

This module contains the 'ConstantPool' data structure and multiple
other types, and classes.
-}
module Language.JVM.ConstantPool
  (
  -- * Constant Pool
  -- $ConstantPool
    ConstantPool (..)
  , access

  , growPool

  , poolCount
  , nextIndex
  , listConstants
  , fromConstants

  , empty
  , PoolAccessError (..)

  , Index
  ) where

import           Control.DeepSeq       (NFData)
import           Control.Monad.Except
import           Data.Binary
-- import           Debug.Trace
import           Data.Binary.Get
import           Data.Binary.Put
import           GHC.Generics          (Generic)

-- base
import Data.Monoid

-- containers
import qualified Data.IntMap           as IM

import           Language.JVM.Constant
import           Language.JVM.Stage
import           Language.JVM.TH


-- $ConstantPool
-- The 'ConstantPool' contains all the constants, and is accessible using the
-- Lookup methods.

-- | A ConstantPool is just an 'IntMap'. A 'IntMap' is used, because constants are
-- accessed using their byte-offset, and sometimes the offset depends on the constant
-- size. See 'constantSize'.
newtype ConstantPool r = ConstantPool
  { unConstantPool :: IM.IntMap (Constant r)
  }

instance Binary (ConstantPool Low) where
  get = do
    len <- fromIntegral <$> getWord16be
    list <- go len 1
    return . ConstantPool $ IM.fromList list
    where
      go len n | len > n = do
        constant <- get
        rest <- go len (n + constantSize constant)
        return $ (fromIntegral n, constant) : rest
      go _ _ = return []
  put (ConstantPool p) = do
    case IM.maxViewWithKey p of
      Just ((key, e), _) -> do
        putWord16be (fromIntegral key + constantSize e)
        forM_ (IM.toAscList p) (put . snd)
      Nothing -> do
        putInt16be 0

-- | A pool access error
data PoolAccessError = PoolAccessError
  { paErrorRef :: !Word16
  , paErrorMsg :: String
  } deriving (Show, Eq, Generic)

instance NFData PoolAccessError

-- | Creates an empty constant pool
empty :: ConstantPool r
empty = ConstantPool (IM.empty)

-- | Access a constant in the constant pool
access :: Index -> ConstantPool r -> Either PoolAccessError (Constant r)
access ref (ConstantPool cp) =
  case IM.lookup (fromIntegral ref) cp of
    Just x  -> Right x
    Nothing -> Left $ PoolAccessError ref "No such element."

poolCount :: ConstantPool r -> Int
poolCount =
  IM.size . unConstantPool

listConstants :: ConstantPool r -> [(Index, Constant r)]
listConstants =
  map (\(i, a) -> (fromIntegral i, a)) . IM.toList . unConstantPool

nextIndex :: ConstantPool r -> Index
nextIndex (ConstantPool im) =
  fromIntegral $ case IM.toDescList im of
    (k, a):_ -> fromIntegral k + constantSize a
    _        -> 1

fromConstants :: Foldable f => f (Constant r) -> ConstantPool r
fromConstants =
  foldl (\b a -> snd . append a $ b) empty

growPool ::
  forall b.
  (ConstantPool High -> Constant Low -> Either b (Constant High))
  -> ConstantPool Low
  -> (ConstantPool High, [(b, (Index, Constant Low))])
growPool f reffed =
  stage' IM.empty (listConstants reffed)
  where
    stage' :: IM.IntMap (Constant High) -> [(Index, Constant Low)] -> (ConstantPool High, [(b, (Index, Constant Low))])
    stage' cp mis =
      case foldMap (grow (ConstantPool cp)) mis of
        (cp', flip appEndo [] -> mis')
          | IM.null cp' ->
              (ConstantPool cp, mis')
          | otherwise ->
          stage' (cp `IM.union` cp') . map snd $ mis'

    grow cp (k,a) =
      case f cp a of
        Right c -> (IM.singleton (fromIntegral k) c, mempty)
        Left b  -> (IM.empty, Endo ((b, (k,a)):) )


{-# INLINE growPool #-}

-- | Append a constant to the constant pool, and get the offset.
append :: Constant r -> ConstantPool r -> (Index, ConstantPool r)
append c cp@(ConstantPool im) =
  (i, ConstantPool $ IM.insert (fromIntegral i) c im)
  where
    i = nextIndex cp

$(deriveBase ''ConstantPool)