using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Reflection;
using System.Text;
using System.Xml;
using AWSPowerShellGenerator.ServiceConfig;
using AWSPowerShellGenerator.Generators;
using AWSPowerShellGenerator.Utils;
using System.Text.RegularExpressions;
using Pluralize.NET.Core;

namespace AWSPowerShellGenerator.Analysis
{
    /// <summary>
    /// Analyzes a service operation during translation to a cmdlet to determine
    /// the parameter names and other cmdlet information that should be emitted.
    /// </summary>
    /// <remarks>
    /// The analyzer determines the set of properties on the operation's request that 
    /// should be emitted as cmdlet parameters (together with applying naming rules so 
    /// that the cmdlet conforms to PowerShell naming guidelines) and whether to apply 
    /// the SupportsShouldProcess cmdlet attribute and processing to allow the -WhatIf, 
    /// -Confirm and -Force parameters.The inspection performs name flattening (for 
    /// nested request properties), shortening (to avoid excessively long flattened 
    /// names) and last-word-fragment singularization (to follow community convention). 
    /// Note that if the service operation contains parameter customization, this turns 
    /// off the automatic analysis for parameter renaming. The configuration can also
    /// bypass the automatic analysis for the SupportsShouldProcess attribute.
    /// </remarks>
    public class OperationAnalyzer
    {
        //Parameter and alias names cannot collide with the following
        private static readonly HashSet<string> ReservedParameterNames = new HashSet<string>(new string[] {
                //From AWSRegionArguments and AWSCommonArguments
                "AK", "SK", "ST",
                "Region", "RegionToCall", "ProfileLocation", "AWSProfilesLocation", "ProfilesLocation", "AccessKey", "SecretKey", "SecretAccessKey", "SessionToken",
                "ProfileName", "StoredCredentials", "AWSProfileName", "ProfileLocation", "Credential", "NetworkCredential",
                //From AnonymousServiceCmdlet
                "EndpointUrl",
                //Common Powershell parameters
                "PassThru", "Force",
                //https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters
                "Debug", "db", "ErrorAction", "ea", "ErrorVariable", "ev", "InformationAction", "infa", "InformationVariable", "iv", "OutVariable", "ov", "OutBuffer", "ob",
                "PipelineVariable", "pv", "Verbose", "vb", "WarningAction", "wa", "WarningVariable", "wv", "WhatIf", "wi", "Confirm", "cf",
                //Custom parameters added to every service cmdlet
                "Select"},
            StringComparer.OrdinalIgnoreCase);

        #region Construction-time properties

        public ConfigModelCollection AllModels { get; set; }
        public ConfigModel CurrentModel { get; set; }
        public ServiceOperation CurrentOperation { get; set; }
        public XmlDocument AssemblyDocumentation { get; set; }

        #endregion

        #region Analysis result properties

        /// <summary>
        /// The set of properties to be emitted as cmdlet parameters. Members of complex
        /// properties are flattened to individual parameters.
        /// </summary>
        public List<SimplePropertyInfo> AnalyzedParameters { get; private set; }

        /// <summary>
        /// The set of actual properties the cmdlet needs to deal with to populate
        /// a service request. Complex types are not flattened here.
        /// </summary>
        public List<SimplePropertyInfo> RequestProperties { get; private set; }

        /// <summary>
        /// Contains the context property names of parameters that are MemoryStream-based 
        /// in the underlying request. These parameters will instead be output as byte[]
        /// types and the requirement to load into a memory stream for the underlying
        /// sdk will be done by the cmdlet during execution.
        /// </summary>
        public HashSet<SimplePropertyInfo> StreamParameters { get; } = new HashSet<SimplePropertyInfo>();

        public HashSet<string> GetAllParameterAliases(SimplePropertyInfo property)
        {
            var aliases = new HashSet<string>();

            // global aliasing; mainly for usability
            if (CurrentModel.CustomParameters.ContainsKey(property.AnalyzedName))
            {
                var globalAliases = CurrentModel.CustomParameters[property.AnalyzedName].Aliases;
                foreach (var a in globalAliases)
                {
                    aliases.Add(a);
                }
            }

            // operation-specific aliasing; mainly used to maintain compat between sdk versions
            // and backwards compat due to auto-name shortening/singularization
            if (property.Customization != null)
            {
                // these aliases come from config entries on a ServiceOperation in the 
                // config or are applied automatically during name inspection
                var propAliases = property.Customization.Aliases;
                foreach (var a in propAliases)
                {
                    aliases.Add(a);
                }

                if (property.Customization.AutoApplyAlias && property.CmdletParameterName != property.AnalyzedName)
                {
                    aliases.Add(property.AnalyzedName);
                }
            }

            // apply a cross-service alias if it's an iteration parameter?
            var autoIteration = AutoIterateSettings;
            if (autoIteration != null)
            {
                var iterAlias = autoIteration.GetIterationParameterAlias(property.AnalyzedName);
                if (!string.IsNullOrEmpty(iterAlias))
                    aliases.Add(iterAlias);
            }

            return aliases;
        }

        /// <summary>
        /// Returns any autoiteration settings that apply, as a combination
        /// of settings defined at the global service level, overridden at the 
        /// operation level if needed.
        /// </summary>
        public AutoIteration AutoIterateSettings
        {
            get
            {
                var autoIteration = AutoIteration.Combine(CurrentModel.AutoIterate, CurrentOperation.AutoIterate);

                //If autoiteration has configured field names for at least Start (input parameter idicating the pagination token) and Next
                //(output value idicating the next pagination token) and the Start parameter is actually present in the input type
                //and the Next value is present in the returned type
                if (autoIteration != null &&
                    !string.IsNullOrEmpty(autoIteration.Start) &&
                    !string.IsNullOrEmpty(autoIteration.Next) &&
                    AnalyzedParameters.Select(s => s.Name).Contains(autoIteration.Start) &&
                    AreResultFieldsPresent(ReturnType, autoIteration.Next))
                {
                    //If autoiteration also has configured a field name for EmitLimit (input parameter idicating the max number of items
                    //to be returned by the service) and the EmitLimit parameter is actually present in the input type
                    if (String.IsNullOrEmpty(autoIteration.EmitLimit) ||
                        CurrentOperation.LegacyPagination != ServiceOperation.LegacyPaginationType.UseEmitLimit ||
                        !AnalyzedParameters.Select(s => s.Name).Contains(autoIteration.EmitLimit))
                    {
                        autoIteration.EmitLimit = null;
                    }

                    return autoIteration;
                }

                return null;
            }
        }

        /// <summary>
        /// Helper to expose method name
        /// </summary>
        public string MethodName
        {
            get
            {
                return CurrentOperation == null ? string.Empty : CurrentOperation.MethodName;
            }
        }

        /// <summary>
        /// The request class that is passed to the operation
        /// </summary>
        public Type RequestType { get; private set; }

        /// <summary>
        /// The SDK type holding the full response data from the api call.
        /// The true output will be either one or more members of this type
        /// or a nested type. The result analyzer will determine which should
        /// be used.
        /// </summary>
        public Type ResponseType { get; private set; }

        /// <summary>
        /// The output type of the method that contains the actual result data.
        /// Usually the same as ResponseType except for scenarios where the SDK
        /// generator has wrapped the true output into another type addressed
        /// by a member of ResponseType.
        /// </summary>
        public Type ReturnType { get; private set; }

        /// <summary>
        /// The results of the analysis of the output type for the cmdlet
        /// </summary>
        public AnalyzedResult AnalyzedResult { get; private set; }

        /// <summary>
        /// True if the cmdlet has no output but has a parameter that can be piped in; this
        /// can be echoed to the pipeline if the user supplies the -PassThru switch.
        /// </summary>
        public bool RequiresPassThruGeneration
        {
            get
            {
                return CurrentOperation.PassThru != null || AcceptsValueFromPipelineParameter != null;
            }                
        }

        public static string FormatTypeName(Type type)
        {
            if (type.IsArray)
            {
                return $"{FormatTypeName(type.GetElementType())}[]";
            }
            else if (type.IsGenericType)
            {
                string typeName = type.FullName.Remove(type.FullName.IndexOf('`'));
                string genericArguments = string.Join(", ", type.GetGenericArguments().Select(genericArgument => FormatTypeName(genericArgument)));
                return $"{typeName}<{genericArguments}>";
            }

            return type.FullName;
        }

