using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Threading; using Microsoft.CodeAnalysis; // Note: This was in the original, but seems unused. Keep for now. using System.Linq; #nullable enable namespace res; // A delegate representing a function that can load a resource of type T. public delegate T Load( string filename ); /// /// Abstract base class for a resource reference. /// Provides a common way to refer to resources and includes debugging/tracking info. /// [DebuggerDisplay( "Path = {Filename}" )] public abstract class Ref { public static bool VerboseLogging { get; set; } = false; public string Filename { get; } public string Reason { get; } public string DbgName { get; } public string DbgPath { get; } public int DbgLine { get; } protected Ref( string filename, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) { Filename = filename; Reason = reason; DbgName = dbgName; DbgPath = dbgPath; DbgLine = dbgLine; if( VerboseLogging ) log.info( $"Ref Created: {GetType().Name} {Filename}" ); } /// /// Looks up and loads the resource. /// /// The loaded resource object. public abstract object Lookup( string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ); /// /// Called when the resource might have changed (optional, base implementation does nothing). /// public virtual void OnChange() { } /// /// Internal method to trigger the initial load (used by deferred loading, if implemented). /// internal virtual void InternalLoad() { } } /// /// A typed reference to a resource of type T. /// Handles lazy loading and access to the resource. /// [Serializable] [DebuggerDisplay( "Path = {Filename} / Res = {m_res}" )] public class Ref : Ref where T : class, new() { [NonSerialized] private T? m_res; /// /// Gets the resource, loading it if necessary. /// public T Res => m_res ?? Lookup(); /// /// Gets the resource, loading it if necessary. /// //[Deprecated("Use Res property instead.")] public T res => m_res ?? Lookup(); public Ref( string filename = "", string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) : base( !string.IsNullOrWhiteSpace( filename ) ? filename : $"{{{dbgName}_{Path.GetFileNameWithoutExtension( dbgPath )}}}", reason, dbgName, dbgPath, dbgLine ) { if( VerboseLogging ) log.info( $"Ref Created: {GetType().Name}<{typeof( T ).Name}> {Filename}" ); m_res = default; } /// /// Looks up and loads the resource. /// public override T Lookup( string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) { // If already loaded, return it. if( m_res != null ) return m_res; // Load using the Mgr. m_res = Mgr.Load( Filename, $"Ref lookup (orig: {Reason}) bcs {reason}", dbgName, dbgPath, dbgLine ); if( VerboseLogging ) log.info( $"Ref.Lookup: {GetType().Name}<{typeof( T ).Name}> {Filename}" ); return m_res; } /* /// /// Overrides the base Lookup to provide a typed result. /// public override object Lookup( string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0) { return Lookup(reason, dbgName, dbgPath, dbgLine); } */ /// /// Internal load implementation. /// internal override void InternalLoad() { if( m_res == null ) { m_res = Mgr.Load( Filename, Reason, DbgName, DbgPath, DbgLine ); if( VerboseLogging ) log.info( $"Ref.InternalLoad: {GetType().Name}<{typeof( T ).Name}> {Filename}" ); } } /// /// Creates a new resource asset and saves it, returning a Ref to it. /// Handles existing files by renaming them. /// public static Ref CreateAsset( T value, string path, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) { if( File.Exists( path ) ) { log.warn( $"Asset exists: {path}. Renaming before creating." ); var newPath = $"{path}_{DateTime.Now:yyyyMMdd_HHmmss}"; File.Move( path, newPath ); log.warn( $"Renamed existing asset to: {newPath}" ); } // Here you would typically save 'value' to 'path' using a registered saver or specific logic. // Since saving isn't defined, we'll assume it happens externally or add it here if needed. // For now, we'll just log and create the Ref. log.info( $"Creating asset Ref for {typeof( T ).Name} at {path}." ); // We need a way to save 'value' to 'path' before creating the Ref. // This part requires a 'Save' function, which isn't in the original. // Let's assume you'll add saving logic here. // Mgr.Save(value, path); // Example: Needs implementation var immMeta = (value as imm.Obj)?.Meta; var createReason = $"CreateAsset: {value.GetType().Name} - {immMeta?.Reason ?? "N/A"}"; var newRef = new Ref( path, $"{createReason} bcs {reason}", dbgName, dbgPath, dbgLine ); // We should make the newRef hold the 'value' immediately, // or ensure loading it back gives the same 'value'. // Setting m_res directly might be an option, or caching it. newRef.m_res = value; Mgr.CacheResource( path, value, $"CreateAsset {typeof( T ).Name}" ); return newRef; } } /// /// Holds a weak reference to a cached resource along with metadata. /// internal record ResourceHolder( WeakReference WeakRef, string Name, DateTime Captured ) where T : class; /// /// Manages resource loading, caching, and loader registration. /// This static class replaces the original `Mgr` instance. /// public static class Mgr { // Internal holder for type-specific loaders. private abstract class LoadHolder { // @@@@ THIS IS AN ATTEMPT TO PASS CALLER INFO INTO THE RESOURCE LOADER // Its not yet working, and maybe shouldnt exist public abstract object Load( string filename, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ); } private class LoadHolder : LoadHolder { private readonly Load _fnLoad; public LoadHolder( Load fnLoad ) { _fnLoad = fnLoad; } public override object Load( string filename, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) => _fnLoad( filename )!; } // Cache for resource holders (contains WeakReferences). private static class ResCache where T : class, new() { public static T s_default = new(); public static ImmutableDictionary> s_cache = ImmutableDictionary>.Empty; } private static ImmutableDictionary s_loaders = ImmutableDictionary.Empty; // Using ConcurrentDictionary for per-file locking objects. private static readonly System.Collections.Concurrent.ConcurrentDictionary s_loadingLocks = new(); /// /// Registers a specific loading function for a type T. /// public static void Register( Load loader ) where T : class, new() { var type = typeof( T ); if( s_loaders.ContainsKey( type ) ) { log.warn( $"Loader for type {type.Name} is already registered. Overwriting." ); } ImmutableInterlocked.Update( ref s_loaders, d => d.SetItem( type, new LoadHolder( loader ) ) ); log.info( $"Registered loader for {type.Name}" ); } /// /// Registers loaders for all subtypes of T found in loaded assemblies, /// assuming they follow the 'res_load' static generic method pattern. /// public static void RegisterSub() => RegisterSub( typeof( T ) ); /// /// Registers loaders for all subtypes of a base type. /// public static void RegisterSub( Type baseType ) { log.info( $"Scanning for subtypes of {baseType.Name} to register loaders..." ); MethodInfo? genericLoadMethod = baseType.GetMethods( BindingFlags.Public | BindingFlags.Static ) .FirstOrDefault( mi => mi.Name == "res_load" && mi.IsGenericMethodDefinition ); if( genericLoadMethod == null ) { log.error( $"Could not find 'public static T res_load(string filename)' method on {baseType.Name} or its bases." ); return; } foreach( var ass in AppDomain.CurrentDomain.GetAssemblies() ) { try { foreach( var t in ass.GetTypes() ) { if( t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom( t ) && t != baseType ) { try { log.debug( $"Found subtype {t.Name}, creating loader." ); var concreteLoadMethod = genericLoadMethod.MakeGenericMethod( t ); // log.debug( $"concreteLoadMethod: {concreteLoadMethod}" ); var loadDelegateType = typeof( Load<> ).MakeGenericType( t ); // log.debug( $"loadDelegateType: {loadDelegateType}" ); var loaderDelegate = Delegate.CreateDelegate( loadDelegateType, concreteLoadMethod ); // log.debug( $"loaderDelegate: {loaderDelegate}" ); var loadHolderType = typeof( LoadHolder<> ).MakeGenericType( t ); var loadHolder = Activator.CreateInstance( loadHolderType, loaderDelegate ) as LoadHolder; if( loadHolder != null ) { if( !ImmutableInterlocked.TryAdd( ref s_loaders, t, loadHolder ) ) { log.debug( $"Loader for {t.Name} already exists, skipping." ); } else { log.info( $"Registered loader for subtype {t.Name}." ); } } } catch( Exception ex ) { log.error( $"Failed to create loader for subtype {t.Name}: {ex.Message}" ); var stArr = ex.StackTrace.Split( "\n" ); var stFirstFew = stArr.Take( Math.Min( stArr.Length, 3 ) ); stFirstFew.ForEach( s => log.error( s ) ); } } } } catch( ReflectionTypeLoadException rtle ) { log.warn( $"Could not fully scan assembly {ass.FullName}: {rtle.Message}" ); } catch( Exception ex ) { log.warn( $"Error scanning assembly {ass.FullName}: {ex.Message}" ); } } } /// /// Creates a Ref for a given filename. /// public static Ref Lookup( string filename, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) where T : class, new() { return new Ref( filename, reason, dbgName, dbgPath, dbgLine ); } /// /// Loads a resource, handling caching and thread-safe loading. /// public static T Load( string filename, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) where T : class, new() { // 1. Check cache first (no lock) if( TryGetFromCache( filename, out var cachedValue ) ) { return cachedValue; } // 2. Get a lock specific to this filename and lock it. var fileLock = s_loadingLocks.GetOrAdd( filename, _ => new object() ); lock( fileLock ) { // 3. Double-check cache *inside* the lock if( TryGetFromCache( filename, out cachedValue ) ) { return cachedValue; } // 4. Perform the actual load log.warn( $"Loading {typeof( T ).Name}: {filename} ({reason} at {dbgName}:{dbgLine})" ); var newValue = ActualLoad( filename, reason, dbgName, dbgPath, dbgLine ); // 5. Cache the new value CacheResource( filename, newValue, reason ); return newValue; } } /// /// Manually adds or updates a resource in the cache. /// internal static void CacheResource( string filename, T resource, string reason ) where T : class, new() { var weak = new WeakReference( resource ); var holder = new ResourceHolder( weak, filename, DateTime.Now ); ImmutableInterlocked.Update( ref ResCache.s_cache, d => d.SetItem( filename, holder ) ); log.info( $"Cached {typeof( T ).Name}: {filename} ({reason})" ); } /// /// Tries to retrieve a resource from the cache. /// private static bool TryGetFromCache( string filename, out T value ) where T : class, new() { if( ResCache.s_cache.TryGetValue( filename, out var holder ) ) { if( holder.WeakRef.TryGetTarget( out var v ) ) { value = v; return true; } log.info( $"{filename} was in cache but dropped (GC'd), needs reloading." ); } value = default!; return false; } /// /// The core loading logic. Must be called within a lock. /// private static T ActualLoad( string filename, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) where T : class, new() { if( s_loaders.TryGetValue( typeof( T ), out var loaderHolder ) ) { var loadedObject = loaderHolder.Load( filename, reason, dbgName, dbgPath, dbgLine ); if( loadedObject is T value ) { var meta = (value as imm.Obj)?.Meta; // If it's an immutable object, record its loading. if( value is imm.Obj imm ) { return (T)imm.Record( $"Loading bcs {reason}", dbgName, dbgPath, dbgLine ); } return value; } else { log.error( $"Loader for {typeof( T ).Name} returned wrong type: {loadedObject?.GetType().Name}" ); return ResCache.s_default; } } else { log.error( $"Loader could not be found for type {typeof( T ).Name} for file {filename}" ); return ResCache.s_default; } } }