// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart'
    show PhysicalResourceProvider;
import 'package:analyzer/src/analysis_options/analysis_options_file.dart';
import 'package:analyzer/src/analysis_options/analysis_options_provider.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/dart/analysis/context_root.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/lint/pub.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/yaml.dart';
import 'package:analyzer/src/utilities/extensions/file_system.dart';
import 'package:analyzer/src/workspace/basic.dart';
import 'package:analyzer/src/workspace/blaze.dart';
import 'package:analyzer/src/workspace/gn.dart';
import 'package:analyzer/src/workspace/pub.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:collection/collection.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart';
import 'package:yaml/yaml.dart';

/// Determines the list of analysis contexts that can be used to analyze the
/// files and folders that should be analyzed given a list of included files and
/// folders and a list of excluded files and folders.
class ContextLocatorImpl {
  /// The resource provider used to access the file system.
  final ResourceProvider resourceProvider;

  /// Initialize a newly created context locator. If a [resourceProvider] is
  /// supplied, it will be used to access the file system. Otherwise the default
  /// resource provider will be used.
  ContextLocatorImpl({ResourceProvider? resourceProvider})
    : resourceProvider = resourceProvider ?? PhysicalResourceProvider.INSTANCE;

  /// Return a list of the context roots that should be used to analyze the
  /// files that are included by the list of [includedPaths] and not excluded by
  /// the list of [excludedPaths].
  ///
  /// If an [optionsFile] is specified, then it is assumed to be the path to the
  /// `analysis_options.yaml` file that should be used in place of the ones that
  /// would be found by looking in the directories containing the context roots.
  ///
  /// If a [packageConfigFile] is specified, then it is assumed to be the path
  /// to the package config file that should be used in place of the one that
  /// would be found by looking in the directories containing the context roots.
  List<ContextRootImpl> locateRoots({
    required List<String> includedPaths,
    List<String>? excludedPaths,
    String? optionsFile,
    String? packageConfigFile,
  }) {
    //
    // Compute the list of folders and files that are to be included.
    //
    List<Folder> includedFolders = <Folder>[];
    List<File> includedFiles = <File>[];
    _resourcesFromPaths(includedPaths, includedFolders, includedFiles);
    //
    // Compute the list of folders and files that are to be excluded.
    //
    List<Folder> excludedFolders = <Folder>[];
    List<File> excludedFiles = <File>[];
    _resourcesFromPaths(
      excludedPaths ?? const <String>[],
      excludedFolders,
      excludedFiles,
    );
    //
    // Use the excluded folders and files to filter the included folders and
    // files.
    //
    includedFolders = includedFolders
        .where(
          (Folder includedFolder) =>
              !_containedInAny(excludedFolders, includedFolder),
        )
        .toList();
    includedFiles = includedFiles
        .where(
          (File includedFile) =>
              !_containedInAny(excludedFolders, includedFile) &&
              !excludedFiles.contains(includedFile),
        )
        .toList();
    //
    // We now have a list of all of the files and folders that need to be
    // analyzed. For each, walk the directory structure and figure out where to
    // create context roots.
    //
    File? defaultOptionsFile;
    if (optionsFile != null) {
      defaultOptionsFile = resourceProvider.getFile(optionsFile);
      if (!defaultOptionsFile.exists) {
        defaultOptionsFile = null;
      }
    }
    File? defaultPackageConfigFile;
    if (packageConfigFile != null) {
      defaultPackageConfigFile = resourceProvider.getFile(packageConfigFile);
      if (!defaultPackageConfigFile.exists) {
        defaultPackageConfigFile = null;
      }
    }

    var workspaceResolutionRootMap = <String, List<Folder>>{};
    var nonWorkspaceResolutionFolders = <Folder>[];
    _sortIncludedFoldersIntoWorkspaceResolutions(
      includedFolders,
      defaultOptionsFile,
      defaultPackageConfigFile,
      nonWorkspaceResolutionFolders,
      workspaceResolutionRootMap,
    );

    var roots = <ContextRootImpl>[];
    for (var workspaceResolution in workspaceResolutionRootMap.entries) {
      var workspaceRootFolder = resourceProvider.getFolder(
        workspaceResolution.key,
      );
      var location = _contextRootLocation(
        workspaceRootFolder,
        defaultOptionsFile: defaultOptionsFile,
        defaultPackageConfigFile: defaultPackageConfigFile,
        defaultRootFolder: () => workspaceRootFolder,
      );

      ContextRootImpl root = _createContextRoot(
        roots,
        rootFolder: workspaceRootFolder,
        workspace: location.workspace,
        optionsFile: location.optionsFile,
        packagesFile: location.packageConfigFile,
      );

      var rootEnabledLegacyPlugins = _getEnabledLegacyPlugins(
        location.workspace,
        location.optionsFile,
      );

      Set<String> visited = {};
      bool usedRoot = false;

      for (var folder in workspaceResolution.value) {
        if (!root.isAnalyzed(folder.path)) {
          root.included.add(folder);
        }

        usedRoot |= _createContextRoots(
          roots,
          visited,
          folder,
          excludedFolders,
          root,
          rootEnabledLegacyPlugins,
          root.excludedGlobs,
          defaultOptionsFile,
          defaultPackageConfigFile,
        );
      }
      if (!usedRoot) {
        // If all included folders under this workspace resolution ended up
        // creating new contexts remove the (not used) root.
        roots.remove(root);
      }
    }

    for (Folder folder in nonWorkspaceResolutionFolders) {
      var location = _contextRootLocation(
        folder,
        defaultOptionsFile: defaultOptionsFile,
        defaultPackageConfigFile: defaultPackageConfigFile,
        defaultRootFolder: () => folder,
      );

      ContextRootImpl? root;
      // Check whether there are existing roots that overlap with this one.
      for (var existingRoot in roots) {
        if (existingRoot.root.isOrContains(folder.path)) {
          if (_matchRootWithLocation(existingRoot, location)) {
            // This root is covered exactly by the existing root (with the same
            // options/packages file) so we can simple use it.
            root = existingRoot;
            break;
          } else {
            // This root is within another (but doesn't share options/packages)
            // so we still need a new root. However, we should exclude this
            // from the existing root so these files aren't analyzed by both.
            //
            // It's possible this folder is already excluded (for example
            // because it's also a project and had a context root created as
            // part of the parent analysis root).
            if (!existingRoot.excluded.contains(folder)) {
              existingRoot.excluded.add(folder);
            }
          }
        }
      }

      root ??= _createContextRoot(
        roots,
        rootFolder: folder,
        workspace: location.workspace,
        optionsFile: location.optionsFile,
        packagesFile: location.packageConfigFile,
      );

      if (!root.isAnalyzed(folder.path)) {
        root.included.add(folder);
      }

      var rootEnabledLegacyPlugins = _getEnabledLegacyPlugins(
        location.workspace,
        location.optionsFile,
      );

      _createContextRootsIn(
        roots,
        {},
        folder,
        excludedFolders,
        root,
        rootEnabledLegacyPlugins,
        root.excludedGlobs,
        defaultOptionsFile,
        defaultPackageConfigFile,
      );
    }

    for (File file in includedFiles) {
      Folder parent = file.parent;

      var location = _contextRootLocation(
        parent,
        defaultOptionsFile: defaultOptionsFile,
        defaultPackageConfigFile: defaultPackageConfigFile,
        defaultRootFolder: () => _fileSystemRoot(parent),
      );

      ContextRootImpl? root;
      for (var existingRoot in roots) {
        if (existingRoot.root.isOrContains(file.path) &&
            _matchRootWithLocation(existingRoot, location)) {
          root = existingRoot;
          break;
        }
      }

      root ??= _createContextRoot(
        roots,
        rootFolder: location.rootFolder,
        workspace: location.workspace,
        optionsFile: location.optionsFile,
        packagesFile: location.packageConfigFile,
      );

      if (!root.isAnalyzed(file.path)) {
        root.included.add(file);
      }
    }
    return roots;
  }

