using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEngine;

namespace UnityTest
{
    public class MemberResolver
    {
        private object m_CallingObjectRef;
        private MemberInfo[] m_Callstack;
        private readonly GameObject m_GameObject;
        private readonly string m_Path;

        public MemberResolver(GameObject gameObject, string path)
        {
            path = path.Trim();
            ValidatePath(path);

            m_GameObject = gameObject;
            m_Path = path.Trim();
        }

        public object GetValue(bool useCache)
        {
            if (useCache && m_CallingObjectRef != null)
            {
                object val = m_CallingObjectRef;
                for (int i = 0; i < m_Callstack.Length; i++)
                    val = GetValueFromMember(val, m_Callstack[i]);
                return val;
            }

            object result = GetBaseObject();
            var fullCallStack = GetCallstack();

            m_CallingObjectRef = result;
            var tempCallstack = new List<MemberInfo>();
            for (int i = 0; i < fullCallStack.Length; i++)
            {
                var member = fullCallStack[i];
                result = GetValueFromMember(result, member);
                tempCallstack.Add(member);
                if (result == null) return null;
                var type = result.GetType();

                //String is not a value type but we don't want to cache it
                if (!IsValueType(type) && type != typeof(System.String))
                {
                    tempCallstack.Clear();
                    m_CallingObjectRef = result;
                }
            }
            m_Callstack = tempCallstack.ToArray();
            return result;
        }

        public Type GetMemberType()
        {
            var callstack = GetCallstack();
            if (callstack.Length == 0) return GetBaseObject().GetType();

            var member = callstack[callstack.Length - 1];
            if (member is FieldInfo)
                return (member as FieldInfo).FieldType;
            if (member is MethodInfo)
                return (member as MethodInfo).ReturnType;
            return null;
        }

        #region Static wrappers
        public static bool TryGetMemberType(GameObject gameObject, string path, out Type value)
        {
            try
            {
                var mr = new MemberResolver(gameObject, path);
                value = mr.GetMemberType();
                return true;
            }
            catch (InvalidPathException)
            {
                value = null;
                return false;
            }
        }

        public static bool TryGetValue(GameObject gameObject, string path, out object value)
        {
            try
            {
                var mr = new MemberResolver(gameObject, path);
                value = mr.GetValue(false);
                return true;
            }
            catch (InvalidPathException)
            {
                value = null;
                return false;
            }
        }
        #endregion

        private object GetValueFromMember(object obj, MemberInfo memberInfo)
        {
            if (memberInfo is FieldInfo)
                return (memberInfo as FieldInfo).GetValue(obj);
            if (memberInfo is MethodInfo)
                return (memberInfo as MethodInfo).Invoke(obj, null);
            throw new InvalidPathException(memberInfo.Name);
        }

        private object GetBaseObject()
        {
            if (string.IsNullOrEmpty(m_Path)) return m_GameObject;
            var firstElement = m_Path.Split('.')[0];
            var comp = m_GameObject.GetComponent(firstElement);
            if (comp != null)
                return comp;
            return m_GameObject;
        }

        private MemberInfo[] GetCallstack()
        {
            if (m_Path == "") return new MemberInfo[0];
            var propsQueue = new Queue<string>(m_Path.Split('.'));

            Type type = GetBaseObject().GetType();
            if (type != typeof(GameObject))
                propsQueue.Dequeue();

            PropertyInfo propertyTemp;
            FieldInfo fieldTemp;
            var list = new List<MemberInfo>();
            while (propsQueue.Count != 0)
            {
                var nameToFind = propsQueue.Dequeue();
                fieldTemp = GetField(type, nameToFind);
                if (fieldTemp != null)
                {
                    type = fieldTemp.FieldType;
                    list.Add(fieldTemp);
                    continue;
                }
                propertyTemp = GetProperty(type, nameToFind);
                if (propertyTemp != null)
                {
                    type = propertyTemp.PropertyType;
                    var getMethod = GetGetMethod(propertyTemp);
                    list.Add(getMethod);
                    continue;
                }
                throw new InvalidPathException(nameToFind);
            }
            return list.ToArray();
        }

        private void ValidatePath(string path)
        {
            bool invalid = false;
            if (path.StartsWith(".") || path.EndsWith("."))
                invalid = true;
            if (path.IndexOf("..") >= 0)
                invalid = true;
            if (Regex.IsMatch(path, @"\s"))
                invalid = true;

            if (invalid)
                throw new InvalidPathException(path);
        }

        private static bool IsValueType(Type type)
        {
            #if !UNITY_METRO
            return type.IsValueType;
            #else
            return false;
            #endif
        }

        private static FieldInfo GetField(Type type, string fieldName)
        {
            #if !UNITY_METRO
            return type.GetField(fieldName);
            #else
            return null;
            #endif
        }

        private static PropertyInfo GetProperty(Type type, string propertyName)
        {
            #if !UNITY_METRO
            return type.GetProperty(propertyName);
            #else
            return null;
            #endif
        }

        private static MethodInfo GetGetMethod(PropertyInfo propertyInfo)
        {
            #if !UNITY_METRO
            return propertyInfo.GetGetMethod();
            #else
            return null;
            #endif
        }
    }
}