using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;

using AIRuntime.Runtime.Capabilities.Attributes;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;

using Object =
    UnityEngine.Object;


namespace AIRuntime.Editor
{
    [Serializable]
    public class EditorObjectTargetArguments
    {
        [Description("Optional live Unity object instance ID returned by another Editor capability.")]
        public int ObjectInstanceId { get; set; }

        [Description("Optional persistent GlobalObjectId returned by another Editor capability.")]
        public string GlobalObjectId { get; set; }
    }


    [Serializable]
    public class EditorSerializedTargetArguments :
        EditorObjectTargetArguments
    {
        [Description("Optional GameObject instance ID. Use with componentType when a component instance ID is unavailable.")]
        public int GameObjectInstanceId { get; set; }

        [Description("Optional exact component instance ID.")]
        public int ComponentInstanceId { get; set; }

        [Description("Optional exact component type name, for example UnityEngine.BoxCollider.")]
        public string ComponentType { get; set; }
    }


    [Serializable]
    internal sealed class EditorSerializedPropertyRecord
    {
        public string Path;

        public string DisplayName;

        public string Type;

        public int Depth;

        public bool IsArray;

        public int ArraySize;

        public bool Editable;

        public JToken Value;
    }


    internal static class EditorCapabilityUtility
    {
        private const string BuiltInResourceGuid =
            "0000000000000000f000000000000000";


        public static Object ResolveObject(
            int objectInstanceId,
            string globalObjectId,
            bool useActiveSelection = true)
        {
            Object target =
                null;

            if (objectInstanceId !=
                0)
            {
                target =
                    EditorUtility.InstanceIDToObject(
                        objectInstanceId);

                if (target !=
                    null)
                {
                    return target;
                }

                // Instance IDs are session-local and can become stale after an
                // import or domain reload. When a persistent identity was also
                // supplied, continue with that identity instead of rejecting an
                // otherwise recoverable reference.
                if (string.IsNullOrWhiteSpace(
                        globalObjectId))
                {
                    throw new InvalidOperationException(
                        $"No live Unity object exists with instance ID {objectInstanceId}. Refresh the relevant inspection capability.");
                }
            }

            if (!string.IsNullOrWhiteSpace(
                    globalObjectId))
            {
                string normalizedGlobalObjectId =
                    globalObjectId.Trim();

                string[] identityParts =
                    normalizedGlobalObjectId.Split('-');

                bool hasPlaceholderGuid =
                    identityParts.Length == 5 &&
                    string.Equals(
                        identityParts[2],
                        "00000000000000000000000000000000",
                        StringComparison.OrdinalIgnoreCase);

                bool hasZeroLocalId =
                    identityParts.Length == 5 &&
                    long.TryParse(
                        identityParts[3],
                        NumberStyles.Integer,
                        CultureInfo.InvariantCulture,
                        out long localId) &&
                    localId == 0;

                if (hasPlaceholderGuid ||
                    hasZeroLocalId)
                {
                    throw new InvalidOperationException(
                        $"GlobalObjectId '{normalizedGlobalObjectId}' does not contain a persistent Unity object identity. Do not invent or reuse placeholder IDs; refresh get_serialized_properties and pass its complete current object-reference value.");
                }

                if (!UnityEditor.GlobalObjectId.TryParse(
                        normalizedGlobalObjectId,
                        out UnityEditor.GlobalObjectId parsed))
                {
                    throw new InvalidOperationException(
                        $"GlobalObjectId '{globalObjectId}' is malformed.");
                }

                target =
                    UnityEditor.GlobalObjectId
                        .GlobalObjectIdentifierToObjectSlow(
                            parsed);

                if (target ==
                    null)
                {
                    target =
                        ResolvePersistentAsset(
                            globalObjectId.Trim());
                }

                if (target ==
                    null)
                {
                    throw new InvalidOperationException(
                        $"GlobalObjectId '{globalObjectId}' does not resolve to a Unity object. Refresh the relevant inspection capability.");
                }

                return target;
            }

            if (useActiveSelection)
            {
                target =
                    Selection.activeObject;

                if (target !=
                    null)
                {
                    return target;
                }
            }

            throw new InvalidOperationException(
                "No target was supplied and no Unity object is selected.");
        }


        private static Object ResolvePersistentAsset(
            string globalObjectId)
        {
            // GlobalObjectId text is version-type-guid-localId-prefabId.
            // GlobalObjectIdentifierToObjectSlow does not consistently load an
            // unloaded asset first, so locate the asset through its GUID and
            // match the local file identifier explicitly.
            string[] parts =
                globalObjectId.Split('-');

            if (parts.Length !=
                    5 ||
                string.IsNullOrWhiteSpace(
                    parts[2]) ||
                !long.TryParse(
                    parts[3],
                    NumberStyles.Integer,
                    CultureInfo.InvariantCulture,
                    out long requestedLocalId))
            {
                return null;
            }

            string assetPath =
                AssetDatabase.GUIDToAssetPath(
                    parts[2]);

            if (string.IsNullOrWhiteSpace(
                    assetPath))
            {
                return null;
            }

            Object[] assets =
                AssetDatabase.LoadAllAssetsAtPath(
                    assetPath);

            foreach (Object asset in assets)
            {
                if (asset ==
                    null)
                {
                    continue;
                }

                if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(
                        asset,
                        out string assetGuid,
                        out long localId) &&
                    string.Equals(
                        assetGuid,
                        parts[2],
                        StringComparison.OrdinalIgnoreCase) &&
                    localId ==
                        requestedLocalId)
                {
                    return asset;
                }
            }