  /// Return `true` if the given [resource] is contained in one or more of the
  /// given [folders].
  bool _containedInAny(Iterable<Folder> folders, Resource resource) =>
      folders.any((Folder folder) => folder.contains(resource.path));

  /// Return the location of a context root for a file in the [parent].
  ///
  /// If the [defaultOptionsFile] is provided, it will be used, not a file
  /// found relative to the [parent].
  ///
  /// If the [defaultPackageConfigFile] is provided, it will be used, not a file
  /// found relative to the [parent].
  ///
  /// The root folder of the context is the parent of either the options, or the
  /// grand-parent of the `.dart_tool/package_config.json` file, whichever is
  /// lower.
  _RootLocation _contextRootLocation(
    Folder parent, {
    required File? defaultOptionsFile,
    required File? defaultPackageConfigFile,
    required Folder Function() defaultRootFolder,
  }) {
    File? optionsFile;
    Folder? optionsFolderToChooseRoot;
    if (defaultOptionsFile != null) {
      optionsFile = defaultOptionsFile;
    } else {
      optionsFile = parent.findAnalysisOptionsYamlFile();
      optionsFolderToChooseRoot = optionsFile?.parent;
    }

    File? packageConfigFile;
    Folder? packagesFolderToChooseRoot;
    if (defaultPackageConfigFile != null) {
      packageConfigFile = defaultPackageConfigFile;
      // If the packages file is in .dart_tool directory, use the grandparent
      // folder, else use the parent folder.
      packagesFolderToChooseRoot =
          _findFolderWithPackageConfigFile(packageConfigFile.parent) ??
          packageConfigFile.parent;
    }

    var buildGnFile = _findBuildGnFile(parent);

    var rootFolder = _lowest([optionsFolderToChooseRoot, buildGnFile?.parent]);

    // If default packages file is given, create workspace for it.
    var workspace = _createWorkspace(
      folder: parent,
      packageConfigFile: packageConfigFile,
      buildGnFile: buildGnFile,
    );

    if (workspace is! BasicWorkspace) {
      rootFolder = _lowest([
        rootFolder,
        resourceProvider.getFolder(workspace.root),
      ]);
    }

    if (workspace is PackageConfigWorkspace) {
      packageConfigFile ??= workspace.packageConfigFile;
      // If the default packages folder is a parent of the workspace root,
      // choose that as the root.
      if (rootFolder != null && packagesFolderToChooseRoot != null) {
        if (packagesFolderToChooseRoot.contains(rootFolder.path)) {
          rootFolder = packagesFolderToChooseRoot;
        }
      }
    }

    if (rootFolder == null) {
      rootFolder = defaultRootFolder();
      if (workspace is BasicWorkspace) {
        workspace = _createWorkspace(
          folder: rootFolder,
          packageConfigFile: packageConfigFile,
          buildGnFile: buildGnFile,
        );
      }
    }

    return _RootLocation(
      rootFolder: rootFolder,
      workspace: workspace,
      optionsFile: optionsFile,
      packageConfigFile: packageConfigFile,
    );
  }

