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

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';

import '../ast_extensions.dart';
import '../constants.dart';
import 'chunk.dart';
import 'rule/argument.dart';
import 'rule/rule.dart';
import 'source_visitor.dart';

/// Helper class for [SourceVisitor] that handles visiting and writing an
/// [ArgumentList], including all of the special code needed to handle
/// block-formatted arguments.
final class ArgumentListVisitor {
  final SourceVisitor _visitor;

  /// The "(" before the argument list.
  final Token _leftParenthesis;

  /// The ")" after the argument list.
  final Token _rightParenthesis;

  /// All of the arguments, positional, named, and functions, in the argument
  /// list.
  final List<Expression> _allArguments;

  /// The normal arguments preceding any block function arguments.
  final ArgumentSublist _arguments;

  /// The contiguous list of block function arguments, if any.
  ///
  /// Otherwise, this is `null`.
  final List<Expression>? _functions;

  /// If there are block function arguments, this is the arguments after them.
  ///
  /// Otherwise, this is `null`.
  final ArgumentSublist? _argumentsAfterFunctions;

  /// Returns `true` if there is only a single positional argument.
  bool get _isSingle =>
      _allArguments.length == 1 && _allArguments.single is! NamedExpression;

  /// Whether this argument list has any arguments that should be formatted as
  /// blocks.
  // TODO(rnystrom): Returning true based on collections is non-optimal. It
  // forces a method chain to break into two but the result collection may not
  // actually split which can lead to a method chain that's allowed to break
  // where it shouldn't.
  bool get hasBlockArguments =>
      _arguments._blocks.isNotEmpty || _functions != null;

  factory ArgumentListVisitor(SourceVisitor visitor, ArgumentList node) {
    return ArgumentListVisitor.forArguments(
        visitor, node.leftParenthesis, node.rightParenthesis, node.arguments);
  }

  factory ArgumentListVisitor.forArguments(
      SourceVisitor visitor,
      Token leftParenthesis,
      Token rightParenthesis,
      List<Expression> arguments) {
    var functionRange = _contiguousFunctions(arguments);

    if (functionRange == null) {
      // No functions, so there is just a single argument list.
      return ArgumentListVisitor._(visitor, leftParenthesis, rightParenthesis,
          arguments, ArgumentSublist(arguments, arguments), null, null);
    }

    // Split the arguments into two independent argument lists with the
    // functions in the middle.
    var argumentsBefore = arguments.take(functionRange[0]).toList();
    var functions = arguments.sublist(functionRange[0], functionRange[1]);
    var argumentsAfter = arguments.skip(functionRange[1]).toList();

    return ArgumentListVisitor._(
        visitor,
        leftParenthesis,
        rightParenthesis,
        arguments,
        ArgumentSublist(arguments, argumentsBefore),
        functions,
        ArgumentSublist(arguments, argumentsAfter));
  }

  ArgumentListVisitor._(
      this._visitor,
      this._leftParenthesis,
      this._rightParenthesis,
      this._allArguments,
      this._arguments,
      this._functions,
      this._argumentsAfterFunctions)
      : assert(_functions == null || _argumentsAfterFunctions != null,
            'If _functions is passed, _argumentsAfterFunctions must be too.');

  /// Builds chunks for the argument list.
  void visit() {
    // If there is just one positional argument, it tends to look weird to
    // split before it, so try not to.
    if (_isSingle) _visitor.builder.startSpan();

    _visitor.builder.startSpan();
    _visitor.token(_leftParenthesis);

    _arguments.visit(_visitor);

    _visitor.builder.endSpan();

    var functions = _functions;
    if (functions != null) {
      // TODO(rnystrom): It might look better to treat the parameter list of the
      // first function as if it were an argument in the preceding argument list
      // instead of just having this little solo split here. That would try to
      // keep the parameter list with other arguments when possible, and, I
      // think, generally look nicer.
      if (functions.first == _allArguments.first) {
        _visitor.soloZeroSplit();
      } else {
        _visitor.soloSplit();
      }

      for (var argument in functions) {
        if (argument != functions.first) _visitor.space();

        _visitor.visit(argument);

        // Write the following comma.
        if (argument.hasCommaAfter) {
          _visitor.token(argument.endToken.next);
        }
      }

      _visitor.builder.startSpan();
      _argumentsAfterFunctions!.visit(_visitor);
      _visitor.builder.endSpan();
    }

    _visitor.token(_rightParenthesis);

    if (_isSingle) _visitor.builder.endSpan();
  }

