packages feed

mischief-assets-0.1.0.0: src/Mischief/Assets/Internal/Asset.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE UnliftedDatatypes #-}

module Mischief.Assets.Internal.Asset where

import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.State (MonadState (get, put), State, runState)
import Data.Data
import Data.Foldable (for_)
import Data.HashTable.IO qualified as H
import Data.Kind (Type)
import Data.Maybe
import Data.Set (Set)
import Data.Set qualified as Set
import Data.String (IsString)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Exts (IsString (..))
import KDL (DecodeValue (valueDecoder))
import KDL qualified
import Mischief.Assets.Internal.Path
import Mischief.ECS
import Mischief.ECS.Entities
import Mischief.ECS.Resources
import Path (Path)
import Path qualified

class (Component a) => Asset a where
  type Loadable a :: Bool
  type Loadable a = True

  type Saveable a :: Bool
  type Saveable a = True

  loadAsset :: FilePath -> Text -> IO (Either Text a)
  default loadAsset :: (Loadable a ~ False) => FilePath -> Text -> IO (Either Text a)
  loadAsset _ _ = pure $ Left $ T.show (typeRep $ Proxy @a) <> " does not implement a load function!"

  saveAsset :: a -> FilePath -> Text -> IO (Either Text ())
  default saveAsset :: (Saveable a ~ False) => a -> FilePath -> Text -> IO (Either Text ())
  saveAsset _ _ _ = pure $ Left $ T.show (typeRep $ Proxy @a) <> " does not implement a save function!"

  rec :: a -> Rec ()
  rec = const $ pure ()

data RecAsset where
  RecAsset :: AssetRef a -> RecAsset

data LoadRec = LoadRec deriving (Component)

newtype RecAssets = RecAssets [RecAsset]

type Rec = State RecAssets

addRec :: AssetRef a -> Rec ()
addRec a' = do
  RecAssets a <- get
  put $ RecAssets $ RecAsset a' : a

type RelativePath = Text

data Loading = Loading deriving (Component)

data Loaded = Loaded deriving (Component)

data AssetEntity = AssetEntity deriving (Component)

loadByRelPath :: forall a. (Asset a, Bundle a) => Text -> System Entity
loadByRelPath !path' = do
  path <- relAssetToAbsFile path'
  loadByAbsPath @a path path'