  ContextRootImpl _createContextRoot(
    List<ContextRootImpl> roots, {
    required Folder rootFolder,
    required Workspace workspace,
    required File? optionsFile,
    required File? packagesFile,
  }) {
    optionsFile ??= _findDefaultOptionsFile(workspace);

    var root = ContextRootImpl(
      resourceProvider,
      rootFolder,
      workspace,
      optionsFile: optionsFile,
      packagesFile: packagesFile,
    );
    if (optionsFile != null) {
      root.optionsFileMap.putIfAbsent(optionsFile, () => {}).add(rootFolder);
    }

    root.excludedGlobs.addAll(_getExcludedGlobs(optionsFile, workspace));
    roots.add(root);
    return root;
  }

  /// If the given [folder] should be the root of a new analysis context, then
  /// create a new context root for it and add it to the list of context
  /// [roots]. The [containingRoot] is the context root from an enclosing
  /// directory and is used to inherit configuration information that isn't
  /// overridden.
  ///
  /// If either the [optionsFile] or [packagesFile] is non-`null` then the given
  /// file will be used even if there is a local version of the file.
  ///
  /// For each directory within the given [folder] that is neither in the list
  /// of [excludedFolders] nor excluded by the [excludedGlobs], recursively
  /// search for nested context roots.
  ///
  /// Returns true if the folder was contained in the root and did not create a
  /// new root, false if it did create a new root.
  bool _createContextRoots(
    List<ContextRootImpl> roots,
    Set<String> visited,
    Folder folder,
    List<Folder> excludedFolders,
    ContextRootImpl containingRoot,
    Set<String> containingRootEnabledLegacyPlugins,
    List<LocatedGlob> excludedGlobs,
    File? optionsFile,
    File? packagesFile, {
    File? optionsFileFromParentInSameRoot,
  }) {
    var packageConfigFileToUse =
        packagesFile ??
        _getPackageConfigFile(folder) ??
        containingRoot.packagesFile;
    var buildGnFile = folder.getExistingFile(file_paths.buildGn);

    var optionsFileToUse = optionsFile;
    if (optionsFileToUse == null) {
      optionsFileToUse = folder.existingAnalysisOptionsYamlFile;
      // If this folder doesn't have one use the one from a parent folder if any,
      // that will be the one we find anyway.
      optionsFileToUse ??= optionsFileFromParentInSameRoot;
      if (optionsFileToUse == null) {
        var parentFolder = folder.parent;
        while (parentFolder != containingRoot.root) {
          optionsFileToUse = parentFolder.existingAnalysisOptionsYamlFile;
          if (optionsFileToUse != null) {
            break;
          }
          parentFolder = parentFolder.parent;
        }
      }
    }

    var localEnabledPlugins = _getEnabledLegacyPlugins(
      containingRoot.workspace,
      optionsFileToUse,
    );
    // Legacy plugins differ only if there is an analysis_options and it
    // contains a different set of plugins from the containing context.
    var pluginsDiffer =
        optionsFileToUse != null &&
        !const SetEquality<String>().equals(
          containingRootEnabledLegacyPlugins,
          localEnabledPlugins,
        );

    bool usedThisRoot = true;

    // Create a context root for the given [folder] if a packages or build file
    // is locally specified, or the set of enabled legacy plugins changed.
    if (pluginsDiffer ||
        packageConfigFileToUse != containingRoot.packagesFile ||
        buildGnFile != null) {
      var workspace = _createWorkspace(
        folder: folder,
        packageConfigFile: packageConfigFileToUse,
        buildGnFile: buildGnFile,
      );
      var root = ContextRootImpl(
        resourceProvider,
        folder,
        workspace,
        optionsFile: optionsFileToUse ?? containingRoot.optionsFile,
        packagesFile: packageConfigFileToUse,
      );
      root.included.add(folder);
      containingRoot.excluded.add(folder);
      roots.add(root);
      containingRoot = root;
      containingRootEnabledLegacyPlugins = localEnabledPlugins;
      excludedGlobs = _getExcludedGlobs(root.optionsFile, workspace);
      root.excludedGlobs.addAll(excludedGlobs);
      usedThisRoot = false;
    }

    if (optionsFileToUse != null &&
        optionsFileToUse != optionsFileFromParentInSameRoot) {
      containingRoot.optionsFileMap
          .putIfAbsent(optionsFileToUse, () => {})
          .add(folder);
      // Add excluded globs only if we found a new options file.
      var excludes = _getExcludedGlobs(
        optionsFileToUse,
        containingRoot.workspace,
      );
      containingRoot.excludedGlobs.addAll(excludes);
    }
    _createContextRootsIn(
      roots,
      visited,
      folder,
      excludedFolders,
      containingRoot,
      containingRootEnabledLegacyPlugins,
      excludedGlobs,
      optionsFile,
      packagesFile,
      optionsFileToUseForFolder: usedThisRoot ? optionsFileToUse : null,
    );

    return usedThisRoot;
  }

