// Copyright (c) 2023, 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/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';

import '../ast_extensions.dart';
import '../piece/adjacent.dart';
import '../piece/assign.dart';
import '../piece/clause.dart';
import '../piece/control_flow.dart';
import '../piece/for.dart';
import '../piece/if_case.dart';
import '../piece/infix.dart';
import '../piece/leading_comment.dart';
import '../piece/list.dart';
import '../piece/piece.dart';
import '../piece/sequence.dart';
import '../piece/type.dart';
import '../piece/variable.dart';
import 'ast_node_visitor.dart';
import 'chain_builder.dart';
import 'comment_writer.dart';
import 'delimited_list_builder.dart';
import 'piece_writer.dart';
import 'sequence_builder.dart';

/// Record type for a destructured binary operator-like syntactic construct.
typedef BinaryOperation = (AstNode left, Token operator, AstNode right);

/// The kind of syntax surrounding a node when being converted to a [Piece], if
/// that surrounding syntax may affect how the child node is formatted.
///
/// For example, binary operators indent their subsequent operands in most
/// places:
///
///     function(
///       operand +
///           operand,
///     );
///
/// But not when they appear on the right-hand side of an assignment or
/// assignment-like structure:
///
///     variable =
///         operand +
///         operand;
///
/// To handle this, when the code for a node recursively visits a child, it can
/// pass in a context describing itself, which the child can then access to
/// decide how it should be formatted.
enum NodeContext {
  /// No specified context.
  none,

  /// The child is the right-hand side of an assignment-like form.
  ///
  /// This includes assignments, variable declarations, named arguments, map
  /// entries, and `=>` function bodies.
  assignment,

  /// The child is the target of a cascade expression.
  cascadeTarget,

  /// The child is the then or else operand of a conditional expression.
  conditionalBranch,

  /// The child is a variable declaration in a for loop.
  forLoopVariable,

  /// The child is a string interpolation inside a multiline string.
  multilineStringInterpolation,

  /// The child is the outermost pattern in a switch expression case.
  switchExpressionCase
}

