// 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.

// Many functions here are mostly camelcase, with an occasional underscore to
// separate phrases.
// ignore_for_file: non_constant_identifier_names

import 'dart:math';

import 'package:analysis_server/src/protocol_server.dart' hide Element;
import 'package:analysis_server/src/services/correction/source_buffer.dart';
import 'package:analysis_server_plugin/edit/correction_utils.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/diagnostic/diagnostic.dart' as engine;
import 'package:analyzer/error/error.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/source/source.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
import 'package:collection/collection.dart';

/// An enumeration of possible statement completion kinds.
abstract final class DartStatementCompletion {
  static const _noCompletion = StatementCompletionKind(
    'No_COMPLETION',
    'No completion available',
  );
  static const _simpleEnter = StatementCompletionKind(
    'SIMPLE_ENTER',
    'Insert a newline at the end of the current line',
  );
  static const _simpleSemicolon = StatementCompletionKind(
    'SIMPLE_SEMICOLON',
    'Add a semicolon and newline',
  );
  static const _completeClassDeclaration = StatementCompletionKind(
    'COMPLETE_CLASS_DECLARATION',
    'Complete class declaration',
  );
  static const _completeControlFlowBlock = StatementCompletionKind(
    'COMPLETE_CONTROL_FLOW_BLOCK',
    'Complete control flow block',
  );
  static const _completeDoStmt = StatementCompletionKind(
    'COMPLETE_DO_STMT',
    'Complete do-statement',
  );
  static const _completeIfStmt = StatementCompletionKind(
    'COMPLETE_IF_STMT',
    'Complete if-statement',
  );
  static const _completeForStmt = StatementCompletionKind(
    'COMPLETE_FOR_STMT',
    'Complete for-statement',
  );
  static const _completeForEachStmt = StatementCompletionKind(
    'COMPLETE_FOR_EACH_STMT',
    'Complete for-each-statement',
  );
  static const _completeFunctionDeclaration = StatementCompletionKind(
    'COMPLETE_FUNCTION_DECLARATION',
    'Complete function declaration',
  );
  static const _completeSwitchStmt = StatementCompletionKind(
    'COMPLETE_SWITCH_STMT',
    'Complete switch-statement',
  );
  static const _completeTryStmt = StatementCompletionKind(
    'COMPLETE_TRY_STMT',
    'Complete try-statement',
  );
  static const _completeVariableDeclaration = StatementCompletionKind(
    'COMPLETE_VARIABLE_DECLARATION',
    'Complete variable declaration',
  );
  static const _completeWhiteStmt = StatementCompletionKind(
    'COMPLETE_WHILE_STMT',
    'Complete while-statement',
  );
}

/// A description of a statement completion.
///
/// Clients may not extend, implement or mix-in this class.
class StatementCompletion {
  /// A description of the assist being proposed.
  final StatementCompletionKind kind;

  /// The change to be made in order to apply the assist.
  final SourceChange change;

  /// Initialize a newly created completion to have the given [kind] and
  /// [change].
  new(this.kind, this.change);
}

/// The context for computing a statement completion.
class StatementCompletionContext {
  final ResolvedUnitResult resolveResult;
  final int selectionOffset;

  new(this.resolveResult, this.selectionOffset);
}

/// A description of a class of statement completions. Instances are intended to
/// hold the information that is common across a number of completions and to be
/// shared by those completions.
///
/// Clients may not extend, implement or mix-in this class.
class StatementCompletionKind {
  /// The name of this kind of statement completion, used for debugging.
  final String name;

  /// A human-readable description of the changes that will be applied by this
  /// kind of statement completion.
  final String message;

  /// Initialize a newly created kind of statement completion to have the given
  /// [name] and [message].
  const new(this.name, this.message);

  @override
  String toString() => name;
}

/// The computer for Dart statement completions.
class StatementCompletionProcessor {
  static final _noCompletion = StatementCompletion(
    DartStatementCompletion._noCompletion,
    SourceChange('', edits: []),
  );

  final StatementCompletionContext statementContext;
  final CorrectionUtils utils;

  // TODO(brianwilkerson): Refactor the code so that the completion is returned
  // from the methods in which it's computed rather than being a field that we
  // have to test.
  StatementCompletion? completion;
  SourceChange change = SourceChange('statement-completion');
  List<engine.Diagnostic> diagnostics = [];
  final Map<String, LinkedEditGroup> linkedPositionGroups =
      <String, LinkedEditGroup>{};
  Position? exitPosition;

  new(this.statementContext)
    : utils = CorrectionUtils(statementContext.resolveResult);

  String get eol => utils.endOfLine;

  String get file => statementContext.resolveResult.path;

  LineInfo get lineInfo => statementContext.resolveResult.lineInfo;

  int get selectionOffset => statementContext.selectionOffset;

  Source get source => unitFragment.source;

  CompilationUnit get unit => statementContext.resolveResult.unit;

  LibraryFragment get unitFragment => unit.declaredFragment!;