  /// For each directory within the given [folder] that is neither in the list
  /// of [excludedFolders] nor excluded by the [excludedGlobs], recursively
  /// search for nested context roots and add them to the list of [roots].
  ///
  /// If either the [optionsFile] or [packagesFile] is non-`null` then the given
  /// file will be used even if there is a local version of the file.
  void _createContextRootsIn(
    List<ContextRootImpl> roots,
    Set<String> visited,
    Folder folder,
    List<Folder> excludedFolders,
    ContextRootImpl containingRoot,
    Set<String> containingRootEnabledLegacyPlugins,
    List<LocatedGlob> excludedGlobs,
    File? optionsFile,
    File? packagesFile, {
    File? optionsFileToUseForFolder,
  }) {
    bool isExcluded(Folder folder) {
      if (excludedFolders.contains(folder) ||
          folder.shortName.startsWith('.')) {
        return true;
      }
      // TODO(scheglov): Why not take it from `containingRoot`?
      for (var pattern in excludedGlobs) {
        if (pattern.matches(folder.path)) {
          return true;
        }
      }
      return false;
    }

    // Stop infinite recursion via links.
    try {
      var canonicalFolderPath = folder.resolveSymbolicLinksSync().path;
      if (!visited.add(canonicalFolderPath)) {
        return;
      }
    } on FileSystemException {
      return;
    }

    //
    // Check each of the subdirectories to see whether a context root needs to
    // be added for it.
    //
    try {
      for (Resource child in folder.getChildren()) {
        if (child is Folder) {
          if (excludedFolders.contains(child)) {
            containingRoot.excluded.add(child);
          } else if (!isExcluded(child)) {
            _createContextRoots(
              roots,
              visited,
              child,
              excludedFolders,
              containingRoot,
              containingRootEnabledLegacyPlugins,
              excludedGlobs,
              optionsFile,
              packagesFile,
              optionsFileFromParentInSameRoot: optionsFileToUseForFolder,
            );
          }
        }
      }
    } on FileSystemException {
      // The directory either doesn't exist or cannot be read. Either way, there
      // are no subdirectories that need to be added.
    }
  }

