packages feed

aztecs-0.12.0: src/Aztecs/ECS/Query/Dynamic.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TupleSections #-}

-- |
-- Module      : Aztecs.ECS.Query.Dynamic
-- Copyright   : (c) Matt Hunzinger, 2025
-- License     : BSD-style (see the LICENSE file in the distribution)
--
-- Maintainer  : matt@hunzinger.me
-- Stability   : provisional
-- Portability : non-portable (GHC extensions)
module Aztecs.ECS.Query.Dynamic
  ( -- * Dynamic queries
    DynamicQuery,
    DynamicQueryT (..),

    -- ** Operations
    entityDyn,
    fetchDyn,
    fetchMaybeDyn,
    fetchMapDyn,
    fetchMapDynM,
    zipFetchMapDyn,
    zipFetchMapAccumDyn,
    zipFetchMapDynM,
    zipFetchMapAccumDynM,

    -- ** Filters
    withDyn,
    withoutDyn,

    -- ** Conversion
    liftQueryDyn,

    -- ** Running
    queryDyn,
    readQuerySingleDyn,
    readQuerySingleMaybeDyn,
    queryEntitiesDyn,
    readQueryDyn,
    querySingleDyn,
    querySingleMaybeDyn,
    readQueryEntitiesDyn,

    -- *** Internal
    QueryFilter (..),
    Operation (..),
    queryFilter,
    runDynQuery,
    runDynQueryEntities,
    readDynQuery,
    readDynQueryEntities,
  )
where

import Aztecs.ECS.Component
import Aztecs.ECS.Entity
import Aztecs.ECS.World.Archetype (Archetype)
import qualified Aztecs.ECS.World.Archetype as A
import Aztecs.ECS.World.Archetypes (Node (..))
import qualified Aztecs.ECS.World.Archetypes as AS
import Aztecs.ECS.World.Entities
import Control.Applicative
import Control.Monad
import Control.Monad.Trans (MonadTrans (..))
import Control.Monad.Identity
import Data.Bifunctor
import Data.Foldable
import qualified Data.Map.Strict as Map
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as Set
import GHC.Stack
import Prelude hiding (reads)

-- @since 0.9
type DynamicQuery = DynamicQueryT Identity

-- | Dynamic query for components by ID.
--
-- @since 0.11
data DynamicQueryT f a where
  Entity :: DynamicQueryT f EntityID
  Pure :: a -> DynamicQueryT f a
  Map :: (a -> b) -> DynamicQueryT f a -> DynamicQueryT f b
  Ap :: DynamicQueryT f (a -> b) -> DynamicQueryT f a -> DynamicQueryT f b
  Lift :: (MonadTrans g, Monad (g f), Monad f) => DynamicQueryT f a -> DynamicQueryT (g f) a
  Op :: ComponentID -> Operation f a -> DynamicQueryT f a