        /// <summary>
        /// Returns the correct object noun to use in confirmation messages we generate
        /// for the ShouldContinue call (if the cmdlet is attributed with SupportsShouldProcess).
        /// Note that some operations do not have a noun at all, so we yield an empty string
        /// in that scenario (the confirmation messages we generate read ok with empty/non-empty
        /// noun values).
        /// </summary>
        /// <returns></returns>
        public string ConfirmationMessageNoun
        {
            get
            {
                if (!string.IsNullOrEmpty(CurrentOperation.ShouldProcessMsgNoun))
                {
                    return CurrentOperation.ShouldProcessMsgNoun;
                }

                return CurrentOperation.OriginalNoun ?? string.Empty;
            }
        }

        /// <summary>
        /// Returns the ConfirmImpact setting. If the shell's $ConfirmPreference 
        /// is equal to or lower than the cmdlet's declared impact, a prompt is output
        /// by the call to ShouldProcess. By default, the shell is at 'high' so only
        /// our deletion cmdlets issue a prompt. 
        /// </summary>
        public ConfirmImpact ConfirmImpactSetting
        {
            get
            {
                return CurrentOperation.ConfirmImpact == null ?
                    (_highImpactVerbs.Contains(CurrentOperation.SelectedVerb) ? ConfirmImpact.High : ConfirmImpact.Medium) :
                    Enum.Parse<ConfirmImpact>(CurrentOperation.ConfirmImpact);
            }
        }

        /// <summary>
        /// Returns formatted string containing details of the operation to be performed
        /// for use in confirmation messages. 
        /// </summary>
        public string FormattedOperationForConfirmationMsg
        {
            get
            {
                return string.Format("{0}-{1} ({2})",
                                     CurrentOperation.SelectedVerb,
                                     CurrentOperation.SelectedNoun,
                                     CurrentOperation.MethodName);

            }
        }

        #endregion

        #region Private properties

        private Pluralizer Pluralizer { get; set; }

        /// <summary>
        /// Terminating word fragments shorter than this will not be made singular
        /// </summary>
        private const int MinLengthForSingularization = 3;

        /// <summary>
        /// Collection of terminating word fragments to retain as-is or manually
        /// update (for cases where the pluralization service gets it wrong).
        /// If the value for a key is empty, we do not alter the fragment otherwise
        /// we replace the fragment with a new value.
        /// </summary>
        private readonly Dictionary<string, string> _manualFragmentRenames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            { "Cpus", "Cpu" },
            { "Data", null },           // pluralization turns it to 'Datum'
            { "Metadata", null },
            { "Efs", null },
            { "Information", null },
            { "Iops", null },           // pluralization yields Iop
            { "Media", null },
            { "Nfs", null },
            { "Status", null },
            { "Cookies", "Cookie" },    // pluralization service yields 'Cooky' 
            { "Lens", null }            // service incorrectly treats as plural
        };

