using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace nadena.dev.modular_avatar.core
{
    [Serializable]
    public class AvatarObjectReference
    {
        private static long HIERARCHY_CHANGED_SEQ = long.MinValue;
        private long ReferencesLockedAtFrame = long.MinValue;

        public static string AVATAR_ROOT = "$$$AVATAR_ROOT$$$";
        public string referencePath;

        [SerializeField] internal GameObject targetObject;

        private long _cacheSeq = long.MinValue;
        private bool _cacheValid;
        private string _cachedPath;
        private GameObject _cachedReference;

#if UNITY_EDITOR
        [InitializeOnLoadMethod]
        private static void Init()
        {
            EditorApplication.hierarchyChanged += () => HIERARCHY_CHANGED_SEQ += 1;
        }
#endif

        internal static void InvalidateAll()
        {
            HIERARCHY_CHANGED_SEQ++;
        }
        
        public AvatarObjectReference Clone()
        {
            return new AvatarObjectReference
            {
                referencePath = referencePath,
                targetObject = targetObject
            };
        }
            
        #if UNITY_EDITOR
        public static GameObject Get(SerializedProperty prop)
        {
            var rootObject = prop.serializedObject.targetObject;
            if (rootObject == null) return null;
            
            var avatarRoot = RuntimeUtil.FindAvatarTransformInParents((rootObject as Component)?.transform ?? (rootObject as GameObject)?.transform);
            if (avatarRoot == null) return null;
            
            var referencePath = prop.FindPropertyRelative("referencePath").stringValue;
            var targetObject = prop.FindPropertyRelative("targetObject").objectReferenceValue as GameObject;
            
            if (targetObject != null && targetObject.transform.IsChildOf(avatarRoot))
                return targetObject;
            
            if (referencePath == AVATAR_ROOT)
                return avatarRoot.gameObject;
            
            return avatarRoot.Find(referencePath)?.gameObject;
        }
        #endif
        
        public GameObject Get(Component container)
        {
            bool cacheValid = _cacheValid || ReferencesLockedAtFrame == Time.frameCount;
            cacheValid &= HIERARCHY_CHANGED_SEQ == _cacheSeq;
            
            if (cacheValid && _cachedPath == referencePath && _cachedReference != null) return _cachedReference;

            _cacheValid = true;
            _cacheSeq = HIERARCHY_CHANGED_SEQ;
            _cachedPath = referencePath;

            if (string.IsNullOrEmpty(referencePath))
            {
                _cachedReference = null;
                return _cachedReference;
            }

            var avatarTransform = RuntimeUtil.FindAvatarTransformInParents(container.transform);
            if (avatarTransform == null) return (_cachedReference = null);

            if (targetObject != null && targetObject.transform.IsChildOf(avatarTransform))
                return _cachedReference = targetObject;

            if (referencePath == AVATAR_ROOT)
            {
                _cachedReference = avatarTransform.gameObject;
                return _cachedReference;
            }

            _cachedReference = avatarTransform.Find(referencePath)?.gameObject;
            if (_cachedReference == null) return null;
            
            // https://github.com/bdunderscore/modular-avatar/issues/308
            // Some avatars have multiple "Armature" objects in order to confuse VRChat into changing the avatar eye
            // position. We need to be smarter than VRChat and find the "true" armature in this case.
            var targetName = _cachedReference.name;
            var parent = _cachedReference.transform.parent;
            if (targetName == "Armature" && parent != null && _cachedReference.transform.childCount == 0)
            {
                foreach (Transform possibleTarget in parent)
                {
                    if (possibleTarget.gameObject.name == targetName && possibleTarget.childCount > 0)
                    {
                        _cachedReference = possibleTarget.gameObject;
                        break;
                    }
                }
            }

            return _cachedReference;
        }

        public void Set(GameObject target)
        {
            if (target == null)
            {
                referencePath = "";
            }
            else if (RuntimeUtil.IsAvatarRoot(target.transform))
            {
                referencePath = AVATAR_ROOT;
            }
            else
            {
                referencePath = RuntimeUtil.AvatarRootPath(target);
            }

            _cachedReference = target;
            _cacheValid = true;
            targetObject = target;
        }

        internal bool IsConsistent(GameObject avatarRoot)
        {
            if (referencePath == AVATAR_ROOT) return targetObject == avatarRoot;
            if (avatarRoot.transform.Find(referencePath)?.gameObject == targetObject)
            {
                return true;
            }

            // If multiple objects match the same path, then we accept that the reference is consistent.
            var targetObjectPath = RuntimeUtil.AvatarRootPath(targetObject);
            return targetObjectPath == referencePath;
        }
        
        protected bool Equals(AvatarObjectReference other)
        {
            return GetDirectTarget() == other.GetDirectTarget() && referencePath == other.referencePath;
        }

        private GameObject GetDirectTarget()
        {
            return targetObject != null ? targetObject : null;
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != this.GetType()) return false;
            return Equals((AvatarObjectReference) obj);
        }

        public override int GetHashCode()
        {
            return (referencePath != null ? referencePath.GetHashCode() : 0);
        }
    }
}