  Future<StatementCompletion> compute() async {
    var node = _selectedNode();
    if (node == null) {
      return _noCompletion;
    }
    node = node.thisOrAncestorMatching(
      (n) => n is Statement || _isNonStatementDeclaration(n),
    );
    if (node == null) {
      return _complete_simpleEnter() ? completion! : _noCompletion;
    }
    if (node is Block) {
      if (node.statements.isNotEmpty) {
        node = node.statements.last;
      }
    }
    if (_isEmptyStatementOrEmptyBlock(node)) {
      node = node.parent!;
    }
    for (var diagnostic in statementContext.resolveResult.diagnostics) {
      if (diagnostic.offset >= node.offset && diagnostic.offset <= node.end) {
        if (diagnostic.diagnosticCode.type == DiagnosticType.SYNTACTIC_ERROR) {
          diagnostics.add(diagnostic);
        }
      }
    }

    _checkExpressions(node);
    if (node is Statement) {
      if (diagnostics.isEmpty) {
        if (_complete_ifStatement(node) ||
            _complete_forStatement2(node) ||
            _complete_whileStatement(node) ||
            _complete_controlFlowBlock(node)) {
          return completion!;
        }
      } else {
        if (_complete_ifStatement(node) ||
            _complete_doStatement(node) ||
            _complete_forStatement2(node) ||
            _complete_functionDeclarationStatement(node) ||
            _complete_switchStatement(node) ||
            _complete_tryStatement(node) ||
            _complete_whileStatement(node) ||
            _complete_controlFlowBlock(node) ||
            _complete_simpleSemicolon(node) ||
            _complete_methodCall(node)) {
          return completion!;
        }
      }
    } else if (node is Declaration) {
      if (diagnostics.isNotEmpty) {
        if (_complete_classDeclaration(node) ||
            _complete_variableDeclaration(node) ||
            _complete_simpleSemicolon(node) ||
            _complete_functionDeclaration(node)) {
          return completion!;
        }
      }
    }
    if (_complete_simpleEnter()) {
      return completion!;
    }
    return _noCompletion;
  }

  void _addInsertEdit(int offset, String text) {
    var edit = SourceEdit(offset, 0, text);
    doSourceChange_addFragmentEdit(change, unitFragment, edit);
  }

  void _addReplaceEdit(SourceRange range, String text) {
    var edit = SourceEdit(range.offset, range.length, text);
    // TODO(brianwilkerson): The commented out function call has been inlined in
    //  order to work around a situation in which _complete_doStatement creates
    //  a conflicting edit that happens to work because of the order in which
    //  the edits are applied. The implementation needs to be cleaned up in
    //  order to prevent the conflicting edit from being generated.
    // doSourceChange_addElementEdit(change, unitElement, edit);
    var fileEdit = change.getFileEdit(unitFragment.source.fullName);
    if (fileEdit == null) {
      fileEdit = SourceFileEdit(file, 0);
      change.addFileEdit(fileEdit);
    }
    var edits = fileEdit.edits;
    var length = edits.length;
    var index = 0;
    while (index < length && edits[index].offset > edit.offset) {
      index++;
    }
    edits.insert(index, edit);
  }

  void _appendEmptyBraces(SourceBuilder sb, [bool needsExitMark = false]) {
    sb.append('{');
    sb.append(eol);
    var indent = utils.getLinePrefix(selectionOffset);
    sb.append(indent);
    sb.append(utils.oneIndent);
    if (needsExitMark && sb.exitOffset == null) {
      sb.setExitOffset();
    }
    sb.append(eol);
    sb.append(indent);
    sb.append('}');
  }

  int _appendNewlinePlusIndent() {
    return _appendNewlinePlusIndentAt(selectionOffset);
  }

  int _appendNewlinePlusIndentAt(int offset) {
    // Append a newline plus proper indent and another newline.
    // Return the position before the second newline.
    var indent = utils.getLinePrefix(offset);
    var loc = utils.getLineNext(offset);
    _addInsertEdit(loc, indent + eol);
    return loc + indent.length;
  }

  String _baseNodeText(AstNode astNode) {
    var text = utils.getNodeText(astNode);
    if (text.endsWith(eol)) {
      text = text.substring(0, text.length - eol.length);
    }
    return text;
  }