  /// Look for a single contiguous range of block function [arguments] that
  /// should receive special formatting.
  ///
  /// Returns a list of (start, end] indexes if found, otherwise returns `null`.
  static List<int>? _contiguousFunctions(List<Expression> arguments) {
    int? functionsStart;
    var functionsEnd = -1;

    // Find the range of block function arguments, if any.
    for (var i = 0; i < arguments.length; i++) {
      var argument = arguments[i];
      if (_isBlockFunction(argument)) {
        functionsStart ??= i;

        // The functions must be one contiguous section.
        if (functionsEnd != -1 && functionsEnd != i) return null;

        functionsEnd = i + 1;
      }
    }

    if (functionsStart == null) return null;

    // Edge case: If all of the arguments are named, but they aren't all
    // functions, then don't handle the functions specially. A function with a
    // bunch of named arguments tends to look best when they are all lined up,
    // even the function ones (unless they are all functions).
    //
    // Prefers:
    //
    //     function(
    //         named: () {
    //           something();
    //         },
    //         another: argument);
    //
    // Over:
    //
    //     function(named: () {
    //       something();
    //     },
    //         another: argument);
    if (_isAllNamed(arguments) &&
        (functionsStart > 0 || functionsEnd < arguments.length)) {
      return null;
    }

    // Edge case: If all of the function arguments are named and there are
    // other named arguments that are "=>" functions, then don't treat the
    // block-bodied functions specially. In a mixture of the two function
    // styles, it looks cleaner to treat them all like normal expressions so
    // that the named arguments line up.
    if (_isAllNamed(arguments.sublist(functionsStart, functionsEnd))) {
      bool isNamedArrow(Expression expression) {
        if (expression is! NamedExpression) return false;
        expression = expression.expression;

        return expression is FunctionExpression &&
            expression.body is ExpressionFunctionBody;
      }

      for (var i = 0; i < functionsStart; i++) {
        if (isNamedArrow(arguments[i])) return null;
      }

      for (var i = functionsEnd; i < arguments.length; i++) {
        if (isNamedArrow(arguments[i])) return null;
      }
    }

    return [functionsStart, functionsEnd];
  }

  /// Returns `true` if every expression in [arguments] is named.
  static bool _isAllNamed(List<Expression> arguments) =>
      arguments.every((argument) => argument is NamedExpression);

  /// Returns `true` if [expression] is a [FunctionExpression] with a non-empty
  /// block body.
  static bool _isBlockFunction(Expression expression) {
    if (expression is NamedExpression) expression = expression.expression;

    // Allow functions wrapped in dotted method calls like "a.b.c(() { ... })".
    if (expression is MethodInvocation) {
      if (!_isValidWrappingTarget(expression.target)) return false;
      if (expression.argumentList.arguments.length != 1) return false;

      return _isBlockFunction(expression.argumentList.arguments.single);
    }

    if (expression is InstanceCreationExpression) {
      if (expression.argumentList.arguments.length != 1) return false;

      return _isBlockFunction(expression.argumentList.arguments.single);
    }

    // Allow immediately-invoked functions like "() { ... }()".
    if (expression is FunctionExpressionInvocation) {
      if (expression.argumentList.arguments.isNotEmpty) return false;

      expression = expression.function;
    }

    // Unwrap parenthesized expressions.
    while (expression is ParenthesizedExpression) {
      expression = expression.expression;
    }

    // Must be a function.
    if (expression is! FunctionExpression) return false;

    // With a curly body.
    if (expression.body is! BlockFunctionBody) return false;

    // That isn't empty.
    var body = expression.body as BlockFunctionBody;
    return body.block.statements.isNotEmpty ||
        body.block.rightBracket.precedingComments != null;
  }

  /// Returns `true` if [expression] is a valid method invocation target for
  /// an invocation that wraps a function literal argument.
  static bool _isValidWrappingTarget(Expression? expression) {
    // Allow bare function calls.
    if (expression == null) return true;

    // Allow property accesses.
    while (expression is PropertyAccess) {
      expression = expression.target;
    }

    if (expression is PrefixedIdentifier) return true;
    if (expression is SimpleIdentifier) return true;

    return false;
  }
}