            return null;
        }


        public static GameObject ResolveGameObject(
            int objectInstanceId,
            string globalObjectId,
            bool useActiveSelection = true)
        {
            Object target =
                ResolveObject(
                    objectInstanceId,
                    globalObjectId,
                    useActiveSelection);

            GameObject gameObject =
                target as GameObject;

            Component component =
                target as Component;

            if (gameObject ==
                    null &&
                component !=
                    null)
            {
                gameObject =
                    component.gameObject;
            }

            if (gameObject ==
                null)
            {
                throw new InvalidOperationException(
                    $"Unity object '{target.name}' is a {target.GetType().FullName}, not a GameObject or Component.");
            }

            return gameObject;
        }


        public static Object ResolveSerializedTarget(
            EditorSerializedTargetArguments arguments)
        {
            if (arguments ==
                null)
            {
                arguments =
                    new EditorSerializedTargetArguments();
            }

            if (arguments.ComponentInstanceId !=
                0)
            {
                Object componentObject =
                    ResolveObject(
                        arguments.ComponentInstanceId,
                        null,
                        false);

                Component component =
                    componentObject as Component;

                if (component ==
                    null)
                {
                    throw new InvalidOperationException(
                        $"Instance ID {arguments.ComponentInstanceId} is not a Component.");
                }

                if (arguments.GameObjectInstanceId !=
                        0 &&
                    component.gameObject.GetInstanceID() !=
                        arguments.GameObjectInstanceId)
                {
                    throw new InvalidOperationException(
                        "The supplied ComponentInstanceId does not belong to the supplied GameObjectInstanceId.");
                }

                return component;
            }

            Object target =
                null;

            if (arguments.ObjectInstanceId !=
                    0 ||
                !string.IsNullOrWhiteSpace(
                    arguments.GlobalObjectId))
            {
                target =
                    ResolveObject(
                        arguments.ObjectInstanceId,
                        arguments.GlobalObjectId,
                        false);
            }
            else if (arguments.GameObjectInstanceId !=
                     0)
            {
                target =
                    ResolveObject(
                        arguments.GameObjectInstanceId,
                        null,
                        false);
            }
            else
            {
                target =
                    ResolveObject(
                        0,
                        null);
            }

            if (string.IsNullOrWhiteSpace(
                    arguments.ComponentType))
            {
                return target;
            }

            GameObject gameObject =
                target as GameObject;

            Component selectedComponent =
                target as Component;

            if (gameObject ==
                    null &&
                selectedComponent !=
                    null)
            {
                gameObject =
                    selectedComponent.gameObject;
            }

            if (gameObject ==
                null)
            {
                throw new InvalidOperationException(
                    $"ComponentType cannot be used because '{target.name}' is not a GameObject or Component.");
            }

            return ResolveComponent(
                gameObject,
                arguments.ComponentType);
        }


        public static Component ResolveComponent(
            GameObject gameObject,
            string componentType)
        {
            if (gameObject ==
                null)
            {
                throw new ArgumentNullException(
                    nameof(gameObject));
            }

            if (string.IsNullOrWhiteSpace(
                    componentType))
            {
                throw new ArgumentException(
                    "Component type cannot be empty.",
                    nameof(componentType));
            }

            string requested =
                componentType.Trim();

            Component[] matches =
                gameObject
                    .GetComponents<Component>()
                    .Where(
                        component =>
                            component !=
                                null &&
                            (string.Equals(
                                 component.GetType().FullName,
                                 requested,
                                 StringComparison.Ordinal) ||
                             string.Equals(
                                 component.GetType().Name,
                                 requested,
                                 StringComparison.Ordinal)))
                    .ToArray();

            if (matches.Length ==
                1)
            {
                return matches[0];
            }

            if (matches.Length >
                1)
            {
                string matchingInstanceIds =
                    string.Join(
                        ", ",
                        matches.Select(
                            match =>
                                match.GetInstanceID()
                                    .ToString()));

                throw new InvalidOperationException(
                    $"GameObject '{gameObject.name}' has {matches.Length} components matching '{requested}'. Matching ComponentInstanceIds: {matchingInstanceIds}. Retry with one exact ComponentInstanceId after using inspect_game_object or inspect_editor_component to identify the intended component.");
            }

            throw new InvalidOperationException(
                $"GameObject '{gameObject.name}' has no component matching '{requested}'.");
        }


        public static string GetGlobalObjectId(
            Object target)
        {
            if (target ==
                null)
            {
                return string.Empty;
            }

            UnityEditor.GlobalObjectId globalObjectId =
                UnityEditor.GlobalObjectId
                    .GetGlobalObjectIdSlow(
                        target);

            string serializedGlobalObjectId =
                globalObjectId.ToString();

            // Unity's built-in resources share a sentinel GUID and local ID
            // that cannot be used as a durable identity. It can appear to
            // round-trip while the resource happens to be loaded and then fail
            // moments later, so never advertise it to a caller.
            if (IsBuiltInResourceGlobalObjectId(
                    serializedGlobalObjectId))
            {
                return string.Empty;
            }

            Object resolved =
                UnityEditor.GlobalObjectId
                    .GlobalObjectIdentifierToObjectSlow(
                        globalObjectId);

            // Unity produces synthetic-looking IDs for some built-in
            // resources (for example the primitive default material), but
            // those IDs cannot be resolved back to an object. Do not expose
            // them as authoritative reusable identities.
            return resolved == target
                ? serializedGlobalObjectId
                : string.Empty;
        }