  void _checkExpressions(AstNode node) {
    // Note: This may queue edits that have to be accounted for later.
    // See _lengthOfInsertions().
    AstNode? diagnosticMatching(
      DiagnosticCode diagnosticCode, {
      Pattern? partialMatch,
    }) {
      var error = _findDiagnostic(diagnosticCode, partialMatch: partialMatch);
      if (error == null) {
        return null;
      }
      var expr = _selectedNode();
      return (expr?.thisOrAncestorOfType<StringInterpolation>() == null)
          ? expr
          : null;
    }

    var expr = diagnosticMatching(diag.unterminatedStringLiteral);
    if (expr != null) {
      var source = utils.getNodeText(expr);
      var content = source;
      var char = content.codeUnitAt(0);
      if (char == 'r'.codeUnitAt(0)) {
        content = source.substring(1);
        char = content.codeUnitAt(0);
      }
      String delimiter;
      int loc;
      if (content.length >= 3 &&
          char == content.codeUnitAt(1) &&
          char == content.codeUnitAt(2)) {
        // multi-line string
        delimiter = content.substring(0, 3);
        var newlineLoc = source.indexOf(eol, selectionOffset - expr.offset);
        if (newlineLoc < 0) {
          newlineLoc = source.length;
        }
        loc = newlineLoc + expr.offset;
      } else {
        // add first char of src
        delimiter = content.substring(0, 1);
        loc = expr.offset + source.length;
      }
      _removeError(diag.unterminatedStringLiteral);
      _addInsertEdit(loc, delimiter);
    }
    expr =
        diagnosticMatching(diag.expectedToken, partialMatch: "']'") ??
        diagnosticMatching(diag.expectedToken, partialMatch: "']'");
    if (expr != null) {
      expr = expr.thisOrAncestorOfType<ListLiteral>();
      if (expr is ListLiteral) {
        if (expr.rightBracket.isSynthetic) {
          var src = utils.getNodeText(expr).trim();
          var loc = expr.offset + src.length;
          if (src.contains(eol)) {
            var indent = utils.getNodePrefix(node);
            _addInsertEdit(loc, ',$eol$indent]');
          } else {
            _addInsertEdit(loc, ']');
          }
          _removeError(diag.expectedToken, partialMatch: "']'");
        }
      }
    }
    // The following code is similar to the code for ']' but does not work well.
    // A closing brace is recognized as belong to the map even if it is intended
    // to close a block of code.
    /*
    expr = errorMatching(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "'}'");
    if (expr != null) {
      expr = expr.getAncestor((n) => n is MapLiteral);
      if (expr != null) {
        MapLiteral lit = expr;
        String src = utils.getNodeText(expr).trim();
        int loc = expr.offset + src.length;
        if (lit.entries.last.separator.isSynthetic) {
          _addInsertEdit(loc, ': ');
        }
        if (!src.endsWith('}')/*lit.rightBracket.isSynthetic*/) {
          _addInsertEdit(loc, '}');
        }
        _removeError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "'}'");
        var ms =
          _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
        if (ms != null) {
          // Ensure the semicolon gets inserted in the correct location.
          ms.offset = loc - 1;
        }
      }
    }
    */
  }

  bool _complete_classDeclaration(AstNode node) {
    if (node is! ClassDeclaration) {
      return false;
    }
    var body = node.body;
    if (body is! BlockClassBody) {
      return false;
    }
    if (body.leftBracket.isSynthetic && diagnostics.length == 1) {
      // The space before the left brace is assumed to exist, even if it does not.
      var sb = SourceBuilder(file, node.end - eol.length);
      sb.append(' ');
      _appendEmptyBraces(sb, true);
      _insertBuilder(sb);
      _setCompletion(DartStatementCompletion._completeClassDeclaration);
      return true;
    }
    return false;
  }

  bool _complete_controlFlowBlock(AstNode node) {
    var expr = (node is ExpressionStatement)
        ? node.expression
        : (node is ReturnStatement ? node.expression : null);
    if (!(node is ReturnStatement || expr is ThrowExpression)) {
      return false;
    }
    var parent = node.parent;
    if (parent is! Block) {
      return false;
    }
    var outer = parent.parent;
    if (!(outer is DoStatement ||
        outer is ForStatement ||
        outer is IfStatement ||
        outer is WhileStatement)) {
      return false;
    }
    var previousInsertions = _lengthOfInsertions();
    var delta = 0;
    if (diagnostics.isNotEmpty) {
      var error = _findDiagnostic(diag.expectedToken, partialMatch: "';'");
      if (error != null) {
        int insertOffset;
        // Fasta scanner reports unterminated string literal errors
        // and generates a synthetic string token with non-zero length.
        // Because of this, check for length == 0 rather than isSynthetic.
        if (expr == null || expr.length == 0) {
          if (node is ReturnStatement) {
            insertOffset = node.returnKeyword.end;
          } else if (node is ExpressionStatement) {
            insertOffset =
                (node.expression as ThrowExpression).throwKeyword.end;
          } else {
            insertOffset = node.end; // Not reached.
          }
        } else {
          insertOffset = expr.end;
        }
        // TODO(messick): Uncomment the following line when error location is fixed.
        //insertOffset = error.offset + error.length;
        _addInsertEdit(insertOffset, ';');
        delta = 1;
      }
    }
    var offset = _appendNewlinePlusIndentAt(parent.end);
    exitPosition = Position(file, offset + delta + previousInsertions);
    _setCompletion(DartStatementCompletion._completeControlFlowBlock);
    return true;
  }

  bool _complete_doStatement(AstNode node) {
    if (node is! DoStatement) {
      return false;
    }
    var sb = _sourceBuilderAfterKeyword(node, node.doKeyword);
    var hasWhileKeyword = !node.whileKeyword.isSynthetic;
    var exitDelta = 0;
    if (!_statementHasValidBody(node.doKeyword, node.body)) {
      var text = utils.getNodeText(node.body);
      var delta = 0;
      if (text.startsWith(';')) {
        delta = 1;
        _addReplaceEdit(range.startLength(node.body, delta), '');
        if (hasWhileKeyword) {
          text = utils.getNodeText(node);
          if (text.indexOf(RegExp(r'do\s*;\s*while')) == 0) {
            var end = text.indexOf('while');
            var start = text.indexOf(';') + 1;
            delta += end - start - 1;
            _addReplaceEdit(SourceRange(start + node.offset, end - start), ' ');
          }
        }
        sb = SourceBuilder(file, sb.offset + delta);
        sb.append(' ');
      }
      _appendEmptyBraces(
        sb,
        !(hasWhileKeyword && _isSyntheticExpression(node.condition)),
      );
      if (delta != 0) {
        exitDelta = sb.length - delta;
      }
    } else if (_isEmptyBlock(node.body)) {
      sb = SourceBuilder(sb.file, node.body.end);
    }
    SourceBuilder? sb2;
    if (hasWhileKeyword) {
      var stmt = _KeywordConditionBlockStructure(
        node.whileKeyword,
        node.leftParenthesis,
        node.condition,
        node.rightParenthesis,
        null,
      );
      sb2 = _complete_keywordCondition(node, stmt);
      if (sb2 == null) {
        return false;
      }
      if (sb2.length == 0) {
        // true if condition is '()'
        var exitPosition = this.exitPosition;
        if (exitPosition != null) {
          if (node.semicolon.isSynthetic) {
            _insertBuilder(sb);
            sb = SourceBuilder(file, exitPosition.offset + 1);
            sb.append(';');
          }
        }
      } else {
        if (sb.exitOffset == null && sb2.exitOffset != null) {
          _insertBuilder(sb);
          sb = sb2;
          sb.append(';');
        } else {
          sb.append(sb2.toString());
        }
      }
    } else {
      sb.append(' while (');
      sb.setExitOffset();
      sb.append(');');
    }
    _insertBuilder(sb);
    if (exitDelta != 0) {
      exitPosition = Position(
        exitPosition!.file,
        exitPosition!.offset + exitDelta,
      );
    }
    _setCompletion(DartStatementCompletion._completeDoStmt);
    return true;
  }

  bool _complete_forEachStatement(
    ForStatement forNode,
    ForEachParts forEachParts,
  ) {
    AstNode name;
    if (forEachParts is ForEachPartsWithIdentifier) {
      name = forEachParts.identifier;
    } else if (forEachParts is ForEachPartsWithDeclaration) {
      name = forEachParts.loopVariable;
    } else {
      throw StateError('Unrecognized for loop parts');
    }
    return _complete_forEachStatementRest(
      forNode,
      forNode.forKeyword,
      forNode.leftParenthesis,
      name,
      forEachParts.inKeyword,
      forEachParts.iterable,
      forNode.rightParenthesis,
      forNode.body,
    );
  }

  bool _complete_forEachStatementRest(
    AstNode node,
    Token forKeyword,
    Token leftParenthesis,
    AstNode? name,
    Token inKeyword,
    Expression? iterable,
    Token rightParenthesis,
    Statement body,
  ) {
    if (inKeyword.isSynthetic) {
      return false; // Can't happen -- would be parsed as a for-statement.
    }
    var sb = SourceBuilder(file, rightParenthesis.offset + 1);
    var src = utils.getNodeText(node);
    if (name == null) {
      exitPosition = Position(file, leftParenthesis.offset + 1);
      src = src.substring(leftParenthesis.offset - node.offset);
      if (src.startsWith(RegExp(r'\(\s*in\s*\)'))) {
        _addReplaceEdit(
          range.startOffsetEndOffset(
            leftParenthesis.offset + 1,
            rightParenthesis.offset,
          ),
          ' in ',
        );
      } else if (src.startsWith(RegExp(r'\(\s*in'))) {
        _addReplaceEdit(
          range.startOffsetEndOffset(
            leftParenthesis.offset + 1,
            inKeyword.offset,
          ),
          ' ',
        );
      }
    } else if (iterable != null && _isSyntheticExpression(iterable)) {
      exitPosition = Position(file, rightParenthesis.offset + 1);
      src = src.substring(inKeyword.offset - node.offset);
      if (src.startsWith(RegExp(r'in\s*\)'))) {
        _addReplaceEdit(
          range.startOffsetEndOffset(
            inKeyword.offset + inKeyword.length,
            rightParenthesis.offset,
          ),
          ' ',
        );
      }
    }
    if (!_statementHasValidBody(forKeyword, body)) {
      sb.append(' ');
      _appendEmptyBraces(sb, exitPosition == null);
    }
    _insertBuilder(sb);
    _setCompletion(DartStatementCompletion._completeForEachStmt);
    return true;
  }

  bool _complete_forStatement(ForStatement forNode, ForParts forParts) {
    SourceBuilder sb;
    var replacementLength = 0;
    if (forNode.leftParenthesis.isSynthetic) {
      if (!forNode.rightParenthesis.isSynthetic) {
        return false;
      }
      // keywordOnly (unit test name suffix that exercises this branch)
      sb = _sourceBuilderAfterKeyword(forNode, forNode.forKeyword);
      sb.append('(');
      sb.setExitOffset();
      sb.append(')');
    } else {
      if (!forParts.rightSeparator.isSynthetic) {
        // Fully-defined init, cond, updaters so nothing more needed here.
        // emptyParts, noError
        sb = SourceBuilder(file, forNode.rightParenthesis.offset + 1);
      } else if (!forParts.leftSeparator.isSynthetic) {
        if (_isSyntheticExpression(forParts.condition)) {
          var text = utils
              .getNodeText(forNode)
              .substring(forParts.leftSeparator.offset - forNode.offset);
          var match = RegExp(r';\s*(/\*.*\*/\s*)?\)[ \t]*').matchAsPrefix(text);
          if (match != null) {
            // emptyCondition, emptyInitializersEmptyCondition
            replacementLength = match.end - match.start;
            sb = SourceBuilder(file, forParts.leftSeparator.offset);
            sb.append('; ${match[1] ?? ''}; )');
            var suffix = text.substring(match.end);
            if (suffix.trim().isNotEmpty) {
              sb.append(' ');
              sb.append(suffix.trim());
              replacementLength += suffix.length;
              if (suffix.endsWith(eol)) {
                // emptyCondition
                replacementLength -= eol.length;
              }
            }
            exitPosition = _newPosition(forParts.leftSeparator.offset + 2);
          } else {
            return false; // Line comment in condition
          }
        } else {
          // emptyUpdaters
          sb = SourceBuilder(file, forNode.rightParenthesis.offset);
          replacementLength = 1;
          sb.append('; )');
          exitPosition = _newPosition(forParts.rightSeparator.offset + 2);
        }
      } else if (forParts is ForPartsWithExpression &&
          _isSyntheticExpression(forParts.initialization)) {
        // emptyInitializers
        exitPosition = _newPosition(forNode.rightParenthesis.offset);
        sb = SourceBuilder(file, forNode.rightParenthesis.offset);
      } else if (forParts is ForPartsWithExpression &&
          forParts.initialization is SimpleIdentifier &&
          forParts.initialization!.beginToken.lexeme == 'in') {
        // looks like a for/each statement missing the loop variable
        return _complete_forEachStatementRest(
          forNode,
          forNode.forKeyword,
          forNode.leftParenthesis,
          null,
          forParts.initialization!.beginToken,
          null,
          forNode.rightParenthesis,
          forNode.body,
        );
      } else {
        var start = forParts.condition!.offset + forParts.condition!.length;
        var text = utils.getNodeText(forNode).substring(start - forNode.offset);
        if (text.startsWith(RegExp(r'\s*\)'))) {
          // missingLeftSeparator
          var end = text.indexOf(')');
          sb = SourceBuilder(file, start);
          _addReplaceEdit(SourceRange(start, end), '; ; ');
          exitPosition = Position(file, start - (end - '; '.length));
        } else {
          // Not possible; any comment following init is attached to init.
          exitPosition = _newPosition(forNode.rightParenthesis.offset);
          sb = SourceBuilder(file, forNode.rightParenthesis.offset);
        }
      }
    }
    var body = forNode.body;
    if (!_statementHasValidBody(forNode.forKeyword, body)) {
      // keywordOnly, noError
      sb.append(' ');
      _appendEmptyBraces(sb, true /*exitPosition == null*/);
    } else if (body is Block) {
      if (body.rightBracket.end <= selectionOffset) {
        // emptyInitializersAfterBody
        diagnostics = []; // Ignore errors; they are for previous statement.
        return false; // If cursor is after closing brace just add newline.
      }
    }
    _insertBuilder(sb, replacementLength);
    _setCompletion(DartStatementCompletion._completeForStmt);
    return true;
  }

  bool _complete_forStatement2(AstNode node) {
    if (node is ForStatement) {
      var forLoopParts = node.forLoopParts;
      if (forLoopParts is ForParts) {
        return _complete_forStatement(node, forLoopParts);
      } else if (forLoopParts is ForEachParts) {
        return _complete_forEachStatement(node, forLoopParts);
      }
    }
    return false;
  }

  bool _complete_functionDeclaration(AstNode node) {
    if (node is! MethodDeclaration && node is! FunctionDeclaration) {
      return false;
    }
    var needsParen = false;
    int computeExitPos(FormalParameterList parameters) {
      if (needsParen = parameters.rightParenthesis.isSynthetic) {
        var error = _findDiagnostic(diag.missingClosingParenthesis);
        if (error != null) {
          return error.offset - 1;
        }
      }
      return node.end - eol.length;
    }

    int paramListEnd;
    if (node is FunctionDeclaration) {
      var parameters = node.functionExpression.parameters;
      if (parameters == null) {
        return false;
      }
      paramListEnd = computeExitPos(parameters);
    } else if (node is MethodDeclaration) {
      var parameters = node.parameters;
      if (parameters == null) {
        return false;
      }
      paramListEnd = computeExitPos(parameters);
    } else {
      return false;
    }
    var sb = SourceBuilder(file, paramListEnd);
    if (needsParen) {
      sb.append(')');
    }
    sb.append(' ');
    _appendEmptyBraces(sb, true);
    _insertBuilder(sb);
    _setCompletion(DartStatementCompletion._completeFunctionDeclaration);
    return true;
  }

  bool _complete_functionDeclarationStatement(AstNode node) {
    if (node is! FunctionDeclarationStatement) {
      return false;
    }
    var error = _findDiagnostic(diag.expectedToken, partialMatch: "';'");
    if (error != null) {
      var src = utils.getNodeText(node);
      var insertOffset = node.functionDeclaration.end - eol.length;
      var body = node.functionDeclaration.functionExpression.body;
      if (body is ExpressionFunctionBody) {
        var fnbOffset = body.functionDefinition.offset;
        var fnSrc = src.substring(fnbOffset - node.offset);
        if (!fnSrc.startsWith('=>')) {
          return false;
        }
        var delta = 0;
        if (body.expression.isSynthetic) {
          if (!fnSrc.startsWith('=> ')) {
            _addInsertEdit(insertOffset, ' ');
            delta = 1;
          }
          _addInsertEdit(insertOffset, ';');
          _appendNewlinePlusIndentAt(insertOffset);
        } else {
          delta = 1;
          _addInsertEdit(insertOffset, ';');
          insertOffset = _appendNewlinePlusIndent();
        }
        _setCompletionAt(
          DartStatementCompletion._simpleSemicolon,
          insertOffset + delta,
        );
        return true;
      }
    }
    return false;
  }

  bool _complete_ifOrWhileStatement(
    AstNode node,
    _KeywordConditionBlockStructure statement,
    StatementCompletionKind kind,
  ) {
    if (_statementHasValidBody(statement.keyword, statement.block!)) {
      return false;
    }
    var sb = _complete_keywordCondition(node, statement);
    if (sb == null) {
      return false;
    }
    var overshoot = _lengthOfDeletions();
    sb.append(' ');
    _appendEmptyBraces(sb, exitPosition == null);
    _insertBuilder(sb);
    if (overshoot != 0) {
      exitPosition = _newPosition(exitPosition!.offset - overshoot);
    }
    _setCompletion(kind);
    return true;
  }

  bool _complete_ifStatement(AstNode node) {
    if (node is! IfStatement) {
      return false;
    }
    var elseKeyword = node.elseKeyword;
    if (elseKeyword != null) {
      if (selectionOffset >= elseKeyword.end &&
          _isEmptyStatement(node.elseStatement)) {
        var sb = SourceBuilder(file, selectionOffset);
        var src = utils.getNodeText(node);
        if (!src
            .substring(elseKeyword.end - node.offset)
            .startsWith(RegExp(r'[ \t]'))) {
          sb.append(' ');
        }
        _appendEmptyBraces(sb, true);
        _insertBuilder(sb);
        _setCompletion(DartStatementCompletion._completeIfStmt);
        return true;
      }
      return false;
    }
    var stmt = _KeywordConditionBlockStructure(
      node.ifKeyword,
      node.leftParenthesis,
      node.expression,
      node.rightParenthesis,
      node.thenStatement,
    );
    return _complete_ifOrWhileStatement(
      node,
      stmt,
      DartStatementCompletion._completeIfStmt,
    );
  }

  SourceBuilder? _complete_keywordCondition(
    AstNode node,
    _KeywordConditionBlockStructure statement,
  ) {
    SourceBuilder sb;
    if (statement.leftParenthesis.isSynthetic) {
      if (!statement.rightParenthesis.isSynthetic) {
        // Quite unlikely to see this so don't try to fix it.
        return null;
      }
      sb = _sourceBuilderAfterKeyword(node, statement.keyword);
      sb.append('(');
      sb.setExitOffset();
      sb.append(')');
    } else {
      if (_isSyntheticExpression(statement.condition)) {
        exitPosition = _newPosition(statement.leftParenthesis.offset + 1);
        sb = SourceBuilder(file, statement.rightParenthesis.offset + 1);
      } else if (statement.rightParenthesis.isSynthetic) {
        sb = SourceBuilder(file, statement.condition.end);
        sb.append(')');
      } else {
        var afterParen = statement.rightParenthesis.offset + 1;
        if (utils
            .getNodeText(node)
            .substring(afterParen - node.offset)
            .startsWith(RegExp(r'[ \t]'))) {
          _addReplaceEdit(SourceRange(afterParen, 1), '');
          sb = SourceBuilder(file, afterParen + 1);
        } else {
          sb = SourceBuilder(file, afterParen);
        }
      }
    }
    return sb;
  }

  bool _complete_methodCall(AstNode node) {
    var parenError = _findDiagnostic(diag.expectedToken, partialMatch: "')'");
    if (parenError == null) {
      return false;
    }
    var argList =
        _selectedNode(
          at: selectionOffset,
        )?.thisOrAncestorOfType<ArgumentList>() ??
        _selectedNode(
          at: parenError.offset,
        )?.thisOrAncestorOfType<ArgumentList>();
    if (argList == null ||
        argList.thisOrAncestorMatching((n) => n == node) == null) {
      return false;
    }
    var loc = min(selectionOffset, argList.end);
    _addInsertEdit(loc, ')');
    var semicolonError = _findDiagnostic(
      diag.expectedToken,
      partialMatch: "';'",
    );
    if (semicolonError != null) {
      _addInsertEdit(loc, ';');
    }
    var indent = utils.getLinePrefix(selectionOffset);
    var exit = utils.getLineNext(selectionOffset);
    var previousInsertions = _lengthOfInsertions();
    _addInsertEdit(exit, indent + eol);
    exit += indent.length + previousInsertions;

    _setCompletionAt(DartStatementCompletion._simpleEnter, exit);
    return true;
  }

  bool _complete_simpleEnter() {
    int offset;
    if (diagnostics.isNotEmpty) {
      offset = selectionOffset;
    } else {
      var indent = utils.getLinePrefix(selectionOffset);
      var loc = utils.getLineNext(selectionOffset);
      _addInsertEdit(loc, indent + eol);
      offset = loc + indent.length;
    }
    _setCompletionAt(DartStatementCompletion._simpleEnter, offset);
    return true;
  }

  bool _complete_simpleSemicolon(AstNode node) {
    if (diagnostics.length != 1) {
      return false;
    }
    var error = _findDiagnostic(diag.expectedToken, partialMatch: "';'");
    if (error != null) {
      var previousInsertions = _lengthOfInsertions();
      // TODO(messick): Fix this to find the correct place in all cases.
      var insertOffset = error.offset + error.length;
      _addInsertEdit(insertOffset, ';');
      var offset = _appendNewlinePlusIndent() + 1 /*';'*/ + previousInsertions;
      _setCompletionAt(DartStatementCompletion._simpleSemicolon, offset);
      return true;
    }
    return false;
  }

  bool _complete_switchStatement(AstNode node) {
    if (node is! SwitchStatement) {
      return false;
    }
    SourceBuilder sb;
    if (node.leftParenthesis.isSynthetic && node.rightParenthesis.isSynthetic) {
      exitPosition = Position(file, node.switchKeyword.end + 2);
      var src = utils.getNodeText(node);
      if (src
          .substring(node.switchKeyword.end - node.offset)
          .startsWith(RegExp(r'[ \t]+'))) {
        sb = SourceBuilder(file, node.switchKeyword.end + 1);
      } else {
        sb = SourceBuilder(file, node.switchKeyword.end);
        sb.append(' ');
      }
      sb.append('()');
    } else if (node.leftParenthesis.isSynthetic ||
        node.rightParenthesis.isSynthetic) {
      return false;
    } else {
      sb = SourceBuilder(file, node.rightParenthesis.offset + 1);
      if (_isSyntheticExpression(node.expression)) {
        exitPosition = Position(file, node.leftParenthesis.offset + 1);
      }
    }
    if (node
        .leftBracket
        .isSynthetic /*&& switchNode.rightBracket.isSynthetic*/ ) {
      // See https://github.com/dart-lang/sdk/issues/29391
      sb.append(' ');
      _appendEmptyBraces(sb, exitPosition == null);
    } else {
      var member = _findInvalidElement(node.members);
      if (member != null) {
        if (member.colon.isSynthetic) {
          var loc = member is SwitchCase
              ? member.expression.end
              : member.keyword.end;
          sb = SourceBuilder(file, loc);
          sb.append(': ');
          exitPosition = Position(file, loc + 2);
        }
      }
    }
    _insertBuilder(sb);
    _setCompletion(DartStatementCompletion._completeSwitchStmt);
    return true;
  }

  bool _complete_tryStatement(AstNode node) {
    if (node is! TryStatement) {
      return false;
    }
    var addSpace = true;
    if (node.body.leftBracket.isSynthetic) {
      var src = utils.getNodeText(node);
      SourceBuilder sb;
      if (src
          .substring(node.tryKeyword.end - node.offset)
          .startsWith(RegExp(r'[ \t]+'))) {
        // keywordSpace
        sb = SourceBuilder(file, node.tryKeyword.end + 1);
      } else {
        // keywordOnly
        sb = SourceBuilder(file, node.tryKeyword.end);
        sb.append(' ');
      }
      _appendEmptyBraces(sb, true);
      _insertBuilder(sb);
    } else {
      SourceBuilder? sb;
      var catchNode = _findInvalidElement(node.catchClauses);
      if (catchNode != null) {
        var onKeyword = catchNode.onKeyword;
        var exceptionType = catchNode.exceptionType;
        if (onKeyword != null && exceptionType != null) {
          if (exceptionType.length == 0 ||
              _findDiagnostic(
                    diag.nonTypeInCatchClause,
                    partialMatch: "name 'catch",
                  ) !=
                  null) {
            var src = utils.getNodeText(catchNode);
            if (src.startsWith(RegExp(r'on[ \t]+'))) {
              if (src.startsWith(RegExp(r'on[ \t][ \t]+'))) {
                // onSpaces
                exitPosition = Position(file, onKeyword.end + 1);
                sb = SourceBuilder(file, onKeyword.end + 2);
                addSpace = false;
              } else {
                // onSpace
                sb = SourceBuilder(file, onKeyword.end + 1);
                sb.setExitOffset();
              }
            } else {
              // onOnly
              sb = SourceBuilder(file, onKeyword.end);
              sb.append(' ');
              sb.setExitOffset();
            }
          } else {
            // onType
            sb = SourceBuilder(file, exceptionType.end);
          }
        }
        var catchKeyword = catchNode.catchKeyword;
        if (catchKeyword != null) {
          // catchOnly
          var struct = _KeywordConditionBlockStructure(
            catchKeyword,
            catchNode.leftParenthesis!,
            catchNode.exceptionParameter!,
            catchNode.rightParenthesis!,
            catchNode.body,
          );
          if (sb != null) {
            // onCatch
            _insertBuilder(sb);
          }
          sb = _complete_keywordCondition(node, struct);
          if (sb == null) {
            return false;
          }
        }
        if (sb != null) {
          if (catchNode.body.leftBracket.isSynthetic) {
            // onOnly and others
            if (addSpace) {
              sb.append(' ');
            }
            _appendEmptyBraces(sb, exitPosition == null);
          }
          _insertBuilder(sb);
        }
      } else if (node.finallyKeyword != null) {
        if (node.finallyBlock!.leftBracket.isSynthetic) {
          // finallyOnly
          sb = SourceBuilder(file, node.finallyKeyword!.end);
          sb.append(' ');
          _appendEmptyBraces(sb, true);
          _insertBuilder(sb);
        }
      }
    }
    _setCompletion(DartStatementCompletion._completeTryStmt);
    return true;
  }

  bool _complete_variableDeclaration(AstNode node) {
    if (node is! VariableDeclaration) {
      return false;
    }
    _addInsertEdit(node.end, ';');
    exitPosition = Position(file, _appendNewlinePlusIndentAt(node.end) + 1);
    _setCompletion(DartStatementCompletion._completeVariableDeclaration);
    return true;
  }

  bool _complete_whileStatement(AstNode node) {
    if (node is! WhileStatement) {
      return false;
    }
    var stmt = _KeywordConditionBlockStructure(
      node.whileKeyword,
      node.leftParenthesis,
      node.condition,
      node.rightParenthesis,
      node.body,
    );
    return _complete_ifOrWhileStatement(
      node,
      stmt,
      DartStatementCompletion._completeWhiteStmt,
    );
  }

  engine.Diagnostic? _findDiagnostic(
    DiagnosticCode code, {
    Pattern? partialMatch,
  }) {
    return diagnostics.firstWhereOrNull(
      (d) =>
          d.diagnosticCode == code &&
          (partialMatch == null || d.message.contains(partialMatch)),
    );
  }

  T? _findInvalidElement<T extends AstNode>(NodeList<T> list) {
    return list.firstWhereOrNull(
      (item) => selectionOffset >= item.offset && selectionOffset <= item.end,
    );
  }

  void _insertBuilder(SourceBuilder builder, [int length = 0]) {
    {
      var range = SourceRange(builder.offset, length);
      var text = builder.toString();
      _addReplaceEdit(range, text);
    }
    // add exit position
    {
      var exitOffset = builder.exitOffset;
      if (exitOffset != null) {
        exitPosition = _newPosition(exitOffset);
      }
    }
  }

  bool _isEmptyBlock(AstNode stmt) {
    return stmt is Block && stmt.statements.isEmpty;
  }

  bool _isEmptyStatement(AstNode? stmt) {
    if (stmt is ExpressionStatement) {
      var expression = stmt.expression;
      if (expression is SimpleIdentifier) {
        return expression.token.isSynthetic;
      }
    }
    return stmt is EmptyStatement;
  }

  bool _isEmptyStatementOrEmptyBlock(AstNode stmt) {
    return _isEmptyStatement(stmt) || _isEmptyBlock(stmt);
  }

  bool _isNonStatementDeclaration(AstNode n) {
    if (n is! Declaration) {
      return false;
    }
    if (n is! VariableDeclaration && n is! FunctionDeclaration) {
      return true;
    }
    var p = n.parent;
    return p is! Statement &&
        p?.parent is! Statement &&
        p?.parent?.parent is! Statement;
  }

  bool _isSyntheticExpression(AstNode? expr) {
    return expr is SimpleIdentifier && expr.isSynthetic;
  }

  int _lengthOfDeletions() {
    if (change.edits.isEmpty) {
      return 0;
    }
    var length = 0;
    for (var edit in change.edits) {
      for (var srcEdit in edit.edits) {
        if (srcEdit.length > 0) {
          length += srcEdit.length - srcEdit.replacement.length;
        }
      }
    }
    return length;
  }

  int _lengthOfInsertions() {
    // Any _complete_*() that may follow changes made by _checkExpressions()
    // must cache the result of this method and add that value to its
    // exit position. That's assuming all edits are done in increasing position.
    // There are currently no editing sequences that produce both insertions and
    // deletions, but if there were this approach would have to be generalized.
    if (change.edits.isEmpty) {
      return 0;
    }
    var length = 0;
    for (var edit in change.edits) {
      for (var srcEdit in edit.edits) {
        if (srcEdit.length == 0) {
          length += srcEdit.replacement.length;
        }
      }
    }
    return length;
  }

  Position _newPosition(int offset) {
    return Position(file, offset);
  }

  void _removeError(DiagnosticCode diagnosticCode, {Pattern? partialMatch}) {
    var error = _findDiagnostic(diagnosticCode, partialMatch: partialMatch);
    if (error != null) {
      diagnostics.remove(error);
    }
  }

  AstNode? _selectedNode({int? at}) =>
      unit.nodeCovering(offset: at ?? selectionOffset);

  void _setCompletion(StatementCompletionKind kind) {
    assert(exitPosition != null);
    change.selection = exitPosition;
    change.message = formatList(kind.message, null);
    for (var group in linkedPositionGroups.values) {
      change.addLinkedEditGroup(group);
    }
    completion = StatementCompletion(kind, change);
  }

  void _setCompletionAt(StatementCompletionKind kind, int offset) {
    exitPosition = _newPosition(offset);
    _setCompletion(kind);
  }

  SourceBuilder _sourceBuilderAfterKeyword(AstNode node, Token keyword) {
    SourceBuilder sb;
    var text = _baseNodeText(node);
    text = text.substring(keyword.offset - node.offset);
    var len = keyword.length;
    if (text.length == len || // onCatchComment
        !text.substring(len, len + 1).contains(RegExp(r'[ \t]'))) {
      sb = SourceBuilder(file, keyword.offset + len);
      sb.append(' ');
    } else {
      sb = SourceBuilder(file, keyword.offset + len + 1);
    }
    return sb;
  }

  bool _statementHasValidBody(Token keyword, Statement body) {
    // A "valid" body is either a non-synthetic block or a single statement
    // on the same line as the parent statement, similar to dart_style.
    if (body.isSynthetic) {
      return false;
    }
    if (body is Block) {
      var block = body;
      return !block.leftBracket.isSynthetic;
    }
    return lineInfo.getLocation(keyword.offset) ==
        lineInfo.getLocation(body.offset);
  }
}

// Encapsulate common structure of if-statement and while-statement.
class _KeywordConditionBlockStructure {
  final Token keyword;
  final Token leftParenthesis, rightParenthesis;
  final AstNode condition;
  final Statement? block;

  new(
    this.keyword,
    this.leftParenthesis,
    this.condition,
    this.rightParenthesis,
    this.block,
  );

  int get offset => keyword.offset;
}
