// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters
{
    internal abstract class SaveTempDataPropertyFilterBase : ISaveTempDataCallback
    {
        protected readonly ITempDataDictionaryFactory _tempDataFactory;

        public SaveTempDataPropertyFilterBase(ITempDataDictionaryFactory tempDataFactory)
        {
            _tempDataFactory = tempDataFactory;
        }

        /// <summary>
        /// Describes the temp data properties which exist on <see cref="Subject"/>
        /// </summary>
        public IReadOnlyList<LifecycleProperty> Properties { get; set; }

        /// <summary>
        /// The <see cref="object"/> which has the temp data properties.
        /// </summary>
        public object Subject { get; set; }

        /// <summary>
        /// Tracks the values which originally existed in temp data.
        /// </summary>
        public IDictionary<PropertyInfo, object> OriginalValues { get; } = new Dictionary<PropertyInfo, object>();

        /// <summary>
        /// Puts the modified values of <see cref="Subject"/> into <paramref name="tempData"/>.
        /// </summary>
        /// <param name="tempData">The <see cref="ITempDataDictionary"/> to be updated.</param>
        public void OnTempDataSaving(ITempDataDictionary tempData)
        {
            if (Subject == null)
            {
                return;
            }

            for (var i = 0; i < Properties.Count; i++)
            {
                var property = Properties[i];
                OriginalValues.TryGetValue(property.PropertyInfo, out var originalValue);

                var newValue = property.GetValue(Subject);
                if (newValue != null && !newValue.Equals(originalValue))
                {
                    tempData[property.Key] = newValue;
                }
            }
        }

        /// <summary>
        /// Sets the values of the properties of <see cref="Subject"/> from <paramref name="tempData"/>.
        /// </summary>
        /// <param name="tempData">The <see cref="ITempDataDictionary"/>.</param>
        protected void SetPropertyValues(ITempDataDictionary tempData)
        {
            if (Properties == null)
            {
                return;
            }

            Debug.Assert(Subject != null, "Subject must be set before this method is invoked.");

            for (var i = 0; i < Properties.Count; i++)
            {
                var property = Properties[i];
                var value = tempData[property.Key];

                OriginalValues[property.PropertyInfo] = value;
                property.SetValue(Subject, value);
            }
        }

        public static IReadOnlyList<LifecycleProperty> GetTempDataProperties(
            TempDataSerializer tempDataSerializer,
            Type type)
        {
            List<LifecycleProperty> results = null;
            var errorMessages = new List<string>();

            var propertyHelpers = PropertyHelper.GetVisibleProperties(type: type);
            for (var i = 0; i < propertyHelpers.Length; i++)
            {
                var propertyHelper = propertyHelpers[i];
                var property = propertyHelper.Property;
                var tempDataAttribute = property.GetCustomAttribute<TempDataAttribute>();
                if (tempDataAttribute != null && ValidateProperty(tempDataSerializer, errorMessages, propertyHelper.Property))
                {
                    if (results == null)
                    {
                        results = new List<LifecycleProperty>();
                    }

                    var key = tempDataAttribute.Key;
                    if (string.IsNullOrEmpty(key))
                    {
                        key = property.Name;
                    }

                    results.Add(new LifecycleProperty(property, key));
                }
            }

            if (errorMessages.Count > 0)
            {
                throw new InvalidOperationException(string.Join(Environment.NewLine, errorMessages));
            }

            return results;
        }

        private static bool ValidateProperty(TempDataSerializer tempDataSerializer, List<string> errorMessages, PropertyInfo property)
        {
            if (!(property.SetMethod != null &&
                property.SetMethod.IsPublic &&
                property.GetMethod != null &&
                property.GetMethod.IsPublic))
            {
                errorMessages.Add(
                    Resources.FormatTempDataProperties_PublicGetterSetter(property.DeclaringType.FullName, property.Name, nameof(TempDataAttribute)));

                return false;
            }

            if (!tempDataSerializer.CanSerializeType(property.PropertyType))
            {
                var errorMessage = Resources.FormatTempDataProperties_InvalidType(
                    tempDataSerializer.GetType().FullName,
                    TypeNameHelper.GetTypeDisplayName(property.DeclaringType),
                    property.Name,
                    TypeNameHelper.GetTypeDisplayName(property.PropertyType));

                errorMessages.Add(errorMessage);

                return false;
            }

            return true;
        }
    }
}