  Workspace _createWorkspace({
    required Folder folder,
    required File? packageConfigFile,
    required File? buildGnFile,
  }) {
    if (buildGnFile != null) {
      var workspace = GnWorkspace.find(buildGnFile);
      if (workspace != null) {
        return workspace;
      }
    }

    Packages packages;
    if (packageConfigFile != null) {
      packages = parsePackageConfigJsonFile(
        resourceProvider,
        packageConfigFile,
      );
    } else {
      packages = Packages.empty;
    }

    var rootPath = folder.path;

    Workspace? workspace;
    workspace = BlazeWorkspace.find(
      resourceProvider,
      rootPath,
      lookForBuildFileSubstitutes: false,
    );
    workspace = _mostSpecificWorkspace(
      workspace,
      PackageConfigWorkspace.find(resourceProvider, packages, rootPath),
    );
    workspace ??= BasicWorkspace.find(resourceProvider, packages, rootPath);
    return workspace;
  }

  File? _findBuildGnFile(Folder folder) {
    for (var current in folder.withAncestors) {
      var file = current.getExistingFile(file_paths.buildGn);
      if (file != null) {
        return file;
      }
    }
    return null;
  }

  File? _findDefaultOptionsFile(Workspace workspace) {
    if (workspace is! WorkspaceWithDefaultAnalysisOptions) {
      return null;
    }

    // TODO(scheglov): Create SourceFactory once.
    var sourceFactory = workspace.createSourceFactory(null, null);
    var uriStr = WorkspaceWithDefaultAnalysisOptions.uri;
    var path = sourceFactory.forUri(uriStr)?.fullName;
    if (path != null) {
      var file = resourceProvider.getFile(path);
      if (file.exists) {
        return file;
      }
    }
    return null;
  }

  /// Returns the folder containing the `.dart_tool/` folder which contains the
  /// package config file to be used to analyze files in the given [folder], or
  /// `null` if there is no package config file in the given folder or any
  /// parent folder.
  Folder? _findFolderWithPackageConfigFile(Folder folder) {
    for (var current in folder.withAncestors) {
      var file = _getPackageConfigFile(current);
      if (file != null) {
        return current;
      }
    }
    return null;
  }

  /// Gets the set of enabled legacy plugins for [optionsFile], taking into
  /// account any includes.
  Set<String> _getEnabledLegacyPlugins(Workspace workspace, File? optionsFile) {
    if (optionsFile == null) {
      return const {};
    }
    try {
      var provider = AnalysisOptionsProvider(
        workspace.createSourceFactory(null, null),
      );

      var options = AnalysisOptionsImpl.fromYaml(
        optionsMap: provider.getOptionsFromFile(optionsFile),
        file: optionsFile,
        resourceProvider: resourceProvider,
      );

      return options.enabledLegacyPluginNames.toSet();
    } catch (_) {
      // No legacy plugins will be enabled if the file doesn't parse or cannot
      // be read for any reason.
      return {};
    }
  }