        private static bool IsBuiltInResourceGlobalObjectId(
            string globalObjectId)
        {
            string[] parts =
                (globalObjectId ?? string.Empty)
                    .Split('-');

            return parts.Length ==
                       5 &&
                   string.Equals(
                       parts[2],
                       BuiltInResourceGuid,
                       StringComparison.OrdinalIgnoreCase);
        }


        public static string GetHierarchyPath(
            GameObject gameObject)
        {
            if (gameObject ==
                null)
            {
                return string.Empty;
            }

            Stack<string> names =
                new Stack<string>();

            Transform current =
                gameObject.transform;

            while (current !=
                   null)
            {
                names.Push(
                    current.name);

                current =
                    current.parent;
            }

            return string.Join(
                "/",
                names);
        }


        public static string GetScenePath(
            GameObject gameObject)
        {
            if (gameObject ==
                    null ||
                !gameObject.scene.IsValid())
            {
                return string.Empty;
            }

            return NormalizePath(
                string.IsNullOrWhiteSpace(
                    gameObject.scene.path)
                    ? gameObject.scene.name
                    : gameObject.scene.path);
        }


        public static string NormalizePath(
            string value)
        {
            return string.IsNullOrWhiteSpace(
                       value)
                ? string.Empty
                : value.Replace(
                    '\\',
                    '/');
        }


        public static bool TryGetEnabled(
            Component component,
            out bool enabled)
        {
            Behaviour behaviour =
                component as Behaviour;

            if (behaviour !=
                null)
            {
                enabled =
                    behaviour.enabled;

                return true;
            }

            Renderer renderer =
                component as Renderer;

            if (renderer !=
                null)
            {
                enabled =
                    renderer.enabled;

                return true;
            }

            Collider collider =
                component as Collider;

            if (collider !=
                null)
            {
                enabled =
                    collider.enabled;

                return true;
            }

            Collider2D collider2D =
                component as Collider2D;

            if (collider2D !=
                null)
            {
                enabled =
                    collider2D.enabled;

                return true;
            }

            enabled =
                false;

            return false;
        }


        public static List<EditorSerializedPropertyRecord>
            ReadSerializedProperties(
                Object target,
                bool includeHidden,
                int maximumProperties,
                out bool truncated)
        {
            if (target ==
                null)
            {
                throw new ArgumentNullException(
                    nameof(target));
            }

            maximumProperties =
                Mathf.Clamp(
                    maximumProperties,
                    1,
                    2048);

            List<EditorSerializedPropertyRecord> result =
                new List<EditorSerializedPropertyRecord>();

            SerializedObject serializedObject =
                new SerializedObject(
                    target);

            serializedObject.UpdateIfRequiredOrScript();

            SerializedProperty iterator =
                serializedObject.GetIterator();

            bool hasProperty =
                includeHidden
                    ? iterator.Next(
                        true)
                    : iterator.NextVisible(
                        true);

            while (hasProperty &&
                   result.Count <
                   maximumProperties)
            {
                result.Add(
                    DescribeSerializedProperty(
                        iterator));

                hasProperty =
                    includeHidden
                        ? iterator.Next(
                            true)
                        : iterator.NextVisible(
                            true);
            }

            truncated =
                hasProperty;

            return result;
        }


        public static EditorSerializedPropertyRecord
            DescribeSerializedProperty(
                SerializedProperty property)
        {
            return new EditorSerializedPropertyRecord
            {
                Path =
                    property.propertyPath,

                DisplayName =
                    property.displayName,

                Type =
                    property.propertyType.ToString(),

                Depth =
                    property.depth,

                IsArray =
                    property.isArray &&
                    property.propertyType !=
                    SerializedPropertyType.String,

                ArraySize =
                    property.isArray &&
                    property.propertyType !=
                    SerializedPropertyType.String
                        ? property.arraySize
                        : 0,

                Editable =
                    property.editable &&
                    !string.Equals(
                        property.propertyPath,
                        "m_Script",
                        StringComparison.Ordinal),

                Value =
                    ReadSerializedValue(
                        property)
            };
        }


