// Full fidelity read-write

        public static CanvasDocument LoadFromSource(string directory2, ErrorContainer errors)
        {
            if (File.Exists(directory2))
            {
                if (directory2.EndsWith(".msapp", StringComparison.OrdinalIgnoreCase))
                {
                    throw new ArgumentException($"Must point to a source directory, not an msapp file ({directory2}");
                }
            }

            if (!Directory.Exists(directory2))
            {
                throw new InvalidOperationException($"No directory {directory2}");
            }
            var dir = new DirectoryReader(directory2);
            var app = new CanvasDocument();

            // Do the manifest check (and version check) first.
            // MAnifest lives in top-level directory.
            foreach (var file in dir.EnumerateFiles("", "*.json"))
            {
                switch (file.Kind)
                {
                case FileKind.CanvasManifest:
                    var manifest = file.ToObject <CanvasManifestJson>();

                    if (manifest.FormatVersion != CurrentSourceVersion)
                    {
                        errors.FormatNotSupported($"This tool only supports {CurrentSourceVersion}, the manifest version is {manifest.FormatVersion}");
                        throw new DocumentException();
                    }

                    app._properties  = manifest.Properties;
                    app._header      = manifest.Header;
                    app._publishInfo = manifest.PublishInfo;
                    app._screenOrder = manifest.ScreenOrder;
                    break;

                case FileKind.Templates:
                    foreach (var kvp in file.ToObject <Dictionary <string, CombinedTemplateState> >())
                    {
                        app._templateStore.AddTemplate(kvp.Key, kvp.Value);
                    }
                    break;

                case FileKind.ComponentReferences:
                    var refs = file.ToObject <ComponentDependencyInfo[]>();
                    app._libraryReferences = refs;
                    break;
                }
            }
            if (app._header == null)
            {
                // Manifest not found.
                errors.FormatNotSupported($"Can't find CanvasManifest.json file - is sources an old version?");
                throw new DocumentException();
            }

            // Load template files, recreate References/templates.json
            LoadTemplateFiles(errors, app, Path.Combine(directory2, PackagesDir), out var templateDefaults);

            foreach (var file in dir.EnumerateFiles(AssetsDir))
            {
                if (file._relativeName == "Resources.json")
                {
                    app._resourcesJson = file.ToObject <ResourcesJson>();
                    continue;
                }
                app.AddAssetFile(file.ToFileEntry());
            }

            foreach (var file in dir.EnumerateFiles(EntropyDir))
            {
                switch (file.Kind)
                {
                case FileKind.Entropy:
                    app._entropy = file.ToObject <Entropy>();
                    break;

                case FileKind.AppCheckerResult:
                    app._appCheckerResultJson = file.ToObject <AppCheckerResultJson>();
                    break;

                case FileKind.Checksum:
                    app._checksum = file.ToObject <ChecksumJson>();
                    app._checksum.ClientBuildDetails = _buildVerJson;
                    break;

                default:
                    errors.GenericWarning($"Unexpected file in Entropy, discarding");
                    break;
                }
            }


            foreach (var file in dir.EnumerateFiles(OtherDir))
            {
                // Special files like Header / Properties
                switch (file.Kind)
                {
                case FileKind.Unknown:
                    // Track any unrecognized files so we can save back.
                    app.AddFile(file.ToFileEntry());
                    break;

                default:
                    // Shouldn't find anything else not unknown in here, but just ignore them for now
                    errors.GenericWarning($"Unexpected file in Other, discarding");
                    break;
                }
            } // each loose file in '\other'


            app.GetLogoFile();

            LoadDataSources(app, dir, errors);
            LoadSourceFiles(app, dir, templateDefaults, errors);

            foreach (var file in dir.EnumerateFiles(ConnectionDir))
            {
                // Special files like Header / Properties
                switch (file.Kind)
                {
                case FileKind.Connections:
                    app._connections = file.ToObject <IDictionary <string, ConnectionJson> >();
                    break;
                }
            }


            // Defaults.
            // - DynamicTypes.Json, Resources.Json , Templates.Json - could all be empty
            // - Themes.json- default to


            app.OnLoadComplete(errors);

            return(app);
        }
        // Full fidelity read-write

        public static CanvasDocument LoadFromSource(string directory2, ErrorContainer errors)
        {
            if (File.Exists(directory2))
            {
                if (directory2.EndsWith(".msapp", StringComparison.OrdinalIgnoreCase))
                {
                    errors.BadParameter($"Must point to a source directory, not an msapp file ({directory2}");
                }
            }

            Utilities.VerifyDirectoryExists(errors, directory2);

            if (errors.HasErrors)
            {
                return(null);
            }

            var    dir = new DirectoryReader(directory2);
            var    app = new CanvasDocument();
            string appInsightsInstumentationKey = null;

            // Do the manifest check (and version check) first.
            // MAnifest lives in top-level directory.
            foreach (var file in dir.EnumerateFiles("", "*.json"))
            {
                switch (file.Kind)
                {
                case FileKind.CanvasManifest:
                    var manifest = file.ToObject <CanvasManifestJson>();

                    if (manifest.FormatVersion != CurrentSourceVersion)
                    {
                        errors.FormatNotSupported($"This tool only supports {CurrentSourceVersion}, the manifest version is {manifest.FormatVersion}");
                        throw new DocumentException();
                    }

                    app._properties  = manifest.Properties;
                    app._header      = manifest.Header;
                    app._publishInfo = manifest.PublishInfo;
                    app._screenOrder = manifest.ScreenOrder;
                    break;

                case FileKind.Templates:
                    foreach (var kvp in file.ToObject <Dictionary <string, CombinedTemplateState> >())
                    {
                        app._templateStore.AddTemplate(kvp.Key, kvp.Value);
                    }
                    break;

                case FileKind.ComponentReferences:
                    var refs = file.ToObject <ComponentDependencyInfo[]>();
                    app._libraryReferences = refs;
                    break;

                case FileKind.AppInsightsKey:
                    var appInsights = file.ToObject <AppInsightsKeyJson>();
                    appInsightsInstumentationKey = appInsights.InstrumentationKey;
                    break;
                }
            }
            if (appInsightsInstumentationKey != null)
            {
                app._properties.InstrumentationKey = appInsightsInstumentationKey;
            }
            if (app._header == null)
            {
                // Manifest not found.
                errors.FormatNotSupported($"Can't find CanvasManifest.json file - is sources an old version?");
                throw new DocumentException();
            }

            // Load template files, recreate References/templates.json
            LoadTemplateFiles(errors, app, Path.Combine(directory2, PackagesDir), out var templateDefaults);

            foreach (var file in dir.EnumerateFiles(EntropyDir))
            {
                switch (file.Kind)
                {
                case FileKind.Entropy:
                    app._entropy = file.ToObject <Entropy>();
                    break;

                case FileKind.AppCheckerResult:
                    app._appCheckerResultJson = file.ToObject <AppCheckerResultJson>();
                    break;

                case FileKind.Checksum:
                    app._checksum = file.ToObject <ChecksumJson>();
                    app._checksum.ClientBuildDetails = _buildVerJson;
                    break;

                default:
                    errors.GenericWarning($"Unexpected file in Entropy, discarding");
                    break;
                }
            }

            // The resource entries for sample data is sharded into individual json files.
            // Add each of these entries back into Resrouces.json
            var resources = new List <ResourceJson>();

            app._resourcesJson = new ResourcesJson()
            {
                Resources = new ResourceJson[0]
            };
            foreach (var file in dir.EnumerateFiles(AssetsDir, "*", false))
            {
                var fileEntry = file.ToFileEntry();
                if (fileEntry.Name.GetExtension() == ".json")
                {
                    // If its a json file then this must be one of the sharded files from Resources.json
                    resources.Add(file.ToObject <ResourceJson>());
                }
            }

            // Add the resources from sharded files to _resourcesJson.Resources
            if (resources.Count > 0)
            {
                app._resourcesJson.Resources = resources.ToArray();
            }

            // We have processed all the json files in Assets directory, now interate through all tge files to add the asset files.
            foreach (var file in dir.EnumerateFiles(AssetsDir))
            {
                // Skip adding the json files which were created to contain the information for duplicate asset files.
                // The name of the such json files is of the format - <assetFileName>.<assetFileExtension>.json (eg. close_1.jpg.json)
                var fileName = file._relativeName;
                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);

                // Check if the original extension was .json and the remaining file name has still got an extension,
                // Then this is an additional file that was created to contain information for duplicate assets.
                if (Path.HasExtension(fileNameWithoutExtension) && Path.GetExtension(fileName) == ".json")
                {
                    var localAssetInfoJson = file.ToObject <LocalAssetInfoJson>();
                    app._localAssetInfoJson.Add(localAssetInfoJson.NewFileName, localAssetInfoJson);
                }
                // Add non json files to _assetFiles
                else if (Path.GetExtension(fileName) != ".json")
                {
                    app.AddAssetFile(file.ToFileEntry());
                }
            }

            app.GetLogoFile();

            // Add the entries for local assets back to resrouces.json
            TranformResourceJson.AddLocalAssetEntriesToResourceJson(app);

            foreach (var file in dir.EnumerateFiles(OtherDir))
            {
                // Special files like Header / Properties
                switch (file.Kind)
                {
                case FileKind.Unknown:
                    // Track any unrecognized files so we can save back.
                    app.AddFile(file.ToFileEntry());
                    break;

                default:
                    // Shouldn't find anything else not unknown in here, but just ignore them for now
                    errors.GenericWarning($"Unexpected file in Other, discarding");
                    break;
                }
            } // each loose file in '\other'

            LoadDataSources(app, dir, errors);
            LoadSourceFiles(app, dir, templateDefaults, errors);

            foreach (var file in dir.EnumerateFiles(ConnectionDir))
            {
                // Special files like Header / Properties
                switch (file.Kind)
                {
                case FileKind.Connections:
                    app._connections = file.ToObject <IDictionary <string, ConnectionJson> >();
                    break;
                }
            }


            // Defaults.
            // - DynamicTypes.Json, Resources.Json , Templates.Json - could all be empty
            // - Themes.json- default to


            app.OnLoadComplete(errors);

            return(app);
        }
        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);
            }
        }
        internal void StabilizeAssetFilePaths(ErrorContainer errors)
        {
            _entropy.LocalResourceFileNames.Clear();


            // If a name matches caseinsensitive but not casesensitive, it is a candidate for rename
            var caseInsensitiveNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var caseSensitiveNames = new HashSet<string>(StringComparer.Ordinal);
            foreach (var resource in _resourcesJson.Resources.OrderBy(resource => resource.Name, StringComparer.Ordinal))
            {
                if (resource.ResourceKind != ResourceKind.LocalFile)
                    continue;

                if (caseInsensitiveNames.Add(resource.Name))
                {
                    caseSensitiveNames.Add(resource.Name);
                }
            }


            // Update AssetFile paths
            foreach (var resource in _resourcesJson.Resources.OrderBy(resource => resource.Name, StringComparer.Ordinal))
            {
                if (resource.ResourceKind != ResourceKind.LocalFile)
                    continue;

                resource.OriginalName = resource.Name;
                var assetFilePath = GetAssetFilePathWithoutPrefix(resource.Path);
                if (!_assetFiles.TryGetValue(assetFilePath, out var fileEntry))
                    continue;

                if (!caseSensitiveNames.Contains(resource.Name) && caseInsensitiveNames.Contains(resource.Name))
                {
                    int i = 1;
                    var newResourceName = resource.Name + '_' + i;
                    while (caseInsensitiveNames.Contains(newResourceName))
                    {
                        ++i;
                        newResourceName = resource.Name + '_' + i;
                    }

                    resource.OriginalName = resource.Name;
                    resource.Name = newResourceName;

                    caseInsensitiveNames.Add(resource.Name);
                    caseSensitiveNames.Add(resource.Name);

                    var colliding = _entropy.LocalResourceFileNames.Keys.First(key => string.Equals(key, resource.OriginalName, StringComparison.OrdinalIgnoreCase));
                    errors.GenericWarning($"Asset named {resource.OriginalName} collides with {colliding}, unpacking as {resource.Name}");
                }

                var extension = assetFilePath.GetExtension();
                var newFileName = resource.Name + extension;

                _entropy.LocalResourceFileNames.Add(resource.Name, resource.FileName);

                var updatedPath = FilePath.FromMsAppPath(Utilities.GetResourceRelativePath(resource.Content)).Append(newFileName);
                resource.Path = updatedPath.ToMsAppPath();
                resource.FileName = newFileName;

                var withoutPrefix = GetAssetFilePathWithoutPrefix(resource.Path);
                fileEntry.Name = withoutPrefix;
                _assetFiles.Remove(assetFilePath);
                _assetFiles.Add(withoutPrefix, fileEntry);
            }
        }