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

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif

namespace Jovian.InspectorTools {
    public class RequiredAttribute : PropertyAttribute, IPropertyCondition {

        public enum Scope {

            None,
            SelfOrChild,
            SceneOnly,
            SceneOrParentPrefabOnly
            // ProjectOnly

            /*
             * ProjectOnly has a quirk I can't figure out. If you reference a prefab in the project then it works fine.
             * However if you also have an instance of that prefab if the same scene/prefab then it will become a reference to that instance
             * instead of the one in the project. Even the icon changes in Unity. I think this may be a Unity Bug.
             */

        }

        public readonly Scope scope;

        public RequiredAttribute() {
            this.scope = Scope.None;
        }

        public RequiredAttribute(Scope scope) {
            this.scope = scope;
        }

#if UNITY_EDITOR
        public bool DoesPropertyMeetCondition(SerializedProperty property, out string description) {
            description = string.Empty;

            if (property.propertyType != SerializedPropertyType.ObjectReference) {
                description = $"{property.name} is not an ObjectReference";
                return false;
            }

            bool hasValue = property.objectReferenceValue != null;

            if (scope == Scope.None) {
                if (!hasValue) {
                    description = $"{property.name} requires a value";
                    return false;
                }

                return true;
            }

            if (scope == Scope.SelfOrChild) {
                if (!hasValue) {
                    description = $"{property.name} requires a value";
                    return false;
                }

                if (!TryGetTargetGameObject(property, out GameObject parentGameObject)) {
                    description = $"{property.name} does not have a GameObject parent";
                    return false;
                }

                if (!TryGetGameObject(property.objectReferenceValue, out GameObject childGameObject)) {
                    description = $"{property.name} has a non-GameObject value";
                    return false;
                }

                if (!IsObjectSelfOrChild(parentGameObject, childGameObject)) {
                    description = $"{property.name} value is not itself or it's child";
                    return false;
                }

                return true;
            }

            if (scope == Scope.SceneOnly) {
                if (property.serializedObject.targetObject is ScriptableObject) {
                    description = $"{property.name} SceneOnly is not supported on ScriptableObjects";
                    return false;
                }

                if (!TryGetTargetGameObject(property, out GameObject thisGameObject)) {
                    description = $"{property.name} does not have a GameObject parent";
                    return false;
                }

                // scene is invalid - this is a prefab in the project.
                if (thisGameObject.scene.IsValid() == false) {
                    // scene isn't valid, can't check
                    description = $"{property.name} has a reference to GameObject not in a Scene";
                    return true;
                }

                PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
                // scene is valid - prefab stage exists - this is a prefab
                if (prefabStage != null) {
                    return true;
                }

                if (!hasValue) {
                    description = $"{property.name} requires a value";
                    return false;
                }

                return true;
            }

            if (scope == Scope.SceneOrParentPrefabOnly) {
                if (property.serializedObject.targetObject is ScriptableObject) {
                    description = $"{property.name} SceneOrParentPrefabOnly is not supported on ScriptableObjects";
                    return false;
                }

                if (!TryGetTargetGameObject(property, out GameObject thisGameObject)) {
                    description = $"{property.name} does not have a GameObject parent";
                    return false;
                }

                // scene is invalid - this is a prefab in the project.
                if (thisGameObject.scene.IsValid() == false) {
                    // scene isn't valid, can't check
                    description = $"{property.name} has a reference to GameObject not in a Scene";
                    return true;
                }

                bool hasTargetGameObject = TryGetGameObject(property.objectReferenceValue, out GameObject targetGameObject);

                PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
                // scene is valid - prefab stage exists - this is a prefab
                if (prefabStage != null) {
                    // if the prefab open has this requirement on its root object
                    if (prefabStage.prefabContentsRoot == thisGameObject) {
                        if (hasTargetGameObject) {
                            // if the value assigned is a gameObject 
                            if (!IsObjectSelfOrChild(thisGameObject, targetGameObject)) {
                                // then fail if it's within the prefab   
                                description = $"{property.name} requires an object external to its prefab / hierarchy";
                                return false;
                            }
                            else {
                                return true;
                            }
                        }
                        else {
                            return true; // if we don't have a target then it is valid (only inside this prefab of ourselves)
                        }
                    }

                    // if the prefab open is a different prefab, then we're valid if a value is assigned
                    if (!IsObjectSelfOrChild(thisGameObject, targetGameObject)) {
                        description = $"{property.name} requires an object external to its prefab / hierarchy";
                        return false;
                    }
                    else {
                        return true;
                    }
                }

                // not in a prefab, value must be set to an object not within the scope of this attribute
                if (!(hasValue && !IsObjectSelfOrChild(thisGameObject, targetGameObject))) {
                    description = $"{property.name} requires an object external to its prefab / hierarchy";
                    return false;
                }
                else {
                    return true;
                }
            }

            /*
             See note above why this is not enabled

             if (scope == Scope.ProjectOnly) {
                if (!hasValue) {
                    return false;
                }

                bool isPersistent = EditorUtility.IsPersistent(property.objectReferenceValue);
                bool objectHasAssetPath = !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(property.objectReferenceValue));
                bool hasNoScene = false;
                bool isPrefabRoot = false;
                bool isInAssetDatabase = AssetDatabase.Contains(property.objectReferenceValue);

                if (TryGetGameObject(property.objectReferenceValue, out GameObject targetGameObject)) {
                    isPrefabRoot = PrefabUtility.IsOutermostPrefabInstanceRoot(targetGameObject);
                    if (targetGameObject.scene.IsValid() == false) {
                        // scene isn't valid, can't check
                        //return true;
                        hasNoScene = true;
                    }
                }

                // if the target has a path, then it is an asset and that is valid!
                return objectHasAssetPath;
            }*/

            if (!hasValue) {
                description = $"{property.name} requires a value";
                return false;
            }

            return true;
        }

