-- | General content types and operations.
{-# LANGUAGE RankNTypes, ScopedTypeVariables, TypeFamilies #-}
module Game.LambdaHack.Kind
  ( -- * General content types
    Id, Speedup(..), Ops(..), COps(..), createOps, stdRuleset
    -- * Arrays of content identifiers
  , Array, (!), (//), listArray, array, bounds, foldlArray
  ) where

import Data.Binary
import qualified Data.List as L
import qualified Data.IntMap as IM
import qualified Data.Map as M
import qualified Data.Word as Word
import qualified Data.Array.Unboxed as A
import qualified Data.Ix as Ix

import Game.LambdaHack.Utils.Assert
import Game.LambdaHack.Utils.Frequency
import Game.LambdaHack.Content.ActorKind
import Game.LambdaHack.Content.CaveKind
import Game.LambdaHack.Content.FactionKind
import Game.LambdaHack.Content.ItemKind
import Game.LambdaHack.Content.PlaceKind
import Game.LambdaHack.Content.RuleKind
import Game.LambdaHack.Content.StrategyKind
import Game.LambdaHack.Content.TileKind
import Game.LambdaHack.CDefs
import Game.LambdaHack.Random
import Game.LambdaHack.Misc

-- | Content identifiers for the content type @c@.
newtype Id c = Id Word8 deriving (Show, Eq, Ord, Ix.Ix)

instance Binary (Id c) where
  put (Id i) = put i
  get = fmap Id get

-- | Type family for auxiliary data structures for speeding up
-- content operations.
data family Speedup a

data instance Speedup TileKind = TileSpeedup
  { isClearTab :: Id TileKind -> Bool
  , isLitTab   :: Id TileKind -> Bool
  }

-- | Content operations for the content of type @a@.
data Ops a = Ops
  { osymbol :: Id a -> Char       -- ^ the symbol of a content element at id
  , oname :: Id a -> String       -- ^ the name of a content element at id
  , okind :: Id a -> a            -- ^ the content element at given id
  , ouniqGroup :: String -> Id a  -- ^ the id of the unique member of
                                  --   a singleton content group
  , opick :: String -> (a -> Bool) -> Rnd (Id a)
                                  -- ^ pick a random id belonging to a group
                                  --   and satisfying a predicate
  , ofoldrWithKey :: forall b. (Id a -> a -> b -> b) -> b -> b
                                  -- ^ fold over all content elements of @a@
  , obounds :: (Id a, Id a)       -- ^ bounds of identifiers of content @a@
  , ospeedup :: Speedup a         -- ^ auxiliary speedup components
  }

-- | Create content operations for type @a@ from definition of content
-- of type @a@.
createOps :: forall a. Show a => CDefs a -> Ops a
createOps CDefs{getSymbol, getName, getFreq, content, validate} =
  let kindAssocs :: [(Word.Word8, a)]
      kindAssocs = L.zip [0..] content
      kindMap :: IM.IntMap a
      kindMap = IM.fromDistinctAscList $ L.zip [0..] content
      kindFreq :: M.Map String (Frequency (Id a, a))
      kindFreq =
        let tuples = [ (group, (n, (Id i, k)))
                     | (i, k) <- kindAssocs
                     , (group, n) <- getFreq k, n > 0 ]
            f m (group, nik) = M.insertWith (++) group [nik] m
            lists =  L.foldl' f M.empty tuples
            nameFreq group = toFreq $ "opick ('" ++ group ++ "')"
        in M.mapWithKey nameFreq lists
      okind (Id i) = kindMap IM.! fromEnum i
      correct a = not (L.null (getName a)) && L.all ((> 0) . snd) (getFreq a)
      offenders = validate content
  in assert (allB correct content) $
     assert (L.null offenders `blame` ("content not validated:", offenders)) $
     Ops
       { osymbol = getSymbol . okind
       , oname = getName . okind
       , okind = okind
       , ouniqGroup = \ group ->
           case runFrequency $ kindFreq M.! group of
             [(n, (i, _))] | n > 0 -> i
             l -> assert `failure` l
       , opick = \ group p -> frequency $ do
           (i, k) <- kindFreq M.! group
           breturn (p k) i
           {- with MonadComprehensions:
           frequency [ i | (i, k) <- kindFreq M.! group, p k ]
           -}
       , ofoldrWithKey = \ f z -> L.foldr (\ (i, a) -> f (Id i) a) z kindAssocs
       , obounds =
         let limits = let (i1, a1) = IM.findMin kindMap
                          (i2, a2) = IM.findMax kindMap
                      in ((Id (toEnum i1), a1), (Id (toEnum i2), a2))
         in (Id 0, (fst . snd) limits)
       , ospeedup = undefined  -- define elsewhere
       }

-- | Operations for all content types, gathered together.
data COps = COps
  { coactor :: !(Ops ActorKind)
  , cocave  :: !(Ops CaveKind)
  , cofact  :: !(Ops FactionKind)
  , coitem  :: !(Ops ItemKind)
  , coplace :: !(Ops PlaceKind)
  , corule  :: !(Ops RuleKind)
  , costrat :: !(Ops StrategyKind)
  , cotile  :: !(Ops TileKind)
  }

-- | The standard ruleset used for level operations.
stdRuleset :: Ops RuleKind -> RuleKind
stdRuleset Ops{ouniqGroup, okind} = okind $ ouniqGroup "standard"

instance Show COps where
  show _ = "Game content."

-- | Arrays of content identifiers pointing to the content type @c@,
-- where the identifiers are represented as @Word8@
-- (and so content of type @c@ can have at most 256 elements).
-- The arrays are indexed by type @i@, e.g., a dungeon tile location.
newtype Array i c = Array (A.UArray i Word.Word8) deriving Show

-- TODO: save/restore is still too slow, but we are already past
-- the point of diminishing returns. A dramatic change would be
-- low-level conversion to ByteString and serializing that.
instance (Ix.Ix i, Binary i) => Binary (Array i c) where
  put (Array a) = put a
  get = fmap Array get

-- | Content identifiers array lookup.
(!) :: Ix.Ix i => Array i c -> i -> Id c
(!) (Array a) i = Id $ a A.! i

-- | Construct a content identifiers array updated with the association list.
(//) :: Ix.Ix i => Array i c -> [(i, Id c)] -> Array i c
(//) (Array a) l = Array $ a A.// [(i, e) | (i, Id e) <- l]

-- | Create a content identifiers array from a list of elements.
listArray :: Ix.Ix i => (i, i) -> [Id c] -> Array i c
listArray bds l = Array $ A.listArray bds [e | Id e <- l]

-- | Create a content identifiers array from an association list.
array :: Ix.Ix i => (i, i) -> [(i, Id c)] -> Array i c
array bds l = Array $ A.array bds [(i, e) | (i, Id e) <- l]

-- | Content identifiers array bounds.
bounds :: Ix.Ix i => Array i c -> (i, i)
bounds (Array a) = A.bounds a

-- | Fold left strictly over an array.
foldlArray :: Ix.Ix i => (a -> Id c -> a) -> a -> Array i c -> a
foldlArray f z0 (Array a) = lgo z0 $ A.elems a
 where lgo z []       = z
       lgo z (x : xs) = let fzx = f z (Id x) in fzx `seq` lgo fzx xs