        public static JToken ReadSerializedValue(
            SerializedProperty property)
        {
            switch (property.propertyType)
            {
                case SerializedPropertyType.Integer:
                case SerializedPropertyType.ArraySize:
                case SerializedPropertyType.Character:
                case SerializedPropertyType.LayerMask:
                    return new JValue(
                        property.longValue);

                case SerializedPropertyType.Boolean:
                    return new JValue(
                        property.boolValue);

                case SerializedPropertyType.Float:
                    return new JValue(
                        property.doubleValue);

                case SerializedPropertyType.String:
                    return new JValue(
                        property.stringValue);

                case SerializedPropertyType.Color:
                    return VectorToJson(
                        property.colorValue);

                case SerializedPropertyType.ObjectReference:
                case SerializedPropertyType.ExposedReference:
                    return ObjectReferenceToJson(
                        property.propertyType ==
                            SerializedPropertyType.ExposedReference
                            ? property.exposedReferenceValue
                            : property.objectReferenceValue);

                case SerializedPropertyType.Enum:
                    return new JObject
                    {
                        ["index"] =
                            property.enumValueIndex,

                        ["name"] =
                            property.enumValueIndex >=
                                0 &&
                            property.enumValueIndex <
                                property.enumNames.Length
                                ? property.enumNames[
                                    property.enumValueIndex]
                                : string.Empty
                    };

                case SerializedPropertyType.Vector2:
                    return VectorToJson(
                        property.vector2Value);

                case SerializedPropertyType.Vector3:
                    return VectorToJson(
                        property.vector3Value);

                case SerializedPropertyType.Vector4:
                    return VectorToJson(
                        property.vector4Value);

                case SerializedPropertyType.Rect:
                    return RectToJson(
                        property.rectValue);

                case SerializedPropertyType.Bounds:
                    return BoundsToJson(
                        property.boundsValue);

                case SerializedPropertyType.Quaternion:
                    return VectorToJson(
                        property.quaternionValue);

                case SerializedPropertyType.Vector2Int:
                    return VectorToJson(
                        property.vector2IntValue);

                case SerializedPropertyType.Vector3Int:
                    return VectorToJson(
                        property.vector3IntValue);

                case SerializedPropertyType.RectInt:
                    return RectToJson(
                        property.rectIntValue);

                case SerializedPropertyType.BoundsInt:
                    return BoundsToJson(
                        property.boundsIntValue);

                case SerializedPropertyType.AnimationCurve:
                    return AnimationCurveToJson(property.animationCurveValue);

                case SerializedPropertyType.Gradient:
                    return GradientToJson(property.gradientValue);

                case SerializedPropertyType.ManagedReference:
                    return new JObject
                    {
                        ["type"] =
                            property.managedReferenceFullTypename ??
                            string.Empty,

                        ["id"] =
                            property.managedReferenceId
                    };

                default:
                    return JValue.CreateNull();
            }
        }


        public static JToken ParseInputValue(
            string valueJson)
        {
            if (valueJson ==
                null)
            {
                return JValue.CreateNull();
            }

            string trimmed =
                valueJson.Trim();

            if (trimmed.Length ==
                0)
            {
                return new JValue(
                    string.Empty);
            }

            try
            {
                return JToken.Parse(
                    trimmed);
            }
            catch (JsonReaderException)
            {
                return new JValue(
                    valueJson);
            }
        }


        public static JToken NormalizeSemanticSerializedInput(
            Object target,
            SerializedProperty property,
            JToken value)
        {
            if (target == null ||
                property == null ||
                property.propertyType != SerializedPropertyType.Integer)
            {
                return value;
            }

            bool hasSemanticFlags =
                value != null &&
                value.Type == JTokenType.Object &&
                value["flags"] != null;

            Type enumType =
                ResolveBackedEnumType(
                    target.GetType(),
                    property.propertyPath);

            if (enumType == null &&
                hasSemanticFlags)
            {
                enumType =
                    ResolveSemanticFlagsEnumType(
                        target.GetType(),
                        value["flags"]);
            }

            if (enumType == null)
            {
                if (hasSemanticFlags)
                {
                    throw new InvalidOperationException(
                        $"Serialized property '{property.propertyPath}' is an integer, but its backing enum could not be resolved uniquely from the supplied flag names.");
                }

                return value;
            }

            string[] validNames =
                Enum.GetNames(enumType);

            bool isDeclaredFlagsEnum =
                enumType.GetCustomAttributes(
                    typeof(FlagsAttribute),
                    false).Length >
                0;

            if (!hasSemanticFlags)
            {
                if (!isDeclaredFlagsEnum)
                {
                    return value;
                }

                throw new InvalidOperationException(
                    $"Serialized property '{property.propertyPath}' is backed by flagged enum {enumType.FullName}. " +
                    "Do not pass a numeric bitmask. Pass ValueJson as {\"flags\":[\"ExactFlagName\"]}. " +
                    $"Valid flag names: {string.Join(", ", validNames)}.");
            }

            JToken flagsToken =
                value["flags"];

            IEnumerable<string> requestedNames =
                flagsToken.Type == JTokenType.Array
                    ? flagsToken.Values<string>()
                    : new[] { flagsToken.Value<string>() };

            long combined = 0;

            foreach (string requestedName in requestedNames)
            {
                if (string.IsNullOrWhiteSpace(requestedName) ||
                    !Enum.TryParse(
                        enumType,
                        requestedName.Trim(),
                        true,
                        out object parsed))
                {
                    throw new InvalidOperationException(
                        $"Flag '{requestedName}' is invalid for {enumType.FullName}. Valid flag names: {string.Join(", ", validNames)}.");
                }

                combined |=
                    Convert.ToInt64(
                        parsed,
                        CultureInfo.InvariantCulture);
            }

            return new JValue(combined);
        }