/// A range of arguments from a complete argument list.
///
/// One of these typically covers all of the arguments in an invocation. But,
/// when an argument list has block functions in the middle, the arguments
/// before and after the functions are treated as separate independent lists.
/// In that case, there will be two of these.
final class ArgumentSublist {
  /// The full argument list from the AST.
  final List<Expression> _allArguments;

  /// If all positional arguments occur before all named arguments, then this
  /// contains the positional arguments, in order. Otherwise (there are no
  /// positional arguments or they are interleaved with named ones), this is
  /// empty.
  final List<Expression> _positional;

  /// The named arguments, in order. If there are any named arguments that occur
  /// before positional arguments, then all arguments are treated as named and
  /// end up in this list.
  final List<Expression> _named;

  /// Maps each block argument, excluding functions, to the first token for that
  /// argument.
  final Map<Expression, Token> _blocks;

  /// The number of leading block arguments, excluding functions.
  ///
  /// If all arguments are blocks, this counts them.
  final int _leadingBlocks;

  /// The number of trailing blocks arguments.
  ///
  /// If all arguments are blocks, this is zero.
  final int _trailingBlocks;

  /// The rule used to split the bodies of all block arguments.
  Rule get blockRule => _blockRule!;
  Rule? _blockRule;

  /// The most recent chunk that split before an argument.
  Chunk? get previousSplit => _previousSplit;
  Chunk? _previousSplit;

  factory ArgumentSublist(
      List<Expression> allArguments, List<Expression> arguments) {
    var argumentLists = _splitArgumentLists(arguments);
    var positional = argumentLists[0];
    var named = argumentLists[1];

    var blocks = <Expression, Token>{};
    for (var argument in arguments) {
      var bracket = _blockToken(argument);
      if (bracket != null) blocks[argument] = bracket;
    }

    // Count the leading arguments that are blocks.
    var leadingBlocks = 0;
    for (var argument in arguments) {
      if (!blocks.containsKey(argument)) break;
      leadingBlocks++;
    }

    // Count the trailing arguments that are blocks.
    var trailingBlocks = 0;
    if (leadingBlocks != arguments.length) {
      for (var argument in arguments.reversed) {
        if (!blocks.containsKey(argument)) break;
        trailingBlocks++;
      }
    }

    // Blocks must all be a prefix or suffix of the argument list (and not
    // both).
    if (leadingBlocks != blocks.length) leadingBlocks = 0;
    if (trailingBlocks != blocks.length) trailingBlocks = 0;

    // Ignore any blocks in the middle of the argument list.
    if (leadingBlocks == 0 && trailingBlocks == 0) blocks.clear();

    return ArgumentSublist._(
        allArguments, positional, named, blocks, leadingBlocks, trailingBlocks);
  }

  ArgumentSublist._(this._allArguments, this._positional, this._named,
      this._blocks, this._leadingBlocks, this._trailingBlocks);

  void visit(SourceVisitor visitor) {
    if (_blocks.isNotEmpty) {
      _blockRule = Rule(Cost.splitBlocks);
    }

    var rule = _visitPositional(visitor);
    _visitNamed(visitor, rule);
  }

  /// Writes the positional arguments, if any.
  PositionalRule? _visitPositional(SourceVisitor visitor) {
    if (_positional.isEmpty) return null;

    // Allow splitting after "(".
    // Only count the blocks in the positional rule.
    var leadingBlocks = math.min(_leadingBlocks, _positional.length);
    var trailingBlocks = math.max(_trailingBlocks - _named.length, 0);
    var rule = PositionalRule(_blockRule,
        argumentCount: _positional.length,
        leadingCollections: leadingBlocks,
        trailingCollections: trailingBlocks);
    _visitArguments(visitor, _positional, rule);

    return rule;
  }

  /// Writes the named arguments, if any.
  void _visitNamed(SourceVisitor visitor, PositionalRule? positionalRule) {
    if (_named.isEmpty) return;

    // Only count the blocks in the named rule.
    var leadingBlocks = math.max(_leadingBlocks - _positional.length, 0);
    var trailingBlocks = math.min(_trailingBlocks, _named.length);
    var namedRule = NamedRule(_blockRule, leadingBlocks, trailingBlocks);

    // Let the positional args force the named ones to split.
    if (positionalRule != null) {
      positionalRule.addNamedArgsConstraints(namedRule);
    }

    _visitArguments(visitor, _named, namedRule);
  }

