// Copyright (c) 2017, 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 'dart:collection';

import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:meta/meta.dart';

/// An object used to merge partial lists of results that were contributed by
/// plugins.
///
/// All of the methods in this class assume that the contributions from the
/// analysis server are the first partial result in the list of partial results
/// to be merged.
class ResultMerger {
  /// Return a list of fixes composed by merging the lists of fixes in the
  /// [partialResultList].
  ///
  /// The resulting list of fixes will contain exactly one fix for every
  /// analysis error for which there are fixes. If two or more plugins
  /// contribute the same fix for a given error, the resulting list will contain
  /// duplications.
  List<plugin.AnalysisErrorFixes> mergeAnalysisErrorFixes(
    List<List<plugin.AnalysisErrorFixes>> partialResultList,
  ) {
    /// Return a key encoding the unique attributes of the given [error].
    String computeKey(AnalysisError error) {
      var buffer = StringBuffer();
      buffer.write(error.location.offset);
      buffer.write(';');
      buffer.write(error.code);
      buffer.write(';');
      buffer.write(error.message);
      buffer.write(';');
      buffer.write(error.correction);
      return buffer.toString();
    }

    var count = partialResultList.length;
    if (count == 0) {
      return <plugin.AnalysisErrorFixes>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var fixesMap = <String, plugin.AnalysisErrorFixes>{};
    for (var fix in partialResultList[0]) {
      fixesMap[computeKey(fix.error)] = fix;
    }
    for (var i = 1; i < count; i++) {
      for (var fix in partialResultList[i]) {
        var key = computeKey(fix.error);
        var mergedFix = fixesMap[key];
        if (mergedFix == null) {
          fixesMap[key] = fix;
        } else {
          // If more than two plugins contribute fixes for the same error, this
          // will result in extra copy operations.
          var mergedChanges = mergedFix.fixes.toList();
          mergedChanges.addAll(fix.fixes);
          var copiedFix = plugin.AnalysisErrorFixes(
            mergedFix.error,
            fixes: mergedChanges,
          );
          fixesMap[key] = copiedFix;
        }
      }
    }
    var mergedFixes = fixesMap.values.toList();
    for (var fixes in mergedFixes) {
      fixes.fixes.sort((first, second) => first.priority - second.priority);
    }
    return mergedFixes;
  }

  /// Return a list of errors composed by merging the lists of errors in the
  /// [partialResultList].
  ///
  /// The resulting list will contain all of the analysis errors from all of the
  /// plugins. If two or more plugins contribute the same error the resulting
  /// list will contain duplications.
  List<AnalysisError> mergeAnalysisErrors(
    List<List<AnalysisError>> partialResultList,
  ) {
    // TODO(brianwilkerson): Consider merging duplicate errors (same code,
    // location, and messages). If we do that, we should return the logical-or
    // of the hasFix fields from the merged errors.
    var count = partialResultList.length;
    if (count == 0) {
      return <AnalysisError>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var mergedErrors = <AnalysisError>[];
    for (var partialResults in partialResultList) {
      mergedErrors.addAll(partialResults);
    }
    return mergedErrors;
  }

  /// Return a list of suggestions composed by merging the lists of suggestions
  /// in the [partialResultList].
  ///
  /// The resulting list will contain all of the suggestions from all of the
  /// plugins. If two or more plugins contribute the same suggestion the
  /// resulting list will contain duplications.
  List<CompletionSuggestion> mergeCompletionSuggestions(
    List<List<CompletionSuggestion>> partialResultList,
  ) {
    var count = partialResultList.length;
    if (count == 0) {
      return <CompletionSuggestion>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var mergedSuggestions = <CompletionSuggestion>[];
    for (var partialResults in partialResultList) {
      mergedSuggestions.addAll(partialResults);
    }
    return mergedSuggestions;
  }

  /// Return a list of regions composed by merging the lists of regions in the
  /// [partialResultList].
  ///
  /// The resulting list will contain all of the folding regions from all of the
  /// plugins. If a plugin contributes a folding region that overlaps a region
  /// from a previous plugin, the overlapping region will be omitted. (For these
  /// purposes, if either region is fully contained within the other they are
  /// not considered to be overlapping.)
  List<FoldingRegion> mergeFoldingRegions(
    List<List<FoldingRegion>> partialResultList,
  ) {
    var count = partialResultList.length;
    if (count == 0) {
      return <FoldingRegion>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var mergedRegions = partialResultList[0].toList();

    /// Return `true` if the [newRegion] does not overlap any of the regions in
    /// the collection of [mergedRegions].
    bool isNonOverlapping(FoldingRegion newRegion) {
      var newStart = newRegion.offset;
      var newEnd = newStart + newRegion.length;
      for (var existingRegion in mergedRegions) {
        var existingStart = existingRegion.offset;
        var existingEnd = existingStart + existingRegion.length;
        if (overlaps(
          newStart,
          newEnd,
          existingStart,
          existingEnd,
          allowNesting: true,
        )) {
          return false;
        }
      }
      return true;
    }

    for (var i = 1; i < count; i++) {
      var partialResults = partialResultList[i];
      for (var region in partialResults) {
        if (isNonOverlapping(region)) {
          mergedRegions.add(region);
        }
      }
    }
    return mergedRegions;
  }

  /// Return a list of regions composed by merging the lists of regions in the
  /// [partialResultList].
  ///
  /// The resulting list will contain all of the highlight regions from all of
  /// the plugins. If two or more plugins contribute the same highlight region
  /// the resulting list will contain duplications.
  List<HighlightRegion> mergeHighlightRegions(
    List<List<HighlightRegion>> partialResultList,
  ) {
    var count = partialResultList.length;
    if (count == 0) {
      return <HighlightRegion>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var mergedRegions = <HighlightRegion>[];
    for (var partialResults in partialResultList) {
      mergedRegions.addAll(partialResults);
    }
    return mergedRegions;
  }

  /// Return navigation notification parameters composed by merging the
  /// parameters in the [partialResultList].
  ///
  /// The resulting list will contain all of the navigation regions from all of
  /// the plugins. If a plugin contributes a navigation region that overlaps a
  /// region from a previous plugin, the overlapping region will be omitted.
  /// (For these purposes, nested regions are considered to be overlapping.)
  AnalysisNavigationParams? mergeNavigation(
    List<AnalysisNavigationParams> partialResultList,
  ) {
    var count = partialResultList.length;
    if (count == 0) {
      return null;
    } else if (count == 1) {
      return partialResultList[0];
    }
    var base = partialResultList[0];
    var file = base.file;
    var mergedRegions = base.regions.toList();
    var mergedTargets = base.targets.toList();
    var mergedFiles = base.files.toList();

    /// Return `true` if the [newRegion] does not overlap any of the regions in
    /// the collection of [mergedRegions].
    bool isNonOverlapping(NavigationRegion newRegion) {
      var newStart = newRegion.offset;
      var newEnd = newStart + newRegion.length;
      for (var mergedRegion in mergedRegions) {
        var mergedStart = mergedRegion.offset;
        var mergedEnd = mergedStart + mergedRegion.length;
        if (overlaps(newStart, newEnd, mergedStart, mergedEnd)) {
          return false;
        }
      }
      return true;
    }

    /// Return the index of the region in the collection of [mergedRegions] that
    /// covers exactly the same region as the [newRegion], or `-1` if there is
    /// no such region.
    int matchingRegion(NavigationRegion newRegion) {
      var newOffset = newRegion.offset;
      var newLength = newRegion.length;
      for (var i = 0; i < mergedRegions.length; i++) {
        var mergedRegion = mergedRegions[i];
        if (newOffset == mergedRegion.offset &&
            newLength == mergedRegion.length) {
          return i;
        }
      }
      return -1;
    }

    for (var i = 1; i < count; i++) {
      // For now we take the optimistic approach of assuming that most or all of
      // the regions will not overlap and that we therefore don't need to remove
      // any unreferenced files or targets from the lists. If that isn't true
      // then this could result in server sending more data to the client than
      // is necessary.
      var result = partialResultList[i];
      var regions = result.regions;
      var targets = result.targets;
      var files = result.files;
      //
      // Merge the file data.
      //
      var fileMap = <int, int>{};
      for (var j = 0; j < files.length; j++) {
        var file = files[j];
        var index = mergedFiles.indexOf(file);
        if (index < 0) {
          index = mergedFiles.length;
          mergedFiles.add(file);
        }
        fileMap[j] = index;
      }
      //
      // Merge the target data.
      //
      var targetMap = <int, int>{};
      for (var j = 0; j < targets.length; j++) {
        var target = targets[j];
        var newIndex = fileMap[target.fileIndex];
        if (newIndex != null && target.fileIndex != newIndex) {
          target = NavigationTarget(
            target.kind,
            newIndex,
            target.offset,
            target.length,
            target.startLine,
            target.startColumn,
            codeOffset: target.codeOffset,
            codeLength: target.codeLength,
          );
        }
        var index = mergedTargets.indexOf(target);
        if (index < 0) {
          index = mergedTargets.length;
          mergedTargets.add(target);
        }
        targetMap[j] = index;
      }
      //
      // Merge the region data.
      //
      for (var j = 0; j < regions.length; j++) {
        var region = regions[j];
        var newTargets = region.targets
            .map((int oldTarget) => targetMap[oldTarget])
            .toList()
            .cast<int>();
        if (region.targets != newTargets) {
          region = NavigationRegion(region.offset, region.length, newTargets);
        }
        var index = matchingRegion(region);
        if (index >= 0) {
          var mergedRegion = mergedRegions[index];
          var mergedTargets = mergedRegion.targets;
          var added = false;
          for (var target in region.targets) {
            if (!mergedTargets.contains(target)) {
              if (added) {
                mergedTargets.add(target);
              } else {
                //
                // This is potentially inefficient. If a merged region matches
                // regions from multiple plugins it will be copied multiple
                // times. The likelihood seems small enough to not warrant
                // optimizing this further.
                //
                mergedTargets = mergedTargets.toList();
                mergedTargets.add(target);
                mergedRegion = NavigationRegion(
                  mergedRegion.offset,
                  mergedRegion.length,
                  mergedTargets,
                );
                mergedRegions[index] = mergedRegion;
                added = true;
              }
            }
          }
          if (added) {
            mergedTargets.sort();
          }
        } else if (isNonOverlapping(region)) {
          mergedRegions.add(region);
        }
      }
    }
    return AnalysisNavigationParams(
      file,
      mergedRegions,
      mergedTargets,
      mergedFiles,
    );
  }

  /// Return a list of occurrences composed by merging the lists of occurrences
  /// in the [partialResultList].
  ///
  /// The resulting list of occurrences will contain exactly one occurrences for
  /// every element for which there is at least one occurrences. If two or more
  /// plugins contribute an occurrences for the same element, the resulting
  /// occurrences for that element will include all of the locations from all of
  /// the plugins without duplications.
  List<Occurrences> mergeOccurrences(
    List<List<Occurrences>> partialResultList,
  ) {
    var count = partialResultList.length;
    if (count == 0) {
      return <Occurrences>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var elementMap = <Element, Set<int>>{};
    for (var partialResults in partialResultList) {
      for (var occurrences in partialResults) {
        var element = occurrences.element;
        var offsets = elementMap.putIfAbsent(element, () => HashSet<int>());
        offsets.addAll(occurrences.offsets);
      }
    }
    var mergedOccurrences = <Occurrences>[];
    elementMap.forEach((Element element, Set<int> offsets) {
      var sortedOffsets = offsets.toList();
      sortedOffsets.sort();
      mergedOccurrences.add(
        Occurrences(element, sortedOffsets, element.name.length),
      );
    });
    return mergedOccurrences;
  }

  /// Return a list of outlines composed by merging the lists of outlines in the
  /// [partialResultList].
  ///
  /// The resulting list of outlines will contain ...
  ///
  /// Throw an exception if any of the outlines are associated with an element
  /// that does not have a location.
  ///
  /// Throw an exception if any outline has children that are also children of
  /// another outline. No exception is thrown if a plugin contributes a
  /// top-level outline that is a child of an outline contributed by a different
  /// plugin.
  List<Outline> mergeOutline(List<List<Outline>> partialResultList) {
    /// Return a key encoding the unique attributes of the given [element].
    String computeKey(Element element) {
      var location = element.location;
      if (location == null) {
        throw StateError(
          'Elements in an outline are expected to have a location',
        );
      }
      var buffer = StringBuffer();
      buffer.write(location.offset);
      buffer.write(';');
      buffer.write(element.kind.name);
      return buffer.toString();
    }

    var count = partialResultList.length;
    if (count == 0) {
      return <Outline>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var mergedOutlines = partialResultList[0].toList();
    var outlineMap = <String, Outline>{};
    var copyMap = <Outline, Outline>{};

    /// Add the given [outline] and all of its children to the [outlineMap].
    void addToMap(Outline outline) {
      var key = computeKey(outline.element);
      if (outlineMap.containsKey(key)) {
        // TODO(brianwilkerson): Decide how to handle this more gracefully.
        throw StateError('Inconsistent outlines');
      }
      outlineMap[key] = outline;
      outline.children?.forEach(addToMap);
    }

    /// Merge the children of the [newOutline] into the list of children of the
    /// [mergedOutline].
    void mergeChildren(Outline mergedOutline, Outline newOutline) {
      var newChildren = newOutline.children;
      if (newChildren != null) {
        for (var newChild in newChildren) {
          var mergedChild = outlineMap[computeKey(newChild.element)];
          if (mergedChild == null) {
            // The [newChild] isn't in the existing list.
            var mergedChildren = _copyList(mergedOutline.children);
            mergedChildren.add(newChild);
            copyMap.putIfAbsent(
              mergedOutline,
              () => Outline(
                mergedOutline.element,
                mergedOutline.offset,
                mergedOutline.length,
                mergedOutline.codeOffset,
                mergedOutline.codeLength,
                children: mergedChildren,
              ),
            );
            addToMap(newChild);
          } else {
            mergeChildren(mergedChild, newChild);
          }
        }
      }
    }

    mergedOutlines.forEach(addToMap);
    for (var i = 1; i < count; i++) {
      for (var outline in partialResultList[i]) {
        var mergedOutline = outlineMap[computeKey(outline.element)];
        if (mergedOutline == null) {
          // The [outline] does not correspond to any previously merged outline.
          mergedOutlines.add(outline);
          addToMap(outline);
        } else {
          // The [outline] corresponds to a previously merged outline, so we
          // just need to add its children to the merged outline's children.
          mergeChildren(mergedOutline, outline);
        }
      }
    }

    /// Perform a depth first traversal of the outline structure rooted at the
    /// given [outline] item, re-building each item if any of its children have
    /// been updated by the merge process.
    Outline traverse(Outline outline) {
      var copiedOutline = copyMap[outline];
      var isCopied = copiedOutline != null;
      copiedOutline ??= outline;
      var currentChildren = copiedOutline.children;
      if (currentChildren == null || currentChildren.isEmpty) {
        return outline;
      }
      var updatedChildren = currentChildren
          .map((Outline child) => traverse(child))
          .toList();
      if (currentChildren != updatedChildren) {
        if (!isCopied) {
          return Outline(
            copiedOutline.element,
            copiedOutline.offset,
            copiedOutline.length,
            copiedOutline.codeOffset,
            copiedOutline.codeLength,
            children: updatedChildren,
          );
        }
        copiedOutline.children = updatedChildren;
        return copiedOutline;
      }
      return outline;
    }

    for (var i = 0; i < mergedOutlines.length; i++) {
      mergedOutlines[i] = traverse(mergedOutlines[i]);
    }
    return mergedOutlines;
  }

  /// Return a list of source changes composed by merging the lists of source
  /// changes in the [partialResultList].
  ///
  /// The resulting list will contain all of the source changes from all of the
  /// plugins. If two or more plugins contribute the same source change the
  /// resulting list will contain duplications.
  List<plugin.PrioritizedSourceChange> mergePrioritizedSourceChanges(
    List<List<plugin.PrioritizedSourceChange>> partialResultList,
  ) {
    var count = partialResultList.length;
    if (count == 0) {
      return <plugin.PrioritizedSourceChange>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var mergedChanges = <plugin.PrioritizedSourceChange>[];
    for (var partialResults in partialResultList) {
      mergedChanges.addAll(partialResults);
    }
    mergedChanges.sort((first, second) => first.priority - second.priority);
    return mergedChanges;
  }

  /// Return a refactoring feedback composed by merging the refactoring
  /// feedbacks in [feedbacks].
  ///
  /// The content of the resulting feedback depends on the kind of feedbacks
  /// being merged.
  ///
  /// Throw an exception if the refactoring feedbacks are of an unhandled type.
  ///
  /// The [feedbacks] are expected to all be of the same type. If that
  /// expectation is violated, and exception might be thrown.
  RefactoringFeedback? mergeRefactoringFeedbacks(
    List<RefactoringFeedback> feedbacks,
  ) {
    var count = feedbacks.length;
    if (count == 0) {
      return null;
    } else if (count == 1) {
      return feedbacks[0];
    }
    var first = feedbacks[0];
    if (first is ConvertGetterToMethodFeedback) {
      // The feedbacks are empty, so there's nothing to merge.
      return first;
    } else if (first is ConvertMethodToGetterFeedback) {
      // The feedbacks are empty, so there's nothing to merge.
      return first;
    } else if (first is ExtractLocalVariableFeedback) {
      var coveringExpressionOffsets = _copyList(
        first.coveringExpressionOffsets,
      );
      var coveringExpressionLengths = _copyList(
        first.coveringExpressionLengths,
      );
      var names = first.names.toList();
      var offsets = first.offsets.toList();
      var lengths = first.lengths.toList();
      for (var i = 1; i < count; i++) {
        var feedback = feedbacks[i] as ExtractLocalVariableFeedback;
        // TODO(brianwilkerson): This doesn't ensure that the covering data is in
        // the right order and consistent.
        var coveringOffsets = feedback.coveringExpressionOffsets;
        if (coveringOffsets != null) {
          coveringExpressionOffsets.addAll(coveringOffsets);
        }
        var coveringLengths = feedback.coveringExpressionLengths;
        if (coveringLengths != null) {
          coveringExpressionLengths.addAll(coveringLengths);
        }
        for (var name in feedback.names) {
          if (!names.contains(name)) {
            names.add(name);
          }
        }
        offsets.addAll(feedback.offsets);
        lengths.addAll(feedback.lengths);
      }
      return ExtractLocalVariableFeedback(
        names.toList(),
        offsets,
        lengths,
        coveringExpressionOffsets: coveringExpressionOffsets.isEmpty
            ? null
            : coveringExpressionOffsets,
        coveringExpressionLengths: coveringExpressionLengths.isEmpty
            ? null
            : coveringExpressionLengths,
      );
    } else if (first is ExtractMethodFeedback) {
      var offset = first.offset;
      var length = first.length;
      var returnType = first.returnType;
      var names = first.names.toList();
      var canCreateGetter = first.canCreateGetter;
      var parameters = first.parameters;
      var offsets = first.offsets.toList();
      var lengths = first.lengths.toList();
      for (var i = 1; i < count; i++) {
        var feedback = feedbacks[i] as ExtractMethodFeedback;
        if (returnType.isEmpty) {
          returnType = feedback.returnType;
        }
        for (var name in feedback.names) {
          if (!names.contains(name)) {
            names.add(name);
          }
        }
        canCreateGetter = canCreateGetter && feedback.canCreateGetter;
        // TODO(brianwilkerson): This doesn't allow plugins to add parameters.
        // TODO(brianwilkerson): This doesn't check for duplicate offsets.
        offsets.addAll(feedback.offsets);
        lengths.addAll(feedback.lengths);
      }
      return ExtractMethodFeedback(
        offset,
        length,
        returnType,
        names.toList(),
        canCreateGetter,
        parameters,
        offsets,
        lengths,
      );
    } else if (first is InlineLocalVariableFeedback) {
      var occurrences = first.occurrences;
      for (var i = 1; i < count; i++) {
        occurrences +=
            (feedbacks[i] as InlineLocalVariableFeedback).occurrences;
      }
      return InlineLocalVariableFeedback(first.name, occurrences);
    } else if (first is InlineMethodFeedback) {
      // There is nothing in the feedback that can reasonably be extended or
      // modified by other plugins.
      return first;
    } else if (first is MoveFileFeedback) {
      // The feedbacks are empty, so there's nothing to merge.
      return first;
    } else if (first is RenameFeedback) {
      // There is nothing in the feedback that can reasonably be extended or
      // modified by other plugins.
      return first;
    }
    throw StateError(
      'Unsupported class of refactoring feedback: ${first.runtimeType}',
    );
  }

  /// Return a list of refactoring kinds composed by merging the lists of
  /// refactoring kinds in the [partialResultList].
  ///
  /// The resulting list will contain all of the refactoring kinds from all of
  /// the plugins, but will not contain duplicate elements.
  List<RefactoringKind> mergeRefactoringKinds(
    List<List<RefactoringKind>> partialResultList,
  ) {
    var count = partialResultList.length;
    if (count == 0) {
      return <RefactoringKind>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    Set<RefactoringKind> mergedKinds = HashSet<RefactoringKind>();
    for (var partialResults in partialResultList) {
      mergedKinds.addAll(partialResults);
    }
    return mergedKinds.toList();
  }

  /// Return the result for a getRefactorings request composed by merging the
  /// results in the [partialResultList].
  ///
  /// The returned result will contain the concatenation of the initial,
  /// options, and final problems. If two or more plugins produce the same
  /// problem, then the resulting list of problems will contain duplications.
  ///
  /// The returned result will contain a merged list of refactoring feedbacks
  /// (as defined by [mergeRefactoringFeedbacks]) and a merged list of source
  /// changes (as defined by `mergeChanges`).
  ///
  /// The returned result will contain the concatenation of the potential edits.
  /// If two or more plugins produce the same potential edit, then the resulting
  /// list of potential edits will contain duplications.
  EditGetRefactoringResult? mergeRefactorings(
    List<EditGetRefactoringResult> partialResultList,
  ) {
    /// Return the result of merging the given list of source [changes] into a
    /// single source change.
    ///
    /// The resulting change will have the first non-null message and the first
    /// non-null selection. The linked edit groups will be a concatenation of
    /// all of the individual linked edit groups because there's no way to
    /// determine when two such groups should be merged. The resulting list of
    /// edits will be merged at the level of the file being edited, but will be
    /// a concatenation of the individual edits within each file, even if
    /// multiple plugins contribute duplicate or conflicting edits.
    SourceChange? mergeChanges(List<SourceChange> changes) {
      var count = changes.length;
      if (count == 0) {
        return null;
      } else if (count == 1) {
        return changes[0];
      }
      var first = changes[0];
      var message = first.message;
      var editMap = <String, SourceFileEdit>{};
      for (var edit in first.edits) {
        editMap[edit.file] = edit;
      }
      var linkedEditGroups = first.linkedEditGroups.toList();
      var selection = first.selection;
      for (var i = 1; i < count; i++) {
        var change = changes[i];
        for (var edit in change.edits) {
          var mergedEdit = editMap[edit.file];
          if (mergedEdit == null) {
            editMap[edit.file] = edit;
          } else {
            // This doesn't detect if multiple plugins contribute the same (or
            // conflicting) edits.
            var edits = mergedEdit.edits.toList();
            edits.addAll(edit.edits);
            editMap[edit.file] = SourceFileEdit(
              mergedEdit.file,
              mergedEdit.fileStamp,
              edits: edits,
            );
          }
        }
        linkedEditGroups.addAll(change.linkedEditGroups);
        selection ??= change.selection;
      }
      return SourceChange(
        message,
        edits: editMap.values.toList(),
        linkedEditGroups: linkedEditGroups,
        selection: selection,
      );
    }

    var count = partialResultList.length;
    if (count == 0) {
      return null;
    } else if (count == 1) {
      return partialResultList[0];
    }
    var result = partialResultList[0];
    var initialProblems = result.initialProblems.toList();
    var optionsProblems = result.optionsProblems.toList();
    var finalProblems = result.finalProblems.toList();
    var feedbacks = <RefactoringFeedback>[];
    var feedback = result.feedback;
    if (feedback != null) {
      feedbacks.add(feedback);
    }
    var changes = <SourceChange>[];
    var change = result.change;
    if (change != null) {
      changes.add(change);
    }
    var potentialEdits = _copyList(result.potentialEdits);
    for (var i = 1; i < count; i++) {
      var result = partialResultList[1];
      initialProblems.addAll(result.initialProblems);
      optionsProblems.addAll(result.optionsProblems);
      finalProblems.addAll(result.finalProblems);
      var feedback = result.feedback;
      if (feedback != null) {
        feedbacks.add(feedback);
      }
      var change = result.change;
      if (change != null) {
        changes.add(change);
      }
      var edits = result.potentialEdits;
      if (edits != null) {
        potentialEdits.addAll(edits);
      }
    }
    return EditGetRefactoringResult(
      initialProblems,
      optionsProblems,
      finalProblems,
      feedback: mergeRefactoringFeedbacks(feedbacks),
      change: mergeChanges(changes),
      potentialEdits: potentialEdits,
    );
  }

  /// Return a list of source changes composed by merging the lists of source
  /// changes in the [partialResultList].
  ///
  /// The resulting list will contain all of the source changes from all of the
  /// plugins. If two or more plugins contribute the same source change the
  /// resulting list will contain duplications.
  List<SourceChange> mergeSourceChanges(
    List<List<SourceChange>> partialResultList,
  ) {
    var count = partialResultList.length;
    if (count == 0) {
      return <SourceChange>[];
    } else if (count == 1) {
      return partialResultList[0];
    }
    var mergedChanges = <SourceChange>[];
    for (var partialResults in partialResultList) {
      mergedChanges.addAll(partialResults);
    }
    return mergedChanges;
  }

  /// Return `true` if a region extending from [leftStart] (inclusive) to
  /// [leftEnd] (exclusive) overlaps a region extending from [rightStart]
  /// (inclusive) to [rightEnd] (exclusive). If [allowNesting] is `true`, then
  /// the regions are allowed to overlap as long as one region is completely
  /// nested within the other region.
  @visibleForTesting
  bool overlaps(
    int leftStart,
    int leftEnd,
    int rightStart,
    int rightEnd, {
    bool allowNesting = false,
  }) {
    if (leftEnd < rightStart || leftStart > rightEnd) {
      return false;
    }
    if (!allowNesting) {
      return true;
    }
    return !((leftStart <= rightStart && rightEnd <= leftEnd) ||
        (rightStart <= leftStart && leftEnd <= rightEnd));
  }

  /// Return a copy of the [list], or an empty list if [list] is `null`.
  List<E> _copyList<E>(List<E>? list) => list == null ? <E>[] : list.toList();
}