        private static Type ResolveBackedEnumType(
            Type targetType,
            string propertyPath)
        {
            if (targetType == null ||
                string.IsNullOrWhiteSpace(propertyPath) ||
                propertyPath.IndexOf('.') >= 0 ||
                !propertyPath.StartsWith("m_", StringComparison.Ordinal))
            {
                return null;
            }

            string memberName =
                propertyPath.Substring(2);

            PropertyInfo property =
                targetType.GetProperty(
                    memberName,
                    BindingFlags.Public |
                    BindingFlags.NonPublic |
                    BindingFlags.Instance |
                    BindingFlags.IgnoreCase);

            Type memberType =
                property?.PropertyType;

            if (memberType == null)
            {
                FieldInfo field =
                    targetType.GetField(
                        memberName,
                        BindingFlags.Public |
                        BindingFlags.NonPublic |
                        BindingFlags.Instance |
                        BindingFlags.IgnoreCase);

                memberType =
                    field?.FieldType;
            }

            // Native Unity properties can be surfaced as integer serialized
            // fields even when their managed API is enum-typed. Some engine
            // versions do not preserve FlagsAttribute on that reflected
            // bridge. An explicit semantic {"flags": [...]} request is still
            // safe to resolve against the enum's declared names; callers using
            // ordinary numeric input are rejected only when the runtime does
            // expose FlagsAttribute.
            return memberType != null &&
                   memberType.IsEnum
                ? memberType
                : null;
        }


