//===============================================================================
// Microsoft patterns & practices Enterprise Library
// Exception Handling Application Block
//===============================================================================
// Copyright © Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================

using System;
using System.Collections.Specialized;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using System.Reflection;
using System.Globalization;
using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WCF.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Common.Utility;

namespace Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WCF
{
	/// <summary>
	/// Exception handler that gets the configured fault contract type and 
	/// wraps it inside a <see cref="FaultContractWrapperException"/>. exception.
	/// </summary>
	[ConfigurationElementType(typeof(FaultContractExceptionHandlerData))]
	public class FaultContractExceptionHandler : IExceptionHandler
	{
		private NameValueCollection attributes;
		private Type faultContractType;
		private string exceptionMessage;
        private readonly IStringResolver exceptionMessageResolver;

		/// <summary>
		/// Initializes a new instance of the <see cref="FaultContractExceptionHandler"/> class.
		/// </summary>
		/// <param name="faultContractType">Type of the fault contract.</param>
		/// <param name="attributes">The attributes.</param>
		public FaultContractExceptionHandler(Type faultContractType, NameValueCollection attributes)
			: this(faultContractType, null, attributes)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="FaultContractExceptionHandler"/> class.
		/// </summary>
		/// <param name="faultContractType">Type of the fault contract.</param>
		/// <param name="exceptionMessage">A fixed exception message that will replace the fault contract message.</param>
		/// <param name="attributes">A collection of nam value paries that specify the mapping
		/// between the properties of the FaultContract class and the values in the <see cref="Exception"/>
		/// instance. You can specify something like: FaultPropertyName = "{Message}" where {Message}
		/// points to the Message property value of the current exception. You can also specify a
		/// {Guid} value that will load the current exception handlingInstanceId value.
		/// See <see cref="HandleException"/>. NOTICE that names and values are case sensitive.</param>
		/// <exception cref="ArgumentNullException"/>
		public FaultContractExceptionHandler(Type faultContractType, string exceptionMessage, NameValueCollection attributes) :
            this(new ConstantStringResolver(exceptionMessage), faultContractType, attributes)
		{
			if (faultContractType == null)
			{
				throw new ArgumentNullException("faultContractType");
			}
			this.faultContractType = faultContractType;
			this.attributes = attributes;
			this.exceptionMessage = exceptionMessage;
		}

        /// <summary>
        /// Initializes a new instance of the <see cref="FaultContractExceptionHandler"/> class.
        /// </summary>
        /// <param name="exceptionMessageResolver">The exception message resolver.</param>
        /// <param name="faultContractType">Type of the fault contract.</param>
        /// <param name="attributes">A collection of nam value paries that specify the mapping
        /// between the properties of the FaultContract class and the values in the <see cref="Exception"/>
        /// instance. You can specify something like: FaultPropertyName = "{Message}" where {Message}
        /// points to the Message property value of the current exception. You can also specify a
        /// {Guid} value that will load the current exception handlingInstanceId value.
        /// See <see cref="HandleException"/>. NOTICE that names and values are case sensitive.</param>
        /// <exception cref="ArgumentNullException"/>
        public FaultContractExceptionHandler(IStringResolver exceptionMessageResolver, Type faultContractType, NameValueCollection attributes)
        {
            if (faultContractType == null)
            {
                throw new ArgumentNullException("faultContractType");
            }

            this.faultContractType = faultContractType;
            this.attributes = attributes;
            this.exceptionMessageResolver = exceptionMessageResolver;
            this.exceptionMessage = this.exceptionMessageResolver.GetString();
        }

		/// <summary>
		/// Handles the exception.
		/// </summary>
		/// <param name="exception">The exception.</param>
		/// <param name="handlingInstanceId">The handling instance id. This value may be injected into the
		/// current FaultContract by spefifying the attribute value "{Guid}".</param>
		/// <returns>An instance of <see cref="FaultContractWrapperException"/> class.</returns>
		public Exception HandleException(Exception exception, Guid handlingInstanceId)
		{
			EnsureDefaultConstructor();
			object fault = Activator.CreateInstance(faultContractType);
			PopulateFaultContractFromException(fault, exception, handlingInstanceId);
			return new FaultContractWrapperException(
				fault, handlingInstanceId, ExceptionUtility.GetMessage(exception, exceptionMessage, handlingInstanceId));
		}

		private void EnsureDefaultConstructor()
		{
			if (faultContractType.GetConstructor(Type.EmptyTypes) == null)
			{
				throw new MissingMethodException(
					string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoDefaultParameterInFaultContract, faultContractType.FullName));
			}
		}

		private void PopulateFaultContractFromException(object fault, Exception exception, Guid handlingInstanceId)
		{
			if (this.attributes != null)
			{
				foreach (PropertyInfo property in this.faultContractType.GetProperties())
				{
					if (PropertyIsMappedInAttributes(property))
					{
						// Try to map with  a configured property first
						string configProperty = this.attributes[property.Name];
						if (!string.IsNullOrEmpty(configProperty))
						{
							// strip out the {}
							configProperty = configProperty.Replace("{", "").Replace("}", "");

							if (PropertyIsGuid(property, configProperty))
							{
								property.SetValue(fault, handlingInstanceId, null);
							}
							else
							{
								PropertyInfo mappedExceptionProperty = GetMappedProperty(exception, configProperty);
								if (PropertiesMatch(property, mappedExceptionProperty))
								{
									// by now it's certain that properties have matching types
									// and values can be both read and written as required
									property.SetValue(fault,
										mappedExceptionProperty.GetValue(exception, null),
										null);
								}
							}
						}
					}
					else
					{
						if (PropertyNamesMatch(property, exception))
						{
							// by now it's certain that properties have matching types
							// and values can be both read and written as required
							property.SetValue(fault,
								GetExceptionProperty(property, exception).GetValue(exception, null),
								null);
						}
					}
				}
			}
		}

		private bool PropertyIsGuid(PropertyInfo property, string configPropertyName)
		{
			return
				configPropertyName.Equals(
					ExceptionShielding.HandlingInstanceIdPropertyMappingName,
					StringComparison.InvariantCultureIgnoreCase)
				&& property.PropertyType == typeof(Guid);
		}

		private bool PropertiesMatch(PropertyInfo faultProperty, PropertyInfo exceptionProperty)
		{
			// matching only occurs if properties have the same type, are not indexers,
			// and the fault contract property can be written while the exception property can be read
			return exceptionProperty != null
				&& exceptionProperty.PropertyType == faultProperty.PropertyType
				&& faultProperty.CanWrite
				&& faultProperty.GetIndexParameters().Length == 0
				&& exceptionProperty.CanRead
				&& exceptionProperty.GetIndexParameters().Length == 0;
		}

		private bool PropertyNamesMatch(PropertyInfo faultProperty, Exception exception)
		{
			PropertyInfo exceptionProperty = GetExceptionProperty(faultProperty, exception);

			return PropertiesMatch(faultProperty, exceptionProperty);
		}

		private bool PropertyIsMappedInAttributes(PropertyInfo property)
		{
			return Array.Exists(this.attributes.AllKeys,
				delegate(string s) { return s == property.Name; });
		}

		private PropertyInfo GetMappedProperty(Exception exception, string configProperty)
		{
			return exception.GetType().GetProperty(configProperty);
		}

		private PropertyInfo GetExceptionProperty(PropertyInfo property, Exception exception)
		{
			return exception.GetType().GetProperty(property.Name);
		}
	}
}
