Editor/TypeLibraryFixes/TypeSerializedObjectFixed.cs
using Sandbox;
using Sandbox.Diagnostics;
using System;
using System.Collections.Generic;
using System.Reflection;

namespace ExtendedEditor.TypeLibraryFixes;

public class TypeSerializedObjectFixed : SerializedObject
{
    private static readonly MethodInfo _notePreChangeMethod = typeof(SerializedProperty)
        .GetMethod("NotePreChange", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, [typeof(SerializedProperty)])!;

    private static readonly Logger _logger = new("ExtendedEditor.TypeSerializedObject");

    public override string TypeIcon => _displayInfo.Icon;
    public override string TypeName => Type.Name;
    public override string TypeTitle => _displayInfo.Name;

    public override bool IsValid
    {
        get => _targetObject switch
        {
            IValid target => target.IsValid,
            null => false,
            _ => base.IsValid
        };
    }

    private DisplayInfo _displayInfo;
    private readonly Func<object?>? _fetchTarget;
    public readonly Type Type;
    internal object _targetObject;
    private string? _fullName;

    public TypeSerializedObjectFixed(object target, Type type, SerializedProperty? parent = null)
    {
        ArgumentNullException.ThrowIfNull(target, nameof(target));
        ArgumentNullException.ThrowIfNull(type, nameof(type));
        if(!target.GetType().IsAssignableTo(type))
            throw new InvalidOperationException($"Got wrong target type target: {target}, type: {type}");

        _targetObject = target;
        Type = type;
        ParentProperty = parent;

        _displayInfo = DisplayInfo.ForMember(type, inherit: true);

        BuildProperties();
    }

    public TypeSerializedObjectFixed(Func<object> fetchTarget, Type type, SerializedProperty? parent = null)
    {
        _fetchTarget = fetchTarget;
        var target = _fetchTarget();

        // only have to dynamically fetch value types 
        if(!type.IsValueType)
            _fetchTarget = null;

        ArgumentNullException.ThrowIfNull(target, nameof(target));
        ArgumentNullException.ThrowIfNull(type, nameof(type));
        if(!target.GetType().IsAssignableTo(type))
            throw new InvalidOperationException($"Got wrong target type target: {target}, type: {type}");

        _targetObject = target;
        Type = type;
        ParentProperty = parent;

        _displayInfo = DisplayInfo.ForMember(type, inherit: true);

        BuildProperties();
    }

    /// <summary>
    /// Get the target object. If the target object is a value type, we'll
    /// call FetchTarget() - which should fetch the latest copy of it from
    /// the parent. 
    /// Note that by design FetchTarget is null if it's not a value type.
    /// </summary>
    internal ref object GetTargetObject()
    {
        if(_fetchTarget is not null)
        {
            Assert.True(Type.IsValueType);
            _targetObject = _fetchTarget()!;
        }

        return ref _targetObject;
    }

    private void BuildProperties()
    {
        _fullName = Type.AssemblyQualifiedName;

        PropertyList ??= [];
        PropertyList.Clear();


        foreach(var property in TypeCache.GetInstanceProperties(Type))
        {
            var serialized = new TypeSerializedPropertyFixed(this, property);
            PropertyList.Add(serialized);
        }

        foreach(var field in TypeCache.GetInstanceFields(Type))
        {
            var serialized = new TypeSerializedPropertyFixed(this, field);
            PropertyList.Add(serialized);
        }

        foreach(var method in TypeCache.GetInstanceMethods(Type))
        {
            var serialized = new TypeSerializedMethodFixed(this, method);
            PropertyList.Add(serialized);
        }
    }

    public override void NoteChanged(SerializedProperty childProperty)
    {
        ParentProperty?.SetValue(_targetObject, childProperty);

        if(childProperty is not null)
            base.NoteChanged(childProperty);
    }


    public override void NotePreChange(SerializedProperty childProperty)
    {
        if(ParentProperty is not null)
            _notePreChangeMethod.Invoke(ParentProperty, [childProperty]);

        if(childProperty is not null)
        {
            base.NotePreChange(childProperty);
        }
    }

    protected override void PrepareEnumerator()
    {
        if(_fullName != Type.AssemblyQualifiedName)
        {
            _logger.Trace($"Rebuilding - detected assembly change [{_fullName}] to [{Type.AssemblyQualifiedName}]");
            BuildProperties();
        }
    }

    public override IEnumerable<object> Targets
    {
        get
        {
            yield return GetTargetObject();
        }
    }
}