        private static Type ResolveSemanticFlagsEnumType(
            Type targetType,
            JToken flagsToken)
        {
            if (targetType == null ||
                flagsToken == null)
            {
                return null;
            }


            string[] requestedNames =
                (flagsToken.Type == JTokenType.Array
                    ? flagsToken.Values<string>()
                    : new[] { flagsToken.Value<string>() })
                .Where(name => !string.IsNullOrWhiteSpace(name))
                .Select(name => name.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray();

            if (requestedNames.Length ==
                0)
            {
                return null;
            }


            BindingFlags memberFlags =
                BindingFlags.Public |
                BindingFlags.NonPublic |
                BindingFlags.Instance;

            Type[] candidates =
                targetType.GetProperties(memberFlags)
                    .Select(property => property.PropertyType)
                    .Concat(
                        targetType.GetFields(memberFlags)
                            .Select(field => field.FieldType))
                    .Where(type => type != null && type.IsEnum)
                    .Distinct()
                    .Where(
                        type =>
                        {
                            HashSet<string> enumNames =
                                new HashSet<string>(
                                    Enum.GetNames(type),
                                    StringComparer.OrdinalIgnoreCase);

                            return requestedNames.All(enumNames.Contains);
                        })
                    .Take(2)
                    .ToArray();

            return candidates.Length ==
                   1
                ? candidates[0]
                : null;
        }


        internal static void ValidateSceneReference(Object owner, Object reference)
        {
            GameObject referencedObject = reference as GameObject ?? (reference as Component)?.gameObject;
            if (referencedObject == null || EditorUtility.IsPersistent(reference)) return;
            GameObject ownerObject = owner as GameObject ?? (owner as Component)?.gameObject;
            if (EditorUtility.IsPersistent(owner) ||
                (ownerObject != null && ownerObject.scene != referencedObject.scene))
            {
                throw new InvalidOperationException(
                    $"Cannot assign scene object '{reference.name}' from '{referencedObject.scene.path}' to '{owner.name}'. Cross-scene references cannot be saved. Resolve an object in the target scene or use a persistent asset.");
            }
        }

        public static void AssignSerializedValue(
            SerializedProperty property,
            JToken value)
        {
            switch (property.propertyType)
            {
                case SerializedPropertyType.Integer:
                case SerializedPropertyType.ArraySize:
                case SerializedPropertyType.Character:
                case SerializedPropertyType.LayerMask:
                    property.longValue =
                        RequireValue<long>(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Boolean:
                    property.boolValue =
                        RequireValue<bool>(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Float:
                    property.doubleValue =
                        RequireValue<double>(
                            value,
                            property);
                    return;

                case SerializedPropertyType.String:
                    property.stringValue =
                        value.Type ==
                            JTokenType.Null
                            ? string.Empty
                            : value.Type ==
                                JTokenType.String
                                ? value.Value<string>()
                                : value.ToString(
                                    Formatting.None);
                    return;

                case SerializedPropertyType.Color:
                    property.colorValue =
                        ParseColor(
                            value,
                            property);
                    return;

                case SerializedPropertyType.ObjectReference:
                    Object reference = ResolveObjectReference(
                            value,
                            property.objectReferenceValue);
                    ValidateSceneReference(property.serializedObject.targetObject, reference);
                    property.objectReferenceValue = reference;
                    return;

                case SerializedPropertyType.ExposedReference:
                    Object exposedReference = ResolveObjectReference(
                            value,
                            property.exposedReferenceValue);
                    ValidateSceneReference(property.serializedObject.targetObject, exposedReference);
                    property.exposedReferenceValue = exposedReference;
                    return;

                case SerializedPropertyType.Enum:
                    property.enumValueIndex =
                        ParseEnumIndex(
                            property,
                            value);
                    return;

                case SerializedPropertyType.Vector2:
                    property.vector2Value =
                        ParseVector2(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Vector3:
                    property.vector3Value =
                        ParseVector3(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Vector4:
                    property.vector4Value =
                        ParseVector4(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Rect:
                    property.rectValue =
                        ParseRect(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Bounds:
                    property.boundsValue =
                        ParseBounds(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Quaternion:
                    property.quaternionValue =
                        ParseQuaternion(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Vector2Int:
                    property.vector2IntValue =
                        ParseVector2Int(
                            value,
                            property);
                    return;

                case SerializedPropertyType.Vector3Int:
                    property.vector3IntValue =
                        ParseVector3Int(
                            value,
                            property);
                    return;

                case SerializedPropertyType.RectInt:
                    property.rectIntValue =
                        ParseRectInt(
                            value,
                            property);
                    return;

                case SerializedPropertyType.BoundsInt:
                    property.boundsIntValue =
                        ParseBoundsInt(
                            value,
                            property);
                    return;

                case SerializedPropertyType.AnimationCurve:
                    property.animationCurveValue = ParseAnimationCurve(value, property);
                    return;

                case SerializedPropertyType.Gradient:
                    property.gradientValue = ParseGradient(value, property);
                    return;

                default:
                    throw new InvalidOperationException(
                        $"Serialized property '{property.propertyPath}' has unsupported writable type {property.propertyType}.");
            }
        }


        private static JToken ObjectReferenceToJson(
            Object value)
        {
            if (value ==
                null)
            {
                return JValue.CreateNull();
            }

            return new JObject
            {
                ["instanceId"] =
                    value.GetInstanceID(),

                ["globalObjectId"] =
                    GetGlobalObjectId(
                        value),

                ["name"] =
                    value.name,

                ["type"] =
                    value.GetType().FullName,

                ["assetPath"] =
                    NormalizePath(
                        AssetDatabase.GetAssetPath(
                            value))
            };
        }


        private static Object ResolveObjectReference(
            JToken value,
            Object currentValue = null)
        {
            if (value ==
                    null ||
                value.Type ==
                    JTokenType.Null)
            {
                return null;
            }

            if (value.Type ==
                JTokenType.Integer)
            {
                return ResolveObject(
                    value.Value<int>(),
                    null,
                    false);
            }

            if (value.Type ==
                JTokenType.String)
            {
                string text =
                    value.Value<string>();

                if (string.IsNullOrWhiteSpace(
                        text) ||
                    string.Equals(
                        text,
                        "null",
                        StringComparison.OrdinalIgnoreCase))
                {
                    return null;
                }

                Object asset =
                    AssetDatabase.LoadMainAssetAtPath(
                        NormalizePath(
                            text));

                if (asset !=
                    null)
                {
                    return asset;
                }

                if (MatchesCurrentBuiltInReference(
                        currentValue,
                        text))
                {
                    return currentValue;
                }

                return ResolveObject(
                    0,
                    text,
                    false);
            }

            JObject description =
                value as JObject;

            if (description ==
                null)
            {
                throw new InvalidOperationException(
                    "Object-reference values must be null, an instance ID, an asset path, a GlobalObjectId, or an object containing instanceId/globalObjectId/assetPath.");
            }

            int instanceId =
                description.GetValue(
                        "instanceId",
                        StringComparison.OrdinalIgnoreCase)
                    ?.Value<int?>() ??
                description.GetValue(
                        "objectInstanceId",
                        StringComparison.OrdinalIgnoreCase)
                    ?.Value<int?>() ??
                0;

            string globalId =
                description.GetValue(
                        "globalObjectId",
                        StringComparison.OrdinalIgnoreCase)
                    ?.Value<string>();

            string assetPath =
                description.GetValue(
                        "assetPath",
                        StringComparison.OrdinalIgnoreCase)
                    ?.Value<string>();

            if (instanceId ==
                    0 &&
                MatchesCurrentBuiltInReference(
                    currentValue,
                    globalId))
            {
                return currentValue;
            }

            if (instanceId !=
                    0 ||
                !string.IsNullOrWhiteSpace(
                    globalId))
            {
                return ResolveObject(
                    instanceId,
                    globalId,
                    false);
            }

            if (string.IsNullOrWhiteSpace(
                    assetPath))
            {
                throw new InvalidOperationException(
                    "The object-reference value did not identify a Unity object. Pass the exact AssetPath returned by create_material, or an instanceId/globalObjectId returned by an Editor inspection capability.");
            }

            Object referencedAsset =
                AssetDatabase.LoadMainAssetAtPath(
                    NormalizePath(
                        assetPath));

            if (referencedAsset ==
                null)
            {
                throw new InvalidOperationException(
                    $"No Unity asset exists at '{assetPath}'.");
            }

            return referencedAsset;
        }


        private static bool MatchesCurrentBuiltInReference(
            Object currentValue,
            string requestedGlobalObjectId)
        {
            if (currentValue ==
                    null ||
                !IsBuiltInResourceGlobalObjectId(
                    requestedGlobalObjectId))
            {
                return false;
            }

            string currentGlobalObjectId =
                UnityEditor.GlobalObjectId
                    .GetGlobalObjectIdSlow(
                        currentValue)
                    .ToString();

            return string.Equals(
                currentGlobalObjectId,
                requestedGlobalObjectId,
                StringComparison.Ordinal);
        }


        private static int ParseEnumIndex(
            SerializedProperty property,
            JToken value)
        {
            if (value.Type ==
                JTokenType.Integer)
            {
                int index =
                    value.Value<int>();

                if (index <
                        0 ||
                    index >=
                        property.enumNames.Length)
                {
                    throw new InvalidOperationException(
                        $"Enum index {index} is invalid for '{property.propertyPath}'. Valid names: {string.Join(", ", property.enumNames)}.");
                }

                return index;
            }

            if (value.Type ==
                    JTokenType.Object &&
                value["index"]?.Type ==
                    JTokenType.Integer &&
                string.IsNullOrWhiteSpace(
                    value.Value<string>("name")))
            {
                int index =
                    value.Value<int>("index");

                if (index <
                        0 ||
                    index >=
                        property.enumNames.Length)
                {
                    throw new InvalidOperationException(
                        $"Enum index {index} is invalid for '{property.propertyPath}'. Valid names: {string.Join(", ", property.enumNames)}.");
                }

                return index;
            }

            string name =
                value.Type ==
                    JTokenType.Object
                    ? value.Value<string>(
                        "name")
                    : value.Value<string>();

            int matched =
                Array.FindIndex(
                    property.enumNames,
                    candidate =>
                        string.Equals(
                            candidate,
                            name,
                            StringComparison.OrdinalIgnoreCase));

            if (matched <
                0)
            {
                throw new InvalidOperationException(
                    $"Enum value '{name}' is invalid for '{property.propertyPath}'. Valid names: {string.Join(", ", property.enumNames)}.");
            }

            return matched;
        }


        private static T RequireValue<T>(
            JToken value,
            SerializedProperty property)
        {
            try
            {
                return value.ToObject<T>();
            }
            catch (Exception exception)
            {
                throw new InvalidOperationException(
                    $"Value for '{property.propertyPath}' must be a valid {typeof(T).Name}.",
                    exception);
            }
        }


        private static JObject RequireObject(
            JToken value,
            SerializedProperty property)
        {
            JObject result =
                value as JObject;

            if (result ==
                null)
            {
                throw new InvalidOperationException(
                    $"Value for '{property.propertyPath}' must be a JSON object.");
            }

            return result;
        }


        private static float Float(
            JObject value,
            string name,
            SerializedProperty property)
        {
            JToken token =
                value[name];

            if (token ==
                null)
            {
                throw new InvalidOperationException(
                    $"Value for '{property.propertyPath}' is missing '{name}'.");
            }

            return token.Value<float>();
        }


        private static int Integer(
            JObject value,
            string name,
            SerializedProperty property)
        {
            JToken token =
                value[name];

            if (token ==
                null)
            {
                throw new InvalidOperationException(
                    $"Value for '{property.propertyPath}' is missing '{name}'.");
            }

            return token.Value<int>();
        }


        private static Vector2 ParseVector2(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new Vector2(
                Float(json, "x", property),
                Float(json, "y", property));
        }


        private static Vector3 ParseVector3(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new Vector3(
                Float(json, "x", property),
                Float(json, "y", property),
                Float(json, "z", property));
        }


        private static Vector4 ParseVector4(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new Vector4(
                Float(json, "x", property),
                Float(json, "y", property),
                Float(json, "z", property),
                Float(json, "w", property));
        }


        private static Quaternion ParseQuaternion(
            JToken value,
            SerializedProperty property)
        {
            Vector4 vector =
                ParseVector4(
                    value,
                    property);

            return new Quaternion(
                vector.x,
                vector.y,
                vector.z,
                vector.w);
        }


        private static Color ParseColor(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new Color(
                Float(json, "r", property),
                Float(json, "g", property),
                Float(json, "b", property),
                json["a"] !=
                    null
                    ? Float(json, "a", property)
                    : 1);
        }


        private static Rect ParseRect(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new Rect(
                Float(json, "x", property),
                Float(json, "y", property),
                Float(json, "width", property),
                Float(json, "height", property));
        }


        private static Bounds ParseBounds(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new Bounds(
                ParseVector3(
                    json["center"],
                    property),
                ParseVector3(
                    json["size"],
                    property));
        }


        private static Vector2Int ParseVector2Int(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new Vector2Int(
                Integer(json, "x", property),
                Integer(json, "y", property));
        }


        private static Vector3Int ParseVector3Int(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new Vector3Int(
                Integer(json, "x", property),
                Integer(json, "y", property),
                Integer(json, "z", property));
        }


        private static RectInt ParseRectInt(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new RectInt(
                Integer(json, "x", property),
                Integer(json, "y", property),
                Integer(json, "width", property),
                Integer(json, "height", property));
        }


        private static BoundsInt ParseBoundsInt(
            JToken value,
            SerializedProperty property)
        {
            JObject json =
                RequireObject(
                    value,
                    property);

            return new BoundsInt(
                ParseVector3Int(
                    json["position"],
                    property),
                ParseVector3Int(
                    json["size"],
                    property));
        }


        private static AnimationCurve ParseAnimationCurve(JToken value, SerializedProperty property)
        {
            JObject source = value as JObject ??
                throw new InvalidOperationException($"Property '{property.propertyPath}' requires an object with keys[].");
            JArray keys = source["keys"] as JArray ?? new JArray();
            Keyframe[] frames = keys.Select(token =>
            {
                JObject key = token as JObject ?? throw new InvalidOperationException("AnimationCurve keys must be objects.");
                Keyframe frame = new Keyframe(
                    key.Value<float?>("time") ?? 0f,
                    key.Value<float?>("value") ?? 0f,
                    key.Value<float?>("inTangent") ?? 0f,
                    key.Value<float?>("outTangent") ?? 0f,
                    key.Value<float?>("inWeight") ?? 0f,
                    key.Value<float?>("outWeight") ?? 0f);
                frame.weightedMode = (WeightedMode)(key.Value<int?>("weightedMode") ?? 0);
                return frame;
            }).ToArray();
            AnimationCurve curve = new AnimationCurve(frames);
            if (Enum.TryParse(source.Value<string>("preWrapMode"), true, out WrapMode pre)) curve.preWrapMode = pre;
            if (Enum.TryParse(source.Value<string>("postWrapMode"), true, out WrapMode post)) curve.postWrapMode = post;
            return curve;
        }

        private static JObject AnimationCurveToJson(AnimationCurve curve)
        {
            curve = curve ?? new AnimationCurve();
            return new JObject
            {
                ["keys"] = new JArray(curve.keys.Select(key => new JObject
                {
                    ["time"] = key.time, ["value"] = key.value,
                    ["inTangent"] = key.inTangent, ["outTangent"] = key.outTangent,
                    ["inWeight"] = key.inWeight, ["outWeight"] = key.outWeight,
                    ["weightedMode"] = (int)key.weightedMode
                })),
                ["preWrapMode"] = curve.preWrapMode.ToString(),
                ["postWrapMode"] = curve.postWrapMode.ToString()
            };
        }

        private static Gradient ParseGradient(JToken value, SerializedProperty property)
        {
            JObject source = value as JObject ??
                throw new InvalidOperationException($"Property '{property.propertyPath}' requires a gradient object.");
            GradientColorKey[] colors = (source["colorKeys"] as JArray ?? new JArray())
                .Select(token =>
                {
                    JObject key = token as JObject ?? throw new InvalidOperationException("Gradient color keys must be objects.");
                    return new GradientColorKey(ParseColor(key["color"], property), key.Value<float?>("time") ?? 0f);
                }).ToArray();
            GradientAlphaKey[] alphas = (source["alphaKeys"] as JArray ?? new JArray())
                .Select(token =>
                {
                    JObject key = token as JObject ?? throw new InvalidOperationException("Gradient alpha keys must be objects.");
                    return new GradientAlphaKey(key.Value<float?>("alpha") ?? 1f, key.Value<float?>("time") ?? 0f);
                }).ToArray();
            Gradient gradient = new Gradient();
            gradient.SetKeys(colors.Length == 0 ? new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) } : colors,
                alphas.Length == 0 ? new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } : alphas);
            if (Enum.TryParse(source.Value<string>("mode"), true, out GradientMode mode)) gradient.mode = mode;
            return gradient;
        }

        private static JObject GradientToJson(Gradient gradient)
        {
            gradient = gradient ?? new Gradient();
            return new JObject
            {
                ["colorKeys"] = new JArray(gradient.colorKeys.Select(key => new JObject
                {
                    ["time"] = key.time,
                    ["color"] = VectorToJson(key.color)
                })),
                ["alphaKeys"] = new JArray(gradient.alphaKeys.Select(key => new JObject
                {
                    ["time"] = key.time,
                    ["alpha"] = key.alpha
                })),
                ["mode"] = gradient.mode.ToString()
            };
        }

        private static JObject VectorToJson(
            Vector2 value)
        {
            return new JObject
            {
                ["x"] = value.x,
                ["y"] = value.y
            };
        }


        private static JObject VectorToJson(
            Vector3 value)
        {
            return new JObject
            {
                ["x"] = value.x,
                ["y"] = value.y,
                ["z"] = value.z
            };
        }


        private static JObject VectorToJson(
            Vector4 value)
        {
            return new JObject
            {
                ["x"] = value.x,
                ["y"] = value.y,
                ["z"] = value.z,
                ["w"] = value.w
            };
        }


        private static JObject VectorToJson(
            Quaternion value)
        {
            return new JObject
            {
                ["x"] = value.x,
                ["y"] = value.y,
                ["z"] = value.z,
                ["w"] = value.w
            };
        }


        private static JObject VectorToJson(
            Color value)
        {
            return new JObject
            {
                ["r"] = value.r,
                ["g"] = value.g,
                ["b"] = value.b,
                ["a"] = value.a
            };
        }


        private static JObject VectorToJson(
            Vector2Int value)
        {
            return new JObject
            {
                ["x"] = value.x,
                ["y"] = value.y
            };
        }


        private static JObject VectorToJson(
            Vector3Int value)
        {
            return new JObject
            {
                ["x"] = value.x,
                ["y"] = value.y,
                ["z"] = value.z
            };
        }


        private static JObject RectToJson(
            Rect value)
        {
            return new JObject
            {
                ["x"] = value.x,
                ["y"] = value.y,
                ["width"] = value.width,
                ["height"] = value.height
            };
        }


        private static JObject RectToJson(
            RectInt value)
        {
            return new JObject
            {
                ["x"] = value.x,
                ["y"] = value.y,
                ["width"] = value.width,
                ["height"] = value.height
            };
        }


        private static JObject BoundsToJson(
            Bounds value)
        {
            return new JObject
            {
                ["center"] =
                    VectorToJson(
                        value.center),

                ["size"] =
                    VectorToJson(
                        value.size)
            };
        }


        private static JObject BoundsToJson(
            BoundsInt value)
        {
            return new JObject
            {
                ["position"] =
                    VectorToJson(
                        value.position),

                ["size"] =
                    VectorToJson(
                        value.size)
            };
        }
    }
}
