internal static void PopulateFieldMappings(string entityName, string currentPath, Type?ellieType, EntitySchema entitySchema, EntitySchema?previousEntitySchema, Dictionary <string, EntitySchema> entityTypes, Dictionary <string, StandardFieldInfo> fields, Dictionary <string, StandardFieldInfo> fieldPatterns, bool extendedFieldInfo, PopulateFieldMappingsAction?action, Func <string, string>?descriptionRetriever, TextWriter?output)
        {
            var requiredProperties = new HashSet <string>();
            var serializeWholeList = false;

            var properties = entitySchema.Properties;

            foreach (var pair in properties)
            {
                var propertyName      = pair.Key;
                var propertySchema    = pair.Value;
                var description       = propertySchema.Description;
                var ellieProperty     = ellieType?.GetTypeInfo().GetDeclaredProperty(propertyName);
                var elliePropertyType = ellieProperty?.PropertyType;
                var fieldId           = propertySchema.FieldId;
                if (!string.IsNullOrEmpty(fieldId))
                {
                    if (string.IsNullOrEmpty(description) && descriptionRetriever != null)
                    {
                        try
                        {
                            description = descriptionRetriever(fieldId);
                        }
                        catch
                        {
                        }
                    }
                    var fieldInfo = new StandardFieldInfo(fieldId, $"{currentPath}.{propertyName}")
                    {
                        Description = description
                    };
                    fieldInfo.NonSerializedFormat = GetFormat(propertySchema);
                    if (extendedFieldInfo)
                    {
                        fieldInfo.Format   = fieldInfo.NonSerializedFormat;
                        fieldInfo.Options  = GetOptions(propertySchema);
                        fieldInfo.ReadOnly = propertySchema.ReadOnly == true;
                    }
                    fields.Add(fieldId, fieldInfo);
                }
                else if (propertySchema.Type == PropertySchemaType.Entity && entityTypes.TryGetValue(propertySchema.EntityType !, out var nestedEntitySchema))
                {
                    if (currentPath != "Loan.CurrentApplication" || propertyName != "ATRQMBorrower")
                    {
                        entityTypes.Remove(propertySchema.EntityType !);
                        if (ellieType?.Name == "LoanContract" && propertyName == "CurrentApplication")
                        {
                            elliePropertyType = ellieType.GetTypeInfo().GetDeclaredProperty("Applications").PropertyType.GetTypeInfo().ImplementedInterfaces.First(i => i.Name == "IEnumerable`1").GenericTypeArguments[0];
                        }
                        else if (ellieType != null && elliePropertyType == null)
                        {
                            output?.WriteLine($"Did not get ellie type for {currentPath}.{propertyName} of type {propertySchema.EntityType.Value}");
                        }
                        PopulateFieldMappings(propertySchema.EntityType !, $"{currentPath}.{propertyName}", elliePropertyType, nestedEntitySchema, entitySchema, entityTypes, fields, fieldPatterns, extendedFieldInfo, action, descriptionRetriever, output);
                    }
                }
        internal static void PopulateFieldMappings(string entityName, string currentPath, Type ellieType, EntitySchema entitySchema, EntitySchema previousEntitySchema, Dictionary <string, EntitySchema> entityTypes, Dictionary <string, StandardFieldInfo> fields, Dictionary <string, StandardFieldInfo> fieldPatterns, bool extendedFieldInfo, PopulateFieldMappingsAction action, Func <string, string> descriptionRetriever, TextWriter output)
        {
            var requiredProperties = new HashSet <string>();
            var serializeWholeList = false;

            var properties = entitySchema.Properties;

            foreach (var pair in properties)
            {
                var propertyName      = pair.Key;
                var propertySchema    = pair.Value;
                var description       = propertySchema.Description;
                var ellieProperty     = ellieType?.GetTypeInfo().GetDeclaredProperty(propertyName);
                var elliePropertyType = ellieProperty?.PropertyType;
                var fieldId           = propertySchema.FieldId;
                if (!string.IsNullOrEmpty(fieldId))
                {
                    if (string.IsNullOrEmpty(description) && descriptionRetriever != null)
                    {
                        try
                        {
                            description = descriptionRetriever(fieldId);
                        }
                        catch
                        {
                        }
                    }
                    var fieldInfo = new StandardFieldInfo {
                        FieldId = fieldId, ModelPath = $"{currentPath}.{propertyName}", Description = description
                    };
                    fieldInfo.NonSerializedFormat = GetFormat(propertySchema);
                    if (extendedFieldInfo)
                    {
                        fieldInfo.Format   = fieldInfo.NonSerializedFormat;
                        fieldInfo.Options  = GetOptions(propertySchema);
                        fieldInfo.ReadOnly = propertySchema.ReadOnly == true;
                    }
                    fields.Add(fieldId, fieldInfo);
                }
                else if (propertySchema.Type == PropertySchemaType.Entity && entityTypes.TryGetValue(propertySchema.EntityType, out var nestedEntitySchema))
                {
                    if (currentPath != "Loan.CurrentApplication" || propertyName != "ATRQMBorrower")
                    {
                        entityTypes.Remove(propertySchema.EntityType);
                        if (ellieType?.Name == "LoanContract" && propertyName == "CurrentApplication")
                        {
                            elliePropertyType = ellieType.GetTypeInfo().GetDeclaredProperty("Applications").PropertyType.GetTypeInfo().ImplementedInterfaces.First(i => i.Name == "IEnumerable`1").GenericTypeArguments[0];
                        }
                        else if (ellieType != null && elliePropertyType == null)
                        {
                            output?.WriteLine($"Did not get ellie type for {currentPath}.{propertyName} of type {propertySchema.EntityType.Value}");
                        }
                        PopulateFieldMappings(propertySchema.EntityType, $"{currentPath}.{propertyName}", elliePropertyType, nestedEntitySchema, entitySchema, entityTypes, fields, fieldPatterns, extendedFieldInfo, action, descriptionRetriever, output);
                    }
                }
                else if ((propertySchema.Type == PropertySchemaType.List || propertySchema.Type == PropertySchemaType.Set) && entityTypes.TryGetValue(propertySchema.ElementType, out var elementEntitySchema))
                {
                    if (currentPath != "Loan" || propertyName != "Applications")
                    {
                        entityTypes.Remove(propertySchema.ElementType);
                        if (ellieProperty != null)
                        {
                            elliePropertyType = elliePropertyType.GetTypeInfo().ImplementedInterfaces.First(i => i.Name == "IEnumerable`1").GenericTypeArguments[0];
                        }
                        else if (ellieType != null)
                        {
                            output?.WriteLine($"Did not get ellie type for {currentPath}.{propertyName} of type {propertySchema.ElementType.Value}");
                        }
                        PopulateFieldMappings(propertySchema.ElementType, $"{currentPath}.{propertyName}", elliePropertyType, elementEntitySchema, entitySchema, entityTypes, fields, fieldPatterns, extendedFieldInfo, action, descriptionRetriever, output);
                    }
                }
                else
                {
                    if (propertySchema.FieldInstances != null)
                    {
                        foreach (var fieldInstancePair in propertySchema.FieldInstances)
                        {
                            fieldId = fieldInstancePair.Key;
                            if (descriptionRetriever != null)
                            {
                                try
                                {
                                    description = descriptionRetriever(fieldId);
                                }
                                catch
                                {
                                }
                            }
                            string modelPath = null;
                            if (fieldInstancePair.Value.Count != 1)
                            {
                                output?.WriteLine($"There must be just one field instance value for {fieldId}");
                            }
                            var fieldInstancePath = fieldInstancePair.Value[0];
                            if (fieldInstancePath == "Borrower" || fieldInstancePath == "Coborrower")
                            {
                                modelPath = $"{currentPath.Substring(0, currentPath.LastIndexOf('.'))}.{fieldInstancePath}.{propertyName}";
                            }
                            else
                            {
                                var firstUnderscore  = fieldInstancePath.IndexOf('_');
                                var secondUnderscore = fieldInstancePath.IndexOf('_', firstUnderscore + 1);
                                var listPropertyName = fieldInstancePath.Substring(firstUnderscore + 1, secondUnderscore - firstUnderscore - 1);

                                var listPropertySchema = previousEntitySchema.Properties[listPropertyName];

                                Instance instance = null;
                                if (listPropertySchema.Instances?.TryGetValue(fieldInstancePath, out instance) == true)
                                {
                                    switch (instance)
                                    {
                                    case IntListInstance intListInstance:
                                        if (intListInstance.Count != 1)
                                        {
                                            output?.WriteLine($"There must be just one int list instance value for {fieldInstancePath}");
                                        }
                                        serializeWholeList = true;
                                        modelPath          = $"{currentPath}[{intListInstance[0]}].{propertyName}";
                                        break;

                                    case StringListInstance stringListInstance:
                                        var index = 0;
                                        foreach (var s in stringListInstance)
                                        {
                                            var keyPropertyName = listPropertySchema.KeyProperties[index];
                                            requiredProperties.Add(keyPropertyName);
                                            var property      = properties[keyPropertyName];
                                            var allowedValues = property.AllowedValues;
                                            if (allowedValues == null)
                                            {
                                                allowedValues          = new List <FieldOption>();
                                                property.AllowedValues = allowedValues;
                                            }
                                            var option = new FieldOption(s);
                                            if (!allowedValues.Contains(option))
                                            {
                                                allowedValues.Add(option);
                                            }
                                            ++index;
                                        }

                                        modelPath = $"{currentPath}[({string.Join(" && ", stringListInstance.Select((s, i) => Tuple.Create(s, i)).Where(t => !string.IsNullOrEmpty(t.Item1)).OrderBy(t => listPropertySchema.KeyProperties[t.Item2]).Select(t => $"{listPropertySchema.KeyProperties[t.Item2]} == '{t.Item1}'"))})].{propertyName}";
                                        break;

                                    case StringDictionaryInstance stringDictionaryInstance:
                                        foreach (var p in stringDictionaryInstance)
                                        {
                                            var keyPropertyName = p.Key;
                                            requiredProperties.Add(keyPropertyName);
                                            var property      = properties[keyPropertyName];
                                            var allowedValues = property.AllowedValues;
                                            if (allowedValues == null)
                                            {
                                                allowedValues          = new List <FieldOption>();
                                                property.AllowedValues = allowedValues;
                                            }
                                            var option = new FieldOption(p.Value);
                                            if (!allowedValues.Contains(option))
                                            {
                                                allowedValues.Add(option);
                                            }
                                        }

                                        modelPath = $"{currentPath}[({string.Join(" && ", stringDictionaryInstance.OrderBy(p => p.Key).Select(p => $"{p.Key} == '{p.Value}'"))})].{propertyName}";
                                        break;

                                    default:
                                        output?.WriteLine($"Bad instance type for {fieldInstancePath}");
                                        break;
                                    }
                                }
                                else
                                {
                                    serializeWholeList = true;
                                    var             colonIndex      = fieldInstancePath.LastIndexOf(':');
                                    var             index           = 0;
                                    InstancePattern instancePattern = null;
                                    if (colonIndex < 0 || !int.TryParse(fieldInstancePath.Substring(colonIndex + 1), NumberStyles.None, null, out index) || listPropertySchema.InstancePatterns?.TryGetValue(fieldInstancePath.Substring(0, colonIndex), out instancePattern) != true)
                                    {
                                        output?.WriteLine($"[{fieldId}]: {fieldInstancePath}");
                                    }

                                    if (instancePattern.Match != null)
                                    {
                                        foreach (var p in instancePattern.Match)
                                        {
                                            var keyPropertyName = p.Key;
                                            requiredProperties.Add(keyPropertyName);
                                            var property      = properties[keyPropertyName];
                                            var allowedValues = property.AllowedValues;
                                            if (allowedValues == null)
                                            {
                                                allowedValues          = new List <FieldOption>();
                                                property.AllowedValues = allowedValues;
                                            }
                                            var option = new FieldOption(p.Value);
                                            if (!allowedValues.Contains(option))
                                            {
                                                allowedValues.Add(option);
                                            }
                                        }
                                    }

                                    modelPath = $"{currentPath.Substring(0, currentPath.LastIndexOf('.'))}.{listPropertyName}{(instancePattern.Match != null ? $"[({string.Join(" && ", instancePattern.Match.OrderBy(p => p.Key).Select(p => $"{p.Key} == '{p.Value}'"))})]" : string.Empty)}[{index}].{propertyName}";
                                }
                            }
                            var fieldInfo = new StandardFieldInfo {
                                FieldId = fieldId, Description = description, ModelPath = modelPath
                            };
                            fieldInfo.NonSerializedFormat = GetFormat(propertySchema);
                            if (extendedFieldInfo)
                            {
                                fieldInfo.Format   = fieldInfo.NonSerializedFormat;
                                fieldInfo.Options  = GetOptions(propertySchema);
                                fieldInfo.ReadOnly = propertySchema.ReadOnly == true;
                            }
                            fields.Add(fieldId, fieldInfo);
                        }
                    }
                    if (propertySchema.FieldPatterns != null)
                    {
                        var i = 1;
                        foreach (var fieldPatternPair in propertySchema.FieldPatterns)
                        {
                            var fieldPattern = fieldPatternPair.Key;
                            if (fieldPattern != "URLAROLNN06" && !fieldPattern.StartsWith("CUSTNN"))
                            {
                                serializeWholeList = true;
                                if (fieldPatternPair.Value.Count != 1)
                                {
                                    output?.WriteLine($"There must be just one field pattern value for {fieldPattern}");
                                }
                                var fieldPatternPath = fieldPatternPair.Value[0];

                                var firstUnderscore  = fieldPatternPath.IndexOf('_');
                                var secondUnderscore = fieldPatternPath.IndexOf('_', firstUnderscore + 1);
                                var listPropertyName = fieldPatternPath.Substring(firstUnderscore + 1, secondUnderscore - firstUnderscore - 1);

                                var listPropertySchema = previousEntitySchema.Properties[listPropertyName];

                                var instancePattern = listPropertySchema.InstancePatterns[fieldPatternPath];

                                if (instancePattern.Match != null)
                                {
                                    foreach (var p in instancePattern.Match)
                                    {
                                        var keyPropertyName = p.Key;
                                        requiredProperties.Add(keyPropertyName);
                                        var property      = properties[keyPropertyName];
                                        var allowedValues = property.AllowedValues;
                                        if (allowedValues == null)
                                        {
                                            allowedValues          = new List <FieldOption>();
                                            property.AllowedValues = allowedValues;
                                        }
                                        var option = new FieldOption(p.Value);
                                        if (!allowedValues.Contains(option))
                                        {
                                            allowedValues.Add(option);
                                        }
                                    }
                                }

                                fieldId = fieldPattern.StartsWith("NBOCNB") ? fieldPattern.Replace("NBOCNB", "NBOC{0:00}") : fieldPattern.Replace("NN", "{0:00}");

                                if (descriptionRetriever != null)
                                {
                                    try
                                    {
                                        description = descriptionRetriever(string.Format(fieldId, 1));
                                        description = description.Replace(" #11", " #{0}").Replace(" #1", " #{0}");
                                    }
                                    catch
                                    {
                                    }
                                }

                                string modelPath;
                                if (fieldPatternPath == "Application_Assets_NN")
                                {
                                    modelPath = $"Loan.CurrentApplication.Assets[(VodIndex == '{{0}}')]{(propertySchema.FieldPatterns.Count == 4 ? $"[{i}]" : string.Empty)}.{propertyName}";
                                    requiredProperties.Add(nameof(Asset.VodIndex));
                                }
                                else
                                {
                                    modelPath = $"{currentPath.Substring(0, currentPath.LastIndexOf('.'))}.{listPropertyName}{(instancePattern.Match != null ? $"[({string.Join(" && ", instancePattern.Match.OrderBy(p => p.Key).Select(p => $"{p.Key} == '{p.Value}'"))})]" : string.Empty)}[{{0}}].{propertyName}";
                                }

                                var fieldInfo = new StandardFieldInfo {
                                    FieldId = fieldId, Description = description, ModelPath = modelPath
                                };
                                fieldInfo.NonSerializedFormat = GetFormat(propertySchema);
                                if (extendedFieldInfo)
                                {
                                    fieldInfo.Format   = fieldInfo.NonSerializedFormat;
                                    fieldInfo.Options  = GetOptions(propertySchema);
                                    fieldInfo.ReadOnly = propertySchema.ReadOnly == true;
                                }
                                fieldPatterns.Add(fieldId, fieldInfo);
                            }
                            ++i;
                        }
                    }
                }
            }

            action?.Invoke(entityName, ellieType, entitySchema, requiredProperties, serializeWholeList);
        }