// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Web.Mvc.Properties;

namespace System.Web.Mvc
{
    // TODO: Unit test ModelState interaction with VDD

    public class ViewDataDictionary : IDictionary<string, object>
    {
        private readonly IDictionary<string, object> _innerDictionary;
        private readonly ModelStateDictionary _modelState;
        private object _model;
        private ModelMetadata _modelMetadata;
        private TemplateInfo _templateMetadata;

        public ViewDataDictionary()
            : this((object)null)
        {
        }

        [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "See note on SetModel() method.")]
        public ViewDataDictionary(object model)
        {
            Model = model;
            _innerDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            _modelState = new ModelStateDictionary();
        }

        [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "See note on SetModel() method.")]
        public ViewDataDictionary(ViewDataDictionary dictionary)
        {
            if (dictionary == null)
            {
                throw new ArgumentNullException("dictionary");
            }

            _innerDictionary = new CopyOnWriteDictionary<string, object>(dictionary, StringComparer.OrdinalIgnoreCase);
            _modelState = new ModelStateDictionary(dictionary.ModelState);

            Model = dictionary.Model;
            TemplateInfo = dictionary.TemplateInfo;

            // PERF: Don't unnecessarily instantiate the model metadata
            _modelMetadata = dictionary._modelMetadata;
        }

        public int Count
        {
            get { return _innerDictionary.Count; }
        }

        public bool IsReadOnly
        {
            get { return _innerDictionary.IsReadOnly; }
        }

        public ICollection<string> Keys
        {
            get { return _innerDictionary.Keys; }
        }

        public object Model
        {
            get { return _model; }
            set
            {
                _modelMetadata = null;
                SetModel(value);
            }
        }

        public virtual ModelMetadata ModelMetadata
        {
            get
            {
                if (_modelMetadata == null && _model != null)
                {
                    _modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => _model, _model.GetType());
                }
                return _modelMetadata;
            }
            set { _modelMetadata = value; }
        }

        public ModelStateDictionary ModelState
        {
            get { return _modelState; }
        }

        public TemplateInfo TemplateInfo
        {
            get
            {
                if (_templateMetadata == null)
                {
                    _templateMetadata = new TemplateInfo();
                }
                return _templateMetadata;
            }
            set { _templateMetadata = value; }
        }

        public ICollection<object> Values
        {
            get { return _innerDictionary.Values; }
        }

        public object this[string key]
        {
            get
            {
                object value;
                _innerDictionary.TryGetValue(key, out value);
                return value;
            }
            set { _innerDictionary[key] = value; }
        }

        // For unit testing
        internal IDictionary<string, object> InnerDictionary
        {
            get { return _innerDictionary; }
        }

        public void Add(KeyValuePair<string, object> item)
        {
            _innerDictionary.Add(item);
        }

        public void Add(string key, object value)
        {
            _innerDictionary.Add(key, value);
        }

        public void Clear()
        {
            _innerDictionary.Clear();
        }

        public bool Contains(KeyValuePair<string, object> item)
        {
            return _innerDictionary.Contains(item);
        }

        public bool ContainsKey(string key)
        {
            return _innerDictionary.ContainsKey(key);
        }

        public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
        {
            _innerDictionary.CopyTo(array, arrayIndex);
        }

        public object Eval(string expression)
        {
            ViewDataInfo info = GetViewDataInfo(expression);
            return (info != null) ? info.Value : null;
        }

        public string Eval(string expression, string format)
        {
            object value = Eval(expression);
            return FormatValueInternal(value, format);
        }

        internal static string FormatValueInternal(object value, string format)
        {
            if (value == null)
            {
                return String.Empty;
            }

            if (String.IsNullOrEmpty(format))
            {
                return Convert.ToString(value, CultureInfo.CurrentCulture);
            }
            else
            {
                return String.Format(CultureInfo.CurrentCulture, format, value);
            }
        }

        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            return _innerDictionary.GetEnumerator();
        }

        public ViewDataInfo GetViewDataInfo(string expression)
        {
            if (String.IsNullOrEmpty(expression))
            {
                throw new ArgumentException(MvcResources.Common_NullOrEmpty, "expression");
            }

            return ViewDataEvaluator.Eval(this, expression);
        }

        public bool Remove(KeyValuePair<string, object> item)
        {
            return _innerDictionary.Remove(item);
        }

        public bool Remove(string key)
        {
            return _innerDictionary.Remove(key);
        }

        // This method will execute before the derived type's instance constructor executes. Derived types must
        // be aware of this and should plan accordingly. For example, the logic in SetModel() should be simple
        // enough so as not to depend on the "this" pointer referencing a fully constructed object.
        protected virtual void SetModel(object value)
        {
            _model = value;
        }

        public bool TryGetValue(string key, out object value)
        {
            return _innerDictionary.TryGetValue(key, out value);
        }

        internal static class ViewDataEvaluator
        {
            public static ViewDataInfo Eval(ViewDataDictionary vdd, string expression)
            {
                //Given an expression "foo.bar.baz" we look up the following (pseudocode):
                //  this["foo.bar.baz.quux"]
                //  this["foo.bar.baz"]["quux"]
                //  this["foo.bar"]["baz.quux]
                //  this["foo.bar"]["baz"]["quux"]
                //  this["foo"]["bar.baz.quux"]
                //  this["foo"]["bar.baz"]["quux"]
                //  this["foo"]["bar"]["baz.quux"]
                //  this["foo"]["bar"]["baz"]["quux"]

                ViewDataInfo evaluated = EvalComplexExpression(vdd, expression);
                return evaluated;
            }

            private static ViewDataInfo EvalComplexExpression(object indexableObject, string expression)
            {
                foreach (ExpressionPair expressionPair in GetRightToLeftExpressions(expression))
                {
                    string subExpression = expressionPair.Left;
                    string postExpression = expressionPair.Right;

                    ViewDataInfo subTargetInfo = GetPropertyValue(indexableObject, subExpression);
                    if (subTargetInfo != null)
                    {
                        if (String.IsNullOrEmpty(postExpression))
                        {
                            return subTargetInfo;
                        }

                        if (subTargetInfo.Value != null)
                        {
                            ViewDataInfo potential = EvalComplexExpression(subTargetInfo.Value, postExpression);
                            if (potential != null)
                            {
                                return potential;
                            }
                        }
                    }
                }
                return null;
            }

            private static IEnumerable<ExpressionPair> GetRightToLeftExpressions(string expression)
            {
                // Produces an enumeration of all the combinations of complex property names
                // given a complex expression. See the list above for an example of the result
                // of the enumeration.

                yield return new ExpressionPair(expression, String.Empty);

                int lastDot = expression.LastIndexOf('.');

                string subExpression = expression;
                string postExpression = String.Empty;

                while (lastDot > -1)
                {
                    subExpression = expression.Substring(0, lastDot);
                    postExpression = expression.Substring(lastDot + 1);
                    yield return new ExpressionPair(subExpression, postExpression);

                    lastDot = subExpression.LastIndexOf('.');
                }
            }

            private static ViewDataInfo GetIndexedPropertyValue(object indexableObject, string key)
            {
                IDictionary<string, object> dict = indexableObject as IDictionary<string, object>;
                object value = null;
                bool success = false;

                if (dict != null)
                {
                    success = dict.TryGetValue(key, out value);
                }
                else
                {
                    TryGetValueDelegate tgvDel = TypeHelpers.CreateTryGetValueDelegate(indexableObject.GetType());
                    if (tgvDel != null)
                    {
                        success = tgvDel(indexableObject, key, out value);
                    }
                }

                if (success)
                {
                    return new ViewDataInfo()
                    {
                        Container = indexableObject,
                        Value = value
                    };
                }

                return null;
            }

            private static ViewDataInfo GetPropertyValue(object container, string propertyName)
            {
                // This method handles one "segment" of a complex property expression

                // First, we try to evaluate the property based on its indexer
                ViewDataInfo value = GetIndexedPropertyValue(container, propertyName);
                if (value != null)
                {
                    return value;
                }

                // If the indexer didn't return anything useful, continue...

                // If the container is a ViewDataDictionary then treat its Model property
                // as the container instead of the ViewDataDictionary itself.
                ViewDataDictionary vdd = container as ViewDataDictionary;
                if (vdd != null)
                {
                    container = vdd.Model;
                }

                // If the container is null, we're out of options
                if (container == null)
                {
                    return null;
                }

                // Second, we try to use PropertyDescriptors and treat the expression as a property name
                PropertyDescriptor descriptor = TypeDescriptor.GetProperties(container).Find(propertyName, true);
                if (descriptor == null)
                {
                    return null;
                }

                return new ViewDataInfo(() => descriptor.GetValue(container))
                {
                    Container = container,
                    PropertyDescriptor = descriptor
                };
            }

            private struct ExpressionPair
            {
                public readonly string Left;
                public readonly string Right;

                public ExpressionPair(string left, string right)
                {
                    Left = left;
                    Right = right;
                }
            }
        }

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _innerDictionary.GetEnumerator();
        }

        #endregion
    }
}
