#region License

/*
 * Copyright © 2002-2005 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#endregion

#region Imports

using System;
using System.Collections;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Serialization;

using Spring.Collections;
using Spring.Core;
using Spring.Core.TypeConversion;
using Spring.Core.TypeResolution;
using Spring.Util;
using Spring.Reflection.Dynamic;

#endregion

namespace Spring.Expressions
{
    /// <summary>
    /// Represents node that navigates to object's property or public field.
    /// </summary>
    /// <author>Aleksandar Seovic</author>
    [Serializable]
    public class PropertyOrFieldNode : BaseNode
    {
        //private static readonly Common.Logging.ILog Log = Common.Logging.LogManager.GetLogger(typeof(PropertyOrFieldNode));

        private const BindingFlags BINDING_FLAGS =
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static |
            BindingFlags.IgnoreCase;

        private string memberName;
        private IValueAccessor accessor;

        /// <summary>
        /// Create a new instance
        /// </summary>
        public PropertyOrFieldNode()
        {
        }

        /// <summary>
        /// Create a new instance from SerializationInfo
        /// </summary>
        protected PropertyOrFieldNode(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
        }

        /// <summary>
        /// Initializes the node.
        /// </summary>
        /// <param name="context">The parent.</param>
        private void InitializeNode(object context)
        {
            Type contextType = (context == null || context is Type ? context as Type : context.GetType());

            if (accessor == null || accessor.RequiresRefresh(contextType))
            {
                memberName = this.getText();

                // clear cached member info if context type has changed (for example, when ASP.NET page is recompiled)
                if (accessor != null && accessor.RequiresRefresh(contextType))
                {
                    accessor = null;
                }

                // initialize this node if necessary
                if (contextType != null && accessor == null)
                {
                    // try to initialize node as enum value first
                    if (contextType.IsEnum)
                    {
                        try
                        {
                            accessor = new EnumValueAccessor(Enum.Parse(contextType, memberName, true));
                        }
                        catch (ArgumentException)
                        {
                            // ArgumentException will be thrown if specified member name is not a valid
                            // enum value for the context type. We should just ignore it and continue processing,
                            // because the specified member could be a property of a Type class (i.e. EnumType.FullName)
                        }
                    }

                    // then try to initialize node as property or field value
                    if (accessor == null)
                    {
                        // check the context type first
                        accessor = GetPropertyOrFieldAccessor(contextType, memberName, BINDING_FLAGS);

                        // if not found, probe the Type type
                        if (accessor == null && context is Type)
                        {
                            accessor = GetPropertyOrFieldAccessor(typeof(Type), memberName, BINDING_FLAGS);
                        }
                    }
                }

                // if there is still no match, try to initialize node as type accessor
                if (accessor == null)
                {
                    try
                    {
                        accessor = new TypeValueAccessor(TypeResolutionUtils.ResolveType(memberName));
                    }
                    catch (TypeLoadException)
                    {
                        if (context == null)
                        {
                            throw new NullValueInNestedPathException("Cannot initialize property or field node '" +
                                                                     memberName +
                                                                     "' because the specified context is null.");
                        }
                        else
                        {
                            throw new InvalidPropertyException(contextType, memberName,
                                                               "'" + memberName +
                                                               "' node cannot be resolved for the specified context [" +
                                                               context + "].");
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Attempts to resolve property or field.
        /// </summary>
        /// <param name="contextType">
        /// Type to search for a property or a field.
        /// </param>
        /// <param name="memberName">
        /// Property or field name.
        /// </param>
        /// <param name="bindingFlags">
        /// Binding flags to use.
        /// </param>
        /// <returns>
        /// Resolved property or field accessor, or <c>null</c> 
        /// if specified <paramref name="memberName"/> cannot be resolved.
        /// </returns>
        private static IValueAccessor GetPropertyOrFieldAccessor(Type contextType, string memberName, BindingFlags bindingFlags)
        {
            try
            {
                PropertyInfo pi = contextType.GetProperty(memberName, bindingFlags);
                if (pi == null)
                {
                    FieldInfo fi = contextType.GetField(memberName, bindingFlags);
                    if (fi != null)
                    {
                        return new FieldValueAccessor(fi);
                    }
                }
                else
                {
                    return new PropertyValueAccessor(pi);
                }
            }
            catch (AmbiguousMatchException)
            {
                PropertyInfo pi = null;

                // search type hierarchy
                while (contextType != typeof(object))
                {
                    pi = contextType.GetProperty(memberName, bindingFlags | BindingFlags.DeclaredOnly);
                    if (pi == null)
                    {
                        FieldInfo fi = contextType.GetField(memberName, bindingFlags | BindingFlags.DeclaredOnly);
                        if (fi != null)
                        {
                            return new FieldValueAccessor(fi);
                        }
                    }
                    else
                    {
                        return new PropertyValueAccessor(pi);
                    }
                    contextType = contextType.BaseType;
                }
            }

            return null;
        }

        /// <summary>
        /// Returns node's value for the given context.
        /// </summary>
        /// <param name="context">Context to evaluate expressions against.</param>
        /// <param name="evalContext">Current expression evaluation context.</param>
        /// <returns>Node's value.</returns>
        protected override object Get(object context, EvaluationContext evalContext)
        {
            lock (this)
            {
                InitializeNode(context);

                if (context == null && accessor.RequiresContext)
                {
                    throw new NullValueInNestedPathException(
                        "Cannot retrieve the value of a field or property '" + this.memberName
                        + "', because context for its resolution is null.");
                }
                if (IsProperty || IsField)
                {
                    return GetPropertyOrFieldValue(context, evalContext);
                }
                else
                {
                    return accessor.Get(context);
                }
            }
        }

        /// <summary>
        /// Sets node's value for the given context.
        /// </summary>
        /// <param name="context">Context to evaluate expressions against.</param>
        /// <param name="evalContext">Current expression evaluation context.</param>
        /// <param name="newValue">New value for this node.</param>
        protected override void Set(object context, EvaluationContext evalContext, object newValue)
        {
            lock (this)
            {
                InitializeNode(context);

                if (context == null && accessor.RequiresContext)
                {
                    throw new NullValueInNestedPathException(
                        "Cannot set the value of a field or property '" + this.memberName
                        + "', because context for its resolution is null.");
                }
                if (IsProperty || IsField)
                {
                    SetPropertyOrFieldValue(context, evalContext, newValue);
                }
                else
                {
                    accessor.Set(context, newValue);
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether this node represents a property.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this node is a property; otherwise, <c>false</c>.
        /// </value>
        private bool IsProperty
        {
            get { return accessor is PropertyValueAccessor; }
        }

        /// <summary>
        /// Gets a value indicating whether this node represents a field.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this node is a field; otherwise, <c>false</c>.
        /// </value>
        private bool IsField
        {
            get { return accessor is FieldValueAccessor; }
        }

        /// <summary>
        /// Retrieves property or field value.
        /// </summary>
        /// <param name="context">Context to evaluate expressions against.</param>
        /// <param name="evalContext">Current expression evaluation context.</param>
        /// <returns>Property or field value.</returns>
        private object GetPropertyOrFieldValue(object context, EvaluationContext evalContext)
        {
            try
            {
                return accessor.Get(context);
            }
            catch (InvalidOperationException)
            {
                throw new NotReadablePropertyException(evalContext.RootContextType, this.memberName);
            }
            catch (TargetInvocationException e)
            {
                throw new InvalidPropertyException(evalContext.RootContextType, this.memberName,
                                                   "Getter for property '" + this.memberName + "' threw an exception.",
                                                   e);
            }
            catch (UnauthorizedAccessException e)
            {
                throw new InvalidPropertyException(evalContext.RootContextType, this.memberName,
                                                   "Illegal attempt to get value for the property '" + this.memberName +
                                                   "'.", e);
            }
        }

        /// <summary>
        /// Sets property value, doing any type conversions that are necessary along the way.
        /// </summary>
        /// <param name="context">Context to evaluate expressions against.</param>
        /// <param name="evalContext">Current expression evaluation context.</param>
        /// <param name="newValue">New value for this node.</param>
        private void SetPropertyOrFieldValue(object context, EvaluationContext evalContext, object newValue)
        {
            bool isWriteable = accessor.IsWriteable;
            Type targetType = accessor.TargetType;

            try
            {
                if (!isWriteable)
                {
                    if (!AddToCollections(context, evalContext, newValue))
                    {
                        throw new NotWritablePropertyException(
                            "Can't change the value of the read-only property or field '" + this.memberName + "'.");
                    }
                }
                else if (targetType.IsPrimitive && (newValue == null || String.Empty.Equals(newValue)))
                {
                    throw new ArgumentException("Invalid value [" + newValue + "] for property or field '" +
                                                this.memberName + "' of primitive type ["
                                                + targetType + "]");
                }
                else if (newValue == null || ObjectUtils.IsAssignable(targetType, newValue)) // targetType.IsAssignableFrom(newValue.GetType())
                {
                    SetPropertyOrFieldValueInternal(context, newValue);
                }
                else if (!RemotingServices.IsTransparentProxy(newValue) &&
                         (newValue is IList || newValue is IDictionary || newValue is ISet))
                {
                    if (!AddToCollections(context, evalContext, newValue))
                    {
                        object tmpValue =
                            TypeConversionUtils.ConvertValueIfNecessary(targetType, newValue, this.memberName);
                        SetPropertyOrFieldValueInternal(context, tmpValue);
                    }
                }
                else
                {
                    object tmpValue = TypeConversionUtils.ConvertValueIfNecessary(targetType, newValue, this.memberName);
                    SetPropertyOrFieldValueInternal(context, tmpValue);
                }
            }
            catch (TargetInvocationException ex)
            {
                PropertyChangeEventArgs propertyChangeEvent =
                    new PropertyChangeEventArgs(this.memberName, null, newValue);
                if (ex.GetBaseException() is InvalidCastException)
                {
                    throw new TypeMismatchException(propertyChangeEvent, targetType, ex.GetBaseException());
                }
                else
                {
                    throw new MethodInvocationException(ex.GetBaseException(), propertyChangeEvent);
                }
            }
            catch (UnauthorizedAccessException ex)
            {
                throw new FatalReflectionException("Illegal attempt to set property '" + this.memberName + "'", ex);
            }
            catch (NotWritablePropertyException)
            {
                throw;
            }
            catch (NotReadablePropertyException)
            {
                throw;
            }
            catch (ArgumentException ex)
            {
                PropertyChangeEventArgs propertyChangeEvent =
                    new PropertyChangeEventArgs(this.memberName, null, newValue);
                throw new TypeMismatchException(propertyChangeEvent, targetType, ex);
            }
        }

        /// <summary>
        /// Sets property or field value using either dynamic or standard reflection.
        /// </summary>
        /// <param name="context">Object to evaluate node against.</param>
        /// <param name="newValue">New value for this node, converted to appropriate type.</param>
        private void SetPropertyOrFieldValueInternal(object context, object newValue)
        {
            accessor.Set(context, newValue);
        }

        /// <summary>
        /// In the case of read only collections or custom collections that are not assignable from
        /// IList, try to add to the collection.
        /// </summary>
        /// <param name="context">Context to evaluate expressions against.</param>
        /// <param name="evalContext">Current expression evaluation context.</param>
        /// <param name="newValue">New value for this node.</param>
        /// <returns>true if was able add to IList, IDictionary, or ISet</returns>
        private bool AddToCollections(object context, EvaluationContext evalContext, object newValue)
        {
            // short-circuit if accessor is not readable
            if (!this.accessor.IsReadable)
            {
                return false;
            }

            bool added = false;

            // try adding values if property is a list...
            if (newValue is IList && !RemotingServices.IsTransparentProxy(newValue))
            {
                IList currentValue = (IList)Get(context, evalContext);
                if (currentValue != null && !currentValue.IsFixedSize && !currentValue.IsReadOnly)
                {
                    foreach (object el in (IList)newValue)
                    {
                        currentValue.Add(el);
                    }
                    added = true;
                }
            }
            // try adding values if property is a dictionary...
            else if (newValue is IDictionary && !RemotingServices.IsTransparentProxy(newValue))
            {
                IDictionary currentValue = (IDictionary)Get(context, evalContext);
                if (currentValue != null && !currentValue.IsFixedSize && !currentValue.IsReadOnly)
                {
                    foreach (DictionaryEntry entry in (IDictionary)newValue)
                    {
                        currentValue[entry.Key] = entry.Value;
                    }
                    added = true;
                }
            }
            // try adding values if property is a set...
            else if (newValue is ISet && !RemotingServices.IsTransparentProxy(newValue))
            {
                ISet currentValue = (ISet)Get(context, evalContext);
                if (currentValue != null)
                {
                    currentValue.AddAll((ICollection)newValue);
                    added = true;
                }
            }
            return added;
        }

        /// <summary>
        /// Utility method that is needed by ObjectWrapper and AbstractAutowireCapableObjectFactory.
        /// We try as hard as we can, but there are instances when we won't be able to obtain PropertyInfo...
        /// </summary>
        /// <param name="context">Context to resolve property against.</param>
        /// <returns>PropertyInfo for this node.</returns>
        internal MemberInfo GetMemberInfo(object context)
        {
            lock (this)
            {
                InitializeNode(context);
            }
            return accessor.MemberInfo;

            //if (IsProperty)
            //{
            //    return (((PropertyValueAccessor) accessor).MemberInfo);
            //}
            //else
            //{
            //    throw new FatalObjectException(
            //        "Cannot obtain PropertyInfo from an expression that does not resolve to a property.");
            //}
        }

        #region IValueAccessor interface

        private interface IValueAccessor
        {
            object Get(object context);
            void Set(object context, object value);

            bool IsReadable { get; }
            bool IsWriteable { get; }
            bool RequiresContext { get; }
            Type TargetType { get; }
            MemberInfo MemberInfo { get; }
            bool RequiresRefresh(Type contextType);
        }

        #endregion

        #region BaseValueAccessor implementation

        private abstract class BaseValueAccessor : IValueAccessor
        {
            public abstract object Get(object context);

            public abstract void Set(object context, object value);

            public virtual bool IsReadable
            {
                get { return true; }
            }

            public virtual bool IsWriteable
            {
                get { return false; }
            }

            public virtual bool RequiresContext
            {
                get { return false; }
            }

            public virtual Type TargetType
            {
                get { throw new NotSupportedException(); }
            }

            public virtual MemberInfo MemberInfo
            {
                get { throw new NotSupportedException(); }
            }

            public virtual bool RequiresRefresh(Type contextType)
            {
                return false;
            }
        }

        #endregion

        #region PropertyValueAccessor implementation

        private class PropertyValueAccessor : BaseValueAccessor
        {
            private SafeProperty property;
            private string name;
            private bool isReadable;
            private bool isWriteable;
            private Type targetType;
            private Type contextType;

            public PropertyValueAccessor(PropertyInfo propertyInfo)
            {
                this.name = propertyInfo.Name;
                this.isReadable = propertyInfo.CanRead;
                this.isWriteable = propertyInfo.CanWrite;
                this.targetType = propertyInfo.PropertyType;
                this.contextType = propertyInfo.DeclaringType;
                this.property = new SafeProperty(propertyInfo);
            }

            public override object Get(object context)
            {
                if (!isReadable)
                {
                    throw new NotReadablePropertyException("Cannot get a non-readable property [" + name + "]");
                }
                return property.GetValue(context);
            }

            public override void Set(object context, object value)
            {
                if (!isWriteable)
                {
                    throw new NotWritablePropertyException("Cannot set a read-only property [" + name + "]");
                }
                property.SetValue(context, value);
            }

            public override bool IsReadable
            {
                get { return isReadable; }
            }

            public override bool IsWriteable
            {
                get { return isWriteable; }
            }

            public override bool RequiresContext
            {
                get { return true; }
            }

            public override Type TargetType
            {
                get { return targetType; }
            }

            public override MemberInfo MemberInfo
            {
                get { return property.PropertyInfo; }
            }

            public override bool RequiresRefresh(Type contextType)
            {
                return this.contextType != contextType;
            }
        }

        #endregion

        #region FieldValueAccessor implementation

        private class FieldValueAccessor : BaseValueAccessor
        {
            private SafeField field;
            private bool isWriteable;
            private Type targetType;
            private Type contextType;

            public FieldValueAccessor(FieldInfo fieldInfo)
            {
                this.field = new SafeField(fieldInfo);
                this.isWriteable = !(fieldInfo.IsInitOnly || fieldInfo.IsLiteral);
                this.targetType = fieldInfo.FieldType;
                this.contextType = fieldInfo.DeclaringType;
            }

            public override object Get(object context)
            {
                return field.GetValue(context);
            }

            public override void Set(object context, object value)
            {
                field.SetValue(context, value);
            }

            public override bool IsWriteable
            {
                get { return isWriteable; }
            }

            public override bool RequiresContext
            {
                get { return true; }
            }

            public override Type TargetType
            {
                get { return targetType; }
            }

            public override MemberInfo MemberInfo
            {
                get { return field.FieldInfo; }
            }

            public override bool RequiresRefresh(Type contextType)
            {
                return this.contextType != contextType;
            }
        }

        #endregion

        #region EnumValueAccessor implementation

        private class EnumValueAccessor : BaseValueAccessor
        {
            private object enumValue;

            public EnumValueAccessor(object enumValue)
            {
                this.enumValue = enumValue;
            }

            public override object Get(object context)
            {
                return enumValue;
            }

            public override void Set(object context, object value)
            {
                throw new NotSupportedException("Cannot set the value of an enum.");
            }
        }

        #endregion

        #region TypeValueAccessor implementation

        private class TypeValueAccessor : BaseValueAccessor
        {
            private Type type;

            public TypeValueAccessor(Type type)
            {
                this.type = type;
            }

            public override object Get(object context)
            {
                return type;
            }

            public override void Set(object context, object value)
            {
                throw new NotSupportedException("Cannot set the value of a type.");
            }
        }

        #endregion
    }
}