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

/// Parenthesize the target of the [expressionStatement]'s expression (assumed
/// to [cascadeExpression]) before removing the cascade.
ExpressionStatement fixCascadeByParenthesizingTarget({
  required ExpressionStatement expressionStatement,
  required CascadeExpression cascadeExpression,
}) {
  cascadeExpression as CascadeExpressionImpl;
  assert(cascadeExpression.cascadeSections.length == 1);

  var newTarget = ParenthesizedExpressionImpl(
    leftParenthesis: Token(TokenType.OPEN_PAREN, 0)
      ..previous = expressionStatement.beginToken.previous
      ..next = cascadeExpression.target.beginToken,
    expression: cascadeExpression.target,
    rightParenthesis: Token(TokenType.CLOSE_PAREN, 0)
      ..previous = cascadeExpression.target.endToken
      ..next = expressionStatement.semicolon,
  );

  return ExpressionStatementImpl(
    expression: CascadeExpressionImpl(
      target: newTarget,
      cascadeSections: cascadeExpression.cascadeSections,
    ),
    semicolon: expressionStatement.semicolon,
  );
}

/// Recursively insert [cascadeTarget] (the LHS of the cascade) into the
/// LHS of the assignment expression that used to be the cascade's RHS.
ExpressionImpl insertCascadeTargetIntoExpression({
  required Expression expression,
  required Expression cascadeTarget,
}) {
  expression as ExpressionImpl;
  cascadeTarget as ExpressionImpl;

  // Base case: We've recursed as deep as possible.
  if (expression == cascadeTarget) return cascadeTarget;

  // Otherwise, copy `expression` and recurse into its LHS.
  if (expression is AssignmentExpressionImpl) {
    return AssignmentExpressionImpl(
      leftHandSide: insertCascadeTargetIntoExpression(
        expression: expression.leftHandSide,
        cascadeTarget: cascadeTarget,
      ),
      operator: expression.operator,
      rightHandSide: expression.rightHandSide,
    );
  } else if (expression is IndexExpressionImpl) {
    var expressionTarget = expression.realTarget;
    var question = expression.question;

    // A null-aware cascade treats the `?` in `?..` as part of the token, but
    // for a non-cascade index, it is a separate `?` token.
    if (expression.period?.type == TokenType.QUESTION_PERIOD_PERIOD) {
      question = _synthesizeToken(TokenType.QUESTION, expression.period!);
    }

    return IndexExpressionImpl(
      target: insertCascadeTargetIntoExpression(
        expression: expressionTarget,
        cascadeTarget: cascadeTarget,
      ),
      period: null,
      question: question,
      leftBracket: expression.leftBracket,
      index: expression.index,
      rightBracket: expression.rightBracket,
    );
  } else if (expression is MethodInvocationImpl) {
    var expressionTarget = expression.realTarget!;
    return MethodInvocationImpl(
      target: insertCascadeTargetIntoExpression(
        expression: expressionTarget,
        cascadeTarget: cascadeTarget,
      ),
      // If we've reached the end, replace the `..` operator with `.`
      operator: expressionTarget == cascadeTarget
          ? _synthesizeToken(TokenType.PERIOD, expression.operator!)
          : expression.operator,
      methodName: expression.methodName,
      typeArguments: expression.typeArguments,
      argumentList: expression.argumentList,
    );
  } else if (expression is PropertyAccessImpl) {
    var expressionTarget = expression.realTarget;
    return PropertyAccessImpl(
      target: insertCascadeTargetIntoExpression(
        expression: expressionTarget,
        cascadeTarget: cascadeTarget,
      ),
      // If we've reached the end, replace the `..` operator with `.`
      operator: expressionTarget == cascadeTarget
          ? _synthesizeToken(TokenType.PERIOD, expression.operator)
          : expression.operator,
      propertyName: expression.propertyName,
    );
  }
  throw UnimplementedError(
    'Unhandled ${expression.runtimeType}'
    '($expression)',
  );
}

/// Synthesize a token with [type] to replace the given [operator].
///
/// Offset, comments, and previous/next links are all preserved.
Token _synthesizeToken(TokenType type, Token operator) =>
    Token(type, operator.offset, operator.precedingComments)
      ..previous = operator.previous
      ..next = operator.next;