  /// Return a list containing the glob patterns used to exclude files from
  /// analysis by the given [optionsFile]. The list will be empty if there is no
  /// options file or if there are no exclusion patterns in the options file.
  List<LocatedGlob> _getExcludedGlobs(File? optionsFile, Workspace workspace) {
    List<LocatedGlob> patterns = [];
    if (optionsFile != null) {
      try {
        var doc = AnalysisOptionsProvider(
          workspace.createSourceFactory(null, null),
        ).getOptionsFromFile(optionsFile);

        var analyzerOptions = doc.valueAt(AnalysisOptionsFile.analyzer);
        if (analyzerOptions is YamlMap) {
          var excludeOptions = analyzerOptions.valueAt(
            AnalysisOptionsFile.exclude,
          );
          if (excludeOptions is YamlList) {
            var pathContext = resourceProvider.pathContext;

            void addGlob(List<String> components) {
              var pattern = posix.joinAll(components);
              patterns.add(
                LocatedGlob(
                  optionsFile.parent,
                  Glob(pattern, context: pathContext),
                ),
              );
            }

            for (String excludedPath in excludeOptions.whereType<String>()) {
              var excludedComponents = posix.split(excludedPath);
              addGlob(excludedComponents);
              if (excludedComponents.last == '**') {
                addGlob(excludedComponents..removeLast());
              }
            }
          }
        }
      } catch (exception) {
        // If we can't read and parse the analysis options file, then there
        // aren't any excluded files that need to be read.
      }
    }
    return patterns;
  }

  /// Returns the package config file in the given [folder], or `null` if the
  /// folder does not contain a package config file.
  File? _getPackageConfigFile(Folder folder) {
    var file = folder
        .getChildAssumingFolder(file_paths.dotDartTool)
        .getChildAssumingFile(file_paths.packageConfigJson);
    if (file.exists) {
      return file;
    }

    return null;
  }

  /// Load the `workspace` paths from the pubspec file in the given [root].
  ///
  /// From https://dart.dev/tools/pub/workspaces a root folder pubspec file will
  /// look like this:
  ///
  /// ```
  /// name: _
  /// publish_to: none
  /// environment:
  ///   sdk: ^3.6.0
  /// workspace:
  ///   - packages/helper
  ///   - packages/client_package
  ///   - packages/server_package
  /// ```
  ///
  /// This loads the paths from the `workspace` entry and return them as
  /// Folders if they exist as folders in the filesystem.
  Set<Folder> _loadWorkspaceDetailsFromPubspec(String root) {
    var result = <Folder>{};
    var rootFolder = resourceProvider.getFolder(root);
    var rootPubspecFile = rootFolder.getChildAssumingFile(
      file_paths.pubspecYaml,
    );
    if (rootPubspecFile.exists) {
      var rootPubspec = Pubspec.parse(rootPubspecFile.readAsStringSync());
      var workspace = rootPubspec.workspace;
      if (workspace != null) {
        for (var entry in workspace) {
          if (entry.text case var relativePath?) {
            var child = rootFolder.getChild(relativePath);
            if (child.exists && child is Folder) {
              result.add(child);
            }
          }
        }
      }
    }
    return result;
  }

  /// Add to the given lists of [folders] and [files] all of the resources in
  /// the given list of [paths] that exist and are not contained within one of
  /// the folders.
  void _resourcesFromPaths(
    List<String> paths,
    List<Folder> folders,
    List<File> files,
  ) {
    for (String path in _uniqueSortedPaths(paths)) {
      Resource resource = resourceProvider.getResource(path);
      if (resource is Folder) {
        folders.add(resource);
      } else if (resource is File) {
        files.add(resource);
      } else {
        // Internal error: unhandled kind of resource.
      }
    }
  }

