/// <summary>
        /// Creates the appropriate code objects needed to create the entry method for the
        /// current request.
        /// </summary>
        /// <param name="part">
        /// The <see cref="OpenXmlPart"/> object and relationship id to build code for.
        /// </param>
        /// <param name="settings">
        /// The <see cref="ISerializeSettings"/> to use during the code generation
        /// process.
        /// </param>
        /// /// <param name="typeCounts">
        /// A lookup <see cref="IDictionary{TKey, TValue}"/> object containing the
        /// number of times a given type was referenced.  This is used for variable naming
        /// purposes.
        /// </param>
        /// <param name="namespaces">
        /// Collection <see cref="ISet{T}"/> used to keep track of all openxml namespaces
        /// used during the process.
        /// </param>
        /// <param name="blueprints">
        /// The collection of <see cref="OpenXmlPartBluePrint"/> objects that have already been
        /// visited.
        /// </param>
        /// <param name="rootVar">
        /// The root variable name and <see cref="Type"/> to use when building code
        /// statements to create new <see cref="OpenXmlPart"/> objects.
        /// </param>
        /// <returns>
        /// A collection of code statements and expressions that could be used to generate
        /// a new <paramref name="part"/> object from code.
        /// </returns>
        public static CodeStatementCollection BuildEntryMethodCodeStatements(
            IdPartPair part,
            ISerializeSettings settings,
            IDictionary <string, int> typeCounts,
            ISet <string> namespaces,
            OpenXmlPartBluePrintCollection blueprints,
            KeyValuePair <string, Type> rootVar)
        {
            // Argument validation
            if (part is null)
            {
                throw new ArgumentNullException(nameof(part));
            }
            if (settings is null)
            {
                throw new ArgumentNullException(nameof(settings));
            }
            if (blueprints is null)
            {
                throw new ArgumentNullException(nameof(blueprints));
            }
            if (String.IsNullOrWhiteSpace(rootVar.Key))
            {
                throw new ArgumentNullException(nameof(rootVar.Key));
            }
            bool hasHandlers = settings?.Handlers != null;

            // Use the custom handler methods if present and provide actual code
            if (hasHandlers && settings.Handlers.TryGetValue(part.OpenXmlPart.GetType(), out IOpenXmlHandler h))
            {
                if (h is IOpenXmlPartHandler partHandler)
                {
                    var customStatements = partHandler.BuildEntryMethodCodeStatements(
                        part, settings, typeCounts, namespaces, blueprints, rootVar);

                    if (customStatements != null)
                    {
                        return(customStatements);
                    }
                }
            }

            var    result                   = new CodeStatementCollection();
            var    partType                 = part.OpenXmlPart.GetType();
            var    partTypeName             = partType.Name;
            var    partTypeFullName         = partType.FullName;
            string varName                  = partType.Name.ToCamelCase();
            bool   customAddNewPartRequired = CheckForCustomAddNewPartMethod(partType, rootVar.Value, out string addNewPartName);

#pragma warning disable IDE0018 // Inline variable declaration
            OpenXmlPartBluePrint bpTemp;
#pragma warning restore IDE0018 // Inline variable declaration

            CodeMethodReferenceExpression referenceExpression;
            CodeMethodInvokeExpression    invokeExpression;
            CodeMethodReferenceExpression methodReference;

            // Make sure that the namespace for the current part is captured
            namespaces.Add(partType.Namespace);

            // If the URI of the current part has already been included into
            // the blue prints collection, build the AddPart invocation
            // code statement and exit current method iteration.
            if (blueprints.TryGetValue(part.OpenXmlPart.Uri, out bpTemp))
            {
                // Surround this snippet with blank lines to make it
                // stand out in the current section of code.
                result.AddBlankLine();
                referenceExpression = new CodeMethodReferenceExpression(
                    new CodeVariableReferenceExpression(rootVar.Key), "AddPart",
                    new CodeTypeReference(part.OpenXmlPart.GetType().Name));
                invokeExpression = new CodeMethodInvokeExpression(referenceExpression,
                                                                  new CodeVariableReferenceExpression(bpTemp.VariableName),
                                                                  new CodePrimitiveExpression(part.RelationshipId));
                result.Add(invokeExpression);
                result.AddBlankLine();
                return(result);
            }

            // Assign the appropriate variable name
            if (typeCounts.ContainsKey(partTypeFullName))
            {
                varName = String.Concat(varName, typeCounts[partTypeFullName]++);
            }
            else
            {
                typeCounts.Add(partTypeFullName, 1);
            }

            // Setup the blueprint
            bpTemp = new OpenXmlPartBluePrint(part.OpenXmlPart, varName);

            // Need to evaluate the current OpenXmlPart type first to make sure the
            // correct "Add" statement is used as not all Parts can be initialized
            // using the "AddNewPart" method

            // Check for image part methods
            if (customAddNewPartRequired)
            {
                referenceExpression = new CodeMethodReferenceExpression(
                    new CodeVariableReferenceExpression(rootVar.Key), addNewPartName);
            }
            else
            {
                // Setup the add new part statement for the current OpenXmlPart object
                referenceExpression = new CodeMethodReferenceExpression(
                    new CodeVariableReferenceExpression(rootVar.Key), "AddNewPart",
                    new CodeTypeReference(partTypeName));
            }

            // Create the invoke expression
            invokeExpression = new CodeMethodInvokeExpression(referenceExpression);

            // Add content type to invoke method for embeddedpackage and image parts.
            if (part.OpenXmlPart is EmbeddedPackagePart || customAddNewPartRequired)
            {
                invokeExpression.Parameters.Add(
                    new CodePrimitiveExpression(part.OpenXmlPart.ContentType));
            }
            else if (!customAddNewPartRequired)
            {
                invokeExpression.Parameters.Add(
                    new CodePrimitiveExpression(part.RelationshipId));
            }

            result.Add(new CodeVariableDeclarationStatement(partTypeName, varName, invokeExpression));

            // Because the custom AddNewPart methods don't consistently take in a string relId
            // as a parameter, the id needs to be assigned after it is created.
            if (customAddNewPartRequired)
            {
                methodReference = new CodeMethodReferenceExpression(
                    new CodeVariableReferenceExpression(rootVar.Key), "ChangeIdOfPart");
                result.Add(new CodeMethodInvokeExpression(methodReference,
                                                          new CodeVariableReferenceExpression(varName),
                                                          new CodePrimitiveExpression(part.RelationshipId)));
            }

            // Add the call to the method to populate the current OpenXmlPart object
            methodReference = new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), bpTemp.MethodName);
            result.Add(new CodeMethodInvokeExpression(methodReference,
                                                      new CodeDirectionExpression(FieldDirection.Ref,
                                                                                  new CodeVariableReferenceExpression(varName))));

            // Add the appropriate code statements if the current part
            // contains any hyperlink relationships
            if (part.OpenXmlPart.HyperlinkRelationships.Count() > 0)
            {
                // Add a line break first for easier reading
                result.AddBlankLine();
                result.AddRange(
                    part.OpenXmlPart.HyperlinkRelationships.BuildHyperlinkRelationshipStatements(varName));
            }

            // Add the appropriate code statements if the current part
            // contains any non-hyperlink external relationships
            if (part.OpenXmlPart.ExternalRelationships.Count() > 0)
            {
                // Add a line break first for easier reading
                result.AddBlankLine();
                result.AddRange(
                    part.OpenXmlPart.ExternalRelationships.BuildExternalRelationshipStatements(varName));
            }

            // put a line break before going through the child parts
            result.AddBlankLine();

            // Add the current blueprint to the collection
            blueprints.Add(bpTemp);

            // Now check to see if there are any child parts for the current OpenXmlPart object.
            if (bpTemp.Part.Parts != null)
            {
                OpenXmlPartBluePrint childBluePrint;

                foreach (var p in bpTemp.Part.Parts)
                {
                    // If the current child object has already been created, simply add a reference to
                    // said object using the AddPart method.
                    if (blueprints.Contains(p.OpenXmlPart.Uri))
                    {
                        childBluePrint = blueprints[p.OpenXmlPart.Uri];

                        referenceExpression = new CodeMethodReferenceExpression(
                            new CodeVariableReferenceExpression(varName), "AddPart",
                            new CodeTypeReference(p.OpenXmlPart.GetType().Name));

                        invokeExpression = new CodeMethodInvokeExpression(referenceExpression,
                                                                          new CodeVariableReferenceExpression(childBluePrint.VariableName),
                                                                          new CodePrimitiveExpression(p.RelationshipId));

                        result.Add(invokeExpression);
                        continue;
                    }

                    // If this is a new part, call this method with the current part's details
                    result.AddRange(BuildEntryMethodCodeStatements(p, settings, typeCounts, namespaces, blueprints,
                                                                   new KeyValuePair <string, Type>(varName, partType)));
                }
            }

            return(result);
        }
        /// <summary>
        /// Converts an <see cref="OpenXmlPackage"/> into a CodeDom object that can be used
        /// to build code in a given .NET language to build the referenced <paramref name="pkg"/>.
        /// </summary>
        /// <param name="pkg">
        /// The <see cref="OpenXmlPackage"/> object to generate source code for.
        /// </param>
        /// <param name="settings">
        /// The <see cref="ISerializeSettings"/> to use during the code generation
        /// process.
        /// </param>
        /// <returns>
        /// A new <see cref="CodeCompileUnit"/> containing the instructions to build
        /// the referenced <see cref="OpenXmlPackage"/>.
        /// </returns>
        public static CodeCompileUnit GenerateSourceCode(this OpenXmlPackage pkg, ISerializeSettings settings)
        {
            const string pkgVarName = "pkg";
            const string paramName = "pathToFile";
            var result = new CodeCompileUnit();
            var pkgType = pkg.GetType();
            var pkgTypeName = pkgType.Name;
            var partTypeCounts = new Dictionary<string, int>();
            var namespaces = new SortedSet<string>();
            var mainNamespace = new CodeNamespace(settings.NamespaceName);
            var bluePrints = new OpenXmlPartBluePrintCollection();
            CodeConditionStatement conditionStatement;
            CodeMemberMethod entryPoint;
            CodeMemberMethod createParts;
            CodeTypeDeclaration mainClass;
            CodeTryCatchFinallyStatement tryAndCatch;
            CodeFieldReferenceExpression docTypeVarRef = null;
            Type docTypeEnum;
            string docTypeEnumVal;
            KeyValuePair<string, Type> rootVarType;

            // Add all initial namespace names first
            namespaces.Add("System");

            // The OpenXmlDocument derived parts all contain a property called "DocumentType"
            // but the property types differ depending on the derived part.  Need to get both
            // the enum name of selected value to use as a parameter for the Create statement.
            switch (pkg)
            {
                case PresentationDocument p:
                    docTypeEnum = p.DocumentType.GetType();
                    docTypeEnumVal = p.DocumentType.ToString();
                    break;
                case SpreadsheetDocument s:
                    docTypeEnum = s.DocumentType.GetType();
                    docTypeEnumVal = s.DocumentType.ToString();
                    break;
                case WordprocessingDocument w:
                    docTypeEnum = w.DocumentType.GetType();
                    docTypeEnumVal = w.DocumentType.ToString();
                    break;
                default:
                    throw new ArgumentException("object is not a recognized OpenXmlPackage type.", nameof(pkg));
            }

            // Create the entry method
            entryPoint = new CodeMemberMethod()
            {
                Name = "CreatePackage",
                ReturnType = new CodeTypeReference(),
                Attributes = MemberAttributes.Public | MemberAttributes.Final
            };
            entryPoint.Parameters.Add(
                new CodeParameterDeclarationExpression(typeof(string).Name, paramName));

            // Create package declaration expression first
            entryPoint.Statements.Add(new CodeVariableDeclarationStatement(pkgTypeName, pkgVarName,
                new CodePrimitiveExpression(null)));

            // Add the required DocumentType parameter here, if available
            if (docTypeEnum != null)
            {
                namespaces.Add(docTypeEnum.Namespace);

                var simpleFieldRef = new CodeVariableReferenceExpression(
                    docTypeEnum.GetObjectTypeName(settings.NamespaceAliasOptions.Order));
                docTypeVarRef = new CodeFieldReferenceExpression(simpleFieldRef, docTypeEnumVal);
            }

            // initialize package var
            var pkgCreateMethod = new CodeMethodReferenceExpression(
                new CodeTypeReferenceExpression(pkgTypeName),
                "Create");
            var pkgCreateInvoke = new CodeMethodInvokeExpression(pkgCreateMethod,
                new CodeArgumentReferenceExpression(paramName),
                docTypeVarRef);
            var initializePkg = new CodeAssignStatement(
                new CodeVariableReferenceExpression(pkgVarName),
                pkgCreateInvoke);

            // Call CreateParts method
            var partsCreateMethod = new CodeMethodReferenceExpression(
                new CodeThisReferenceExpression(),
                "CreateParts");
            var partsCreateInvoke = new CodeMethodInvokeExpression(
                partsCreateMethod,
                new CodeDirectionExpression(FieldDirection.Ref,
                    new CodeVariableReferenceExpression(pkgVarName)));

            // Clean up pkg var
            var pkgDisposeMethod = new CodeMethodReferenceExpression(
                new CodeVariableReferenceExpression(pkgVarName),
                "Dispose");
            var pkgDisposeInvoke = new CodeMethodInvokeExpression(
                pkgDisposeMethod);

            // Setup the try/catch statement to properly initialize the pkg var
            tryAndCatch = new CodeTryCatchFinallyStatement();

            // Try statements
            tryAndCatch.TryStatements.Add(initializePkg);
            tryAndCatch.TryStatements.AddBlankLine();
            tryAndCatch.TryStatements.Add(partsCreateInvoke);

            // If statement to ensure pkgVarName is not null before trying to dispose
            conditionStatement = new CodeConditionStatement(
                new CodeBinaryOperatorExpression(
                    new CodeVariableReferenceExpression(pkgVarName),
                    CodeBinaryOperatorType.IdentityInequality,
                    new CodePrimitiveExpression(null)));

            conditionStatement.TrueStatements.Add(pkgDisposeInvoke);

            // Finally statements
            tryAndCatch.FinallyStatements.Add(conditionStatement);
            entryPoint.Statements.Add(tryAndCatch);

            // Create the CreateParts method
            createParts = new CodeMemberMethod()
            {
                Name = "CreateParts",
                ReturnType = new CodeTypeReference(),
                Attributes = MemberAttributes.Private | MemberAttributes.Final
            };
            createParts.Parameters.Add(new CodeParameterDeclarationExpression(pkgTypeName, pkgVarName)
                { Direction = FieldDirection.Ref });

            // Add all of the child part references here
            if (pkg.Parts != null)
            {
                var customNewPartTypes = new Type[] { typeof(PresentationPart), typeof(WorkbookPart), typeof(MainDocumentPart) };
                OpenXmlPartBluePrint bpTemp;
                CodeMethodReferenceExpression referenceExpression;
                CodeMethodInvokeExpression invokeExpression;
                CodeMethodReferenceExpression methodReference;
                Type currentPartType;
                string varName = null;
                string initMethodName = null;
                string mainPartTypeName;

                foreach (var pair in pkg.Parts)
                {
                    // Need special handling rules for WorkbookPart, MainDocumentPart, and PresentationPart
                    // objects.  They cannot be created using the usual "AddNewPart" methods, unfortunately.
                    currentPartType = pair.OpenXmlPart.GetType();
                    if (customNewPartTypes.Contains(currentPartType))
                    {
                        namespaces.Add(currentPartType.Namespace);
                        mainPartTypeName = currentPartType.Name;
                        if (pair.OpenXmlPart is PresentationPart)
                        {
                            varName = "presentationPart";
                            initMethodName = "AddPresentationPart";
                        }
                        else if (pair.OpenXmlPart is WorkbookPart)
                        {
                            varName = "workbookPart";
                            initMethodName = "AddWorkbookPart";
                        }
                        else if (pair.OpenXmlPart is MainDocumentPart)
                        {
                            varName = "mainDocumentPart";
                            initMethodName = "AddMainDocumentPart";
                        }
                        rootVarType = new KeyValuePair<string, Type>(varName, currentPartType);

                        // Setup the blueprint
                        bpTemp = new OpenXmlPartBluePrint(pair.OpenXmlPart, varName);

                        // Setup the add new part statement for the current OpenXmlPart object
                        referenceExpression = new CodeMethodReferenceExpression(
                            new CodeArgumentReferenceExpression(pkgVarName), initMethodName);

                        invokeExpression = new CodeMethodInvokeExpression(referenceExpression);

                        createParts.Statements.Add(new CodeVariableDeclarationStatement(mainPartTypeName, varName, invokeExpression));

                        // Add the call to the method to populate the current OpenXmlPart object
                        methodReference = new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), bpTemp.MethodName);
                        createParts.Statements.Add(new CodeMethodInvokeExpression(methodReference,
                            new CodeDirectionExpression(FieldDirection.Ref, new CodeVariableReferenceExpression(varName))));

                        // Add the current main part to the collection of blueprints to ensure that the appropriate 'Generate*'
                        // method is added to the collection of helper methods.
                        bluePrints.Add(bpTemp);

                        // Add a blank line for clarity
                        createParts.Statements.AddBlankLine();

                        // now create the child parts for the current one an continue the loop to avoid creating
                        // an additional invalid 'AddNewPart' method for the current main part.
                        foreach (var child in pair.OpenXmlPart.Parts)
                        {
                            createParts.Statements.AddRange(
                                OpenXmlPartExtensions.BuildEntryMethodCodeStatements(
                                    child, settings, partTypeCounts, namespaces, bluePrints, rootVarType));
                        }
                        continue;
                    }

                    rootVarType = new KeyValuePair<string, Type>(pkgVarName, pkgType);
                    createParts.Statements.AddRange(
                        OpenXmlPartExtensions.BuildEntryMethodCodeStatements(
                            pair, settings, partTypeCounts, namespaces, bluePrints, rootVarType));
                }
            }

            // Setup the main class next
            mainClass = new CodeTypeDeclaration($"{pkgTypeName}BuilderClass")
            {
                IsClass = true,
                Attributes = MemberAttributes.Public
            };

            // Setup the main class members
            mainClass.Members.Add(entryPoint);
            mainClass.Members.Add(createParts);
            mainClass.Members.AddRange(OpenXmlPartExtensions.BuildHelperMethods
                (bluePrints, settings, namespaces));

            // Setup the imports
            var codeNameSpaces = new List<CodeNamespaceImport>(namespaces.Count);
            foreach (var ns in namespaces)
            {
                codeNameSpaces.Add(ns.GetCodeNamespaceImport(settings.NamespaceAliasOptions));
            }
            codeNameSpaces.Sort(new CodeNamespaceImportComparer());

            mainNamespace.Imports.AddRange(codeNameSpaces.ToArray());
            mainNamespace.Types.Add(mainClass);

            // Finish up
            result.Namespaces.Add(mainNamespace);
            return result;
        }
        /// <summary>
        /// Converts an <see cref="OpenXmlPart"/> into a CodeDom object that can be used
        /// to build code in a given .NET language to build the referenced <see cref="OpenXmlPart"/>.
        /// </summary>
        /// <param name="part">
        /// The <see cref="OpenXmlPart"/> object to generate source code for.
        /// </param>
        /// <param name="settings">
        /// The <see cref="ISerializeSettings"/> to use during the code generation
        /// process.
        /// </param>
        /// <returns>
        /// A new <see cref="CodeCompileUnit"/> containing the instructions to build
        /// the referenced <see cref="OpenXmlPart"/>.
        /// </returns>
        public static CodeCompileUnit GenerateSourceCode(this OpenXmlPart part, ISerializeSettings settings)
        {
            CodeMethodReferenceExpression methodRef;
            OpenXmlPartBluePrint          mainBluePrint;
            var result           = new CodeCompileUnit();
            var eType            = part.GetType();
            var partTypeName     = eType.Name;
            var partTypeFullName = eType.FullName;
            var varName          = eType.Name.ToCamelCase();
            var partTypeCounts   = new Dictionary <string, int>();
            var namespaces       = new SortedSet <string>();
            var mainNamespace    = new CodeNamespace(settings.NamespaceName);
            var bluePrints       = new OpenXmlPartBluePrintCollection();

            // Assign the appropriate variable name
            if (partTypeCounts.ContainsKey(partTypeFullName))
            {
                varName = String.Concat(varName, partTypeCounts[partTypeFullName]++);
            }
            else
            {
                partTypeCounts.Add(partTypeFullName, 1);
            }

            // Generate a new blue print for the current part to help create the main
            // method reference then add it to the blue print collection
            mainBluePrint = new OpenXmlPartBluePrint(part, varName);
            bluePrints.Add(mainBluePrint);
            methodRef = new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), mainBluePrint.MethodName);

            // Build the entry method
            var entryMethod = new CodeMemberMethod()
            {
                Name       = $"Create{partTypeName}",
                ReturnType = new CodeTypeReference(),
                Attributes = MemberAttributes.Public | MemberAttributes.Final
            };

            entryMethod.Parameters.Add(
                new CodeParameterDeclarationExpression(partTypeName, methodParamName)
            {
                Direction = FieldDirection.Ref
            });

            // Add all of the child part references here
            if (part.Parts != null)
            {
                var rootPartPair = new KeyValuePair <string, Type>(methodParamName, eType);
                foreach (var pair in part.Parts)
                {
                    entryMethod.Statements.AddRange(BuildEntryMethodCodeStatements(
                                                        pair, settings, partTypeCounts, namespaces, bluePrints, rootPartPair));
                }
            }

            entryMethod.Statements.Add(new CodeMethodInvokeExpression(methodRef,
                                                                      new CodeArgumentReferenceExpression(methodParamName)));

            // Setup the main class next
            var mainClass = new CodeTypeDeclaration($"{eType.Name}BuilderClass")
            {
                IsClass    = true,
                Attributes = MemberAttributes.Public
            };

            mainClass.Members.Add(entryMethod);
            mainClass.Members.AddRange(BuildHelperMethods(bluePrints, settings, namespaces));

            // Setup the imports
            var codeNameSpaces = new List <CodeNamespaceImport>(namespaces.Count);

            foreach (var ns in namespaces)
            {
                codeNameSpaces.Add(ns.GetCodeNamespaceImport(settings.NamespaceAliasOptions));
            }
            codeNameSpaces.Sort(new CodeNamespaceImportComparer());

            mainNamespace.Imports.AddRange(codeNameSpaces.ToArray());
            mainNamespace.Types.Add(mainClass);

            // Finish up
            result.Namespaces.Add(mainNamespace);
            return(result);
        }