  void _visitArguments(
      SourceVisitor visitor, List<Expression> arguments, ArgumentRule rule) {
    visitor.builder.startRule(rule);

    // Split before the first argument.
    _previousSplit =
        visitor.builder.split(space: arguments.first != _allArguments.first);
    rule.beforeArgument(_previousSplit);

    // Try to not split the positional arguments.
    if (arguments == _positional) {
      visitor.builder.startSpan(Cost.positionalArguments);
    }

    for (var argument in arguments) {
      _visitArgument(visitor, rule, argument);

      // Write the split.
      if (argument != arguments.last) {
        _previousSplit = visitor.split();
        rule.beforeArgument(_previousSplit);
      }
    }

    if (arguments == _positional) visitor.builder.endSpan();

    visitor.builder.endRule();
  }

  void _visitArgument(
      SourceVisitor visitor, ArgumentRule rule, Expression argument) {
    // If we're about to write a block argument, handle it specially.
    var argumentBlock = _blocks[argument];
    if (argumentBlock != null) {
      rule.disableSplitOnInnerRules();

      // Tell it to use the rule we've already created.
      visitor.beforeBlock(argumentBlock, blockRule, previousSplit);
    } else if (_allArguments.length > 1 ||
        _allArguments.first is RecordLiteral) {
      // Edge case: Only bump the nesting if there are multiple arguments. This
      // lets us avoid spurious indentation in cases like:
      //
      //     function(function(() {
      //       body;
      //     }));
      //
      // Do bump the nesting if the single argument is a record because records
      // are formatted like regular values when they appear in argument lists
      // even though they internally get block-like formatting.
      visitor.builder.startBlockArgumentNesting();
    } else if (argument is! NamedExpression) {
      // Edge case: Likewise, don't force the argument to split if there is
      // only a single positional one, like:
      //
      //     outer(inner(
      //         longArgument));
      rule.disableSplitOnInnerRules();
    }

    if (argument is NamedExpression) {
      visitor.visitNamedNode(argument.name.label.token, argument.name.colon,
          argument.expression, rule as NamedRule);
    } else {
      visitor.visit(argument);
    }

    if (argumentBlock != null) {
      rule.enableSplitOnInnerRules();
    } else if (_allArguments.length > 1 ||
        _allArguments.first is RecordLiteral) {
      visitor.builder.endBlockArgumentNesting();
    } else if (argument is! NamedExpression) {
      rule.enableSplitOnInnerRules();
    }

    // Write the following comma.
    if (argument.hasCommaAfter) {
      visitor.token(argument.endToken.next);
    }
  }

  /// Splits [arguments] into two lists: the list of leading positional
  /// arguments and the list of trailing named arguments.
  ///
  /// If positional arguments are interleaved with the named arguments then
  /// all arguments are treat as named since that provides simpler, consistent
  /// output.
  ///
  /// Returns a list of two lists: the positional arguments then the named ones.
  static List<List<Expression>> _splitArgumentLists(
      List<Expression> arguments) {
    var positional = <Expression>[];
    var named = <Expression>[];
    var inNamed = false;
    for (var argument in arguments) {
      if (argument is NamedExpression) {
        inNamed = true;
      } else if (inNamed) {
        // Got a positional argument after a named one.
        return [[], arguments];
      }

      if (inNamed) {
        named.add(argument);
      } else {
        positional.add(argument);
      }
    }

    return [positional, named];
  }

  /// If [expression] can be formatted as a block, returns the token that opens
  /// the block, such as a collection's bracket.
  ///
  /// Block-formatted arguments can get special indentation to make them look
  /// more statement-like.
  static Token? _blockToken(Expression expression) {
    if (expression is NamedExpression) {
      expression = expression.expression;
    }

    // TODO(rnystrom): Should we step into parenthesized expressions?

    if (expression is ListLiteral) return expression.leftBracket;
    if (expression is RecordLiteral) return expression.leftParenthesis;
    if (expression is SetOrMapLiteral) return expression.leftBracket;
    if (expression is SingleStringLiteral && expression.isMultiline) {
      return expression.beginToken;
    }

    // Not a collection literal.
    return null;
  }
}