  /// Sorts [includedFolders] into either pub workspace resolution or not.
  ///
  /// For each [Folder] in [includedFolders] sort into either
  /// [nonWorkspaceResolutionFolders] or [workspaceResolutionRootMap] depending
  /// on `pubspec.yaml` specifications.
  ///
  /// Folders with `pubspec.yaml` files with a `resolution: workspace` setting
  /// that matches a root-folders `pubspec.yaml` files `workspace` list is
  /// sorted into the [workspaceResolutionRootMap] map. Other folders end up in
  /// [nonWorkspaceResolutionFolders].
  void _sortIncludedFoldersIntoWorkspaceResolutions(
    List<Folder> includedFolders,
    File? defaultOptionsFile,
    File? defaultPackageConfigFile,
    List<Folder> nonWorkspaceResolutionFolders,
    Map<String, List<Folder>> workspaceResolutionRootMap,
  ) {
    var rootWorkspaceSpecification = <String, Set<Folder>>{};
    for (Folder folder in includedFolders) {
      var location = _contextRootLocation(
        folder,
        defaultOptionsFile: defaultOptionsFile,
        defaultPackageConfigFile: defaultPackageConfigFile,
        defaultRootFolder: () => folder,
      );

      var addedToWorkspace = false;

      if (folder.path == location.workspace.root) {
        // If opening the root don't try to do anything special.
        var known = rootWorkspaceSpecification[location.workspace.root] ??= {};
        known.clear();
        nonWorkspaceResolutionFolders.addAll(
          workspaceResolutionRootMap[location.workspace.root] ?? [],
        );
      } else {
        var pubspecFile = folder.getChildAssumingFile(file_paths.pubspecYaml);
        if (pubspecFile.exists) {
          var pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
          var resolution = pubspec.resolution;
          if (resolution != null && resolution.value.text == 'workspace') {
            var known = rootWorkspaceSpecification[location.workspace.root] ??=
                _loadWorkspaceDetailsFromPubspec(location.workspace.root);
            if (known.contains(folder)) {
              (workspaceResolutionRootMap[location.workspace.root] ??= []).add(
                folder,
              );
              addedToWorkspace = true;
            }
          }
        }
      }
      if (!addedToWorkspace) {
        nonWorkspaceResolutionFolders.add(folder);
      }
    }
  }

  /// Return a list of paths that contains all of the unique elements from the
  /// given list of [paths], sorted such that shorter paths are first.
  List<String> _uniqueSortedPaths(List<String> paths) {
    Set<String> uniquePaths = Set<String>.from(paths);
    List<String> sortedPaths = uniquePaths.toList();
    sortedPaths.sort((a, b) => a.length - b.length);
    return sortedPaths;
  }

  static Folder _fileSystemRoot(Resource resource) {
    for (var current = resource.parent; ; current = current.parent) {
      if (current.isRoot) {
        return current;
      }
    }
  }

  /// Every element in [folders] must be a folder on the path from a file to
  /// the root of the file system. As such, they are either the same folder,
  /// or one is strictly above the other.
  static Folder? _lowest(List<Folder?> folders) {
    return folders.fold<Folder?>(null, (result, folder) {
      if (result == null) {
        return folder;
      } else if (folder != null && result.contains(folder.path)) {
        return folder;
      } else {
        return result;
      }
    });
  }

  /// Return `true` if the configuration of [existingRoot] is the same as
  /// the requested configuration for the [location].
  static bool _matchRootWithLocation(
    ContextRootImpl existingRoot,
    _RootLocation location,
  ) {
    if (existingRoot.optionsFile != location.optionsFile) {
      return false;
    }

    if (existingRoot.packagesFile != location.packageConfigFile) {
      return false;
    }

    // BasicWorkspace has no special meaning, so can be ignored.
    // Other workspaces have semantic meaning, so must match.
    var workspace = location.workspace;
    if (workspace is! BasicWorkspace) {
      if (existingRoot.workspace.root != workspace.root) {
        return false;
      }
    }

    return true;
  }

  /// Pick a workspace with the most specific root. If the root of [first] is
  /// non-null and is within the root of [second], return [second]. If any of
  /// [first] and [second] is null, return the other one. If the roots aren't
  /// within each other, return [first].
  static Workspace? _mostSpecificWorkspace(
    Workspace? first,
    Workspace? second,
  ) {
    if (first == null) return second;
    if (second == null) return first;
    if (isWithin(first.root, second.root)) {
      return second;
    }
    return first;
  }
}

class _RootLocation {
  final Folder rootFolder;
  final Workspace workspace;
  final File? optionsFile;
  final File? packageConfigFile;

  _RootLocation({
    required this.rootFolder,
    required this.workspace,
    required this.optionsFile,
    required this.packageConfigFile,
  });
}