loadByAbsPath :: forall a. (Asset a, Bundle a) => Path Path.Abs Path.File -> Text -> System Entity
loadByAbsPath !path !relPath = do
  a <- tryFindAsset @a path
  relDir <- T.pack . Path.toFilePath . Path.parent <$> Path.parseRelFile (T.unpack relPath)

  case a of
    Just a -> pure a
    Nothing -> do
      entity <- spawn (AssetEntity, Loading, AssetType $ Proxy @a, AssetFilePath {absFilePath = path, relFilePath = relPath})

      Just (AssetRegistry registry) <- res @(AssetRegistry a)
      AssetTypeRegistry typeRegistry <- resOrInsertM @AssetTypeRegistry $ AssetTypeRegistry <$> liftIO H.new
      void $ liftIO $ H.mutate typeRegistry path $ \case
        Just x -> (Just $ Set.insert (AssetType $ Proxy @a) x, path)
        Nothing -> (Just $ Set.singleton (AssetType $ Proxy @a), path)

      liftIO $ H.insert registry path entity

      runAfter
        ( do
            loadAsset @a (Path.toFilePath path) relDir
        )
        ( \(!assetData) -> do
            case assetData of
              Left t -> err [i|Error when loading asset: #{t}|]
              Right assetData -> do
                Just loadRec <- single [q|entity. Has LoadRec|]

                remove (C @Loading, C @LoadRec) entity
                insert (assetData, Loaded) entity
                trigger (OnLoad @a entity)

                when loadRec $ handleLoadRec assetData (Set.singleton $ typeRep $ Proxy @a)
        )

      pure entity

handleLoadRec :: (Asset a) => a -> Set TypeRep -> System ()
handleLoadRec a prev = do
  let (_, RecAssets recs) = runState (rec a) (RecAssets [])
  for_ recs $ \(RecAsset a) -> void $ getAsset a True prev

data AssetFilePath = AssetFilePath {absFilePath :: Path Path.Abs Path.File, relFilePath :: Text} deriving (Component)

reloadByRelPath :: forall a. (Asset a, Bundle a) => Text -> System Entity
reloadByRelPath !path' = do
  path <- relAssetToAbsFile path'
  reloadByAbsPath @a path path'

reloadByAbsPath :: forall a. (Asset a, Bundle a) => Path Path.Abs Path.File -> Text -> System Entity
reloadByAbsPath !path !relPath = do
  a <- tryFindAsset @a path
  relDir <- T.pack . Path.toFilePath . Path.parent <$> Path.parseRelFile (T.unpack relPath)

  entity <- case a of
    Just a -> pure a
    Nothing -> do
      spawn (AssetEntity, Loading, AssetType $ Proxy @a, AssetFilePath {absFilePath = path, relFilePath = relPath})

  Just (AssetRegistry registry) <- res @(AssetRegistry a)
  AssetTypeRegistry typeRegistry <- resOrInsertM @AssetTypeRegistry $ AssetTypeRegistry <$> liftIO H.new
  void $ liftIO $ H.mutate typeRegistry path $ \case
    Just x -> (Just $ Set.insert (AssetType $ Proxy @a) x, path)
    Nothing -> (Just $ Set.singleton (AssetType $ Proxy @a), path)

  liftIO $ H.insert registry path entity

  runAfter
    ( do
        loadAsset @a (Path.toFilePath path) relDir
    )
    ( \(!assetData) -> do
        case assetData of
          Left t -> err [i|Error when loading asset: #{t}|]
          Right assetData -> do
            remove (C @Loading) entity
            insert (assetData, Loaded) entity
            trigger (OnLoad @a entity)
    )

  pure entity

-- retrieve' :: forall a. (Asset a) => Text -> System (Maybe a)
-- retrieve' !path' = do
--   e <- retrieveE @a path'
--   case e of
--     Nothing -> pure Nothing
--     Just e -> single [q|e. a|]

-- retrieveE :: forall a. (Asset a) => Text -> System (Maybe Entity)
-- retrieveE !path' = do
--   path <- relAssetToAbsFile path'
--   tryFindAsset @a path

store :: forall a. (Asset a, Bundle a) => Text -> a -> System Entity
store !path' !a = do
  path <- relAssetToAbsFile path'
  a' <- tryFindAsset @a path

  entity <- case a' of
    Just e -> do
      insert a e
      pure e
    Nothing -> do
      entity <- spawn $ bundle (AssetEntity, Loading, AssetType $ Proxy @a, a)
      when (T.head path' /= '!') $ insert AssetFilePath {absFilePath = path, relFilePath = path'} entity

      Just (AssetRegistry registry) <- res @(AssetRegistry a)
      liftIO $ H.insert registry path entity

      AssetTypeRegistry typeRegistry <- resOrInsertM @AssetTypeRegistry $ AssetTypeRegistry <$> liftIO H.new
      void $ liftIO $ H.mutate typeRegistry path $ \case
        Just x -> (Just $ Set.insert (AssetType $ Proxy @a) x, path)
        Nothing -> (Just $ Set.singleton (AssetType $ Proxy @a), path)

      pure entity

  trigger (OnLoad @a entity)
  pure entity

save :: forall a. (Asset a) => Entity -> System ()
save e = do
  t <- single [q|e. a, AssetFilePath|]
  case t of
    Nothing -> warn $ "Asset of type " <> T.show (typeRep $ Proxy @a) <> " not found at " <> T.show e <> "."
    Just (a, AssetFilePath {absFilePath = path, relFilePath = relPath}) -> do
      runAfter
        ( do
            saveAsset a (Path.toFilePath path) relPath
        )
        ( \case
            Left t -> warn t
            _ -> pure ()
        )

tryFindAsset :: forall a. (Asset a) => Path Path.Abs Path.File -> System (Maybe Entity)
tryFindAsset !path = do
  AssetRegistry registry <- resOrInsertM @(AssetRegistry a) (AssetRegistry <$> liftIO H.new)
  liftIO $ H.lookup registry path

-- newtype Path = Path AssetPath deriving anyclass (Component)

newtype OnLoad a = OnLoad {entity :: Entity}
  deriving anyclass (Event)
  deriving stock (Show)

data AssetType where
  AssetType :: (Asset a) => Proxy a -> AssetType
  deriving (Component)

instance Eq AssetType where
  AssetType a == AssetType b = typeRep a == typeRep b

instance Ord AssetType where
  compare (AssetType a) (AssetType b) = compare (typeRep a) (typeRep b)

type HashMap k v = H.BasicHashTable k v

newtype AssetRegistry a = AssetRegistry (HashMap (Path Path.Abs Path.File) Entity) deriving anyclass (Component)

newtype AssetTypeRegistry = AssetTypeRegistry (HashMap (Path Path.Abs Path.File) (Set AssetType)) deriving anyclass (Component)

data AssetRef a where
  Ref :: (Asset a, Bundle a) => Text -> AssetRef a
  EntityRef :: (Asset a, Bundle a) => Entity -> AssetRef a
  SubRef :: AssetRef b -> (b -> a) -> AssetRef a
  TransRef :: forall a b. AssetRef b -> (b -> AssetRef a) -> AssetRef a
  PureRef :: forall a. a -> AssetRef a
  AppRef :: AssetRef (a -> b) -> AssetRef a -> AssetRef b
  NullRef :: AssetRef a

decodeRef :: (Asset a, Bundle a) => Text -> KDL.ValueDecoder (AssetRef a)
decodeRef path = do
  t <- valueDecoder @Text
  pure $ Ref (path <> "/" <> t)

-- | Gets an AssetRef's entity. If it is unloaded, it will create an entity and return it.
getRefEntity :: forall a. AssetRef a -> System (Maybe Entity)
getRefEntity (Ref p') = do
  p <- relAssetToAbsFile p'
  e <- tryFindAsset @a p
  case e of
    Nothing -> Just <$> loadByAbsPath @a p p'
    Just x -> pure $ Just x
-- registry <- res @(AssetRegistry a)
-- case registry of
--   Nothing -> pure Nothing
--   Just (AssetRegistry registry) -> do
--     liftIO $ H.lookup registry p
getRefEntity (EntityRef e) = pure $ Just e
getRefEntity (SubRef _ _) = do
  warn "Attempted to retrieve the entity of a sub asset (data inside a loaded asset). Returning Nothing"
  pure Nothing
getRefEntity (TransRef ref f) = do
  a <- getAsset' ref
  case a of
    Nothing -> pure Nothing
    Just a -> getRefEntity $ f a
getRefEntity (PureRef _) = do
  warn "Attempted to retrieve the entity of a pure AssetRef. Returning Nothing."
  pure Nothing
getRefEntity (AppRef _ _) = do
  warn "Attempted to retrieve the entity of an AppRef. Returning Nothing."
  pure Nothing
getRefEntity NullRef = pure Nothing

-- | Gets an AssetRef's entity. Won't trigger any IO.
getRefEntity' :: forall a w m. (MonadSystem w m) => AssetRef a -> m (Maybe Entity)
getRefEntity' (Ref p) = do
  registry <- res @(AssetRegistry a)
  case registry of
    Nothing -> pure Nothing
    Just (AssetRegistry registry) -> do
      p <- relAssetToAbsFile p
      liftIO $ H.lookup registry p
getRefEntity' (EntityRef e) = pure $ Just e
getRefEntity' (SubRef _ _) = do
  warn "Attempted to retrieve the entity of a sub asset (data inside a loaded asset). Returning Nothing"
  pure Nothing
getRefEntity' (TransRef ref f) = do
  a <- getAsset' ref
  case a of
    Nothing -> pure Nothing
    Just a -> getRefEntity' $ f a
getRefEntity' (PureRef _) = do
  warn "Attempted to retrieve the entity of a pure AssetRef. Returning Nothing"
  pure Nothing
getRefEntity' (AppRef _ _) = do
  warn "Attempted to retrieve the entity of an AppRef. Returning Nothing."
  pure Nothing
getRefEntity' NullRef = pure Nothing

-- | Try to get an asset by its AssetRef. Won't trigger any IO.
getAsset' :: forall a w m. (MonadSystem w m) => AssetRef a -> m (Maybe a)
getAsset' (Ref a) = do
  e <- getRefEntity' (Ref @a a)
  case e of
    Nothing -> pure Nothing
    Just e -> single [q|e. a|]
getAsset' (EntityRef e) = single [q|e. a|]
getAsset' (SubRef a f) = fmap f <$> getAsset' a
getAsset' (TransRef a f) = do
  x <- getAsset' a
  case x of
    Nothing -> pure Nothing
    Just x -> do
      getAsset' (f x)
getAsset' (PureRef a) = pure $ Just a
getAsset' (AppRef f a) = do
  x <- getAsset' f
  y <- getAsset' a
  let z = (,) <$> x <*> y
  case z of
    Nothing -> pure Nothing
    Just (f, a) -> do
      pure $ Just $ f a
getAsset' NullRef = pure Nothing

-- loaded :: forall a w m. (MonadSystem w m) => AssetRef a -> m Bool
-- loaded ref = isJust <$> getAsset' ref

type IsRec = Bool

-- | Try to get an AssetRef's value. Will trigger it to load it it's currently unloaded.
getAsset :: forall a. AssetRef a -> IsRec -> Set TypeRep -> System (Maybe a)
getAsset (EntityRef e) b prev = do
  r <- single [q|e. Maybe a, Maybe AssetFilePath, Has Loading, Has LoadRec|]
  case r of
    Nothing -> Nothing <$ warn [i|Trying to get asset of invalid entity #{e}|]
    Just (Just a, _, _, _) -> do
      when b $ do
        let t = typeRep $ Proxy @a
        when (Set.member t prev) $ handleLoadRec a (Set.insert t prev)
      pure $ Just a
    Just (_, Nothing, _, _) -> Nothing <$ warn [i|Trying to load an asset without a file path #{e}|]
    Just (_, Just (AssetFilePath {absFilePath, relFilePath}), False, _) -> do
      void $ reloadByAbsPath @a absFilePath relFilePath
      when b $ insert LoadRec e
      pure Nothing
    Just (_, _, True, loadRec) -> do
      when (b && not loadRec) $ insert LoadRec e
      pure Nothing
getAsset (Ref path) b' prev = do
  e <- getRefEntity' (Ref @a path)
  case e of
    Just e -> do
      b <- single $ mkGet e (M @a, Has @Loading, Has @LoadRec)
      case b of
        Nothing -> Nothing <$ loadByRelPath @a path
        Just (Just a, _, _) -> do
          when b' $ do
            let t = typeRep $ Proxy @a
            when (Set.member t prev) $ handleLoadRec a (Set.insert t prev)
          pure $ Just a
        Just (Nothing, False, _) -> do
          void $ loadByRelPath @a path
          insert LoadRec e
          pure Nothing
        Just (Nothing, True, loadRec) -> do
          when (b' && not loadRec) $ insert LoadRec e
          pure Nothing
    Nothing -> do
      e <- loadByRelPath @a path
      when b' $ insert LoadRec e
      pure Nothing
getAsset (SubRef e f) b prev = fmap f <$> getAsset e b prev
getAsset (TransRef a f) b prev = do
  x <- getAsset a b prev
  case x of
    Nothing -> pure Nothing
    Just x -> do
      getAsset (f x) b prev
getAsset (PureRef a) _ _ = pure $ Just a
getAsset (AppRef f a) b prev = do
  x <- getAsset f b prev
  y <- getAsset a b prev
  let z = (,) <$> x <*> y
  case z of
    Nothing -> pure Nothing
    Just (f, a) -> do
      pure $ Just $ f a
getAsset NullRef _ _ = pure Nothing

-- loaded :: forall a. AssetRef a -> System Bool
-- loaded ref = isJust <$> getAsset ref

instance Show (AssetRef a) where
  show (Ref p) = "Ref " ++ show p
  show (EntityRef p) = "Ref " ++ show p
  show (SubRef a _) = "SubRef " ++ show a
  show (TransRef a _) = "TransRef " ++ show a
  show (PureRef _) = "PureRef"
  show (AppRef f a) = "AppRef" ++ show f ++ " " ++ show a
  show NullRef = "NullRef"

instance Functor AssetRef where
  fmap f a = SubRef a f

instance Applicative AssetRef where
  pure :: a -> AssetRef a
  pure = PureRef
  (<*>) :: AssetRef (a -> b) -> AssetRef a -> AssetRef b
  (<*>) = AppRef

instance Monad AssetRef where
  (>>=) :: AssetRef a -> (a -> AssetRef b) -> AssetRef b
  (>>=) = TransRef

class ToAssetRef r where
  toRef :: (Asset a, Bundle a) => r a -> AssetRef a

instance (Asset a) => IsString (AssetRef a) where
  fromString :: String -> AssetRef a
  fromString s = Ref (T.pack s)