instance Functor (DynamicQueryT f) where
  {-# INLINE fmap #-}
  fmap = Map

-- | @since 0.11
instance Applicative (DynamicQueryT f) where
  {-# INLINE pure #-}
  pure = Pure

  {-# INLINE (<*>) #-}
  (<*>) = Ap

{-# INLINE entityDyn #-}
entityDyn :: DynamicQueryT f EntityID
entityDyn = Entity

{-# INLINE fetchDyn #-}
fetchDyn :: (Component a) => ComponentID -> DynamicQueryT f a
fetchDyn cId = Op cId Fetch

{-# INLINE fetchMaybeDyn #-}
fetchMaybeDyn :: (Component a) => ComponentID -> DynamicQueryT f (Maybe a)
fetchMaybeDyn cId = Op cId FetchMaybe

{-# INLINE fetchMapDyn #-}
fetchMapDyn :: (Component a) => (a -> a) -> ComponentID -> DynamicQueryT f a
fetchMapDyn f cId = Op cId $ FetchMap f

{-# INLINE fetchMapDynM #-}
fetchMapDynM :: (Monad f, Component a) => (a -> f a) -> ComponentID -> DynamicQueryT f a
fetchMapDynM f cId = Op cId $ FetchMapM f

{-# INLINE zipFetchMapDyn #-}
zipFetchMapDyn ::
  (Component a) => (b -> a -> a) -> ComponentID -> DynamicQueryT f b -> DynamicQueryT f a
zipFetchMapDyn f cId q = snd <$> Op cId (ZipFetchMap (\b a -> ((), f b a)) q)

{-# INLINE zipFetchMapAccumDyn #-}
zipFetchMapAccumDyn ::
  (Component a) => (b -> a -> (c, a)) -> ComponentID -> DynamicQueryT f b -> DynamicQueryT f (c, a)
zipFetchMapAccumDyn f cId q = Op cId $ ZipFetchMap f q

{-# INLINE zipFetchMapDynM #-}
zipFetchMapDynM ::
  (Monad f, Component a) =>
  (b -> a -> f a) ->
  ComponentID ->
  DynamicQueryT f b ->
  DynamicQueryT f a
zipFetchMapDynM f cId q = snd <$> zipFetchMapAccumDynM (\b a -> ((),) <$> f b a) cId q

{-# INLINE zipFetchMapAccumDynM #-}
zipFetchMapAccumDynM ::
  (Monad f, Component a) =>
  (b -> a -> f (c, a)) ->
  ComponentID ->
  DynamicQueryT f b ->
  DynamicQueryT f (c, a)
zipFetchMapAccumDynM f cId q = Op cId $ ZipFetchMapM f q

{-# INLINE withDyn #-}
withDyn :: ComponentID -> DynamicQueryT f ()
withDyn cId = Op cId With

{-# INLINE withoutDyn #-}
withoutDyn :: ComponentID -> DynamicQueryT f ()
withoutDyn cId = Op cId Without

{-# INLINE liftQueryDyn #-}
liftQueryDyn :: (MonadTrans g, Monad (g f), Monad f) => DynamicQueryT f a -> DynamicQueryT (g f) a
liftQueryDyn = Lift

-- | Match all entities.
--
-- @since 0.11
readQueryDyn :: (Applicative f) => DynamicQueryT f a -> Entities -> f [a]
readQueryDyn q es =
  let qf = queryFilter q
   in if Set.null $ filterWith qf
        then readDynQuery q $ A.empty {A.entities = Map.keysSet $ entities es}
        else
          let go n = readDynQuery q $ AS.nodeArchetype n
           in concat <$> traverse go (AS.find (filterWith qf) (filterWithout qf) $ archetypes es)

-- | Match a single entity.
--
-- @since 0.11
readQuerySingleDyn :: (HasCallStack, Applicative f) => DynamicQueryT f a -> Entities -> f a
readQuerySingleDyn q es = do
  res <- readQuerySingleMaybeDyn q es
  return $ case res of
    Just a -> a
    _ -> error "singleDyn: expected a single entity"

-- | Match a single entity, or `Nothing`.
--
-- @since 0.11
readQuerySingleMaybeDyn :: (Applicative f) => DynamicQueryT f a -> Entities -> f (Maybe a)
readQuerySingleMaybeDyn q es =
  let qf = queryFilter q
   in if Set.null $ filterWith qf
        then case Map.keys $ entities es of
          [eId] -> do
            res <- readDynQuery q $ A.singleton eId
            return $ case res of
              [a] -> Just a
              _ -> Nothing
          _ -> pure Nothing
        else case Map.elems $ AS.find (filterWith qf) (filterWithout qf) $ archetypes es of
          [n] -> do
            res <- readDynQuery q $ AS.nodeArchetype n
            return $ case res of
              [a] -> Just a
              _ -> Nothing
          _ -> pure Nothing

readQueryEntitiesDyn :: (Applicative f) => [EntityID] -> DynamicQueryT f a -> Entities -> f [a]
readQueryEntitiesDyn eIds q es =
  let qf = queryFilter q
   in if Set.null $ filterWith qf
        then readDynQueryEntities eIds q A.empty {A.entities = Map.keysSet $ entities es}
        else
          let go n = readDynQuery q $ AS.nodeArchetype n
           in concat <$> traverse go (AS.find (filterWith qf) (filterWithout qf) $ archetypes es)

-- | Match and update all matched entities.
--
-- @since 0.11
{-# INLINE queryDyn #-}
queryDyn :: (Applicative f) => DynamicQueryT f a -> Entities -> f ([a], Entities)
queryDyn q es =
  let qf = queryFilter q
   in if Set.null $ filterWith qf
        then (,es) . fst <$> runDynQuery q A.empty {A.entities = Map.keysSet $ entities es}
        else
          let go (aId, n) = do
                res <- runDynQuery q $ nodeArchetype n
                return $
                  let (as', arch') = res
                   in (as', aId, n {nodeArchetype = arch' <> nodeArchetype n})
              matches = Map.toList . AS.find (filterWith qf) (filterWithout qf) $ archetypes es
              res' = traverse go matches
              folder (acc, esAcc) (as, aId, node) =
                let nodes = Map.insert aId node . AS.nodes $ archetypes esAcc
                 in (as ++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}})
           in fmap (foldl' folder ([], es)) res'

-- | Match and update a single entity.
--
-- @since 0.11
querySingleDyn :: (HasCallStack, Applicative m) => DynamicQueryT m a -> Entities -> m (a, Entities)
querySingleDyn q es = do
  res <- querySingleMaybeDyn q es
  return $ case res of
    (Just a, es') -> (a, es')
    _ -> error "mapSingleDyn: expected single matching entity"

-- | Match and update a single entity, or @Nothing@.
--
-- @since 0.11
{-# INLINE querySingleMaybeDyn #-}
querySingleMaybeDyn :: (Applicative f) => DynamicQueryT f a -> Entities -> f (Maybe a, Entities)
querySingleMaybeDyn q es =
  let qf = queryFilter q
   in if Set.null $ filterWith qf
        then case Map.keys $ entities es of
          [eId] -> do
            res <- runDynQuery q $ A.singleton eId
            return $ case res of
              ([a], _) -> (Just a, es)
              _ -> (Nothing, es)
          _ -> pure (Nothing, es)
        else case Map.toList $ AS.find (filterWith qf) (filterWithout qf) $ archetypes es of
          [(aId, n)] -> do
            res <- runDynQuery q $ AS.nodeArchetype n
            return $ case res of
              ([a], arch') ->
                let nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes es
                 in (Just a, es {archetypes = (archetypes es) {AS.nodes = nodes}})
              _ -> (Nothing, es)
          _ -> pure (Nothing, es)

{-# INLINE queryEntitiesDyn #-}
queryEntitiesDyn ::
  (Monad m) =>
  [EntityID] ->
  DynamicQueryT m a ->
  Entities ->
  m ([a], Entities)
queryEntitiesDyn eIds q es =
  let qf = queryFilter q
      go = runDynQueryEntities eIds q
   in if Set.null $ filterWith qf
        then do
          (as, _) <- go A.empty {A.entities = Map.keysSet $ entities es}
          return (as, es)
        else
          let go' (acc, esAcc) (aId, n) = do
                (as', arch') <- go $ nodeArchetype n
                let n' = n {nodeArchetype = arch' <> nodeArchetype n}
                    nodes = Map.insert aId n' . AS.nodes $ archetypes esAcc
                return (as' ++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}})
           in foldlM go' ([], es) $ Map.toList . AS.find (filterWith qf) (filterWithout qf) $ archetypes es

{-# INLINE queryFilter #-}
queryFilter :: DynamicQueryT f a -> QueryFilter
queryFilter (Pure _) = mempty
queryFilter (Map _ q) = queryFilter q
queryFilter (Ap f g) = queryFilter f <> queryFilter g
queryFilter (Lift q) = queryFilter q
queryFilter Entity = mempty
queryFilter (Op cId op) = opFilter cId op

{-# INLINE readDynQuery #-}
readDynQuery :: (Applicative f) => DynamicQueryT f a -> Archetype -> f [a]
readDynQuery (Pure a) arch = pure $ replicate (length $ A.entities arch) a
readDynQuery (Map f q) arch = fmap f <$> readDynQuery q arch
readDynQuery (Ap f g) arch = do
  as <- readDynQuery g arch
  bs <- readDynQuery f arch
  pure $ zipWith ($) bs as
readDynQuery (Lift q) arch = lift $ readDynQuery q arch
readDynQuery Entity arch = pure $ Set.toList $ A.entities arch
readDynQuery (Op cId op) arch = readOp cId op arch

{-# INLINE readDynQueryEntities #-}
readDynQueryEntities :: (Applicative f) => [EntityID] -> DynamicQueryT f a -> Archetype -> f [a]
readDynQueryEntities es (Pure a) _ = pure $ replicate (length es) a
readDynQueryEntities es (Map f q) arch = fmap f <$> readDynQueryEntities es q arch
readDynQueryEntities es (Ap f g) arch = do
  a <- readDynQueryEntities es g arch
  b <- readDynQueryEntities es f arch
  pure $ b <*> a
readDynQueryEntities es (Lift q) arch = lift $ readDynQueryEntities es q arch
readDynQueryEntities es Entity _ = pure es
readDynQueryEntities es (Op cId op) arch = readOpEntities cId es op arch

{-# INLINE runDynQuery #-}
runDynQuery :: (Applicative f) => DynamicQueryT f a -> Archetype -> f ([a], Archetype)
runDynQuery (Pure a) arch = pure (replicate (length $ A.entities arch) a, mempty)
runDynQuery (Map f q) arch = do
  res <- runDynQuery q arch
  return $ first (fmap f) res
runDynQuery (Ap f g) arch = do
  res <- runDynQuery g arch
  res' <- runDynQuery f arch
  return $
    let (as, arch') = res
        (bs, arch'') = res'
     in (zipWith ($) bs as, arch'' <> arch')
runDynQuery (Lift q) arch = lift $ runDynQuery q arch
runDynQuery Entity arch = (,arch) <$> readDynQuery Entity arch
runDynQuery (Op cId op) arch = runOp cId op arch

runDynQueryEntities :: (Applicative f) => [EntityID] -> DynamicQueryT f a -> Archetype -> f ([a], Archetype)
runDynQueryEntities es (Pure a) _ = pure (replicate (length es) a, mempty)
runDynQueryEntities es (Map f q) arch = first (fmap f) <$> runDynQueryEntities es q arch
runDynQueryEntities es (Ap f g) arch = do
  res <- runDynQueryEntities es g arch
  res' <- runDynQueryEntities es f arch
  return $
    let (as, arch') = res
        (bs, arch'') = res'
     in (zipWith ($) bs as, arch'' <> arch')
runDynQueryEntities es (Lift q) arch = lift $ runDynQueryEntities es q arch
runDynQueryEntities es Entity _ = pure (es, mempty)
runDynQueryEntities es (Op cId op) arch = runOpEntities cId es op arch

data Operation f a where
  Fetch :: (Component a) => Operation f a
  FetchMaybe :: (Component a) => Operation f (Maybe a)
  FetchMap :: (Component a) => (a -> a) -> Operation f a
  FetchMapM :: (Monad f, Component a) => (a -> f a) -> Operation f a
  ZipFetchMap :: (Component a) => (b -> a -> (c, a)) -> (DynamicQueryT f b) -> Operation f (c, a)
  ZipFetchMapM :: (Monad f, Component a) => (b -> a -> f (c, a)) -> (DynamicQueryT f b) -> Operation f (c, a)
  With :: Operation f ()
  Without :: Operation f ()

{-# INLINE opFilter #-}
opFilter :: ComponentID -> Operation f a -> QueryFilter
opFilter cId Fetch = mempty {filterWith = Set.singleton cId}
opFilter cId FetchMaybe = mempty {filterWith = Set.singleton cId}
opFilter cId (FetchMap _) = mempty {filterWith = Set.singleton cId}
opFilter cId (FetchMapM _) = mempty {filterWith = Set.singleton cId}
opFilter cId (ZipFetchMap _ q) = queryFilter q <> mempty {filterWith = Set.singleton cId}
opFilter cId (ZipFetchMapM _ q) = queryFilter q <> mempty {filterWith = Set.singleton cId}
opFilter cId With = mempty {filterWith = Set.singleton cId}
opFilter cId Without = mempty {filterWithout = Set.singleton cId}

{-# INLINE readOp #-}
readOp :: (Applicative f) => ComponentID -> Operation f a -> Archetype -> f [a]
readOp cId Fetch arch = pure $ A.lookupComponentsAsc cId arch
readOp cId FetchMaybe arch =
  pure $
    case A.lookupComponentsAscMaybe cId arch of
      Just as -> fmap Just as
      Nothing -> replicate (length $ A.entities arch) Nothing
readOp cId (FetchMap f) arch = do
  bs <- readOp cId Fetch arch
  return $ map f bs
readOp cId (FetchMapM f) arch = do
  bs <- readOp cId Fetch arch
  mapM f bs
readOp cId (ZipFetchMap f q) arch = do
  as <- readDynQuery q arch
  bs <- readOp cId Fetch arch
  return $ zipWith f as bs
readOp cId (ZipFetchMapM f q) arch = do
  as <- readDynQuery q arch
  bs <- readOp cId Fetch arch
  zipWithM f as bs
readOp _ With _ = pure []
readOp _ Without _ = pure []

{-# INLINE runOp #-}
runOp :: (Applicative f) => ComponentID -> Operation f a -> Archetype -> f ([a], Archetype)
runOp cId (FetchMap f) arch = pure $ A.map f cId arch
runOp cId (FetchMapM f) arch = do
  (as, arch') <- A.mapM f cId arch
  return (as, arch')
runOp cId (ZipFetchMap f q) arch = do
  res <- runDynQuery q arch
  return $
    let (bs, arch') = res
        (as, arch'') = A.zipMap bs f cId arch
     in (as, arch'' <> arch')
runOp cId (ZipFetchMapM f q) arch = do
  (as, arch') <- runDynQuery q arch
  (bs, arch'') <- A.zipMapM as f cId arch
  return (bs, arch'' <> arch')
runOp cId op arch = (,mempty) <$> readOp cId op arch

{-# INLINE readOpEntities #-}
readOpEntities :: (Applicative f) => ComponentID -> [EntityID] -> Operation f a -> Archetype -> f [a]
readOpEntities cId es Fetch arch =
  pure
    . map snd
    . filter (\(e, _) -> e `elem` es)
    . Map.toList
    $ A.lookupComponents cId arch
readOpEntities cId es FetchMaybe arch =
  pure
    . map (\(e, a) -> if e `elem` es then Just a else Nothing)
    . Map.toList
    $ A.lookupComponents cId arch
readOpEntities cId es (FetchMap f) arch = do
  b <- readOpEntities cId es Fetch arch
  pure $ map f b
readOpEntities cId es (FetchMapM f) arch = do
  b <- readOpEntities cId es Fetch arch
  mapM f b
readOpEntities cId es (ZipFetchMap f q) arch = do
  a <- readDynQueryEntities es q arch
  b <- readOpEntities cId es Fetch arch
  pure $ zipWith f a b
readOpEntities cId es (ZipFetchMapM f q) arch = do
  a <- readDynQueryEntities es q arch
  b <- readOpEntities cId es Fetch arch
  zipWithM f a b
readOpEntities _ _ With _ = pure []
readOpEntities _ _ Without _ = pure []

runOpEntities :: (Applicative f) => ComponentID -> [EntityID] -> Operation f a -> Archetype -> f ([a], Archetype)
runOpEntities cId es (FetchMap f) arch =
  pure $
    let go e a =
          if e `elem` es
            then let a' = f a in (Just a', a')
            else (Nothing, a)
        (as, arch') = A.zipMap es go cId arch
     in (mapMaybe fst as, arch')
runOpEntities cId es (FetchMapM f) arch = do
  (as, arch') <- runOpEntities cId es (ZipFetchMapM (\() a -> (,a) <$> f a) (pure ())) arch
  return (map snd as, arch')
runOpEntities cId es (ZipFetchMap f q) arch = do
  res <- runDynQuery q arch
  return $
    let go (e, b) a =
          if e `elem` es
            then let (x, y) = f b a in (Just x, y)
            else (Nothing, a)
        (bs, arch') = res
        (as, arch'') = A.zipMap (zip es bs) go cId arch
     in (mapMaybe (\(m, b) -> fmap (,b) m) as, arch'' <> arch')
runOpEntities cId es (ZipFetchMapM f q) arch = do
  (bs, arch') <- runDynQuery q arch
  let go (e, b) a =
        if e `elem` es
          then do
            (x, y) <- f b a
            return (Just x, y)
          else return (Nothing, a)
  (as, arch'') <- A.zipMapM (zip es bs) go cId arch
  return (mapMaybe (\(m, b) -> fmap (,b) m) as, arch'' <> arch')
runOpEntities cId es op arch = (,arch) <$> readOpEntities cId es op arch

-- | `Query` filter.
--
-- @since 0.11
data QueryFilter = QueryFilter
  { filterWith :: !(Set ComponentID),
    filterWithout :: !(Set ComponentID)
  }
  deriving (Show)

-- | @since 0.9
instance Semigroup QueryFilter where
  QueryFilter r1 w1 <> QueryFilter r2 w2 = QueryFilter (r1 <> r2) (w1 <> w2)

-- | @since 0.9
instance Monoid QueryFilter where
  mempty = QueryFilter mempty mempty