Editor/TypeLibraryFixes/TypeSerializedPropertyFixed.cs
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace ExtendedEditor.TypeLibraryFixes;

public class TypeSerializedPropertyFixed : SerializedProperty
{
    public override bool IsProperty => true;

    public override SerializedObject Parent => _typeSerializedObject;
    public override string DisplayName => _displayInfo.Name;
    public override string Name { get; }
    public override string Description => _displayInfo.Description;
    public override Type PropertyType { get; }
    public override string SourceFile { get; } = null!;
    public override int SourceLine { get; } = 0;
    public override string GroupName => _displayInfo.Group;
    public override bool IsEditable => !_displayInfo.ReadOnly && _canWrite;
    public override bool IsPublic { get; }
    public override int Order => _displayInfo.Order;


    private Sandbox.DisplayInfo _displayInfo;
    private TypeSerializedObjectFixed _typeSerializedObject;

    private readonly MemberInfo _memberInfo;
    private readonly PropertyInfo? _propertyInfo;
    private readonly FieldInfo? _fieldInfo;

    private readonly bool _canWrite;
    private readonly object? _defaultValue;

    public TypeSerializedPropertyFixed(TypeSerializedObjectFixed typeSerializedObject, PropertyInfo propertyInfo)
    {
        ArgumentNullException.ThrowIfNull(typeSerializedObject, nameof(typeSerializedObject));
        ArgumentNullException.ThrowIfNull(propertyInfo, nameof(propertyInfo));

        _typeSerializedObject = typeSerializedObject;
        _memberInfo = _propertyInfo = propertyInfo;

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

        var isGetMethodPublic = propertyInfo.GetMethod?.IsPublic ?? false;
        var isSetMethodPublic = propertyInfo.SetMethod?.IsPublic ?? false;
        IsPublic = isGetMethodPublic || isSetMethodPublic;
        PropertyType = propertyInfo.PropertyType;
        Name = propertyInfo.Name;
        _canWrite = propertyInfo.CanWrite;

        var sourceLocationAttribute = propertyInfo.GetCustomAttribute<SourceLocationAttribute>();
        if(sourceLocationAttribute is not null)
        {
            SourceLine = sourceLocationAttribute.Line;
            SourceFile = sourceLocationAttribute.Path;
        }

        _defaultValue = PropertyType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic, [])?.Invoke(null) ?? null;
    }

    public TypeSerializedPropertyFixed(TypeSerializedObjectFixed typeSerializedObject, FieldInfo fieldInfo)
    {
        ArgumentNullException.ThrowIfNull(typeSerializedObject, nameof(typeSerializedObject));
        ArgumentNullException.ThrowIfNull(fieldInfo, nameof(fieldInfo));

        _typeSerializedObject = typeSerializedObject;
        _memberInfo = _fieldInfo = fieldInfo;

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

        IsPublic = fieldInfo.IsPublic;
        PropertyType = fieldInfo.FieldType;
        Name = fieldInfo.Name;
        _canWrite = true;

        var sourceLocationAttribute = fieldInfo.GetCustomAttribute<SourceLocationAttribute>();
        if(sourceLocationAttribute is not null)
        {
            SourceLine = sourceLocationAttribute.Line;
            SourceFile = sourceLocationAttribute.Path;
        }

        _defaultValue = PropertyType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic, [])?.Invoke(null) ?? null;
    }

    public override void SetValue<T>(T value)
    {
        try
        {
            NotePreChange();
            SetValueInternal(value);
            NoteChanged();

        }
        catch(System.Exception e)
        {
            var l = new Logger("TypeSerializedPropertyFixed");
            l.Warning(e, $"Error setting {PropertyType} to {value} ({value?.GetType()})");
        }
    }

    /// <summary>
    /// When setting because a child property changed, we don't trigger NoteChanged
    /// because the expectation is that the NoteChanged from setting that property
    /// will instead propogate up, and will be more accurate.
    /// </summary>
    public override void SetValue<T>(T value, SerializedProperty source)
    {
        try
        {
            SetValueInternal(value);
        }
        catch(System.Exception e)
        {
            var l = new Logger("TypeSerializedPropertyFixed");
            l.Warning(e, $"Error setting {PropertyType} to {value} ({value?.GetType()})");
        }
    }

    private void SetValueInternal(object? value)
    {
        value = SerializedProperty.ValueToTypeFixed(PropertyType, value, _defaultValue);
        if(_propertyInfo is not null)
            _propertyInfo.SetValue(_typeSerializedObject.GetTargetObject(), value);
        else
            _fieldInfo!.SetValue(_typeSerializedObject.GetTargetObject(), value);
    }

    public override T GetValue<T>(T defaultValue)
    {
        try
        {
            return ValueToType(GetValueInternal(), defaultValue);
        }
        catch(System.Exception)
        {
            return defaultValue;
        }
    }

    private object GetValueInternal()
    {
        if(_propertyInfo is not null)
            return _propertyInfo.GetValue(_typeSerializedObject.GetTargetObject())!;
        else
            return _fieldInfo!.GetValue(_typeSerializedObject.GetTargetObject())!;
    }

    /// <inheritdoc />
    public override IEnumerable<Attribute> GetAttributes()
    {
        if(_propertyInfo is not null)
            return _propertyInfo.GetCustomAttributes();
        else
            return _fieldInfo!.GetCustomAttributes();
    }

    /// <inheritdoc/>
    public override bool TryGetAsObject(out SerializedObject obj)
    {
        obj = null!;

        if(Parent is not TypeSerializedObjectFixed szObj)
            return false;

        if(TryGetAsContainer(out obj))
            return true;

        var targetType = PropertyType;

        if(targetType is null)
            return false;

        object targetValue = GetValueInternal();

        if(targetValue is null)
            return false;

        targetType = targetValue.GetType();

        if(targetType is null)
            return false;

        obj = new TypeSerializedObjectFixed(GetValueInternal, targetType, this);
        return true;
    }

    private bool TryGetAsContainer(out SerializedObject obj)
    {
        obj = null!;

        var so = CreateSerializedCollection(PropertyType);
        if(so is not null)
        {
            so.ParentProperty = this;

            var targetObject = GetValue<object>(null!);

            // This is very presumptuous but if we have a null list, create an empty list and modify the original object target
            // Otherwise we'll be editing nothing
            // Alternatively we could bitch and moan they haven't initialized their list..
            if(targetObject is null)
            {
                targetObject = PropertyType.IsSZArray
                    ? Array.CreateInstance(PropertyType.GetElementType()!, 0)
                    : Activator.CreateInstance(PropertyType);

                // Do not use SetValue, we don't want to propagate NoteChanged consumers
                SetValueInternal(targetObject);
                _typeSerializedObject.ParentProperty?.SetValue(_typeSerializedObject._targetObject);
            }

            so.SetTargetObject(targetObject, this);
            so.PropertyToObject = PropertyToObjectFixed;
            obj = so;
            return true;
        }

        return false;
    }


    private static readonly MethodInfo _createSerializedCollectionMethod = typeof(SerializedCollection)
        .GetMethod("Create", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, [typeof(Type)])!;


    public static SerializedObject PropertyToObjectFixed(SerializedProperty property)
    {
        var targetType = property.PropertyType;
        if(targetType is null)
            return null!;

        var so = CreateSerializedCollection(targetType);
        if(so is not null)
        {
            so.ParentProperty = property;

            var targetObject = property.GetValue<object>(null!);

            // This is very presumptuous but if we have a null list, create an empty list and modify the original object target
            // Otherwise we'll be editing nothing
            // Alternatively we could bitch and moan they haven't initialized their list..
            if(targetObject is null)
            {
                targetObject = targetType.IsSZArray
                    ? Array.CreateInstance(targetType.GetElementType()!, 0)
                    : Activator.CreateInstance(targetType);

                // TODO: We shouldn't use SetValue, we don't want to propagate NoteChanged consumers
                property.SetValue(targetObject);
                property.Parent?.ParentProperty?.SetValue(property.Parent.Targets.First());
            }

            so.SetTargetObject(targetObject, property);
            so.PropertyToObject = PropertyToObjectFixed;
            return so;
        }

        property.CreateObjectValue();

        object getTarget() => property.GetValue<object>();
        var value = getTarget();
        if(value is null)
            return null!;

        // get type from the object, incase we're a derived type.
        targetType = value.GetType();

        return new TypeSerializedObjectFixed(getTarget, targetType, property);
    }

    private static SerializedCollection CreateSerializedCollection(Type propertyType)
    {
        return (SerializedCollection)_createSerializedCollectionMethod.Invoke(null, [propertyType])!;
    }
}