/// Utility methods for creating pieces that share formatting logic across
/// multiple parts of the language.
///
/// Many AST nodes are structurally similar and receive similar formatting. For
/// example, imports and exports are mostly the same, with exports a subset of
/// imports. Likewise, assert statements are formatted like function calls and
/// argument lists.
///
/// This mixin defines functions that represent a general construct that is
/// formatted a certain way. The function builds up an appropriate set of
/// [Piece]s given the various AST subcomponents passed in as parameters. The
/// main [AstNodeVisitor] class then calls those for all of the AST nodes that
/// should receive that similar formatting.
///
/// These are all void functions because they generally push their result into
/// the [PieceWriter].
///
/// Naming these functions can be hard. For example, there isn't an obvious
/// word for "import or export directive" or "named thing with argument list".
/// To avoid that, we pick one concrete construct formatted by the function,
/// usually the most common, and name it after that, as in [createImport()].
mixin PieceFactory {
  /// A stack that handles forcing nested list, map, and set literals to split.
  ///
  /// Each entry corresponds to a collection currently being visited and the
  /// value is whether or not it should be forced to split because an inner
  /// collection was found inside it.
  ///
  /// When we begin a collection, we set all of the existing elements to `true`
  /// then push `false` for the new collection. When done visiting the elements,
  /// we pop the last value, If it's `true`, we know we visited a nested
  /// collection so we force this one to split.
  final List<bool> _collectionSplits = [];

  PieceWriter get pieces;

  CommentWriter get comments;

  NodeContext get parentContext;

  void visitNode(AstNode node, NodeContext context);

  /// Writes a [ListPiece] for an argument list.
  void writeArgumentList(
      Token leftBracket, List<AstNode> elements, Token rightBracket) {
    writeList(
        leftBracket: leftBracket,
        elements,
        rightBracket: rightBracket,
        allowBlockArgument: true);
  }

  /// Writes a bracket-delimited block or declaration body.
  ///
  /// If [forceSplit] is `true`, then the block will split even if empty. This
  /// is used, for example, with empty blocks in `if` statements followed by
  /// `else` clauses:
  ///
  ///     if (condition) {
  ///     } else {}
  void writeBody(Token leftBracket, List<AstNode> contents, Token rightBracket,
      {bool forceSplit = false}) {
    // If the body is completely empty, write the brackets directly inline so
    // that we create fewer pieces.
    if (!forceSplit && !contents.canSplit(rightBracket)) {
      pieces.token(leftBracket);
      pieces.token(rightBracket);
      return;
    }

    var sequence = SequenceBuilder(this);
    sequence.leftBracket(leftBracket);

    for (var node in contents) {
      sequence.visit(node);

      // If the node has a non-empty braced body, then require a blank line
      // between it and the next node.
      if (node.hasNonEmptyBody) sequence.addBlank();
    }

    sequence.rightBracket(rightBracket);
    pieces.add(sequence.build(forceSplit: forceSplit));
  }

  /// Writes a [SequencePiece] for a given [Block].
  ///
  /// If [forceSplit] is `true`, then the block will split even if empty. This
  /// is used, for example, with empty blocks in `if` statements followed by
  /// `else` clauses:
  ///
  ///     if (condition) {
  ///     } else {}
  void writeBlock(Block block, {bool forceSplit = false}) {
    writeBody(block.leftBracket, block.statements, block.rightBracket,
        forceSplit: forceSplit);
  }

  /// Writes a piece for a `break` or `continue` statement.
  void writeBreak(Token keyword, SimpleIdentifier? label, Token semicolon) {
    pieces.token(keyword);
    pieces.visit(label, spaceBefore: true);
    pieces.token(semicolon);
  }

  void writeChain(Expression node) {
    pieces.add(ChainBuilder(this, node)
        .build(isCascadeTarget: parentContext == NodeContext.cascadeTarget));
  }

  /// Writes a [ListPiece] for a collection literal or pattern.
  ///
  /// If [splitOnNestedCollection] is `true`, then this collection is forced to
  /// split if it contains any non-empty collections where
  /// [splitOnNestedCollection] is also `true`, even if the collection would
  /// otherwise not need to split. This is `true` for list, map, and set
  /// expressions because they are often used for composite data structures and
  /// they're easier to read if they don't get packed too densely:
  ///
  ///     // Prefer:
  ///     data = {
  ///       'a': [1, 2, 3],
  ///       'b': [
  ///         4,
  ///         [5],
  ///         6,
  ///       ]
  ///       'c': [7, 8],
  ///     };
  ///
  ///     // Over:
  ///     data = {'a': [1, 2, 3], 'b': [4, [5], 6] 'c': [7, 8]};
  ///
  /// We don't do this for record expressions because those are not unbounded
  /// in size and generally represent aggregations of data where the fields are
  /// more "closely" bundled together. Record expressions are sort of like
  /// constructor invocations for an anonymous constructor.
  ///
  /// We don't do this for patterns because it's better to fit a pattern on a
  /// single line when possible for parallel cases in switches.
  ///
  /// If [preserveNewlines] is `true`, then any newlines or lack of newlines
  /// between pairs of elements in the input are preserved in the output. This
  /// is used for collection literals that contain line comments to preserve
  /// the author's deliberate structuring, as in:
  ///
  ///     matrix = [
  ///       // X, Y, Z:
  ///       1, 2, 3,
  ///       4, 5, 6,
  ///       7, 8, 9,
  ///     ];
  void writeCollection(
    Token leftBracket,
    List<AstNode> elements,
    Token rightBracket, {
    Token? constKeyword,
    TypeArgumentList? typeArguments,
    ListStyle style = const ListStyle(),
    bool splitOnNestedCollection = false,
    bool preserveNewlines = false,
  }) {
    pieces.modifier(constKeyword);
    pieces.visit(typeArguments);

    // If the list is completely empty, write the brackets inline so we create
    // fewer pieces.
    if (!elements.canSplit(rightBracket)) {
      pieces.token(leftBracket);
      pieces.token(rightBracket);
      return;
    }

    if (splitOnNestedCollection) {
      // If this collection isn't empty, force all of the surrounding
      // collections to split if they care to.
      if (elements.isNotEmpty) {
        _collectionSplits.fillRange(0, _collectionSplits.length, true);
      }

      // Add this collection to the stack.
      _collectionSplits.add(false);
    }

    var collection = pieces.build(() {
      writeList(
        leftBracket: leftBracket,
        elements,
        rightBracket: rightBracket,
        style: style,
        preserveNewlines: preserveNewlines,
      );
    });

    // If there is a collection inside this one, force this one to split.
    if (splitOnNestedCollection) {
      if (_collectionSplits.removeLast()) collection.pin(State.split);
    }

    pieces.add(collection);
  }

  /// Creates a comma-separated [ListPiece] for [nodes].
  Piece createCommaSeparated(Iterable<AstNode> nodes) {
    var builder =
        DelimitedListBuilder(this, const ListStyle(commas: Commas.nonTrailing));
    nodes.forEach(builder.visit);
    return builder.build();
  }

  /// If [leadingComments] is not empty, returns [piece] wrapped in a
  /// [LeadingCommentPiece] that prepends them.
  ///
  /// Otherwise, just returns [piece].
  Piece prependLeadingComments(List<Piece> leadingComments, Piece piece) {
    if (leadingComments.isEmpty) return piece;
    return LeadingCommentPiece(leadingComments, piece);
  }

  /// Writes the leading keyword and parenthesized expression at the beginning
  /// of an `if`, `while`, or `switch` expression or statement.
  void writeControlFlowStart(Token keyword, Token leftParenthesis,
      Expression value, Token rightParenthesis) {
    pieces.token(keyword);
    pieces.space();
    pieces.token(leftParenthesis);
    pieces.visit(value);
    pieces.token(rightParenthesis);
  }

  /// Writes a dotted or qualified identifier.
  void writeDotted(NodeList<SimpleIdentifier> components) {
    for (var component in components) {
      // Write the preceding ".".
      if (component != components.first) {
        pieces.token(component.beginToken.previous!);
      }

      pieces.visit(component);
    }
  }

  /// Creates a [Piece] for an enum constant.
  ///
  /// If the constant is in an enum declaration that also declares members, then
  /// [semicolon] should be the `;` token before the members, and
  /// [isLastConstant] is `true` if [node] is the last constant before the
  /// members.
  Piece createEnumConstant(EnumConstantDeclaration node,
      {bool isLastConstant = false, Token? semicolon}) {
    return pieces.build(metadata: node.metadata, () {
      pieces.token(node.name);
      if (node.arguments case var arguments?) {
        pieces.visit(arguments.typeArguments);
        pieces.visit(arguments.constructorSelector);
        pieces.visit(arguments.argumentList);
      }

      if (semicolon != null) {
        if (!isLastConstant) {
          pieces.token(node.commaAfter);
        } else {
          // Discard the trailing comma if there is one since there is a
          // semicolon to use as the separator, but preserve any comments before
          // the discarded comma.
          pieces.add(
              pieces.tokenPiece(discardedToken: node.commaAfter, semicolon));
        }
      }
    });
  }

  /// Writes a piece for a for statement or element.
  void writeFor(
      {required Token? awaitKeyword,
      required Token forKeyword,
      required Token leftParenthesis,
      required ForLoopParts forLoopParts,
      required Token rightParenthesis,
      required AstNode body,
      required bool hasBlockBody,
      bool forceSplitBody = false}) {
    var forKeywordPiece = pieces.build(() {
      pieces.modifier(awaitKeyword);
      pieces.token(forKeyword);
    });

    Piece forPartsPiece;
    switch (forLoopParts) {
      // Edge case: A totally empty for loop is formatted just as `(;;)` with
      // no splits or spaces anywhere.
      case ForPartsWithExpression(
            initialization: null,
            leftSeparator: Token(precedingComments: null),
            condition: null,
            rightSeparator: Token(precedingComments: null),
            updaters: NodeList(isEmpty: true),
          )
          when rightParenthesis.precedingComments == null:
        forPartsPiece = pieces.build(() {
          pieces.token(leftParenthesis);
          pieces.token(forLoopParts.leftSeparator);
          pieces.token(forLoopParts.rightSeparator);
          pieces.token(rightParenthesis);
        });

      case ForParts forParts &&
            ForPartsWithDeclarations(variables: AstNode? initializer):
      case ForParts forParts &&
            ForPartsWithExpression(initialization: AstNode? initializer):
      case ForParts forParts &&
            ForPartsWithPattern(variables: AstNode? initializer):
        // In a C-style for loop, treat the for loop parts like an argument list
        // where each clause is a separate argument. This means that when they
        // split, they split like:
        //
        //     for (
        //       initializerClause;
        //       conditionClause;
        //       incrementClause
        //     ) {
        //       body;
        //     }
        var partsList =
            DelimitedListBuilder(this, const ListStyle(commas: Commas.none));
        partsList.leftBracket(leftParenthesis);

        // The initializer clause.
        if (initializer != null) {
          partsList.addCommentsBefore(initializer.beginToken);
          partsList.add(pieces.build(() {
            pieces.visit(initializer, context: NodeContext.forLoopVariable);
            pieces.token(forParts.leftSeparator);
          }));
        } else {
          // No initializer, so look at the comments before `;`.
          partsList.addCommentsBefore(forParts.leftSeparator);
          partsList.add(tokenPiece(forParts.leftSeparator));
        }

        // The condition clause.
        if (forParts.condition case var conditionExpression?) {
          partsList.addCommentsBefore(conditionExpression.beginToken);
          partsList.add(pieces.build(() {
            pieces.visit(conditionExpression);
            pieces.token(forParts.rightSeparator);
          }));
        } else {
          partsList.addCommentsBefore(forParts.rightSeparator);
          partsList.add(tokenPiece(forParts.rightSeparator));
        }

        // The update clauses.
        if (forParts.updaters.isNotEmpty) {
          partsList.addCommentsBefore(forParts.updaters.first.beginToken);
          partsList.add(createCommaSeparated(forParts.updaters));
        }

        partsList.rightBracket(rightParenthesis);
        forPartsPiece = partsList.build();

      case ForEachParts forEachParts &&
            ForEachPartsWithDeclaration(loopVariable: AstNode variable):
      case ForEachParts forEachParts &&
            ForEachPartsWithIdentifier(identifier: AstNode variable):
        // If a for-in loop, treat the for parts like an assignment, so they
        // split like:
        //
        //     for (var variable in [
        //       initializer,
        //     ]) {
        //       body;
        //     }
        // TODO(tall): Passing `canBlockSplitLeft: true` allows output like:
        //
        //     // 1
        //     for (variable in longExpression +
        //         thatWraps) {
        //       ...
        //     }
        //
        // Versus the split in the initializer forcing a split before `in` too:
        //
        //     // 2
        //     for (variable
        //         in longExpression +
        //             thatWraps) {
        //       ...
        //     }
        //
        // This is also allowed:
        //
        //     // 3
        //     for (variable
        //         in longExpression + thatWraps) {
        //       ...
        //     }
        //
        // Currently, the formatter prefers 1 over 3. We may want to revisit
        // that and prefer 3 instead. Or perhaps we shouldn't pass
        // `canBlockSplitLeft: true` and force the `in` to split if the
        // initializer does. That would be consistent with how we handle
        // splitting before `case` when the pattern has a newline in an if-case
        // statement or element.
        forPartsPiece = pieces.build(() {
          pieces.token(leftParenthesis);
          writeForIn(variable, forEachParts.inKeyword, forEachParts.iterable);
          pieces.token(rightParenthesis);
        });

      case ForEachParts forEachParts &&
            ForEachPartsWithPattern(:var keyword, :var metadata, :var pattern):
        forPartsPiece = pieces.build(() {
          pieces.token(leftParenthesis);

          // Use a nested piece so that the metadata precedes the keyword and
          // not the `(`.
          pieces.withMetadata(metadata, inlineMetadata: true, () {
            pieces.token(keyword);
            pieces.space();

            writeForIn(pattern, forEachParts.inKeyword, forEachParts.iterable);
          });
          pieces.token(rightParenthesis);
        });
    }

    var bodyPiece = nodePiece(body);

    // If there is metadata before the for loop variable or pattern, then make
    // sure that the entire contents of the for loop parts are indented so that
    // the annotations are indented.
    var indentHeader = switch (forLoopParts) {
      ForEachPartsWithDeclaration(:var loopVariable) =>
        loopVariable.metadata.isNotEmpty,
      ForEachPartsWithPattern(:var metadata) => metadata.isNotEmpty,
      _ => false,
    };

    if (hasBlockBody) {
      pieces
          .add(ForPiece(forKeywordPiece, forPartsPiece, indent: indentHeader));
      pieces.space();
      pieces.add(bodyPiece);
    } else {
      var forPiece = ControlFlowPiece();
      forPiece.add(
          ForPiece(forKeywordPiece, forPartsPiece, indent: indentHeader),
          bodyPiece,
          isBlock: false);

      if (forceSplitBody) forPiece.pin(State.split);
      pieces.add(forPiece);
    }
  }

  /// Writes a normal (not function-typed) formal parameter with a name and/or
  /// type annotation.
  ///
  /// If [mutableKeyword] is given, it should be the `var` or `final` keyword.
  /// If [fieldKeyword] and [period] are given, the former should be the `this`
  /// or `super` keyword for an initializing formal or super parameter.
  void writeFormalParameter(
      FormalParameter node, TypeAnnotation? type, Token? name,
      {Token? mutableKeyword, Token? fieldKeyword, Token? period}) {
    // If the parameter has a default value, the parameter node will be wrapped
    // in a DefaultFormalParameter node containing the default.
    (Token separator, Expression value)? defaultValueRecord;
    if (node.parent
        case DefaultFormalParameter(:var separator?, :var defaultValue?)) {
      defaultValueRecord = (separator, defaultValue);
    }

    writeParameter(
        metadata: node.metadata,
        modifiers: [
          node.requiredKeyword,
          node.covariantKeyword,
          mutableKeyword,
        ],
        type,
        fieldKeyword: fieldKeyword,
        period: period,
        name,
        defaultValue: defaultValueRecord);
  }

  /// Writes a function, method, getter, or setter declaration.
  ///
  /// If [modifierKeyword] is given, it should be the `static` or `abstract`
  /// modifier on a method declaration. If [operatorKeyword] is given, it
  /// should be the `operator` keyword on an operator declaration. If
  /// [propertyKeyword] is given, it should be the `get` or `set` keyword on a
  /// getter or setter declaration.
  void writeFunction(
      {List<Annotation> metadata = const [],
      List<Token?> modifiers = const [],
      TypeAnnotation? returnType,
      Token? operatorKeyword,
      Token? propertyKeyword,
      Token? name,
      TypeParameterList? typeParameters,
      FormalParameterList? parameters,
      required FunctionBody body}) {
    // Create a piece to attach metadata to the function.
    pieces.withMetadata(metadata, () {
      writeFunctionAndReturnType(modifiers, returnType, () {
        // If there's no return type, attach modifiers to the signature.
        if (returnType == null) {
          for (var keyword in modifiers) {
            pieces.modifier(keyword);
          }
        }

        pieces.modifier(operatorKeyword);
        pieces.modifier(propertyKeyword);
        pieces.token(name);
        pieces.visit(typeParameters);
        pieces.visit(parameters);
        pieces.visit(body);
      });
    });
  }

  /// Writes a return type followed by either a function signature (when writing
  /// a function type annotation or function-typed formal) or a signature and a
  /// body (when writing a function declaration).
  ///
  /// The [writeFunction] callback should write the function's signature and
  /// body if there is one.
  ///
  /// If there is no return type, invokes [writeFunction] directly and returns.
  /// Otherwise, writes the return type and function and wraps them in a piece
  /// to allow splitting after the return type.
  void writeFunctionAndReturnType(List<Token?> modifiers,
      TypeAnnotation? returnType, void Function() writeFunction) {
    if (returnType == null) {
      writeFunction();
      return;
    }

    var returnTypePiece = pieces.build(() {
      for (var keyword in modifiers) {
        pieces.modifier(keyword);
      }

      pieces.visit(returnType);
    });

    var signature = pieces.build(() {
      writeFunction();
    });

    pieces.add(VariablePiece(returnTypePiece, [signature], hasType: true));
  }

  /// If [parameter] has a [defaultValue] then writes a piece for the parameter
  /// followed by that default value.
  ///
  /// Otherwise, just writes [parameter].
  void writeDefaultValue(
      Piece parameter, (Token separator, Expression value)? defaultValue) {
    if (defaultValue == null) {
      pieces.add(parameter);
      return;
    }

    var (separator, value) = defaultValue;
    var operatorPiece = pieces.build(() {
      if (separator.type == TokenType.EQ) pieces.space();
      pieces.token(separator);
      if (separator.type != TokenType.EQ) pieces.space();
    });

    var valuePiece = nodePiece(value, context: NodeContext.assignment);

    pieces.add(AssignPiece(
        left: parameter,
        operatorPiece,
        valuePiece,
        canBlockSplitRight: value.canBlockSplit));
  }

  /// Writes a function type or function-typed formal.
  ///
  /// If creating a piece for a function-typed formal, then [parameter] is the
  /// formal parameter. If there is a default value, then [defaultValue] is
  /// the `=` or `:` separator followed by the constant expression.
  ///
  /// If this is a function-typed initializing formal (`this.foo()`), then
  /// [fieldKeyword] is `this` and [period] is the `.`. Likewise, for a
  /// function-typed super parameter, [fieldKeyword] is `super`.
  void writeFunctionType(
      TypeAnnotation? returnType,
      Token functionKeywordOrName,
      TypeParameterList? typeParameters,
      FormalParameterList parameters,
      Token? question,
      {FormalParameter? parameter,
      Token? fieldKeyword,
      Token? period}) {
    var metadata = parameter?.metadata ?? const <Annotation>[];
    pieces.withMetadata(metadata, inlineMetadata: true, () {
      void write() {
        // If there's no return type, attach the parameter modifiers to the
        // signature.
        if (parameter != null && returnType == null) {
          pieces.modifier(parameter.requiredKeyword);
          pieces.modifier(parameter.covariantKeyword);
        }

        pieces.token(fieldKeyword);
        pieces.token(period);
        pieces.token(functionKeywordOrName);
        pieces.visit(typeParameters);
        pieces.visit(parameters);
        pieces.token(question);
      }

      var returnTypeModifiers = parameter != null
          ? [parameter.requiredKeyword, parameter.covariantKeyword]
          : const <Token?>[];

      // TODO(rnystrom): It would be good if the AssignPiece created for the
      // default value could treat the parameter list on the left-hand side as
      // block-splittable. But since it's a FunctionPiece and not directly a
      // ListPiece, AssignPiece doesn't support block-splitting it. If #1466 is
      // fixed, that may enable us to handle block-splitting here too. In
      // practice, it doesn't really matter since function-typed formals are
      // deprecated, default values on function-typed parameters are rare, and
      // when both occur, they rarely split.
      // If the type is a function-typed parameter with a default value, then
      // grab the default value from the parent node and attach it to the
      // function.
      if (parameter?.parent
          case DefaultFormalParameter(:var separator?, :var defaultValue?)) {
        var function = pieces.build(() {
          writeFunctionAndReturnType(returnTypeModifiers, returnType, write);
        });

        writeDefaultValue(function, (separator, defaultValue));
      } else {
        writeFunctionAndReturnType(returnTypeModifiers, returnType, write);
      }
    });
  }

  /// Writes a piece for the header -- everything from the `if` keyword to the
  /// closing `)` -- of an if statement, if element, if-case statement, or
  /// if-case element.
  void writeIfCondition(Token ifKeyword, Token leftParenthesis,
      Expression expression, CaseClause? caseClause, Token rightParenthesis) {
    pieces.token(ifKeyword);
    pieces.space();
    pieces.token(leftParenthesis);

    if (caseClause != null) {
      var expressionPiece = nodePiece(expression);

      var casePiece = pieces.build(() {
        pieces.token(caseClause.caseKeyword);
        pieces.space();
        pieces.visit(caseClause.guardedPattern.pattern);
      });

      var guardPiece = optionalNodePiece(caseClause.guardedPattern.whenClause);

      pieces.add(IfCasePiece(expressionPiece, casePiece, guardPiece,
          canBlockSplitPattern:
              caseClause.guardedPattern.pattern.canBlockSplit));
    } else {
      pieces.visit(expression);
    }

    pieces.token(rightParenthesis);
  }

  /// Writes a [TryPiece] for try statement.
  void writeTry(TryStatement tryStatement) {
    pieces.token(tryStatement.tryKeyword);
    pieces.space();
    writeBlock(tryStatement.body);

    for (var i = 0; i < tryStatement.catchClauses.length; i++) {
      var catchClause = tryStatement.catchClauses[i];

      pieces.space();
      if (catchClause.onKeyword case var onKeyword?) {
        pieces.token(onKeyword, spaceAfter: true);
        pieces.visit(catchClause.exceptionType);
      }

      if (catchClause.onKeyword != null && catchClause.catchKeyword != null) {
        pieces.space();
      }

      if (catchClause.catchKeyword case var catchKeyword?) {
        pieces.token(catchKeyword);
        pieces.space();

        var parameters = DelimitedListBuilder(this);
        parameters.leftBracket(catchClause.leftParenthesis!);
        if (catchClause.exceptionParameter case var exceptionParameter?) {
          parameters.visit(exceptionParameter);
        }
        if (catchClause.stackTraceParameter case var stackTraceParameter?) {
          parameters.visit(stackTraceParameter);
        }
        parameters.rightBracket(catchClause.rightParenthesis!);
        pieces.add(parameters.build());
      }

      pieces.space();

      // Edge case: When there's another catch/on/finally after this one, we
      // want to force the block to split even if it's empty.
      //
      //     try {
      //       ..
      //     } on Foo {
      //     } finally Bar {
      //       body;
      //     }
      var forceSplit = i < tryStatement.catchClauses.length - 1 ||
          tryStatement.finallyBlock != null;
      writeBlock(catchClause.body, forceSplit: forceSplit);
    }

    if (tryStatement.finallyBlock case var finallyBlock?) {
      pieces.space();
      pieces.token(tryStatement.finallyKeyword!);
      pieces.space();
      writeBlock(finallyBlock);
    }
  }

  /// Writes an [ImportPiece] for an import or export directive.
  void writeImport(NamespaceDirective directive, Token keyword,
      {Token? deferredKeyword, Token? asKeyword, SimpleIdentifier? prefix}) {
    pieces.withMetadata(directive.metadata, () {
      // Build a piece for the directive itself.
      var directivePiece = pieces.build(() {
        pieces.token(keyword);
        pieces.space();
        pieces.visit(directive.uri);
      });

      // Add all of the clauses and combinators.
      var clauses = <Piece>[];

      // The language specifies that configurations must appear after any `as`
      // clause but the parser incorrectly accepts them before it and code in
      // the wild relies on that. Instead of failing with an "unexpected output"
      // error, just preserve the order of the clauses if they are out of order.
      // See: https://github.com/dart-lang/sdk/issues/56641
      var wroteConfigurations = false;
      if (directive.configurations.isNotEmpty &&
          asKeyword != null &&
          directive.configurations.first.ifKeyword.offset < asKeyword.offset) {
        for (var configuration in directive.configurations) {
          clauses.add(nodePiece(configuration));
        }

        wroteConfigurations = true;
      }

      // Include the `as` clause.
      if (asKeyword != null) {
        clauses.add(pieces.build(() {
          pieces.token(deferredKeyword, spaceAfter: true);
          pieces.token(asKeyword);
          pieces.space();
          pieces.visit(prefix!);
        }));
      }

      // Include any `if` clauses.
      if (!wroteConfigurations) {
        for (var configuration in directive.configurations) {
          clauses.add(nodePiece(configuration));
        }
      }

      // Include the `show` and `hide` clauses.
      for (var combinatorNode in directive.combinators) {
        switch (combinatorNode) {
          case HideCombinator(hiddenNames: var names):
          case ShowCombinator(shownNames: var names):
            clauses.add(InfixPiece([
              tokenPiece(combinatorNode.keyword),
              for (var name in names) tokenPiece(name.token, commaAfter: true),
            ]));
        }
      }

      // If there are clauses, include them.
      if (clauses.isNotEmpty) {
        pieces.add(ClausePiece(directivePiece, clauses));
      } else {
        pieces.add(directivePiece);
      }

      pieces.token(directive.semicolon);
    });
  }

  /// Writes a [Piece] for an index expression.
  void writeIndexExpression(IndexExpression index) {
    // TODO(tall): Consider whether we should allow splitting between
    // successive index expressions, like:
    //
    //     jsonData['some long key']
    //         ['another long key'];
    //
    // The current formatter allows it, but it's very rarely used (0.021% of
    // index expressions in a corpus of pub packages).
    pieces.token(index.question);
    pieces.token(index.period);
    pieces.token(index.leftBracket);
    pieces.visit(index.index);
    pieces.token(index.rightBracket);
  }

  /// Writes a single infix operation.
  ///
  /// If [hanging] is `true` then the operator goes at the end of the first
  /// line, like `+`. Otherwise, it goes at the beginning of the second, like
  /// `as`.
  ///
  /// The [operator2] parameter may be passed if the "operator" is actually two
  /// separate tokens, as in `foo is! Bar`.
  void writeInfix(AstNode left, Token operator, AstNode right,
      {bool hanging = false, Token? operator2}) {
    // Hoist any comments before the first operand so they don't force the
    // infix operator to split.
    var leadingComments = pieces.takeCommentsBefore(left.firstNonCommentToken);

    var leftPiece = pieces.build(() {
      pieces.visit(left);
      if (hanging) {
        pieces.space();
        pieces.token(operator);
        pieces.token(operator2);
      }
    });

    var rightPiece = pieces.build(() {
      if (!hanging) {
        pieces.token(operator);
        pieces.token(operator2);
        pieces.space();
      }

      pieces.visit(right);
    });

    pieces.add(prependLeadingComments(
        leadingComments, InfixPiece([leftPiece, rightPiece])));
  }

  /// Writes a chained infix operation: a binary operator expression, or
  /// binary pattern.
  ///
  /// In a tree of binary AST nodes, all operators at the same precedence are
  /// treated as a single chain of operators that either all split or none do.
  /// Operands within those (which may themselves be chains of higher
  /// precedence binary operators) are then formatted independently.
  ///
  /// [T] is the type of node being visited and [destructure] is a callback
  /// that takes one of those and yields the operands and operator. We need
  /// this since there's no interface shared by the various binary operator
  /// AST nodes.
  ///
  /// If [precedence] is given, then this only flattens binary nodes with that
  /// same precedence.
  void writeInfixChain<T extends AstNode>(
      T node, BinaryOperation Function(T node) destructure,
      {int? precedence, bool indent = true}) {
    // Hoist any comments before the first operand so they don't force the
    // infix operator to split.
    var leadingComments = pieces.takeCommentsBefore(node.firstNonCommentToken);

    var operands = <Piece>[];

    void traverse(AstNode e) {
      // If the node is one if our infix operators, then recurse into the
      // operands.
      if (e is T) {
        var (left, operator, right) = destructure(e);
        if (precedence == null || operator.type.precedence == precedence) {
          operands.add(pieces.build(() {
            traverse(left);
            pieces.space();
            pieces.token(operator);
          }));

          traverse(right);
          return;
        }
      }

      // Otherwise, just write the node itself.
      pieces.visit(e);
    }

    operands.add(pieces.build(() {
      traverse(node);
    }));

    pieces.add(prependLeadingComments(
        leadingComments, InfixPiece(operands, indent: indent)));
  }

  /// Writes a [ListPiece] for the given bracket-delimited set of elements.
  ///
  /// If [preserveNewlines] is `true`, then any newlines or lack of newlines
  /// between pairs of elements in the input are preserved in the output. This
  /// is used for collection literals that contain line comments to preserve
  /// the author's deliberate structuring, as in:
  ///
  ///     matrix = [
  ///       1, 2, 3, //
  ///       4, 5, 6,
  ///       7, 8, 9,
  ///     ];
  void writeList(List<AstNode> elements,
      {required Token leftBracket,
      required Token rightBracket,
      ListStyle style = const ListStyle(),
      bool preserveNewlines = false,
      bool allowBlockArgument = false}) {
    // If the list is completely empty, write the brackets directly inline so
    // that we create fewer pieces.
    if (!elements.canSplit(rightBracket)) {
      pieces.token(leftBracket);
      pieces.token(rightBracket);
      return;
    }

    var builder = DelimitedListBuilder(this, style);

    builder.leftBracket(leftBracket);

    if (preserveNewlines && elements.containsLineComments(rightBracket)) {
      _preserveNewlinesInCollection(elements, builder);
    } else {
      builder.visitAll(elements, allowBlockArgument: allowBlockArgument);
    }

    builder.rightBracket(rightBracket);
    pieces.add(builder.build());
  }

  /// Writes [elements] into [builder], preserving the original newlines (or
  /// lack thereof) between elements.
  ///
  /// This is used for formatting collection literals that contain at least one
  /// line comment between elements. In that case, we use the line comment as a
  /// single to prefer the author's chosen newlines between elements. For
  /// example, if the user writes:
  ///
  ///     list = [
  ///       1,2,   3, 4,
  ///       // comment
  ///       5,6,    7
  ///     ];
  ///
  /// The formatter produces:
  ///
  ///     list = [
  ///       1, 2, 3, 4,
  ///       // comment
  ///       5, 6, 7
  ///     ];
  void _preserveNewlinesInCollection(
      List<AstNode> elements, DelimitedListBuilder builder) {
    // Builder for all of the elements on a single line. We use a ListPiece for
    // this too because even though we prefer to keep all elements that are on
    // a single line in the input also on a single line in the output, we will
    // split them if they don't fit.
    var lineStyle = const ListStyle(commas: Commas.nonTrailing);
    var lineBuilder = DelimitedListBuilder(this, lineStyle);
    var atLineStart = true;

    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];

      if (!atLineStart &&
          comments.hasNewlineBetween(
              elements[i - 1].endToken, element.beginToken)) {
        // This element begins a new line. Add the elements on the previous
        // line to the list builder and start a new line.
        builder.add(lineBuilder.build());
        lineBuilder = DelimitedListBuilder(this, lineStyle);
        atLineStart = true;
      }

      // Let the main list builder handle comments that occur between elements
      // that aren't on the same line.
      if (atLineStart) builder.addCommentsBefore(element.beginToken);

      lineBuilder.visit(element);

      // There is an element on this line now.
      atLineStart = false;
    }

    if (!atLineStart) builder.add(lineBuilder.build());
  }

  /// Writes a [VariablePiece] for a named or wildcard variable pattern.
  void writePatternVariable(Token? keyword, TypeAnnotation? type, Token name) {
    // If it's a wildcard with no declaration keyword or type, there is just a
    // name token.
    if (keyword == null && type == null) {
      pieces.token(name);
      return;
    }

    var header = pieces.build(() {
      pieces.modifier(keyword);
      pieces.visit(type);
    });

    pieces
        .add(VariablePiece(header, [tokenPiece(name)], hasType: type != null));
  }

  /// Writes a [Piece] for an AST node followed by an unsplittable token.
  void writePostfix(AstNode node, Token? operator) {
    pieces.visit(node);
    pieces.token(operator);
  }

  /// Writes a [Piece] for an AST node preceded by an unsplittable token.
  ///
  /// If [space] is `true` and there is an operator, writes a space between the
  /// operator and operand.
  void writePrefix(Token? operator, AstNode? node, {bool space = false}) {
    pieces.token(operator, spaceAfter: space);
    pieces.visit(node);
  }

  /// Writes an [AdjacentPiece] for a given record type field.
  void writeRecordTypeField(RecordTypeAnnotationField node) {
    writeParameter(metadata: node.metadata, node.type, node.name);
  }

  /// Writes a [ListPiece] for a record literal or pattern.
  void writeRecord(
    Token leftParenthesis,
    List<AstNode> fields,
    Token rightParenthesis, {
    Token? constKeyword,
    bool preserveNewlines = false,
  }) {
    var style = switch (fields) {
      // Record types or patterns with a single named field don't add a trailing
      // comma unless it's split, like:
      //
      //     ({int n}) x;
      //
      // Or:
      //
      //     if (obj case (name: value)) {
      //       ;
      //     }
      [PatternField(name: _?)] => const ListStyle(commas: Commas.trailing),
      [NamedExpression()] => const ListStyle(commas: Commas.trailing),

      // Record types or patterns with a single positional field always have a
      // trailing comma to disambiguate from parenthesized expressions or
      // patterns, like:
      //
      //     (int,) x;
      //
      // Or:
      //
      //     if (obj case (pattern,)) {
      //       ;
      //     }
      [_] => const ListStyle(commas: Commas.alwaysTrailing),

      // Record types or patterns with multiple fields have regular trailing
      // commas when split.
      _ => const ListStyle(commas: Commas.trailing)
    };

    writeCollection(
      constKeyword: constKeyword,
      leftParenthesis,
      fields,
      rightParenthesis,
      style: style,
      preserveNewlines: preserveNewlines,
    );
  }

  /// Writes a class, enum, extension, extension type, mixin, or mixin
  /// application class declaration.
  ///
  /// The [keywords] list is the ordered list of modifiers and keywords at the
  /// beginning of the declaration.
  ///
  /// For all but a mixin application class, [body] should a record containing
  /// the bracket delimiters and the list of member declarations for the type's
  /// body. For mixin application classes, [body] is `null` and instead
  /// [equals], [superclass], and [semicolon] are provided.
  ///
  /// If the type is an extension, then [onType] is a record containing the
  /// `on` keyword and the on type.
  ///
  /// If the type is an extension type, then [representation] is the primary
  /// constructor for it.
  void writeType(
      NodeList<Annotation> metadata, List<Token?> keywords, Token? name,
      {TypeParameterList? typeParameters,
      Token? equals,
      NamedType? superclass,
      RepresentationDeclaration? representation,
      ExtendsClause? extendsClause,
      MixinOnClause? onClause,
      WithClause? withClause,
      ImplementsClause? implementsClause,
      NativeClause? nativeClause,
      (Token, TypeAnnotation)? onType,
      TypeBodyType bodyType = TypeBodyType.block,
      required Piece Function() body}) {
    // Begin a piece to attach the metadata to the type.
    pieces.withMetadata(metadata, () {
      var header = pieces.build(() {
        var space = false;
        for (var keyword in keywords) {
          if (space) pieces.space();
          pieces.token(keyword);
          if (keyword != null) space = true;
        }

        pieces.token(name, spaceBefore: true);

        if (typeParameters != null) {
          pieces.visit(typeParameters);
        }

        // Mixin application classes have ` = Superclass` after the declaration
        // name.
        if (equals != null) {
          pieces.space();
          pieces.token(equals);
          pieces.space();
          pieces.visit(superclass!);
        }

        // Extension types have a representation type.
        if (representation != null) {
          pieces.visit(representation);
        }
      });

      var clauses = <Piece>[];

      void typeClause(Token keyword, List<AstNode> types) {
        clauses.add(InfixPiece([
          tokenPiece(keyword),
          for (var type in types) nodePiece(type, commaAfter: true),
        ]));
      }

      if (extendsClause != null) {
        typeClause(extendsClause.extendsKeyword, [extendsClause.superclass]);
      }

      if (onClause != null) {
        typeClause(onClause.onKeyword, onClause.superclassConstraints);
      }

      if (withClause != null) {
        typeClause(withClause.withKeyword, withClause.mixinTypes);
      }

      if (implementsClause != null) {
        typeClause(
            implementsClause.implementsKeyword, implementsClause.interfaces);
      }

      if (onType case (var onKeyword, var onType)?) {
        typeClause(onKeyword, [onType]);
      }

      if (nativeClause != null) {
        typeClause(nativeClause.nativeKeyword,
            [if (nativeClause.name case var name?) name]);
      }

      if (clauses.isNotEmpty) {
        header = ClausePiece(header, clauses,
            allowLeadingClause: extendsClause != null || onClause != null);
      }

      pieces.add(TypePiece(header, body(), bodyType: bodyType));
    });
  }

  /// Writes a [ListPiece] for a type argument or type parameter list.
  void writeTypeList(
      Token leftBracket, List<AstNode> elements, Token rightBracket) {
    writeList(
        leftBracket: leftBracket,
        elements,
        rightBracket: rightBracket,
        style: const ListStyle(commas: Commas.nonTrailing, splitCost: 3));
  }

  /// Handles the `async`, `sync*`, or `async*` modifiers on a function body.
  void writeFunctionBodyModifiers(FunctionBody body) {
    // The `async` or `sync` keyword.
    pieces.token(body.keyword);
    pieces.token(body.star);
    if (body.keyword != null) pieces.space();
  }

  /// Writes a [Piece] with "assignment-like" splitting.
  ///
  /// This is used, obviously, for assignments and variable declarations to
  /// handle splitting after the `=`, but is also used in any context where an
  /// expression follows something that it "defines" or "initializes":
  ///
  /// * Assignment
  /// * Variable declaration
  /// * Constructor initializer
  /// * Expression (`=>`) function body
  /// * Named argument or named record field (`:`)
  /// * Map entry (`:`)
  ///
  /// If [canBlockSplitLeft] is `true`, then the left-hand operand supports
  /// being block-formatted without indenting it farther, like:
  ///
  ///     var [
  ///       element,
  ///     ] = list;
  void writeAssignment(
      AstNode leftHandSide, Token operator, AstNode rightHandSide,
      {bool includeComma = false,
      bool canBlockSplitLeft = false,
      NodeContext leftHandSideContext = NodeContext.none}) {
    // If an operand can have block formatting, then a newline in it doesn't
    // force the operator to split, as in:
    //
    //    var [
    //      element,
    //    ] = list;
    //
    // Or:
    //
    //    var list = [
    //      element,
    //    ];
    canBlockSplitLeft |= switch (leftHandSide) {
      // Treat method chains and cascades on the LHS as if they were blocks.
      // They don't really fit the "block" term, but it looks much better to
      // force a method chain to split on the left than to try to avoid
      // splitting it and split at the assignment instead:
      //
      //    // Worse:
      //    target.method(
      //          argument,
      //        ).setter =
      //        value;
      //
      //    // Better:
      //    target.method(argument)
      //        .setter = value;
      //
      MethodInvocation() => true,
      PropertyAccess() => true,
      PrefixedIdentifier() => true,

      // Otherwise, it must be an actual block construct.
      Expression() => leftHandSide.canBlockSplit,
      DartPattern() => leftHandSide.canBlockSplit,
      _ => false
    };

    var canBlockSplitRight = switch (rightHandSide) {
      Expression() => rightHandSide.canBlockSplit,
      DartPattern() => rightHandSide.canBlockSplit,
      _ => false
    };

    var leftPiece = nodePiece(leftHandSide, context: leftHandSideContext);

    var operatorPiece = pieces.build(() {
      if (operator.type != TokenType.COLON) pieces.space();
      pieces.token(operator);
    });

    var rightPiece = nodePiece(rightHandSide,
        commaAfter: includeComma, context: NodeContext.assignment);

    pieces.add(AssignPiece(
        left: leftPiece,
        operatorPiece,
        rightPiece,
        canBlockSplitLeft: canBlockSplitLeft,
        canBlockSplitRight: canBlockSplitRight));
  }

  /// Writes a [Piece] for the `<variable> in <expression>` part of a for-in
  /// loop.
  void writeForIn(AstNode leftHandSide, Token inKeyword, Expression sequence) {
    var leftPiece =
        nodePiece(leftHandSide, context: NodeContext.forLoopVariable);

    var sequencePiece = pieces.build(() {
      // Put the `in` at the beginning of the sequence.
      pieces.token(inKeyword);
      pieces.space();
      pieces.visit(sequence);
    });

    pieces.add(ForInPiece(leftPiece, sequencePiece,
        canBlockSplitSequence: sequence.canBlockSplit));
  }

  /// Writes a piece for a parameter-like constructor: Either a simple formal
  /// parameter or a record type field, which is syntactically similar to a
  /// parameter.
  ///
  /// If the parameter has a default value, then [defaultValue] contains the
  /// `:` or `=` separator and the constant value expression.
  void writeParameter(TypeAnnotation? type, Token? name,
      {List<Annotation> metadata = const [],
      List<Token?> modifiers = const [],
      Token? fieldKeyword,
      Token? period,
      (Token separator, Expression value)? defaultValue}) {
    // Begin a piece to attach metadata to the parameter.
    pieces.withMetadata(metadata, inlineMetadata: true, () {
      Piece? typePiece;
      if (type != null) {
        typePiece = pieces.build(() {
          for (var keyword in modifiers) {
            pieces.modifier(keyword);
          }

          pieces.visit(type);
        });
      }

      Piece? namePiece;
      if (name != null) {
        namePiece = pieces.build(() {
          // If there is a type annotation, the modifiers will be before the
          // type. Otherwise, they go before the name.
          if (type == null) {
            for (var keyword in modifiers) {
              pieces.modifier(keyword);
            }
          }

          pieces.token(fieldKeyword);
          pieces.token(period);
          pieces.token(name);
        });
      }

      Piece parameterPiece;
      if (typePiece != null && namePiece != null) {
        // We have both a type and name, allow splitting between them.
        parameterPiece = VariablePiece(typePiece, [namePiece], hasType: true);
      } else {
        // Will have at least a type or name.
        parameterPiece = typePiece ?? namePiece!;
      }

      // If there's a default value, include it. We do that inside here so that
      // any metadata surrounds the entire assignment instead of being part of
      // the assignment's left-hand side where a split in the metadata would
      // force a split at the default value separator.
      writeDefaultValue(parameterPiece, defaultValue);
    });
  }

  /// Visits [node] and creates a piece from it.
  ///
  /// If [commaAfter] is `true`, looks for a comma token after [node] and
  /// writes it to the piece as well.
  Piece nodePiece(AstNode node,
      {bool commaAfter = false, NodeContext context = NodeContext.none}) {
    var result = pieces.build(() {
      visitNode(node, context);
    });

    if (commaAfter) {
      var nextToken = node.endToken.next!;
      if (nextToken.lexeme == ',') {
        var comma = tokenPiece(nextToken);
        result = AdjacentPiece([result, comma]);
      }
    }

    return result;
  }

  /// Visits [node] and creates a piece from it if not `null`.
  ///
  /// Otherwise returns `null`.
  Piece? optionalNodePiece(AstNode? node) {
    if (node == null) return null;
    return nodePiece(node);
  }

  /// Creates a piece for only [token].
  ///
  /// If [commaAfter] is `true`, will look for and write a comma following the
  /// token if there is one.
  Piece tokenPiece(Token token, {bool commaAfter = false}) {
    return pieces.tokenPiece(token, commaAfter: commaAfter);
  }
}