        /// <summary>
        /// The collection of verbs that do not require the cmdlet to be attributed
        /// with 'SupportsShouldProcess'. From the PowerShell SDK docs - '...any cmdlet 
        /// that modifies the system' needs to be attributed.
        /// </summary>
        private readonly HashSet<string> _supportsShouldProcessVerbSuppressions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "get", "test", "find"
        };

        /// <summary>
        /// Ordered collection of parameter name suffixes that we will look for when
        /// attempting to auto-discover the parameter that should be used in the
        /// ShouldProcess test in ProcessRecord. If none of the parameters ends with
        /// one of these suffixes, or multiple parameters have the same suffix or
        /// multiple parameters exhibit more than one suffix, an error will be logged
        /// requiring the user to specify manual configuration using ShouldProcessTarget.
        /// </summary>
        private readonly List<string> _supportsShouldProcessParameterSuffixes = new List<string>
        {
            "Id", "Name", "Arn", "Identifier"
        };

        /// <summary>
        /// Collection of verbs for which the cmdlet's ConfirmImpact setting should be 'high'
        /// instead of our default of 'medium'.
        /// </summary>
        private readonly HashSet<string> _highImpactVerbs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "remove"
        };

        /// <summary>
        /// The set of PowerShell approved verbs we can select from.
        /// </summary>
        private static readonly HashSet<string> ApprovedVerbs = GetApprovedVerbs();

        private readonly Dictionary<Type, List<SimplePropertyInfo>> _rootSimplePropertiesCache = new Dictionary<Type, List<SimplePropertyInfo>>();
        private readonly Dictionary<Type, List<SimplePropertyInfo>> _flatPropertiesCache = new Dictionary<Type, List<SimplePropertyInfo>>();

        #endregion

        public class UnexpectedPropertyTypeException : Exception
        {
            public UnexpectedPropertyTypeException(string message) : base(message) { }

            public UnexpectedPropertyTypeException(string message, Exception innerException) : base(message, innerException) { }
        }

        public MethodInfo Method { get; private set; }

        public OperationAnalyzer(ConfigModelCollection allModels, ConfigModel currentModel, ServiceOperation currentOperation, XmlDocument assemblyDocumentation)
        {
            // Doesn't appear to be a costly operation, so init per instance for now. Invariant
            // culture is not supported (yields exception).
            Pluralizer = new Pluralizer();

            AllModels = allModels;
            CurrentModel = currentModel;
            CurrentOperation = currentOperation;
            AssemblyDocumentation = assemblyDocumentation;
        }

        /// <summary>
        /// Analyzes the properties in the operation's request class to arrive at the
        /// set of parameters to be output for the cmdlet. The set, on output, will
        /// contain only the parameters to be generated complete with the final names
        /// and any relevant aliases (for backwards compatibility or so configured)
        /// attached the service operations parameter aliases maps.
        /// </summary>
        public void Analyze(CmdletGenerator generator, MethodInfo methodInfo)
        {
            // keep this since we can use it to recover documentation when we
            // write the cmdlet source code
            Method = methodInfo;

            if (methodInfo.ReturnType.BaseType != typeof(System.Threading.Tasks.Task))
            {
                throw new ArgumentException($"Method {methodInfo.Name} doesn't have Task<> as return value.");
            }

            RequestType = GetRequestType(methodInfo);
            ResponseType = methodInfo.ReturnType.GenericTypeArguments[0];

            if (string.IsNullOrEmpty(CurrentOperation.OutputWrapper))
            {
                ReturnType = ResponseType;
            }
            else
            {
                // the true return type exists as a nested member inside the
                // method response type so we must go one layer deeper to get
                // at the class containing the data to analyze for output
                var outputMember = ResponseType.GetMember(CurrentOperation.OutputWrapper).FirstOrDefault();
                if (outputMember != null && outputMember.MemberType == MemberTypes.Property)
                {
                    ReturnType = ((PropertyInfo)outputMember).PropertyType;
                }
                else
                {
                    AnalysisError.MissingOutputWrapperProperty(CurrentModel, CurrentOperation, CurrentOperation.OutputWrapper);
                    return;
                }
            }

            DetermineVerbAndNoun(generator);
            DetermineParameters(generator);
            DeterminePipelineParameter(generator);
            DetermineSupportsShouldProcessRequirement(generator);
            DetermineResult(generator);
            DeterminePassThruRequirement(generator);

            if (!string.IsNullOrEmpty(CurrentOperation.LegacyAlias))
            {
                generator.AddLegacyAlias($"{CurrentOperation.SelectedVerb}-{CurrentOperation.SelectedNoun}", CurrentOperation.LegacyAlias);
            }
        }

        /// <summary>
        /// Creates a simplified property for the specified request or response field. If the field's 
        /// type is derived from the SDK's ConstantClass 'enum' type, we will also register to emit
        /// an argument completer unless the field is a member of the result type.
        /// If the parameter type is a MemoryStream the parameter name is registered for replacement
        /// with a byte[] during cmdlet generation.
        /// </summary>
        /// <param name="property"></param>
        /// <param name="parent"></param>
        /// <param name="isCmdletParameter">
        /// True if the simplified property will represent a parameter on the cmdlet.
        /// </param>
        /// <returns></returns>
        public SimplePropertyInfo CreateSimplePropertyFor(PropertyInfo property, SimplePropertyInfo parent, bool isCmdletParameter)
        {
            var shouldFlatten = true;

            string propertyTypeFullName;
            var collectionType = SimplePropertyInfo.PropertyCollectionType.NoCollection;
            Type[] genericCollectionTypes = null;

            string propertyTypeName;
            try
            {
                propertyTypeName = GetValidTypeName(property.PropertyType);
            }
            catch (UnexpectedPropertyTypeException)
            {
                return null;
            }

            // determine if property should be flattened based solely on its type, or is a generic List<T> we
            // can replace with T[] to follow PS parameter convention
            if (property.PropertyType.IsGenericType)
            {
                // if the property is a generic List, it would be more pleasant for users, and
                // more in line with PS convention, to remap it here to an array type - something
                // to consider. The change would affect the parameter but not the inner context
                // and (of course) the request object property. It would affect help though.
                // Note that some services also employ List<List<T>>, so we have to detect inner
                // case too.
                // Note services started using List<List<List<List<T>>>>.
                if (property.PropertyType.GetGenericTypeDefinition().Name.StartsWith("List`"))
                {
                    genericCollectionTypes = property.PropertyType.GetGenericArguments();
                    if (genericCollectionTypes[0].Name.StartsWith("Dictionary`"))
                    {
                        genericCollectionTypes = genericCollectionTypes[0].GetGenericArguments();
                        collectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericListOfGenericDictionary;
                    }
                    else if (genericCollectionTypes[0].Name.StartsWith("List`"))
                    {
                        if (genericCollectionTypes[0].GenericTypeArguments[0].Name.StartsWith("List`"))
                        {
                            if (genericCollectionTypes[0].GenericTypeArguments[0].GenericTypeArguments[0].Name.StartsWith("List`"))
                            {
                                if (genericCollectionTypes[0].GenericTypeArguments[0].GenericTypeArguments[0].GenericTypeArguments[0].Name.StartsWith("List`"))
                                {
                                    throw new UnexpectedPropertyTypeException($"Only four levels of List<T> are supported, detected five or more for property {property.Name}");
                                }
                                genericCollectionTypes = genericCollectionTypes[0].GenericTypeArguments[0].GenericTypeArguments[0].GetGenericArguments();
                                collectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericListOfGenericListOfGenericListOfGenericList;
                            }
                            else
                            {
                                genericCollectionTypes = genericCollectionTypes[0].GenericTypeArguments[0].GetGenericArguments();
                                collectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericListOfGenericListOfGenericList;
                            }
                        }
                        else
                        {
                            genericCollectionTypes = genericCollectionTypes[0].GetGenericArguments();
                            collectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericListOfGenericList;
                        }
                    }
                    else
                    {
                        collectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericList;
                    }
                }
                else if (property.PropertyType.GetGenericTypeDefinition().Name.StartsWith("Dictionary`"))
                {
                    genericCollectionTypes = property.PropertyType.GetGenericArguments();
                    collectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericDictionary;
                }

                propertyTypeFullName = property.PropertyType.GetGenericTypeDefinition().FullName;
            }
            else
            {
                propertyTypeFullName = property.PropertyType.FullName;
            }

            shouldFlatten = ShouldFlattenType(propertyTypeFullName);

            var simpleProperty = new SimplePropertyInfo(property,
                                                        parent,
                                                        propertyTypeName,
                                                        AssemblyDocumentation,
                                                        collectionType,
                                                        genericCollectionTypes);

            // if requiring substitution due to being a memory stream type,
            // add to the collection we'll iterate over when initializing one or
            // more memory streams during cmdlet execution
            if (isCmdletParameter && simpleProperty.IsStreamType)
            {
                StreamParameters.Add(simpleProperty);
            }

            if (simpleProperty.IsConstrainedToSet && isCmdletParameter)
            {
                // push the set members and a reference from the current cmdlet into the service 
                // model so that argument completers can be generated later
                if (!CurrentModel.ArgumentCompleters.IsConstantClassRegistered(propertyTypeName))
                {
                    var setMembers = SimplePropertyInfo.GetConstantClassMembers(property.PropertyType);
                    CurrentModel.ArgumentCompleters.AddConstantClass(propertyTypeName, setMembers);
                }
                CurrentModel.ArgumentCompleters.AddConstantClassReference(propertyTypeName, 
                                                                          simpleProperty.CmdletParameterName, 
                                                                          string.Format("{0}-{1}", 
                                                                                        CurrentOperation.SelectedVerb, 
                                                                                        CurrentOperation.SelectedNoun));
            }

            if (shouldFlatten)
            {
                var propertyTypeProperties = property.PropertyType.GetProperties().Where(p =>
                    // skip properties that aren't read/write and index properties
                    p.CanWrite && p.CanRead && p.GetIndexParameters().Length == 0
                ).ToArray();

                if (propertyTypeProperties.Length > 0)
                {
                    foreach (var childProperty in propertyTypeProperties
                        .Select(propertyTypeProperty => CreateSimplePropertyFor(propertyTypeProperty, simpleProperty, isCmdletParameter))
                        .Where(sp => sp != null))
                    {
                        simpleProperty.Children.Add(childProperty);
                    }
                }
            }
            return simpleProperty;
        }

        /// <summary>
        /// Checks to see if the specified type has been designated as non-flattenable. By
        /// default we flatten all complex types to individual parameters during codegen.
        /// </summary>
        /// <param name="propertyTypeFullName">The full typename of the property being considered</param>
        /// <returns>True if the type can be flattened, false if we should emit a parameter of complex type.</returns>
        public bool ShouldFlattenType(string propertyTypeFullName)
        {
            return !CurrentOperation.TypesNotToFlatten.Contains(propertyTypeFullName) &&
                   !CurrentModel.TypesNotToFlatten.Contains(propertyTypeFullName) &&
                   !AllModels.TypesNotToFlatten.Contains(propertyTypeFullName);
        }

        public static string ToLowerCamelCase(string name)
        {
            var sb = new StringBuilder(name);
            sb[0] = char.ToLowerInvariant(sb[0]);
            return sb.ToString();
        }

        /// <summary>
        /// Returns true if the parameter (identified by fully flattened name) is tagged as excluded
        /// at either the operation or model level.
        /// </summary>
        /// <param name="analyzedName"></param>
        /// <returns></returns>
        public bool IsExcludedParameter(string analyzedName)
        {
            return IsExcludedParameter(analyzedName, CurrentModel, CurrentOperation);
        }

        /// <summary>
        /// Returns true if the parameter (identified by fully flattened name) is tagged as excluded
        /// at either the operation or model level.
        /// </summary>
        /// <param name="analyzedName"></param>
        /// <param name="model"></param>
        /// <param name="operation"></param>
        /// <returns></returns>
        public static bool IsExcludedParameter(string analyzedName, ConfigModel model, ServiceOperation operation)
        {
            return model.ShouldExcludeParameter(analyzedName) || operation.ShouldExcludeParameter(analyzedName);
        }

        /// <summary>
        /// Returns the closest available parameter customization, null if the
        /// parameter has not been customized
        /// </summary>
        /// <param name="analyzedName"></param>
        /// <returns></returns>
        public Param GetParameterCustomization(string analyzedName)
        {
            return GetParameterCustomization(analyzedName, CurrentModel, CurrentOperation);
        }

        /// <summary>
        /// Returns the closest available parameter customization, null if the
        /// parameter has not been customized
        /// </summary>
        /// <param name="analyzedName"></param>
        /// <param name="model"></param>
        /// <param name="operation"></param>
        /// <returns></returns>
        public static Param GetParameterCustomization(string analyzedName, ConfigModel model, ServiceOperation operation)
        {
            if (operation.CustomParameters.ContainsKey(analyzedName))
            {
                return operation.CustomParameters[analyzedName];
            }

            if (model.CustomParameters.ContainsKey(analyzedName))
            {
                return model.CustomParameters[analyzedName];
            }

            return null;
        }

        public string GetValidTypeName(Type type)
        {
            return GetValidTypeName(type, CurrentModel);
        }

        public static string GetValidTypeName(Type type, ConfigModel currentModel)
        {
            if (type.IsArray)
            {
                return GetValidTypeName(type.GetElementType(), currentModel) + "[]";
            }

            if (type.IsGenericType)
            {
                var genericArguments = type.GetGenericArguments();
                var genericType = type.GetGenericTypeDefinition();

                if (genericType.IsAssignableFrom(typeof(List<>)))
                {
                    return string.Format("List<{0}>", GetValidTypeName(genericArguments[0], currentModel));
                }

                if (genericType.IsAssignableFrom(typeof(IEnumerable<>)))
                {
                    return string.Format("IEnumerable<{0}>", GetValidTypeName(genericArguments[0], currentModel));
                }

                if (genericType.IsAssignableFrom(typeof(Dictionary<,>)))
                {
                    return string.Format("Dictionary<{0}, {1}>",
                                         GetValidTypeName(genericArguments[0], currentModel),
                                         GetValidTypeName(genericArguments[1], currentModel));
                }

                if (genericType.FullName.Equals("Amazon.S3.Model.Tuple`2"))
                {
                    return string.Format("Amazon.S3.Model.Tuple<{0}, {1}>",
                                         GetValidTypeName(genericArguments[0], currentModel),
                                         GetValidTypeName(genericArguments[1], currentModel));
                }

                // we'll mark the parameter later that we need to check the BoundParameters collection
                // before attempting to use
                if (genericType.IsAssignableFrom(typeof(Nullable<>)))
                {
                    return string.Format("{0}", GetValidTypeName(genericArguments[0], currentModel));
                }

                throw new UnexpectedPropertyTypeException($"Can't determine generic type. Type = [{type.FullName}], GenericType = [{genericType.FullName}]");
            }

            // always return namespace-scoped type names to avoid cross-service conflicts
            return string.Format("{0}.{1}", type.Namespace, type.Name);
        }

        /// <summary>
        /// Check if the parameter can be set by by-value pipeline input. If so, the parameter
        /// attribute for the parameter will include 'PipelineParameter = true'. Each service
        /// operation can supply an operation-specific value or we can fall back to a cross-
        /// operation setting for the service.
        /// </summary>
        /// <param name="paramName"></param>
        /// <returns></returns>
        public bool CanAcceptValueFromPipeline(string paramName)
        {
            // only consider using service global definition if there is not a local operation override,
            // otherwise we can tag multiple parameters as value-from-pipeline for cmdlets who use service-global
            // parameter as a secondary parameter on the cmdlet
            if (!string.IsNullOrEmpty(CurrentOperation.PipelineParameter))
            {
                return CurrentOperation.PipelineParameter == paramName;
            }

            return CurrentModel.PipelineParameter == paramName;
        }

        /// <summary>
        /// Scans the set of service-global and operation-specific PositionalParameter lists
        /// to determine whether a Position value should be emitted for the specified parameter.
        /// The order is determined by the parameter names declared for the global and per-operation
        /// PositionalParameters lists.
        /// </summary>
        /// <param name="paramName">The name of the parameter being emitted (real name)</param>
        /// <returns>-1 if no Position data should be emitted, otherwise the position ordinal as declared in the config</returns>
        public int GetParameterPositionData(string paramName)
        {
            // If the operation did not override value-from-pipeline, the positional set is the
            // ordered union of service global value-from-pipeline + service specific positional params 
            // otherwise we use the ordered union of the operation's value-from-pipeline plus operation-
            // specific positional params.
            // To simplify the configuration and avoid data duplication, we rule that the pipelineable parameter 
            // is always position 0. A service configuration containing just positional data without a pipeline
            // value is technically invalid.
            if (paramName == CurrentOperation.PipelineParameter)
            {
                return 0;
            }

            // we also allow the config to override value-from-pipeline but reuse service-global positional
            // data
            for (int i = 0; i < CurrentOperation.PositionalParametersList.Length; i++)
            {
                if (paramName == CurrentOperation.PositionalParametersList[i])
                {
                    return i + 1;
                }
            }

            return -1;
        }

        /// <summary>
        /// Returns the true number of parameters for an operation, disregarding any that
        /// have been declared as part of auto-iteration support.
        /// </summary>
        public IEnumerable<SimplePropertyInfo> NonIterationParameters
        {
            get
            {
                var iterationSettings = AutoIterateSettings;
                if (iterationSettings == null)
                {
                    return AnalyzedParameters;
                }

                return AnalyzedParameters.Where(p => !iterationSettings.IsIterationParameter(p.AnalyzedName));
            }
        }

        /// <summary>
        /// Inspects the api method and determines the verb/noun combination to use, unless directed otherwise
        /// by config data. Returns the selected verb/noun pair in the ServiceOperation
        /// </summary>
        /// <param name="generator"></param>
        /// <returns></returns>
        private bool DetermineVerbAndNoun(CmdletGenerator generator)
        {
            var methodName = CurrentOperation.MethodName;

            string noun = null;

            string verb;
            if (!AllModels.OperationNameMappings.TryGetValue(methodName, out verb))
            {
                verb = methodName;
            }

            for (var i = 1; i < verb.Length; i++)
            {
                if (Char.IsUpper(verb[i]))
                {
                    noun = verb.Substring(i);
                    verb = verb.Substring(0, i);
                    break;
                }
            }

            // save the noun part of the split method name so we can potentially use it for the 'operation'
            // message if the cmdlet requires confirmation
            CurrentOperation.OriginalNoun = noun;

            var originalVerb = verb;
            var originalNoun = noun;

            if (!string.IsNullOrEmpty(CurrentOperation.RequestedVerb))
            {
                verb = CurrentOperation.RequestedVerb;
            }
            else if (CurrentOperation.IsAutoConfiguring)
            {
                verb = AssignVerb(verb);
            }

            if (!string.IsNullOrEmpty(CurrentOperation.RequestedNoun))
            {
                if (!CheckNounIsSingular(CurrentOperation.RequestedNoun))
                {
                    AnalysisError.ConfiguredNounIsPlural(CurrentModel, CurrentOperation);
                }

                noun = CurrentOperation.RequestedNoun;
            }
            else if (CurrentOperation.IsAutoConfiguring)
            {
                noun = AssignNoun(noun);
            }

            if (verb.Length <= 2 || string.IsNullOrEmpty(noun))
            {
                AnalysisError.CannotDetermineVerbAndNoun(CurrentModel, CurrentOperation);
            }

            var nounWithPrefix = noun;
            var originalNounWithPrefix = noun;
            // prepend service prefix
            if (!string.IsNullOrEmpty(CurrentModel.ServiceNounPrefix))
            {
                nounWithPrefix = CurrentModel.ServiceNounPrefix + noun;
                originalNounWithPrefix = CurrentModel.ServiceNounPrefix + originalNoun;
            }

            if (verb != originalVerb || nounWithPrefix != originalNounWithPrefix)
            {
                AliasStore.Instance.AddAlias(string.Format("{0}-{1}", verb, nounWithPrefix),
                                             string.Format("{0}-{1}", originalVerb, originalNounWithPrefix));
            }
            if (verb != originalVerb && nounWithPrefix != originalNounWithPrefix)
            {
                AliasStore.Instance.AddAlias(string.Format("{0}-{1}", verb, nounWithPrefix),
                                             string.Format("{0}-{1}", originalVerb, nounWithPrefix));
            }

            if (!ApprovedVerbs.Contains(verb))
            {
                AnalysisError.UnapprovedVerb(CurrentModel, CurrentOperation, verb);
            }

            CurrentOperation.SelectedVerb = verb;
            CurrentOperation.SelectedNoun = nounWithPrefix;

            if (CurrentOperation.RequestedVerb != verb || CurrentOperation.RequestedNoun != noun)
            {
                if (!CurrentOperation.IsAutoConfiguring)
                {
                    AnalysisError.MustUpdateVerbAndNoun(CurrentModel, CurrentOperation, verb, noun);
                }

                CurrentOperation.RequestedVerb = verb;
                CurrentOperation.RequestedNoun = noun;
            }
            return true;
        }

        // Called when auto-assigning a noun an operation. Returns the singularized noun
        // (singularizing the last portion only) or null if no change is needed.
        private string SingularizeNoun(string noun)
        {
            var nounArray = Regex.Split(noun, @"(?<!^)(?=[A-Z])");
            var nounTermination = nounArray[nounArray.Length - 1];
            // service yields some nounds as plural but they are not for cmdlet name purposes

            var singularizedNounTermination = SingularizeTerm(nounTermination);

            if (nounTermination != singularizedNounTermination)
            {
                var suggestedNoun = new StringBuilder();
                for (var i = 0; i < nounArray.Length - 1; i++)
                {
                    suggestedNoun.Append(nounArray[i]);
                }
                suggestedNoun.Append(singularizedNounTermination);
                return suggestedNoun.ToString();
            }

            return noun;
        }

        /// <summary>
        /// Reflect over the request type properties, unrolling nested types, to obtain the 
        /// flattened set of properties to be exposed as parameters on the cmdlet.
        /// </summary>
        /// <param name="generator"></param>
        private void DetermineParameters(CmdletGenerator generator)
        {
            // analyze the request members to get a flattened set of all
            // properties
            var allProperties = GetFlatProperties(RequestType).ToList();

            // remove excluded properties, if any, from the flattened set
            AnalyzedParameters = allProperties
                .Where(prop => !IsExcludedParameter(prop.AnalyzedName, CurrentModel, CurrentOperation))
                .ToList();

            ApplyPropertyCustomization();

            FixPaginationParameters();

            FinalizeParameterNames();

            ValidateParameterNamesDuplications();

            // also gather the internal root (non-flattened) properties -- these are what the
            // cmdlet will bind the parameters to in the executor
            var rootProperties = GetRootSimpleProperties(RequestType)
                                    .Where(simpleProperty => !IsExcludedParameter(simpleProperty.AnalyzedName, CurrentModel, CurrentOperation))
                                    .ToList();
            RequestProperties = new List<SimplePropertyInfo>(rootProperties);
        }

        private void ValidateParameterNamesDuplications()
        {
            var allParametersNames = AnalyzedParameters.Select(parameter => parameter.CmdletParameterName);
            var groupedParameterNames = allParametersNames.GroupBy(parameter => parameter, StringComparer.OrdinalIgnoreCase).ToArray();
            if (groupedParameterNames.Any(parameter => parameter.Count() > 1))
            {
                AnalysisError.DuplicatedParameterNames(CurrentModel, CurrentOperation,
                    groupedParameterNames.Where(parameter => parameter.Count() > 1).Select(parameter => parameter.Key));
                allParametersNames = groupedParameterNames.Select(parameter => parameter.Key).ToArray();
            }

            var allAliases = AnalyzedParameters.SelectMany(parameter => GetAllParameterAliases(parameter)).ToArray();
            var groupedAliases = allAliases.GroupBy(alias => alias, StringComparer.OrdinalIgnoreCase).ToArray();
            if (groupedAliases.Any(alias => alias.Count() > 1))
            {
                AnalysisError.DuplicatedAliasNames(CurrentModel, CurrentOperation,
                    groupedAliases.Where(parameter => parameter.Count() > 1).Select(parameter => parameter.Key));
                allAliases = groupedAliases.Select(alias => alias.Key).ToArray();
            }

            var invalidParameters = allParametersNames.Intersect(ReservedParameterNames, StringComparer.OrdinalIgnoreCase).ToArray();
            if (invalidParameters.Any())
            {
                AnalysisError.ReservedParameterNames(CurrentModel, CurrentOperation, invalidParameters);
            }

            var invalidAliases = allAliases.Intersect(ReservedParameterNames, StringComparer.OrdinalIgnoreCase).ToArray();
            if (invalidAliases.Any())
            {
                AnalysisError.ReservedAliasNames(CurrentModel, CurrentOperation, invalidAliases);
            }

            var aliasParameterConflicts = allParametersNames.Intersect(allAliases, StringComparer.OrdinalIgnoreCase).ToArray();
            if (aliasParameterConflicts.Any())
            {
                AnalysisError.AliasParameterConflicts(CurrentModel, CurrentOperation, aliasParameterConflicts);
            }
        }

        /// <summary>
        /// This fixes the properties of pagination parameters. This must be done after AnalyzedParameters
        /// is filled because the AutoIterateSettings property getter uses AnalyzedParameters.
        /// </summary>
        private void FixPaginationParameters()
        {
            var autoIterateSettings = AutoIterateSettings;

            if (autoIterateSettings != null)
            {
                if (StreamParameters.Any())
                {
                    AnalysisError.StreamParametersNotSupportedForPaginatedCmdlets(CurrentModel, CurrentOperation, StreamParameters);
                }

                foreach (var parameter in AnalyzedParameters)
                {
                    if (parameter.AnalyzedName == autoIterateSettings.EmitLimit)
                    {
                        parameter.UseParameterValueOnlyIfBound = true;
                        parameter.PropertyTypeName = "int";

                        if (autoIterateSettings.ServicePageSize != -1)
                        {
                            if ((parameter.MaxValue.HasValue && parameter.MaxValue < autoIterateSettings.ServicePageSize) ||
                                (parameter.MinValue.HasValue && parameter.MinValue > autoIterateSettings.ServicePageSize))
                            {
                                AnalysisError.InvalidServicePageSize(CurrentModel, CurrentOperation, autoIterateSettings.ServicePageSize, autoIterateSettings.EmitLimit, parameter.MinValue, parameter.MaxValue);
                            }
                        }
                        else if ((parameter.IsRequired || autoIterateSettings.PageSizeIsRequired) && !parameter.MaxValue.HasValue)
                        {
                            AnalysisError.ServicePageSizeIsRequired(CurrentModel, CurrentOperation, autoIterateSettings.EmitLimit);
                        }

                        if ((parameter.IsRequired || autoIterateSettings.PageSizeIsRequired) && parameter.DefaultValue == null)
                        {
                            parameter.DefaultValue = (autoIterateSettings.ServicePageSize != -1 ? autoIterateSettings.ServicePageSize : parameter.MaxValue ?? 99).ToString();
                        }
                    }
                }
            }
        }

        private void ApplyPropertyCustomization()
        {
            foreach (var parameter in AnalyzedParameters)
            {
                var customization = GetParameterCustomization(parameter.AnalyzedName);
                if (customization != null)
                {
                    parameter.DefaultValue = customization.DefaultValue;
                }
            }
        }

        /// <summary>
        /// If the operation has a single parameter that does not match any service-level
        /// declaration of PipelineParameter, and there is no PipelineParameter declaration
        /// on the service operation, then adopt the one and only parameter as being
        /// acceptable to be piped in.
        /// </summary>
        /// <param name="generator"></param>
        private void DeterminePipelineParameter(CmdletGenerator generator)
        {
            if (CurrentOperation.NoPipelineParameter && !string.IsNullOrEmpty(CurrentOperation.PipelineParameter))
            {
                AnalysisError.NoPipelineParameterAndPipelineParameterSpecified(CurrentModel, CurrentOperation);
            }
            if (!NonIterationParameters.Any() || CurrentOperation.NoPipelineParameter)
            {
                return;
            }

            if (!string.IsNullOrEmpty(CurrentOperation.PipelineParameter))
            {
                if (!NonIterationParameters.Any(param => param.AnalyzedName == CurrentOperation.PipelineParameter))
                {
                    AnalysisError.InvalidPipelineConfiguration(CurrentModel, CurrentOperation, CurrentOperation.PipelineParameter, NonIterationParameters);
                }
            }
            else
            {
                string pipelineParam = null;
                if (!string.IsNullOrEmpty(CurrentModel.PipelineParameter) && NonIterationParameters.Any(param => param.AnalyzedName == CurrentModel.PipelineParameter))
                {
                    pipelineParam = CurrentModel.PipelineParameter;
                }
                else
                {
                    var candidateParameters = SelectPreferredCandidateParameters(NonIterationParameters);
                    switch (candidateParameters.Count)
                    {
                        case 0:
                            return;
                        case 1:
                            pipelineParam = candidateParameters.First().AnalyzedName;
                            break;
                        default:
                            AnalysisError.MissingPipelineConfiguration(CurrentModel, CurrentOperation, candidateParameters);
                            return;
                    }
                }

                if (CurrentOperation.IsAutoConfiguring)
                {
                    CurrentOperation.PipelineParameter = pipelineParam;
                }
                else
                {
                    AnalysisError.OutdatedPipelineConfiguration(CurrentModel, CurrentOperation, pipelineParam);
                }
            }
        }

        public bool RequiresShouldProcessPromt
        {
            get
            {
                return !_supportsShouldProcessVerbSuppressions.Contains(CurrentOperation.SelectedVerb) &&
                       !CurrentOperation.IgnoreSupportsShouldProcess;
            }
        }

        /// <summary>
        /// If the cmdlet changes system state, indicate that it must be attributed with 
        /// SupportsShouldProcess and determine by configuration or parameter name analysis 
        /// which parameter holds the identity of the resource that the shell will use when 
        /// it displays the confirmation/whatif message. The recorded target is the internal
        /// name of the parameter, not the singularized/shortened public name.
        /// </summary>
        /// <remarks>This inspection can be done any time after the parameters have been determined.</remarks>
        /// <param name="generator"></param>
        private void DetermineSupportsShouldProcessRequirement(CmdletGenerator generator)
        {
            if (!RequiresShouldProcessPromt ||
                CurrentOperation.AnonymousShouldProcessTarget)
            {
                if (!string.IsNullOrEmpty(CurrentOperation.ShouldProcessTarget))
                {
                    AnalysisError.ShouldProcessTargetMustBeEmpty(CurrentModel, CurrentOperation);
                }

                return;
            }

            if (!string.IsNullOrEmpty(CurrentOperation.ShouldProcessTarget))
            {
                // the config specifies the parameter to use as the target then obey
                var target = NonIterationParameters.SingleOrDefault(parameter => parameter.AnalyzedName == CurrentOperation.ShouldProcessTarget);
                if (target == null)
                {
                    AnalysisError.InvalidShouldProcessTargetConfiguration(CurrentModel, CurrentOperation, NonIterationParameters);
                }
            }
            else if (NonIterationParameters.Any())
            {
                //we are supposed to have a target parameter but it is not configured
                DetermineSupportsShouldProcessParameter();
            }
        }

        private void DetermineSupportsShouldProcessParameter()
        {
            // otherwise attempt auto-discovery based on parameter name suffixes - note
            // that we use the finalized names of the parameters here, since PowerShell
            // will introspect on them.
            var potentialTargets = SelectPreferredCandidateParameters(
                _supportsShouldProcessParameterSuffixes.SelectMany(suffix =>
                    NonIterationParameters.Where(parameter => parameter.CmdletParameterName.EndsWith(suffix)))); ;

             SimplePropertyInfo targetParameter = null;

            if (NonIterationParameters.Count() == 1) //Single parameter, auto-selected as target
            {
                targetParameter = NonIterationParameters.First();
            }
            else
            {
                switch (potentialTargets.Count)
                {
                    case 0: //auto-assigned from pipeline parameter
                        targetParameter = AcceptsValueFromPipelineParameter;
                        if (targetParameter == null)
                        {
                            var suggestedParameters = SelectPreferredCandidateParameters(NonIterationParameters);
                            AnalysisError.MultipleTargetsForShouldProcessParameter(CurrentModel, CurrentOperation, suggestedParameters);
                        }
                        break;
                    case 1: //single parameter with recognized suffix
                        targetParameter = potentialTargets[0];
                        break;
                    default: //potentialTargets.Count > 1
                        // When multiple targets exist, if one of them is the value-from-pipeline 
                        // parameter we can (probably) safely assume it should be the target (if this
                        // is wrong it can be safely rectified by adding a manual entry to the config
                        // for the operation).
                        var pipelineParameter = AcceptsValueFromPipelineParameter;
                        if (pipelineParameter != null)
                        {
                            targetParameter = potentialTargets.Where(potentialTarget => potentialTarget.AnalyzedName == pipelineParameter.AnalyzedName).SingleOrDefault();
                        }
                        else
                        {
                            AnalysisError.MultipleTargetsForShouldProcessParameter(CurrentModel, CurrentOperation, potentialTargets);
                        }
                        break;
                }
            }

            if (CurrentOperation.IsAutoConfiguring)
            {
                //Setting the value to string.Empty instead of null makes the attribute appear in the configuration file so that it is easy to fill the value in.
                CurrentOperation.ShouldProcessTarget = targetParameter?.AnalyzedName ?? string.Empty;
            }
            else if (targetParameter != null)
            {
                AnalysisError.OutdatedShouldProcessTargetConfiguration(CurrentModel, CurrentOperation, targetParameter?.AnalyzedName);
            }
        }

        //If there are multiple candidates, try further restricting the list by only using required root parameters

        private List<SimplePropertyInfo> SelectPreferredCandidateParameters(IEnumerable<SimplePropertyInfo> parameters)
        {
            var autoIterateSettings = AutoIterateSettings;
            var result = parameters
                //Excluding collections
                .Where(param => param.PropertyType == typeof(string) ||
                                !typeof(System.Collections.IEnumerable).IsAssignableFrom(param.PropertyType))
                //Excluding metadata and deprecated properties
                .Where(param => !(AllModels.MetadataParameterNames.Contains(param.AnalyzedName) ||
                                 CurrentModel.MetadataPropertyNames.Contains(param.AnalyzedName) ||
                                 param.IsDeprecated ||
                                 (autoIterateSettings?.IsIterationParameter(param.AnalyzedName) ?? false)))
                .ToList();

            if (result.Count > 1)
            {
                var requiredParameters = result.Where(parameter => parameter.IsRecursivelyRequired).ToList();
                if (requiredParameters.Any())
                {
                    result = requiredParameters;
                }
            }

            if (result.Count > 1)
            {
                var rootParameters = result.Where(parameter => parameter.Parent == null).ToList();
                if (rootParameters.Any())
                {
                    result = rootParameters;
                }
            }

            return result;
        }

        /// <summary>
        /// Wraps the return type of the method.
        /// </summary>
        /// <param name="generator"></param>
        private void DetermineResult(CmdletGenerator generator)
        {
            var autoIterateSettings = AutoIterateSettings;

            var allOutputProperties = ResponseType
                .GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
                .Where(p => p.GetCustomAttributes(false).Count(a => a is ObsoleteAttribute) == 0)
                .ToList();

            if (CurrentOperation.IsAutoConfiguring)
            {
                Func<PropertyInfo, bool> isNotMetadataProperty = p =>
                    !AllModels.MetadataPropertyNames.Contains(p.Name) &&
                    !CurrentModel.MetadataPropertyNames.Contains(p.Name) &&
                    autoIterateSettings?.Next != p.Name;

                var nonMetadataProperties = allOutputProperties.Where(isNotMetadataProperty).ToArray();

                if (CurrentOperation.IsAutoConfiguring)
                {
                    if (allOutputProperties.Count == 0) //We only make the cmdlet actually return void when the
                    {                                   //result has no properties at all (including metadata).
                        CurrentOperation.OutputProperty = null;
                    }
                    else if (nonMetadataProperties.Length == 1)
                    {
                        CurrentOperation.OutputProperty = nonMetadataProperties[0].Name;
                    }
                    else
                    {
                        CurrentOperation.OutputProperty = "*";
                    }
                }
            }
            else
            {
                if (CurrentOperation.OutputWrapper != null && (CurrentOperation.OutputProperty == null || (CurrentOperation.OutputProperty != CurrentOperation.OutputWrapper &&
                                                                                                           !CurrentOperation.OutputProperty.StartsWith(CurrentOperation.OutputWrapper + "."))))
                {
                    AnalysisError.OutputWrapperOutputPropertyConflict(CurrentModel, CurrentOperation);
                }
            }

            Type cmdletReturnType = null;
            SimplePropertyInfo singleResultProperty = null;
            if (allOutputProperties.Count == 0) //returns void
            {
                if (CurrentOperation.OutputProperty != null)
                {
                    AnalysisError.OutputTypeError(CurrentModel, CurrentOperation, 0);
                }
            }
            else  if (CurrentOperation.OutputProperty == null)
            {
                AnalysisError.OutputTypeError(CurrentModel, CurrentOperation, allOutputProperties.Count);
            }
            else
            {
                if (CurrentOperation.OutputProperty == "*") //returns response
                {
                    cmdletReturnType = ResponseType;
                }
                else //returns a single property
                {
                    var identifiedOutputProperty = ResolveOutputProperty(ResponseType, CurrentOperation.OutputProperty);
                    if (identifiedOutputProperty == null)
                    {
                        AnalysisError.NonExistingOutputProperty(CurrentModel, CurrentOperation);
                    }
                    singleResultProperty = DetermineSingleResultProperty(identifiedOutputProperty);
                    cmdletReturnType = singleResultProperty.GenericCollectionTypes?[0] ?? singleResultProperty.PropertyType;
                }
            }

            AnalyzedResult = new AnalyzedResult(cmdletReturnType, singleResultProperty);
        }

        public PropertyInfo ResolveOutputProperty(Type type, string outputProperty)
        {
            PropertyInfo result = null;
            foreach (var property in outputProperty.Split('.'))
            {
                result = type.GetProperty(property, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
                if (result == null)
                {
                    break;
                }
                type = result.PropertyType;
            }
            return result;
        }

        private SimplePropertyInfo DetermineSingleResultProperty(PropertyInfo property)
        {
            var singleResultProperty = CreateSimplePropertyFor(property, null, false);
            // if the output is a collection, extract the inner type so we report that as the cmdlet
            // output in help, not the List wrapper (grab the full name so we can be explicit in help)
            if (property.PropertyType.IsGenericType)
            {
                singleResultProperty.GenericCollectionTypes = property.PropertyType.GetGenericArguments();

                // evaluate List<T>
                if (property.PropertyType.GetGenericTypeDefinition().Name.StartsWith("List`", StringComparison.Ordinal))
                {
                    var innerCollectionType = property.PropertyType.GetGenericArguments();
                    
                    // evaluate List<List<T>>
                    if (innerCollectionType[0].Name.StartsWith("List`", StringComparison.Ordinal))
                    {
                        var additionalNested = innerCollectionType[0].GenericTypeArguments[0];

                        // evaluate List<List<List<T>>>
                        if (additionalNested.IsNested)
                        {
                            // evaluate List<List<List<List<T>>>>
                            if (additionalNested.GenericTypeArguments[0].IsNested)
                            {
                                // evaluate List<List<list<List<List<T>>>>>
                                if (additionalNested.GenericTypeArguments[0].GenericTypeArguments[0].IsNested)
                                {
                                    throw new UnexpectedPropertyTypeException($"Only four levels of List<T> are supported, detected five or more for property {property.Name}");
                                }
                                singleResultProperty.GenericCollectionTypes = new[] { additionalNested.GenericTypeArguments[0] };
                                singleResultProperty.CollectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericListOfGenericListOfGenericListOfGenericList;
                            }
                            else
                            {
                                singleResultProperty.GenericCollectionTypes = new[] { additionalNested };
                                singleResultProperty.CollectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericListOfGenericListOfGenericList;
                            }
                        }
                        else
                        {
                            singleResultProperty.CollectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericListOfGenericList;
                        }
                    }
                    else if (innerCollectionType[0].Name.StartsWith("Dictionary`", StringComparison.Ordinal))
                    {
                        singleResultProperty.CollectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericListOfGenericDictionary;
                    }
                    else
                    {
                        singleResultProperty.CollectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericList;
                    }
                }
                else if (property.PropertyType.GetGenericTypeDefinition().Name.StartsWith("Dictionary`", StringComparison.Ordinal))
                {
                    singleResultProperty.CollectionType = SimplePropertyInfo.PropertyCollectionType.IsGenericDictionary;
                }
            }

            return singleResultProperty;
        }

        /// <summary>
        /// Phase 4 of the analysis: if the output from the cmdlet is void but an object can 
        /// be piped in, record that we should add the -PassThru switch parameter and if set
        /// by the user, echo the input object to the pipeline.
        /// </summary>
        /// <remarks>This inspection must be performed after the result has been analyzed.</remarks>
        /// <param name="generator"></param>
        private void DeterminePassThruRequirement(CmdletGenerator generator)
        {
            if (CurrentOperation.PassThru != null)
            {
                if (string.IsNullOrEmpty(CurrentOperation.PassThru.Expression) || string.IsNullOrEmpty(CurrentOperation.PassThru.Documentation))
                {
                    AnalysisError.NonConfiguredPassThru(CurrentModel, CurrentOperation);
                }
            }
        }

        private string AssignVerb(string verb)
        {
            string newVerb = null;
            if (AllModels.VerbMappings.ContainsKey(verb))
            {
                newVerb = AllModels.VerbMappings[verb];
            }

            if (newVerb == null && CurrentModel.VerbMappings.ContainsKey(verb))
            {
                newVerb = CurrentModel.VerbMappings[verb];
            }

            if (string.IsNullOrEmpty(newVerb))
            {
                if (verb.Equals("list", StringComparison.OrdinalIgnoreCase))
                {
                    newVerb = "Get";
                    CurrentOperation.IsRemappedListOperation = true;
                }                
            }

            return newVerb ?? verb;
        }

        private bool CheckNounIsSingular(string noun)
        {
            const string listSuffix = "List";

            if (noun.EndsWith(listSuffix) && noun.Length > listSuffix.Length)
            {
                noun = noun.Substring(0, noun.Length - listSuffix.Length);
            }

            return SingularizeNoun(noun) == noun;
        }

        private string AssignNoun(string noun)
        {
            string newNoun;
            if (!CurrentModel.NounMappings.TryGetValue(noun, out newNoun) &&
                !AllModels.NounMappings.TryGetValue(noun, out newNoun))
            {
                newNoun = SingularizeNoun(noun);
            }

            if (CurrentOperation.IsRemappedListOperation)
            {
                newNoun = newNoun + "List";
            }

            return newNoun;
        }

        /// <summary>
        /// Evaluate the analyzed parameters to find the one (if any) that matches the
        /// PipelineParameter declared for the operation or globally at the
        /// service config level. Null is returned if the cmdlet has no parameter that
        /// can be piped into.
        /// </summary>
        public SimplePropertyInfo AcceptsValueFromPipelineParameter
        {
            get
            {
                if (!string.IsNullOrEmpty(CurrentOperation.PipelineParameter))
                {
                    return AnalyzedParameters.FirstOrDefault(parameter => parameter.AnalyzedName == CurrentOperation.PipelineParameter);
                }

                return null;
            }
        }

        private Type GetRequestType(MethodInfo method)
        {
            var requestParameters = method.GetParameters();
            if (requestParameters.Length == 0)
            {
                return null;
            }
            if (requestParameters.Length != 2)
            {
                return null;
            }
            var requestType = requestParameters[0].ParameterType;
            if (requestType.IsInterface)
            {
                return null;
            }
            return requestType;
        }

        private static HashSet<string> GetApprovedVerbs()
        {
            var allVerbs = new HashSet<string>();
            AddVerbs(typeof(VerbsCommon), allVerbs);
            AddVerbs(typeof(VerbsCommunications), allVerbs);
            AddVerbs(typeof(VerbsData), allVerbs);
            AddVerbs(typeof(VerbsDiagnostic), allVerbs);
            AddVerbs(typeof(VerbsLifecycle), allVerbs);
            AddVerbs(typeof(VerbsOther), allVerbs);
            AddVerbs(typeof(VerbsSecurity), allVerbs);

            // remove "Resize" and "Optimize", as these are 3.0 verbs
            allVerbs.Remove("Resize");
            allVerbs.Remove("Optimize");
            
            // remove "Resize" and "Optimize", as these are 6.0 verbs and we are still supporting 5.1
            allVerbs.Remove("Build");
            allVerbs.Remove("Deploy");

            // Should we have an assert here for the number of verbs? If we change the reference 
            // and get more verbs, that should cause a quick break.

            return allVerbs;
        }

        private static void AddVerbs(IReflect type, ISet<string> allVerbs)
        {
            var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);

            foreach (var name in fields.Select(field => field.Name))
            {
                allVerbs.Add(name);
            }
        }

        /// <summary>
        /// Analyze the supplied properties to arrive at the final name that should be used.
        /// Names can be forced via configuration entries or by automatic analysis. Automatic
        /// analysis can shorten flattened structure names to just one _ component and make
        /// the final word fragment singular.
        /// </summary>
        private void FinalizeParameterNames()
        {
            // possible scenarios:
            //   param Name="foo" Alias="baz"                      => add alias, also auto-rename
            //   param Name="foo" Exclude="true"                   => do not emit parameter
            //   param Name="foo" NewName="bar"                    => rename param, no alias
            //   param Name="foo" NewName="bar" Alias="baz"        => rename param with alias
            //   param Name="foo" AutoRename="false"               => do not rename param
            //   param Name="foo" AutoRename="false" Alias="baz"   => do not rename but add alias

            foreach (var property in AnalyzedParameters)
            {
                var attemptAutoRename = true;
                var parameterCustomization = GetParameterCustomization(property.AnalyzedName);
                if (parameterCustomization != null)
                {
                    // wire up the analyzed parameter with its config customization - this
                    // is the earliest opportunity to do this
                    property.Customization = parameterCustomization;

                    // useful to throw an error here is Exclude is specified with other data?
                    if (parameterCustomization.Exclude)
                    {
                        continue;
                    }

                    if (!string.IsNullOrEmpty(parameterCustomization.NewName))
                    {
                        RecordParameterRename(property, parameterCustomization.NewName);
                        attemptAutoRename = false;
                    }
                    else
                    {
                        attemptAutoRename = parameterCustomization.AutoRename;
                    }
                }

                if (attemptAutoRename)
                {
                    // inspect the name to determine if it can be reduced in length if we flattened a deep
                    // property hierarchy
                    var alternateName = property.AnalyzedName;
                    var underscores = property.AnalyzedName.Count(c => c == '_');
                    if (underscores > 1)
                    {
                        alternateName = RebaseNameToFinalParent(alternateName);
                    }

                    var attemptToSingularize = property.IsValidForSingularization;
                    if (attemptToSingularize)
                    {
                        string namePrefix;
                        string finalFragment;
                        SplitName(alternateName, out namePrefix, out finalFragment);
                        alternateName = finalFragment != null
                            ? string.Concat(namePrefix, SingularizeTerm(finalFragment))
                            : SingularizeTerm(alternateName);
                    }

                    if (alternateName.Length != property.AnalyzedName.Length)
                    {
                        RecordParameterRename(property, alternateName);
                    }
                }
            }
        }

        /// <summary>
        /// Attempts to convert the supplied term to singular form provided
        /// it exceeds minimum length limits, it not a term marked as needing
        /// to not be singularized and is actually plural. If the term cannot
        /// be modified, the original value is returned.
        /// </summary>
        /// <param name="term"></param>
        /// <returns></returns>
        private string SingularizeTerm(string term)
        {
            if (term.Length >= MinLengthForSingularization)
            {
                if (_manualFragmentRenames.ContainsKey(term))
                {
                    var replacement = _manualFragmentRenames[term];
                    return replacement ?? term;
                }

                return Pluralizer.Singularize(term) ?? term;
            }

            return term;
        }

        /// <summary>
        /// Returns the last parent_property name combination from the supplied name
        /// </summary>
        /// <param name="fullName"></param>
        /// <returns></returns>
        private static string RebaseNameToFinalParent(string fullName)
        {
            var index = fullName.LastIndexOf('_') - 1;
            if (index < 0)
            {
                return fullName;
            }

            index = fullName.LastIndexOf('_', index);
            return fullName.Substring(index + 1);
        }

        /// <summary>
        /// Splits the parameter name into a prefix and a final fragment, determined
        /// by the last uppercase letter. If the name does not split, finalFragment
        /// is null.
        /// </summary>
        /// <param name="fullName"></param>
        /// <param name="prefix"></param>
        /// <param name="finalFragment"></param>
        private static void SplitName(string fullName, out string prefix, out string finalFragment)
        {
            var index = fullName.Length - 1;
            while (index > 0 && !char.IsUpper(fullName[index]))
            {
                index--;
            }

            if (index > 0)
            {
                prefix = fullName.Substring(0, index);
                finalFragment = fullName.Substring(index);
            }
            else
            {
                prefix = fullName;
                finalFragment = null;
            }
        }

        /// <summary>
        /// Records that the supplied parameter needs to be renamed, after first checking
        /// that the proposed name does not conflict with any parameters processed so far.
        /// An alias mapping to the original name is applied, regardless of whether the
        /// name was remapped automatically or via the config file, unless the config file
        /// suppresses this using AutoApplyAlias="false" .
        /// </summary>
        /// <param name="property"></param>
        /// <param name="newName"></param>
        /// <param name="processedParameters"></param>
        private void RecordParameterRename(SimplePropertyInfo property, string newName)
        {
            var customization = property.Customization;
            if (customization == null)
            {
                customization = new Param
                {
                    Origin = Param.CustomizationOrigin.DuringGeneration,
                    Name = property.AnalyzedName
                };
                property.Customization = customization;
            }
            else
            {
                // if the customization came from the config but did not
                // specify an alternate name, reset it to record the name
                // was constructed automatically (generation time code
                // relies on this to determine the context member name)
                if (string.IsNullOrEmpty(customization.NewName))
                {
                    customization.Origin = Param.CustomizationOrigin.DuringGeneration;
                }
            }

            customization.NewName = newName;
        }

        private IEnumerable<SimplePropertyInfo> GetRootSimpleProperties(Type requestType)
        {
            List<SimplePropertyInfo> simpleProperties;
            if (!_rootSimplePropertiesCache.TryGetValue(requestType, out simpleProperties))
            {
                var properties = requestType.GetProperties();
                simpleProperties = properties
                    .Select(p => CreateSimplePropertyFor(p, null, true))
                    .Where(sp => sp != null && sp.IsReadWrite)
                    .ToList();
                _rootSimplePropertiesCache[requestType] = simpleProperties;
            }
            return simpleProperties;
        }

        private IEnumerable<SimplePropertyInfo> GetFlatProperties(Type requestType)
        {
            List<SimplePropertyInfo> flatProperties;
            if (!_flatPropertiesCache.TryGetValue(requestType, out flatProperties))
            {
                var properties = GetRootSimpleProperties(requestType);
                flatProperties = properties
                    .SelectMany(FlattenProperties)
                    .ToList();
                _flatPropertiesCache[requestType] = flatProperties;
            }

            return flatProperties;
        }

        private static IEnumerable<SimplePropertyInfo> FlattenProperties(SimplePropertyInfo property)
        {
            if (property.Children.Count > 0)
            {
                foreach (var child in property.Children)
                {
                    if (child.Children.Count == 0)
                    {
                        yield return child;
                    }
                    else
                    {
                        foreach (var flatProperty in FlattenProperties(child))
                        {
                            yield return flatProperty;
                        }
                    }
                }
            }
            else
            {
                yield return property;
            }
        }

        public static bool AreRequestFieldsPresent(IEnumerable<SimplePropertyInfo> requestProperties, params string[] fieldNames)
        {
            return fieldNames.All(fieldName
                        => requestProperties.FirstOrDefault(s => s.Name == fieldName) != null);
        }

        public static bool AreResultFieldsPresent(Type resultType, params string[] fieldNames)
        {
            // if the response type has a base class with matching prefix but ending
            // in 'Result', use that to get the real properties considered output. S3 does
            // not have this structure.
            var inspectedType = resultType;
            if (resultType.BaseType != null)
            {
                if (resultType.BaseType.Name.EndsWith("Result"))
                {
                    inspectedType = resultType.BaseType;
                }
            }

            var props = inspectedType.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
            return fieldNames.Intersect(props.Select(p => p.Name)).Count() == fieldNames.Count();
        }
    }
}