/// This writes out the IR, editor state cache, and potentially component templates
        /// for a single top level control, such as the App object, a screen, or component
        /// Name refers to the control name
        private static void WriteTopParent(
            DirectoryWriter dir,
            CanvasDocument app,
            string name,
            BlockNode ir,
            string subDir)
        {
            var controlName = name;
            var text        = PAWriterVisitor.PrettyPrint(ir);

            string filename = controlName + ".pa.yaml";


            dir.WriteAllText(subDir, filename, text);

            var extraData = new Dictionary <string, ControlState>();

            foreach (var item in app._editorStateStore.GetControlsWithTopParent(controlName))
            {
                extraData.Add(item.Name, item);
            }

            // Write out of all the other state for roundtripping
            string extraContent = controlName + ".editorstate.json";

            dir.WriteAllJson(EditorStateDir, extraContent, extraData);

            // Write out component templates next to the component
            if (app._templateStore.TryGetTemplate(name, out var templateState))
            {
                dir.WriteAllJson(subDir, controlName + ".json", templateState);
            }
        }
        /// This writes out the IR, editor state cache, and potentially component templates
        /// for a single top level control, such as the App object, a screen, or component
        /// Name refers to the control name
        /// Only in case of AppTest, the topParentName is passed down, since for AppTest the TestSuites are sharded into individual files.
        /// We truncate the control names to limit it to 50 charactes length (escaped name).
        private static void WriteTopParent(
            DirectoryWriter dir,
            CanvasDocument app,
            string name,
            BlockNode ir,
            string subDir,
            string topParentName = null)
        {
            var controlName    = name;
            var newControlName = Utilities.TruncateNameIfTooLong(controlName);

            string filename = newControlName + ".fx.yaml";

            // For AppTest control shard each test suite into individual files.
            if (controlName == AppTestControlName)
            {
                foreach (var child in ir.Children)
                {
                    WriteTopParent(dir, app, child.Properties.FirstOrDefault(x => x.Identifier == "DisplayName").Expression.Expression.Trim(new char[] { '"' }), child, subDir, controlName);
                }

                // Clear the children since they have already been sharded into their individual files.
                ir.Children.Clear();
            }

            var text = PAWriterVisitor.PrettyPrint(ir);

            dir.WriteAllText(subDir, filename, text);

            // For TestSuite controls, only the top parent control has an editor state created.
            // For other control types, create an editor state.
            if (string.IsNullOrEmpty(topParentName))
            {
                string editorStateFilename = $"{newControlName}.editorstate.json";

                var controlStates = new Dictionary <string, ControlState>();
                foreach (var item in app._editorStateStore.GetControlsWithTopParent(controlName))
                {
                    controlStates.Add(item.Name, item);
                }

                ControlTreeState editorState = new ControlTreeState
                {
                    ControlStates = controlStates,
                    TopParentName = controlName
                };

                // Write out of all the other state properties on the control for roundtripping.
                dir.WriteAllJson(EditorStateDir, editorStateFilename, editorState);
            }

            // Write out component templates next to the component
            if (app._templateStore.TryGetTemplate(name, out var templateState))
            {
                dir.WriteAllJson(subDir, newControlName + ".json", templateState);
            }
        }
        /// This writes out the IR, editor state cache, and potentially component templates
        /// for a single top level control, such as the App object, a screen, or component
        /// Name refers to the control name
        /// Only in case of AppTest, the topParentName is passed down, since for AppTest the TestSuites are sharded into individual files.
        /// We truncate the control names to limit it to 50 charactes length (escaped name).
        private static void WriteTopParent(
            DirectoryWriter dir,
            CanvasDocument app,
            string name,
            BlockNode ir,
            string subDir,
            string topParentname = null)
        {
            var controlName    = name;
            var newControlName = Utilities.TruncateNameIfTooLong(controlName);

            string filename = newControlName + ".fx.yaml";

            // For AppTest control shard each test suite into individual file.
            if (controlName == AppTestControlName)
            {
                foreach (var child in ir.Children)
                {
                    WriteTopParent(dir, app, child.Properties.FirstOrDefault(x => x.Identifier == "DisplayName").Expression.Expression.Trim(new char[] { '"' }), child, subDir, controlName);
                }

                // Clear the children since they have already been sharded into their individual files.
                ir.Children.Clear();
            }

            var text = PAWriterVisitor.PrettyPrint(ir);

            dir.WriteAllText(subDir, filename, text);

            var extraData = new Dictionary <string, ControlState>();

            foreach (var item in app._editorStateStore.GetControlsWithTopParent(topParentname ?? controlName))
            {
                extraData.Add(item.Name, item);
            }

            // Write out of all the other state for roundtripping
            string extraContent = (topParentname ?? newControlName) + ".editorstate.json";

            // We write editorstate.json file per top parent control, and hence for the TestSuite control since it is not a top parent
            // use the top parent name (i.e. Test_7F478737223C4B69) to create the editorstate.json file.
            if (!dir.FileExists(EditorStateDir, extraContent))
            {
                dir.WriteAllJson(EditorStateDir, extraContent, extraData);
            }

            // Write out component templates next to the component
            if (app._templateStore.TryGetTemplate(name, out var templateState))
            {
                dir.WriteAllJson(subDir, newControlName + ".json", templateState);
            }
        }
        private static void WriteDataSources(DirectoryWriter dir, CanvasDocument app, ErrorContainer errors)
        {
            // Data Sources  - write out each individual source.
            HashSet <string> filenames = new HashSet <string>();

            foreach (var kvp in app.GetDataSources())
            {
                // Filename doesn't actually matter, but careful to avoid collisions and overwriting.
                // Also be determinstic.
                string filename = kvp.Key + ".json";

                if (!filenames.Add(filename.ToLower()))
                {
                    int index       = 1;
                    var altFileName = kvp.Key + "_" + index + ".json";
                    while (!filenames.Add(altFileName.ToLower()))
                    {
                        ++index;
                    }

                    errors.GenericWarning("Data source name collision: " + filename + ", writing as " + altFileName + " to avoid.");
                    filename = altFileName;
                }
                var dataSourceStateToWrite         = kvp.Value.JsonClone().OrderBy(ds => ds.Name, StringComparer.Ordinal);
                DataSourceDefinition dataSourceDef = null;

                // Split out the changeable parts of the data source.
                foreach (var ds in dataSourceStateToWrite.Where(ds => ds.Type != "ViewInfo"))
                {
                    // CDS DataSource
                    if (ds.TableDefinition != null)
                    {
                        dataSourceDef = new DataSourceDefinition();
                        dataSourceDef.TableDefinition = Utility.JsonParse <DataSourceTableDefinition>(ds.TableDefinition);
                        dataSourceDef.DatasetName     = ds.DatasetName;
                        dataSourceDef.EntityName      = ds.RelatedEntityName ?? ds.Name;
                        ds.DatasetName     = null;
                        ds.TableDefinition = null;
                    }
                    // CDP DataSource
                    else if (ds.DataEntityMetadataJson != null)
                    {
                        if (ds.ApiId == "/providers/microsoft.powerapps/apis/shared_commondataservice")
                        {
                            // This is the old CDS connector, we can't support it since it's optionset format is incompatable with the newer one
                            errors.ValidationError($"Connection {ds.Name} is using the old CDS connector which is incompatable with this tool");
                            throw new DocumentException();
                        }
                        dataSourceDef = new DataSourceDefinition();
                        dataSourceDef.DataEntityMetadataJson = ds.DataEntityMetadataJson;
                        dataSourceDef.EntityName             = ds.Name;
                        dataSourceDef.TableName   = ds.TableName;
                        ds.TableName              = null;
                        ds.DataEntityMetadataJson = null;
                    }
                    else if (ds.Type == "OptionSetInfo")
                    {
                        // This looks like a left over from previous versions of studio, account for it by
                        // tracking optionsets with empty dataset names
                        ds.DatasetName = ds.DatasetName == null ? string.Empty : null;
                    }
                    else if (ds.WadlMetadata != null)
                    {
                        // For some reason some connectors have both, investigate if one could be discarded by the server?
                        if (ds.WadlMetadata.WadlXml != null)
                        {
                            dir.WriteAllXML(WadlPackageDir, filename.Replace(".json", ".xml"), ds.WadlMetadata.WadlXml);
                        }
                        if (ds.WadlMetadata.SwaggerJson != null)
                        {
                            dir.WriteAllJson(SwaggerPackageDir, filename, JsonSerializer.Deserialize <SwaggerDefinition>(ds.WadlMetadata.SwaggerJson, Utility._jsonOpts));
                        }
                        ds.WadlMetadata = null;
                    }
                }

                if (dataSourceDef != null)
                {
                    TrimViewNames(dataSourceStateToWrite, dataSourceDef.DatasetName);
                }

                if (dataSourceDef?.DatasetName != null && app._dataSourceReferences.TryGetValue(dataSourceDef.DatasetName, out var referenceJson))
                {
                    // copy over the localconnectionreference
                    if (referenceJson.dataSources.TryGetValue(dataSourceDef.EntityName, out var dsRef))
                    {
                        dataSourceDef.LocalReferenceDSJson = dsRef;
                    }
                    dataSourceDef.InstanceUrl   = referenceJson.instanceUrl;
                    dataSourceDef.ExtensionData = referenceJson.ExtensionData;
                }

                if (dataSourceDef != null)
                {
                    dir.WriteAllJson(DataSourcePackageDir, filename, dataSourceDef);
                }

                dir.WriteAllJson(DataSourcesDir, filename, dataSourceStateToWrite);
            }
        }
        // Write out to a directory (this shards it)
        public static void SaveAsSource(CanvasDocument app, string directory2, ErrorContainer errors)
        {
            var dir = new DirectoryWriter(directory2);

            dir.DeleteAllSubdirs();

            // Shard templates, parse for default values
            var templateDefaults = new Dictionary <string, ControlTemplate>();

            foreach (var template in app._templates.UsedTemplates)
            {
                var filename = $"{template.Name}_{template.Version}.xml";
                dir.WriteAllXML(PackagesDir, filename, template.Template);
                if (!ControlTemplateParser.TryParseTemplate(app._templateStore, template.Template, app._properties.DocumentAppType, templateDefaults, out _, out _))
                {
                    throw new NotSupportedException($"Unable to parse template file {template.Name}");
                }
            }

            // Also add Screen and App templates (not xml, constructed in code on the server)
            GlobalTemplates.AddCodeOnlyTemplates(app._templateStore, templateDefaults, app._properties.DocumentAppType);

            var importedComponents = app.GetImportedComponents();

            foreach (var control in app._screens)
            {
                string controlName = control.Key;
                var    isTest      = controlName == AppTestControlName;
                var    subDir      = isTest ? TestDir : CodeDir;

                WriteTopParent(dir, app, control.Key, control.Value, subDir);
            }

            foreach (var control in app._components)
            {
                string controlName = control.Key;
                app._templateStore.TryGetTemplate(controlName, out var templateState);

                bool isImported = importedComponents.Contains(templateState.TemplateOriginalName);
                var  subDir     = (isImported) ? ComponentPackageDir : ComponentCodeDir;
                WriteTopParent(dir, app, control.Key, control.Value, subDir);
            }

            // Write out control templates at top level, skipping component templates which are written alongside components
            var nonComponentControlTemplates = app._templateStore.Contents.Where(kvp => !(kvp.Value.IsComponentTemplate ?? false)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

            dir.WriteAllJson("", "ControlTemplates.json", nonComponentControlTemplates);

            if (app._checksum != null)
            {
                app._checksum.ClientBuildDetails = _buildVerJson;
                dir.WriteAllJson(EntropyDir, FileKind.Checksum, app._checksum);
            }

            if (app._appCheckerResultJson != null)
            {
                dir.WriteAllJson(EntropyDir, FileKind.AppCheckerResult, app._appCheckerResultJson);
            }

            foreach (var file in app._assetFiles.Values)
            {
                dir.WriteAllBytes(AssetsDir, file.Name, file.RawBytes);
            }

            if (app._logoFile != null)
            {
                dir.WriteAllBytes(AssetsDir, app._logoFile.Name, app._logoFile.RawBytes);
            }

            if (app._themes != null)
            {
                dir.WriteAllJson(CodeDir, "Themes.json", app._themes);
            }

            if (app._resourcesJson != null)
            {
                dir.WriteAllJson(AssetsDir, "Resources.json", app._resourcesJson);
            }

            WriteDataSources(dir, app, errors);

            // Loose files.
            foreach (FileEntry file in app._unknownFiles.Values)
            {
                // Standardize the .json files so they're determinsitc and comparable
                if (file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
                {
                    ReadOnlyMemory <byte> span = file.RawBytes;
                    var je      = JsonDocument.Parse(span).RootElement;
                    var jsonStr = JsonNormalizer.Normalize(je);
                    dir.WriteAllText(OtherDir, file.Name, jsonStr);
                }
                else
                {
                    dir.WriteAllBytes(OtherDir, file.Name, file.RawBytes);
                }
            }

            var manifest = new CanvasManifestJson
            {
                FormatVersion = CurrentSourceVersion,
                Properties    = app._properties,
                Header        = app._header,
                PublishInfo   = app._publishInfo,
                ScreenOrder   = app._screenOrder
            };

            dir.WriteAllJson("", FileKind.CanvasManifest, manifest);

            if (app._connections != null)
            {
                dir.WriteAllJson(ConnectionDir, FileKind.Connections, app._connections);
            }

            if (app._libraryReferences != null)
            {
                dir.WriteAllJson("", FileKind.ComponentReferences, app._libraryReferences);
            }

            dir.WriteAllJson(EntropyDir, FileKind.Entropy, app._entropy);
        }