        private static bool TryGetTargetGameObject(SerializedProperty property, out GameObject gameObject) {
            return TryGetGameObject(property.serializedObject.targetObject, out gameObject);
        }

        private static bool TryGetGameObject(Object obj, out GameObject gameObject) {
            if (obj == null) {
                gameObject = null;
                return false;
            }

            gameObject = obj switch {
                Component component => component.gameObject,
                GameObject targetGameObject => targetGameObject,
                _ => null
            };
            return gameObject != null;
        }

        private static bool IsObjectSelfOrChild(GameObject parentObject, GameObject queryObject) {
            if (parentObject == null || queryObject == null) {
                return false;
            }

            return parentObject == queryObject || queryObject.transform.IsChildOf(parentObject.transform);
        }
#endif

    }
}

#if UNITY_EDITOR
namespace Jovian.InspectorTools.Internal {
    [CustomPropertyDrawer(typeof(RequiredAttribute))]
    public class RequiredAttributePropertyDrawer : PropertyDrawer {

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
            if (property.propertyType != SerializedPropertyType.ObjectReference) {
                Debug.LogError($"Required should only be used on Unity Object references. Target='{property.serializedObject.targetObject.name}.{property.propertyPath}'");
                EditorGUI.PropertyField(position, property, label);
                return;
            }

            RequiredAttribute requiredAttribute = (RequiredAttribute)attribute;

            Color guiColor = GUI.color;

            if (!requiredAttribute.DoesPropertyMeetCondition(property, out string text)) {
                RequiredUtil.LayoutRequired(ref position, text, false);
                RequiredUtil.DrawRequired(ref position, text, false);
            }

            label = EditorGUI.BeginProperty(position, label, property);
            position = EditorGUI.PrefixLabel(position, label);

            EditorGUI.BeginChangeCheck();

            EditorGUI.PropertyField(position, property, GUIContent.none);

            if (EditorGUI.EndChangeCheck()) {
                property.serializedObject.ApplyModifiedProperties();
            }

            EditorGUI.EndProperty();

            GUI.color = guiColor;
        }

    }
}
#endif