// Serialization // Copyright 2020 Kybernetik //

#if UNITY_EDITOR

using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

// Shared File Last Modified: 2019-12-06.
namespace Animancer.Editor
// namespace InspectorGadgets.Editor
{
    /// <summary>[Editor-Only] Various serialization utilities.</summary>
    public partial class Serialization
    {
        /// <summary>[Editor-Only]
        /// A serializable reference to a <see cref="SerializedProperty"/>.
        /// </summary>
        [Serializable]
        public sealed class PropertyReference
        {
            /************************************************************************************************************************/

            [SerializeField] private ObjectReference[] _TargetObjects;

            /// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.targetObject"/>.</summary>
            public ObjectReference TargetObject
            {
                get
                {
                    return _TargetObjects != null && _TargetObjects.Length > 0 ?
                        _TargetObjects[0] : null;
                }
            }

            /// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.targetObjects"/>.</summary>
            public ObjectReference[] TargetObjects { get { return _TargetObjects; } }

            /************************************************************************************************************************/

            [SerializeField] private ObjectReference _Context;

            /// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.context"/>.</summary>
            public ObjectReference Context { get { return _Context; } }

            /************************************************************************************************************************/

            [SerializeField] private string _PropertyPath;

            /// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedProperty.propertyPath"/>.</summary>
            public string PropertyPath { get { return _PropertyPath; } }

            /************************************************************************************************************************/

            [NonSerialized] private bool _IsInitialised;

            /// <summary>Indicates whether the <see cref="Property"/> has been accessed.</summary>
            public bool IsInitialised { get { return _IsInitialised; } }

            /************************************************************************************************************************/

            [NonSerialized] private SerializedProperty _Property;

            /// <summary>[<see cref="SerializeField"/>] The referenced <see cref="SerializedProperty"/>.</summary>
            public SerializedProperty Property
            {
                get
                {
                    Initialise();
                    return _Property;
                }
            }

            /************************************************************************************************************************/

            /// <summary>
            /// Constructs a new <see cref="PropertyReference"/> which wraps the specified `property`.
            /// </summary>
            public PropertyReference(SerializedProperty property)
            {
                _TargetObjects = ObjectReference.Convert(property.serializedObject.targetObjects);

                _Context = property.serializedObject.context;
                _PropertyPath = property.propertyPath;

                // Don't set the _Property. If it gets accessed we want to create out own instance.
            }

            /************************************************************************************************************************/

            /// <summary>
            /// Constructs a new <see cref="PropertyReference"/> which wraps the specified `property`.
            /// </summary>
            public static implicit operator PropertyReference(SerializedProperty property)
            {
                return new PropertyReference(property);
            }

            /// <summary>
            /// Returns the target <see cref="Property"/>.
            /// </summary>
            public static implicit operator SerializedProperty(PropertyReference reference)
            {
                return reference.Property;
            }

            /************************************************************************************************************************/

            private void Initialise()
            {
                if (_IsInitialised)
                {
                    if (!TargetsExist)
                        Dispose();
                    return;
                }

                _IsInitialised = true;

                if (string.IsNullOrEmpty(_PropertyPath) ||
                    _TargetObjects.Length == 0 ||
                    !TargetsExist)
                    return;

                var targetObjects = ObjectReference.Convert(_TargetObjects);
                var serializedObject = new SerializedObject(targetObjects, _Context);
                _Property = serializedObject.FindProperty(_PropertyPath);
            }

            /************************************************************************************************************************/

            /// <summary>
            /// Returns true if the specified property and objects match the targets of this reference.
            /// </summary>
            public bool IsTarget(SerializedProperty property, Object[] targetObjects)
            {
                if (_Property == null ||
                    _Property.propertyPath != property.propertyPath ||
                    _TargetObjects.Length != targetObjects.Length)
                    return false;

                for (int i = 0; i < _TargetObjects.Length; i++)
                {
                    if (_TargetObjects[i] != targetObjects[i])
                        return false;
                }

                return true;
            }

            /************************************************************************************************************************/

            /// <summary>
            /// Returns true if there is at least one target and none of them are null.
            /// </summary>
            private bool TargetsExist
            {
                get
                {
                    if (_TargetObjects.Length == 0)
                        return false;

                    for (int i = 0; i < _TargetObjects.Length; i++)
                    {
                        if (_TargetObjects[i].Object == null)
                            return false;
                    }

                    return true;
                }
            }

            /************************************************************************************************************************/

            /// <summary>
            /// Calls <see cref="SerializedObject.Update"/> if the <see cref="Property"/> has been initialised.
            /// </summary>
            public void Update()
            {
                if (_Property == null)
                    return;

                if (!TargetsExist)
                {
                    Dispose();
                    return;
                }

                _Property.serializedObject.Update();
            }

            /// <summary>
            /// Calls <see cref="SerializedObject.ApplyModifiedProperties"/> if the <see cref="Property"/> has been initialised.
            /// </summary>
            public void ApplyModifiedProperties()
            {
                if (_Property == null)
                    return;

                if (!TargetsExist)
                {
                    Dispose();
                    return;
                }

                _Property.serializedObject.ApplyModifiedProperties();
            }

            /// <summary>
            /// Calls <see cref="SerializedObject.Dispose"/> if the <see cref="Property"/> has been initialised.
            /// </summary>
            public void Dispose()
            {
                if (_Property != null)
                {
                    _Property.serializedObject.Dispose();
                    _Property = null;
                }
            }

            /************************************************************************************************************************/

            /// <summary>Gets the height needed to draw the target property.</summary>
            public float GetPropertyHeight()
            {
                if (_Property == null)
                    return 0;

                return EditorGUI.GetPropertyHeight(_Property, _Property.isExpanded);
            }

            /************************************************************************************************************************/

            /// <summary>Draws the target object within the specified `area`.</summary>
            public void DoTargetGUI(Rect area)
            {
                area.height = EditorGUIUtility.singleLineHeight;

                Initialise();

                if (_Property == null)
                {
                    GUI.Label(area, "Missing " + this);
                    return;
                }

                var targets = _Property.serializedObject.targetObjects;

                var enabled = GUI.enabled;
                GUI.enabled = false;

                var showMixedValue = EditorGUI.showMixedValue;
                EditorGUI.showMixedValue = targets.Length > 1;

                var target = targets.Length > 0 ? targets[0] : null;
                EditorGUI.ObjectField(area, target, typeof(Object), true);

                EditorGUI.showMixedValue = showMixedValue;
                GUI.enabled = enabled;
            }

            /************************************************************************************************************************/

            /// <summary>Draws the target property within the specified `area`.</summary>
            public void DoPropertyGUI(Rect area)
            {
                Initialise();

                if (_Property == null)
                    return;

                _Property.serializedObject.Update();

                GUI.BeginGroup(area);
                area.x = area.y = 0;

                EditorGUI.PropertyField(area, _Property, _Property.isExpanded);

                GUI.EndGroup();

                _Property.serializedObject.ApplyModifiedProperties();
            }

            /************************************************************************************************************************/
        }

        /************************************************************************************************************************/

        /// <summary>Returns true if the `reference` and <see cref="PropertyReference.Property"/> are not null.</summary>
        public static bool IsValid(this PropertyReference reference)
        {
            return
                reference != null &&
                reference.Property != null;
        }

        /************************************************************************************************************************/
    }
}

#endif